Skip to content

Commit 403910e

Browse files
authored
Merge pull request #379 from polymorph-components/investigate/wasmtime-test-failures
harness: correct Wasmtime instantiation verdicts and diagnostics
2 parents 654d0e4 + cbb800c commit 403910e

12 files changed

Lines changed: 350 additions & 62 deletions

‎contracts/plan-format.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,32 @@ nondeterminism as a bug.
322322

323323
## Open items
324324

325+
- **Declared-import checking** ([#372](https://github.com/polymorph-components/polyengine/issues/372)):
326+
`imports` contains runtime-used leaves from environ's `component.imports`,
327+
not the complete `component.import_types` surface. Unused imports and
328+
equality-bound resource aliases can disappear, so the current executor cannot
329+
check their presence, kind, or supplied resource identity. This is a linking
330+
gap, not evidence that those declarations impose no constraints.
331+
332+
The next design must retain the declared import tree and its type constraints
333+
while keeping runtime import indices stable for initializer references.
334+
Resource aliases must refer to the existing resource identity, not allocate a
335+
second token. Validate supplied aliases against that identity before running
336+
initializers. Under the Wasmtime-compatible host policy, an omitted
337+
equality-bound alias or recursively empty instance can be synthesized; a
338+
supplied value must still match. See the pinned Explainer's type bounds and
339+
resource substitution rules, and Wasmtime `component/matching.rs` for this
340+
host policy.
341+
342+
Core-module matching also needs typed import/export metadata: the JS module
343+
reflection API exposes names and kinds, not signatures or limits. Do not
344+
claim to check module subtyping from that reflection alone. Named-instance
345+
wiring in the WAST harness must preserve exported resource identities and
346+
module metadata; missing providers must not satisfy a type-mismatch assertion.
347+
Cover matching, mismatching, unused, and omitted imports, and verify rejection
348+
before start-function side effects. This is a follow-up design constraint,
349+
not an optional field or a change to formatVersion 5; the schema and its
350+
version transition must be reviewed together with the implementation.
325351
- `values` section (the component-level value-definition feature): out of scope
326352
(wasmtime parity, docs/architecture.md §7).
327353
- Imported-module instantiation (`InstantiateModule::Import`) and re-export

‎harness/README.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ That row-specific diagnostic divergence is spec-compatible, but it is not a
135135
global message equivalence because the runtime text also covers a distinct
136136
successful-prior-write condition.
137137

138+
Variant lifting reports the rejected discriminant and case-count range exactly.
139+
Link-error assertions accept native link failures and host-resource import type
140+
mismatches; native start traps, translation failures, and generic plan errors
141+
remain distinct.
142+
138143
At the current pin, the remaining async subset is 13 classified failures and
139144
zero skips: those eleven diagnostic rows, the unavailable native
140145
`set-max-table-capacity` provider row, and its one no-current-instance cascade.

‎harness/src/runner.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,14 @@ class FileRunner {
246246
try {
247247
await this.executor.instantiate(artifact, "link-error");
248248
} catch (e) {
249-
if (e instanceof LinkError) return undefined;
249+
if (e instanceof LinkError) {
250+
if (!trapMatches(command.text, e.message)) {
251+
throw new Error(
252+
`expected link error "${command.text}", got "${e.message}"`,
253+
);
254+
}
255+
return undefined;
256+
}
250257
throw e;
251258
}
252259
throw new Error(
@@ -331,6 +338,26 @@ const TRAP_MESSAGE_EQUIVALENTS: Array<
331338
// Pinned Wasmtime resources.wast:459-481 passes literal slot 2 to the
332339
// outer component's empty table; Table.get rejects it as out of range.
333340
["unknown handle index 2", ["table index out of range"]],
341+
// Pinned Wasmtime resources.wast:927 and definitions.py lift_own
342+
// (third_party/component-model/design/mvp/canonical-abi/definitions.py:1482).
343+
["cannot remove owned resource while borrowed", ["handle still lent out"]],
344+
// Pinned Wasmtime strings.wast:21,23 and definitions.py
345+
// load_string_from_range (definitions.py:1395).
346+
["string pointer not aligned to 2", ["misaligned string pointer"]],
347+
// Pinned Wasmtime resources.wast:167,174. The executor emits these only
348+
// from its dedicated host-resource import type verdict.
349+
[
350+
"was not found",
351+
[
352+
"host import 'host/missing' must be a HostResourceType (the component imports a resource type); got undefined",
353+
],
354+
],
355+
[
356+
"expected resource found func",
357+
[
358+
"host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
359+
],
360+
],
334361
// Official resources/handle-table.wast:322,324 and the corresponding
335362
// resource-type checks in runtime/src/cabi/handles.ts:239-263.
336363
[

‎harness/src/runtime-executor.ts‎

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Translator } from "@polyengine/runtime/shim";
1313
import {
1414
type ComponentHandle,
1515
type HostImports,
16+
HostResourceImportTypeError,
1617
instantiateComponent,
1718
} from "../../runtime/src/exec/mod.ts";
1819
import {
@@ -63,6 +64,30 @@ function maybeCapability(e: unknown, what: string): void {
6364
}
6465
}
6566

67+
/** Internal instantiation-verdict mapper, exported only for harness tests. */
68+
export function rethrowInstantiationError(
69+
e: unknown,
70+
expect: InstantiateExpectation,
71+
): never {
72+
if (e instanceof Trap) {
73+
if (expect === "trap") throw new TrapError(e.message);
74+
asCapabilityOrRethrow(e, "instantiate");
75+
}
76+
if (e instanceof WebAssembly.RuntimeError && expect === "trap") {
77+
throw new TrapError(e.message);
78+
}
79+
if (
80+
expect === "link-error" &&
81+
(e instanceof HostResourceImportTypeError ||
82+
e instanceof WebAssembly.LinkError)
83+
) {
84+
throw new LinkError(e instanceof Error ? e.message : String(e));
85+
}
86+
if (e instanceof PlanError) asCapabilityOrRethrow(e, "instantiate");
87+
maybeCapability(e, "instantiate");
88+
throw e;
89+
}
90+
6691
interface ComponentInstanceRef extends InstanceRef {
6792
readonly kind: "component";
6893
handle: ComponentHandle;
@@ -181,16 +206,7 @@ export class RuntimeExecutor implements CommandExecutor {
181206
trapOnIdle: true,
182207
});
183208
} catch (e) {
184-
if (e instanceof Trap) {
185-
if (expect === "trap") throw new TrapError(e.message);
186-
asCapabilityOrRethrow(e, "instantiate");
187-
}
188-
if (e instanceof PlanError) asCapabilityOrRethrow(e, "instantiate");
189-
// Capability-gated trampolines (UnsupportedFeatureError et al.) match
190-
// via CAPABILITY_MARKERS regardless of error class:
191-
maybeCapability(e, "instantiate");
192-
if (expect === "link-error") throw new LinkError(String(e));
193-
throw e;
209+
rethrowInstantiationError(e, expect);
194210
}
195211
if (expect !== "success") {
196212
throw new Error(

‎harness/src/wasmtime-expectations.ts‎

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -97,33 +97,6 @@ export const WASMTIME_EXPECTATION_GROUPS: readonly WasmtimeExpectationGroup[] =
9797
status: "failed",
9898
}],
9999
},
100-
{
101-
file: "resources.json",
102-
rows: [{
103-
lines: [927],
104-
cause:
105-
'Error: expected trap "cannot remove owned resource while borrowed", got "handle still lent out"',
106-
status: "failed",
107-
}],
108-
},
109-
{
110-
file: "strings.json",
111-
rows: [{
112-
lines: [21, 23],
113-
cause:
114-
'Error: expected trap "string pointer not aligned to 2", got "misaligned string pointer"',
115-
status: "failed",
116-
}],
117-
},
118-
{
119-
file: "types.json",
120-
rows: [{
121-
lines: [378],
122-
cause:
123-
'Error: expected trap "discriminant 2 out of range [0..2)", got "invalid variant discriminant"',
124-
status: "failed",
125-
}],
126-
},
127100
],
128101
},
129102
{
@@ -212,14 +185,6 @@ export const WASMTIME_EXPECTATION_GROUPS: readonly WasmtimeExpectationGroup[] =
212185
status: "failed",
213186
}],
214187
},
215-
{
216-
file: "instance.json",
217-
rows: [{
218-
lines: [79],
219-
cause: "RuntimeError: unreachable",
220-
status: "failed",
221-
}],
222-
},
223188
{
224189
file: "linking.json",
225190
rows: [{
@@ -271,16 +236,6 @@ export const WASMTIME_EXPECTATION_GROUPS: readonly WasmtimeExpectationGroup[] =
271236
{
272237
file: "resources.json",
273238
rows: [{
274-
lines: [167],
275-
cause:
276-
"PlanError: host import 'host/missing' must be a HostResourceType (the component imports a resource type); got undefined",
277-
status: "failed",
278-
}, {
279-
lines: [174],
280-
cause:
281-
"PlanError: host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
282-
status: "failed",
283-
}, {
284239
lines: [201],
285240
cause:
286241
"Error: expected instantiation link-error, but component instantiated successfully",

‎harness/tests/runner_unit_test.ts‎

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import type { WastJson } from "../src/schema.ts";
99
import { CoreOnlyExecutor } from "../src/executor.ts";
1010
import type { CommandExecutor } from "../src/executor.ts";
11-
import { TrapError } from "../src/executor.ts";
11+
import { LinkError, TrapError } from "../src/executor.ts";
1212
import { runWastJson, trapMatches } from "../src/runner.ts";
1313

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

207+
Deno.test("assert_unlinkable requires a matching link diagnostic", async () => {
208+
for (
209+
const [message, status] of [
210+
["expected resource found func", "passed"],
211+
["different cause", "failed"],
212+
[
213+
"prefix host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
214+
"failed",
215+
],
216+
] as const
217+
) {
218+
const executor = new CoreOnlyExecutor() as CommandExecutor;
219+
executor.instantiate = () => Promise.reject(new LinkError(message));
220+
const result = await runWastJson(
221+
doc([{
222+
type: "assert_unlinkable",
223+
line: 1,
224+
filename: "comp.0.wasm",
225+
module_type: "binary",
226+
text: "expected resource found func",
227+
}]),
228+
load,
229+
executor,
230+
);
231+
assertEq(result.results[0].status, status, message);
232+
}
233+
});
234+
207235
// Exact diagnostic equivalents: the core `unreachable` trap row. The runtime
208236
// (runtime/src/exec/boundary.ts mapCoreException) passes each JS engine's raw
209237
// trap text through untouched; this table is where the suite's
@@ -373,12 +401,44 @@ Deno.test("trapMatches: verified diagnostic equivalents match only their named o
373401
"wasm `unreachable` instruction executed",
374402
"guest trapped: unreachable",
375403
],
404+
[
405+
"cannot remove owned resource while borrowed",
406+
"handle still lent out",
407+
],
408+
["string pointer not aligned to 2", "misaligned string pointer"],
409+
[
410+
"was not found",
411+
"host import 'host/missing' must be a HostResourceType (the component imports a resource type); got undefined",
412+
],
413+
[
414+
"expected resource found func",
415+
"host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
416+
],
376417
];
377418
for (const [expected, actual] of equivalents) {
378419
assertEq(trapMatches(expected, actual), true, `${expected} / ${actual}`);
379420
}
380421
});
381422

423+
Deno.test("generic variant diagnostic is not globally equivalent", () => {
424+
assertEq(
425+
trapMatches(
426+
"discriminant 2 out of range [0..2)",
427+
"invalid variant discriminant",
428+
),
429+
false,
430+
"global matcher",
431+
);
432+
assertEq(
433+
trapMatches(
434+
"discriminant 2 out of range [0..2)",
435+
"discriminant 3 out of range [0..3)",
436+
),
437+
false,
438+
"different discriminant and case count",
439+
);
440+
});
441+
382442
Deno.test("trapMatches: narrow diagnostic rows reject adjacent but different traps", () => {
383443
const nonEquivalents: Array<[string, string]> = [
384444
["integer overflow", "integer underflow"],
@@ -407,6 +467,11 @@ Deno.test("trapMatches: narrow diagnostic rows reject adjacent but different tra
407467
"uncaught exception propagated out of component",
408468
"guest trapped: unreachable",
409469
],
470+
["string pointer not aligned to 2", "misaligned list pointer"],
471+
[
472+
"was not found",
473+
"prefix host import 'host/missing' must be a HostResourceType (the component imports a resource type); got undefined",
474+
],
410475
];
411476
for (const [expected, actual] of nonEquivalents) {
412477
assertEq(trapMatches(expected, actual), false, `${expected} / ${actual}`);
@@ -440,6 +505,11 @@ Deno.test("trapMatches: exact equivalents reject expected and actual affixes", (
440505
"invalid `task.return` signature and/or options for current task",
441506
"task.return with canonical options differing from the task's",
442507
],
508+
["string pointer not aligned to 2", "misaligned string pointer"],
509+
[
510+
"expected resource found func",
511+
"host import 'host/return-three' must be a HostResourceType (the component imports a resource type); got a function",
512+
],
443513
]
444514
) {
445515
for (

‎harness/tests/runtime_validation_test.ts‎

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { PlanError, TranslateError } from "@polyengine/runtime/plan";
2+
import { HostResourceImportTypeError } from "../../runtime/src/exec/mod.ts";
23
import { Translator } from "@polyengine/runtime/shim";
4+
import { LinkError, TrapError } from "../src/executor.ts";
35
import type { Artifact } from "../src/executor.ts";
46
import { runWastJson } from "../src/runner.ts";
5-
import { RuntimeExecutor } from "../src/runtime-executor.ts";
7+
import {
8+
rethrowInstantiationError,
9+
RuntimeExecutor,
10+
} from "../src/runtime-executor.ts";
611

712
const shim = await Deno.readFile(
813
new URL(
@@ -150,3 +155,46 @@ Deno.test("runtime validation: pipeline failures propagate unchanged and fail ne
150155
Translator.prototype.translate = original;
151156
}
152157
});
158+
159+
Deno.test("runtime instantiation verdicts preserve failure phase", async () => {
160+
const nativeTrap = new WebAssembly.RuntimeError("unreachable");
161+
const mappedTrap = await thrown(() =>
162+
rethrowInstantiationError(nativeTrap, "trap")
163+
);
164+
if (
165+
!(mappedTrap instanceof TrapError) || mappedTrap.message !== "unreachable"
166+
) {
167+
throw new Error(`native trap was not preserved: ${mappedTrap}`);
168+
}
169+
if (
170+
await thrown(() => rethrowInstantiationError(nativeTrap, "link-error")) !==
171+
nativeTrap
172+
) {
173+
throw new Error("native trap satisfied a link-error expectation");
174+
}
175+
176+
const resourceMismatch = new HostResourceImportTypeError("wrong resource");
177+
const mappedLink = await thrown(() =>
178+
rethrowInstantiationError(resourceMismatch, "link-error")
179+
);
180+
if (
181+
!(mappedLink instanceof LinkError) ||
182+
mappedLink.message !== "wrong resource"
183+
) {
184+
throw new Error(`resource mismatch was not a link error: ${mappedLink}`);
185+
}
186+
187+
for (
188+
const error of [
189+
new PlanError("generic plan failure"),
190+
new Error("generic executor failure"),
191+
]
192+
) {
193+
if (
194+
await thrown(() => rethrowInstantiationError(error, "link-error")) !==
195+
error
196+
) {
197+
throw new Error(`${error} satisfied a link-error expectation`);
198+
}
199+
}
200+
});

0 commit comments

Comments
 (0)