Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions contracts/plan-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,32 @@ nondeterminism as a bug.

## Open items

- **Declared-import checking** ([#372](https://github.com/polymorph-components/polyengine/issues/372)):
`imports` contains runtime-used leaves from environ's `component.imports`,
not the complete `component.import_types` surface. Unused imports and
equality-bound resource aliases can disappear, so the current executor cannot
check their presence, kind, or supplied resource identity. This is a linking
gap, not evidence that those declarations impose no constraints.

The next design must retain the declared import tree and its type constraints
while keeping runtime import indices stable for initializer references.
Resource aliases must refer to the existing resource identity, not allocate a
second token. Validate supplied aliases against that identity before running
initializers. Under the Wasmtime-compatible host policy, an omitted
equality-bound alias or recursively empty instance can be synthesized; a
supplied value must still match. See the pinned Explainer's type bounds and
resource substitution rules, and Wasmtime `component/matching.rs` for this
host policy.

Core-module matching also needs typed import/export metadata: the JS module
reflection API exposes names and kinds, not signatures or limits. Do not
claim to check module subtyping from that reflection alone. Named-instance
wiring in the WAST harness must preserve exported resource identities and
module metadata; missing providers must not satisfy a type-mismatch assertion.
Cover matching, mismatching, unused, and omitted imports, and verify rejection
before start-function side effects. This is a follow-up design constraint,
not an optional field or a change to formatVersion 5; the schema and its
version transition must be reviewed together with the implementation.
- `values` section (the component-level value-definition feature): out of scope
(wasmtime parity, docs/architecture.md §7).
- Imported-module instantiation (`InstantiateModule::Import`) and re-export
Expand Down
5 changes: 5 additions & 0 deletions harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ That row-specific diagnostic divergence is spec-compatible, but it is not a
global message equivalence because the runtime text also covers a distinct
successful-prior-write condition.

Variant lifting reports the rejected discriminant and case-count range exactly.
Link-error assertions accept native link failures and host-resource import type
mismatches; native start traps, translation failures, and generic plan errors
remain distinct.

At the current pin, the remaining async subset is 13 classified failures and
zero skips: those eleven diagnostic rows, the unavailable native
`set-max-table-capacity` provider row, and its one no-current-instance cascade.
Expand Down
29 changes: 28 additions & 1 deletion harness/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,14 @@ class FileRunner {
try {
await this.executor.instantiate(artifact, "link-error");
} catch (e) {
if (e instanceof LinkError) return undefined;
if (e instanceof LinkError) {
if (!trapMatches(command.text, e.message)) {
throw new Error(
`expected link error "${command.text}", got "${e.message}"`,
);
}
return undefined;
}
throw e;
}
throw new Error(
Expand Down Expand Up @@ -331,6 +338,26 @@ const TRAP_MESSAGE_EQUIVALENTS: Array<
// Pinned Wasmtime resources.wast:459-481 passes literal slot 2 to the
// outer component's empty table; Table.get rejects it as out of range.
["unknown handle index 2", ["table index out of range"]],
// Pinned Wasmtime resources.wast:927 and definitions.py lift_own
// (third_party/component-model/design/mvp/canonical-abi/definitions.py:1482).
["cannot remove owned resource while borrowed", ["handle still lent out"]],
// Pinned Wasmtime strings.wast:21,23 and definitions.py
// load_string_from_range (definitions.py:1395).
["string pointer not aligned to 2", ["misaligned string pointer"]],
// Pinned Wasmtime resources.wast:167,174. The executor emits these only
// from its dedicated host-resource import type verdict.
[
"was not found",
[
"host import 'host/missing' must be a HostResourceType (the component imports a resource type); got undefined",
],
],
[
"expected resource found func",
[
"host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
],
],
// Official resources/handle-table.wast:322,324 and the corresponding
// resource-type checks in runtime/src/cabi/handles.ts:239-263.
[
Expand Down
36 changes: 26 additions & 10 deletions harness/src/runtime-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Translator } from "@polyengine/runtime/shim";
import {
type ComponentHandle,
type HostImports,
HostResourceImportTypeError,
instantiateComponent,
} from "../../runtime/src/exec/mod.ts";
import {
Expand Down Expand Up @@ -63,6 +64,30 @@ function maybeCapability(e: unknown, what: string): void {
}
}

/** Internal instantiation-verdict mapper, exported only for harness tests. */
export function rethrowInstantiationError(
e: unknown,
expect: InstantiateExpectation,
): never {
if (e instanceof Trap) {
if (expect === "trap") throw new TrapError(e.message);
asCapabilityOrRethrow(e, "instantiate");
}
if (e instanceof WebAssembly.RuntimeError && expect === "trap") {
throw new TrapError(e.message);
}
if (
expect === "link-error" &&
(e instanceof HostResourceImportTypeError ||
e instanceof WebAssembly.LinkError)
) {
throw new LinkError(e instanceof Error ? e.message : String(e));
}
if (e instanceof PlanError) asCapabilityOrRethrow(e, "instantiate");
maybeCapability(e, "instantiate");
throw e;
}

interface ComponentInstanceRef extends InstanceRef {
readonly kind: "component";
handle: ComponentHandle;
Expand Down Expand Up @@ -181,16 +206,7 @@ export class RuntimeExecutor implements CommandExecutor {
trapOnIdle: true,
});
} catch (e) {
if (e instanceof Trap) {
if (expect === "trap") throw new TrapError(e.message);
asCapabilityOrRethrow(e, "instantiate");
}
if (e instanceof PlanError) asCapabilityOrRethrow(e, "instantiate");
// Capability-gated trampolines (UnsupportedFeatureError et al.) match
// via CAPABILITY_MARKERS regardless of error class:
maybeCapability(e, "instantiate");
if (expect === "link-error") throw new LinkError(String(e));
throw e;
rethrowInstantiationError(e, expect);
}
if (expect !== "success") {
throw new Error(
Expand Down
45 changes: 0 additions & 45 deletions harness/src/wasmtime-expectations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,33 +97,6 @@ export const WASMTIME_EXPECTATION_GROUPS: readonly WasmtimeExpectationGroup[] =
status: "failed",
}],
},
{
file: "resources.json",
rows: [{
lines: [927],
cause:
'Error: expected trap "cannot remove owned resource while borrowed", got "handle still lent out"',
status: "failed",
}],
},
{
file: "strings.json",
rows: [{
lines: [21, 23],
cause:
'Error: expected trap "string pointer not aligned to 2", got "misaligned string pointer"',
status: "failed",
}],
},
{
file: "types.json",
rows: [{
lines: [378],
cause:
'Error: expected trap "discriminant 2 out of range [0..2)", got "invalid variant discriminant"',
status: "failed",
}],
},
],
},
{
Expand Down Expand Up @@ -212,14 +185,6 @@ export const WASMTIME_EXPECTATION_GROUPS: readonly WasmtimeExpectationGroup[] =
status: "failed",
}],
},
{
file: "instance.json",
rows: [{
lines: [79],
cause: "RuntimeError: unreachable",
status: "failed",
}],
},
{
file: "linking.json",
rows: [{
Expand Down Expand Up @@ -271,16 +236,6 @@ export const WASMTIME_EXPECTATION_GROUPS: readonly WasmtimeExpectationGroup[] =
{
file: "resources.json",
rows: [{
lines: [167],
cause:
"PlanError: host import 'host/missing' must be a HostResourceType (the component imports a resource type); got undefined",
status: "failed",
}, {
lines: [174],
cause:
"PlanError: host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
status: "failed",
}, {
lines: [201],
cause:
"Error: expected instantiation link-error, but component instantiated successfully",
Expand Down
72 changes: 71 additions & 1 deletion harness/tests/runner_unit_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { WastJson } from "../src/schema.ts";
import { CoreOnlyExecutor } from "../src/executor.ts";
import type { CommandExecutor } from "../src/executor.ts";
import { TrapError } from "../src/executor.ts";
import { LinkError, TrapError } from "../src/executor.ts";
import { runWastJson, trapMatches } from "../src/runner.ts";

// (module) - the empty core module, hand-encoded.
Expand Down Expand Up @@ -204,6 +204,34 @@ Deno.test("assert_uninstantiable rejects an unrelated trap cause", async () => {
assertEq(result.results[0].status, "failed", "status");
});

Deno.test("assert_unlinkable requires a matching link diagnostic", async () => {
for (
const [message, status] of [
["expected resource found func", "passed"],
["different cause", "failed"],
[
"prefix host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
"failed",
],
] as const
) {
const executor = new CoreOnlyExecutor() as CommandExecutor;
executor.instantiate = () => Promise.reject(new LinkError(message));
const result = await runWastJson(
doc([{
type: "assert_unlinkable",
line: 1,
filename: "comp.0.wasm",
module_type: "binary",
text: "expected resource found func",
}]),
load,
executor,
);
assertEq(result.results[0].status, status, message);
}
});

// Exact diagnostic equivalents: the core `unreachable` trap row. The runtime
// (runtime/src/exec/boundary.ts mapCoreException) passes each JS engine's raw
// trap text through untouched; this table is where the suite's
Expand Down Expand Up @@ -373,12 +401,44 @@ Deno.test("trapMatches: verified diagnostic equivalents match only their named o
"wasm `unreachable` instruction executed",
"guest trapped: unreachable",
],
[
"cannot remove owned resource while borrowed",
"handle still lent out",
],
["string pointer not aligned to 2", "misaligned string pointer"],
[
"was not found",
"host import 'host/missing' must be a HostResourceType (the component imports a resource type); got undefined",
],
[
"expected resource found func",
"host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
],
];
for (const [expected, actual] of equivalents) {
assertEq(trapMatches(expected, actual), true, `${expected} / ${actual}`);
}
});

Deno.test("generic variant diagnostic is not globally equivalent", () => {
assertEq(
trapMatches(
"discriminant 2 out of range [0..2)",
"invalid variant discriminant",
),
false,
"global matcher",
);
assertEq(
trapMatches(
"discriminant 2 out of range [0..2)",
"discriminant 3 out of range [0..3)",
),
false,
"different discriminant and case count",
);
});

Deno.test("trapMatches: narrow diagnostic rows reject adjacent but different traps", () => {
const nonEquivalents: Array<[string, string]> = [
["integer overflow", "integer underflow"],
Expand Down Expand Up @@ -407,6 +467,11 @@ Deno.test("trapMatches: narrow diagnostic rows reject adjacent but different tra
"uncaught exception propagated out of component",
"guest trapped: unreachable",
],
["string pointer not aligned to 2", "misaligned list pointer"],
[
"was not found",
"prefix host import 'host/missing' must be a HostResourceType (the component imports a resource type); got undefined",
],
];
for (const [expected, actual] of nonEquivalents) {
assertEq(trapMatches(expected, actual), false, `${expected} / ${actual}`);
Expand Down Expand Up @@ -440,6 +505,11 @@ Deno.test("trapMatches: exact equivalents reject expected and actual affixes", (
"invalid `task.return` signature and/or options for current task",
"task.return with canonical options differing from the task's",
],
["string pointer not aligned to 2", "misaligned string pointer"],
[
"expected resource found func",
"host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
],
]
) {
for (
Expand Down
50 changes: 49 additions & 1 deletion harness/tests/runtime_validation_test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { PlanError, TranslateError } from "@polyengine/runtime/plan";
import { HostResourceImportTypeError } from "../../runtime/src/exec/mod.ts";
import { Translator } from "@polyengine/runtime/shim";
import { LinkError, TrapError } from "../src/executor.ts";
import type { Artifact } from "../src/executor.ts";
import { runWastJson } from "../src/runner.ts";
import { RuntimeExecutor } from "../src/runtime-executor.ts";
import {
rethrowInstantiationError,
RuntimeExecutor,
} from "../src/runtime-executor.ts";

const shim = await Deno.readFile(
new URL(
Expand Down Expand Up @@ -150,3 +155,46 @@ Deno.test("runtime validation: pipeline failures propagate unchanged and fail ne
Translator.prototype.translate = original;
}
});

Deno.test("runtime instantiation verdicts preserve failure phase", async () => {
const nativeTrap = new WebAssembly.RuntimeError("unreachable");
const mappedTrap = await thrown(() =>
rethrowInstantiationError(nativeTrap, "trap")
);
if (
!(mappedTrap instanceof TrapError) || mappedTrap.message !== "unreachable"
) {
throw new Error(`native trap was not preserved: ${mappedTrap}`);
}
if (
await thrown(() => rethrowInstantiationError(nativeTrap, "link-error")) !==
nativeTrap
) {
throw new Error("native trap satisfied a link-error expectation");
}

const resourceMismatch = new HostResourceImportTypeError("wrong resource");
const mappedLink = await thrown(() =>
rethrowInstantiationError(resourceMismatch, "link-error")
);
if (
!(mappedLink instanceof LinkError) ||
mappedLink.message !== "wrong resource"
) {
throw new Error(`resource mismatch was not a link error: ${mappedLink}`);
}

for (
const error of [
new PlanError("generic plan failure"),
new Error("generic executor failure"),
]
) {
if (
await thrown(() => rethrowInstantiationError(error, "link-error")) !==
error
) {
throw new Error(`${error} satisfied a link-error expectation`);
}
}
});
Loading
Loading