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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ up via JSPI where the engine provides it.

Pre-1.0, but densely gated:

- **Official Component Model test suite**: 1281 passing / 0 failing commands
- **Official Component Model test suite**: 1284 passing / 0 failing commands
across all directories (remaining: named xfail classes — deferred threads
([#12](https://github.com/polymorph-components/polyengine/issues/12)),
cm705-sync-sched ([#249](https://github.com/polymorph-components/polyengine/issues/249)),
Expand Down
11 changes: 9 additions & 2 deletions contracts/intrinsics.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,15 @@ components.
entry *decision*: an async-lowered call reports STARTING only if the
callee is still unstarted after the callee instance's runnable work is
drained to quiescence (`Store.hasRunnableWork`, consumed by
`createAsyncStartCall`'s determinacy park). `async-start-call` is
`Suspending`-wrapped for the determinacy park; plain mode provably never
`createAsyncStartCall`'s determinacy park). `async-start-call` (and
`subtask.cancel`, for its own post-`on_cancel` determinacy park) is
`Suspending`-wrapped for the park, and **wrapped implies marked**: pin (c)
traps a Suspending import reached from a non-promising activation even
when it returns a plain value, so a guest importing one gets
promising-wrapped entries — as a separate evidence tier that neither
selects jspi mode nor promotes a FACT adapter's pass-through exports
(which would promising-wrap eagerly-completing lift callees). Plain mode
provably never
needs the drain (without JSPI a frame cannot park mid-invocation, so a
held gate always belongs to the currently-running activation — the one
obstacle a drain cannot remove), so the plain path stays zero-cost for
Expand Down
4 changes: 2 additions & 2 deletions harness/browser/expectations/chromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ export const chromium: LaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1281,
passed: 1284,
failed: 0,
xfail: 130,
xfail: 127,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/browser/expectations/firefox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export const firefox: LaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1281,
passed: 1284,
failed: 0,
xfail: 130,
xfail: 127,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/bun-pinned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export const bunPinned: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1281,
passed: 1284,
failed: 0,
xfail: 130,
xfail: 127,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/jsc-pinned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export const jscPinned: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1281,
passed: 1284,
failed: 0,
xfail: 130,
xfail: 127,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/jsc-trunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ export const jscTrunk: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1281,
passed: 1284,
failed: 0,
xfail: 130,
xfail: 127,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/node-pinned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ export const nodePinned: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1281,
passed: 1284,
failed: 0,
xfail: 130,
xfail: 127,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/sm-nightly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export const smNightly: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1281,
passed: 1284,
failed: 0,
xfail: 130,
xfail: 127,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/sm-pinned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ export const smPinned: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1281,
passed: 1284,
failed: 0,
xfail: 130,
xfail: 127,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
40 changes: 2 additions & 38 deletions harness/src/xfail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,35 +658,8 @@ export const XFAIL: XfailEntry[] = [
reason: "same cascade as line 214, see that entry",
},
// --- async/futures-must-write.json: root cause: STREAMS ---
// --- async/reentrance.json: BRAND NEW file (test/async/reentrance.wast is
// 100% new content added by CM#705's "remove the may_enter flag/trap",
// polyengine#173). The `fact-reentrance-47` static-stub class (#248,
// retired — see the note at the top of this file) previously MASKED the
// three rows below: with the stub gone, each now surfaces a genuine
// scheduler/reentrance semantic gap of its own, not a regression from
// this bump. Classed `cm705-reentrance`,
// https://github.com/polymorph-components/polyengine/issues/279.
// Lines 42/60/99/100/151/198/237/346 (the pure fact-reentrance-47 rows,
// with no residue underneath) all PASS now and are pruned. ---
{
file: "async/reentrance.json",
line: 429,
reason:
"expected trap \"deadlock detected: event loop cannot make further " +
"progress\", got SuspendError: trying to suspend JS frames — the " +
"deadlock is never diagnosed because the wait reaches a Suspending " +
"import through a JS callback frame; cm705-reentrance, " +
"https://github.com/polymorph-components/polyengine/issues/279",
},
{
file: "async/reentrance.json",
line: 517,
reason:
"expected return, got trap \"cannot drop a subtask which has not " +
"yet resolved\" — subtask.cancel on parked callback tasks while the " +
"callee instance is on the stack does not settle the cancellation " +
"before subtask.drop; cm705-reentrance, https://github.com/polymorph-components/polyengine/issues/279",
},
// --- async/reentrance.json: the remaining entries are the deferred
// thread-built-in cascade (#12), NOT reentrance. ---
{
file: "async/reentrance.json",
line: 522,
Expand All @@ -710,15 +683,6 @@ export const XFAIL: XfailEntry[] = [
"cascade of line 657 (module pending-capability, deferred thread " +
"built-ins, https://github.com/polymorph-components/polyengine/issues/12): no current instance (not reentrance-related)",
},
{
file: "async/reentrance.json",
line: 837,
reason:
"expected trap \"waitable cannot be used synchronously while added " +
"to a waitable set\", got \"guest trapped: unreachable\" — the " +
"reentrant `back` does not raise the sync-use-in-waitable-set trap; " +
"cm705-reentrance, https://github.com/polymorph-components/polyengine/issues/279",
},
// --- async/self-switch-traps.json: NEW file added by the CM#687
// thread.*-then-promote built-ins (third_party/component-model advance
// 2f13265 -> 7c67611, this dispatch). Its Tester component needs a host
Expand Down
105 changes: 81 additions & 24 deletions runtime/src/exec/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,33 +331,52 @@ class Executor {

/**
* Core functions exported by a core instance that imports at least one
* genuinely-blocking trampoline (`trampolineNeedsSuspension`, per
* DECLARATION — the async form of a copy/cancel built-in never blocks and
* does not mark) or a function from an already-marked instance. FACT
* consults this to decide whether a callee needs its own `promising`
* entry; wrapping one that cannot block forces asynchrony the ABI forbids
* (an eagerly-completing callee must report RETURNED, not STARTED).
* `Suspending`-wrapped trampoline (`trampolineCanBlock`, per DECLARATION —
* the async form of a copy built-in never blocks, is not wrapped and does
* not mark) or a function from an already-marked instance. FACT consults
* this to decide whether a callee needs its own `promising` entry.
*
* Instance granularity is still an over-approximation — a module exporting
* both a blocking and a non-blocking function marks both — but with two
* mitigations it no longer produces wrong answers on the official corpus:
* WRAPPED IMPLIES MARKED, and must: jspi pin (c) traps a Suspending import
* called from a non-promising activation unconditionally, plain-value path
* included (see `importValue`).
*
* * per-declaration classification keeps async-form-only importers (and
* the FACT `[adapter-callee]*` pass-through wrappers reached through
* them) out of the set entirely;
* * a needlessly-wrapped callee no longer changes observable state:
* `async-start-call` parks the caller until the callee is determinate
* (fact_calls.ts), reconstructing the reference's synchronous
* run-to-first-block across the engine's microtask hops (jspi pin (j)).
* Instance granularity is still an over-approximation — a module exporting
* both a blocking and a non-blocking function marks both — and so is the
* marking of `async-start-call`/`subtask-cancel` importers, whose wrap
* exists for a park that often does not happen. Neither produces wrong
* answers on the official corpus, because a needlessly-wrapped callee no
* longer changes observable state: `async-start-call` parks the caller
* until the callee is determinate (fact_calls.ts), reconstructing the
* reference's synchronous run-to-first-block across the engine's microtask
* hops (jspi pin (j)) — so an eagerly-completing callee still reports
* RETURNED rather than STARTED.
*
* Per-FUNCTION reachability (a call-graph pass in the translator, where
* wasmparser already is) would still shrink the set — as a wrapping-cost
* optimization now, not a correctness need.
*/
readonly suspendableFuncs = new WeakSet<object>();

/**
* Exports of instances with GENUINE blocking evidence — the tier that
* propagates in full through `importValue`. A superset relationship holds
* by construction: these are also in `suspendableFuncs`.
*/
private readonly blockingFuncs = new WeakSet<object>();
/**
* Exports of instances whose only evidence is a `Suspending`-WRAPPED but
* not-necessarily-blocking import (`async-start-call`, `subtask-cancel`).
* Enough to require a `promising` entry (jspi pin (c) — see `importValue`),
* not enough to claim the function blocks; propagates as its own tier so a
* FACT adapter carrying it does not promote the lift callees reached
* through it (see `importValue`).
*/
private readonly wrapFuncs = new WeakSet<object>();

/** Scratch: set by `importValue` while one module's imports are resolved. */
private sawBlockingImport = false;
/** Scratch: the wrap-only tier of the same question. */
private sawWrapImport = false;
/** LoweredIndex-es whose host functions carry the `suspending()` brand —
* populated by `buildLoweredImport`, read by `importValue`. */
private readonly suspendableLowerings = new Set<number>();
Expand Down Expand Up @@ -582,6 +601,7 @@ class Executor {
// suspension point; every function it exports is therefore
// potentially-blocking, and everything else is not.
this.sawBlockingImport = false;
this.sawWrapImport = false;
// Which component instance this core module belongs to — the plan
// states it here and nowhere else (`instance: null` = FACT adapter,
// contracts/plan-format.md). `unsafeIntrinsic` reads it while the
Expand Down Expand Up @@ -662,11 +682,24 @@ class Executor {
}
throw e;
}
if (this.sawBlockingImport) {
if (this.sawBlockingImport || this.sawWrapImport) {
// Wrap-only evidence gives a GUEST module its own `promising`
// entry (jspi pin (c)) but leaves a FACT adapter
// (`instance: null`) out of `suspendableFuncs`: the adapter's
// pass-through export is what `*-start-call` receives as a LIFT
// CALLEE, and promising-wrapping an eagerly-completing callee is
// the STARTED-vs-RETURNED divergence `trampolineCanBlock`
// (jspi/bridge.ts) warns about — measured as
// test/async/drop-subtask.wast:140 under POLYENGINE_SCHED_SEED=1.
// Either way the evidence PROPAGATES, in its own tier, so the
// guest importing that adapter export still gets marked.
const promising = this.sawBlockingImport || init.instance !== null;
for (const exported of Object.values(instance.exports)) {
if (typeof exported === "function") {
this.suspendableFuncs.add(exported as unknown as object);
}
if (typeof exported !== "function") continue;
const fn = exported as unknown as object;
(this.sawBlockingImport ? this.blockingFuncs : this.wrapFuncs)
.add(fn);
if (promising) this.suspendableFuncs.add(fn);
}
}
this.instances.push(instance);
Expand Down Expand Up @@ -995,7 +1028,13 @@ class Executor {
// broke the handshake pins.
if (
typeof value === "function" &&
this.suspendableFuncs.has(value as unknown as object)
this.wrapFuncs.has(value as unknown as object)
) {
this.sawWrapImport = true;
}
if (
typeof value === "function" &&
this.blockingFuncs.has(value as unknown as object)
) {
this.sawBlockingImport = true;
}
Expand Down Expand Up @@ -1037,11 +1076,29 @@ class Executor {
) as unknown as Importable;
}
if (!trampolineCanBlock(d, optionsAsync)) return value;
// `async-start-call` is wrapped (its jspi-only determinacy park must be
// able to suspend the caller) but does NOT mark the importer: see
// `trampolineCanBlock` in jspi/bridge.ts for why marking on it is wrong.
// WRAPPED IMPLIES MARKED. Anything handed to wasm as a
// `WebAssembly.Suspending` makes its importer's frames suspendable, so
// that importer's entries must be `promising`-wrapped — jspi pin (c) is
// unconditional: a Suspending import called from a non-promising
// activation traps EVEN WHEN it produces its value synchronously
// (measured: "trying to suspend without WebAssembly.promising"; with an
// outer promising entry and JS frames in between, "trying to suspend JS
// frames"). `async-start-call` and `subtask-cancel` used to be wrapped
// WITHOUT marking, on the grounds that a needlessly-promising callee
// reported STARTED where the reference reports RETURNED. That reason has
// expired: `async-start-call`'s determinacy park (intrinsics/fact_calls.ts)
// reconstructs the reference's run-to-first-block across the hop, which is
// what the note above `suspendableFuncs` already records as the mitigation.
// What the omission cost was a hard trap in the one shape where the
// importer had no other blocking import: test/async/reentrance.wast:429,
// whose `$MC` imports only `b`'s async-start-call, `waitable-set.new` and
// `waitable.join` — it trapped with SuspendError instead of reaching the
// deadlock verdict. The two tiers (`blockingFuncs`/`wrapFuncs`) keep the
// marking from over-reaching — see the post-instantiate marking.
if (trampolineNeedsSuspension(d, optionsAsync)) {
this.sawBlockingImport = true;
} else {
this.sawWrapImport = true;
}
this.noteImport();
return suspendingImport(
Expand Down
Loading
Loading