Skip to content

Suspendable host imports: suspending()-marked sync lowers park the frame (A1) - #51

Merged
lann merged 1 commit into
mainfrom
implement-jspi
Aug 10, 2026
Merged

Suspendable host imports: suspending()-marked sync lowers park the frame (A1)#51
lann merged 1 commit into
mainfrom
implement-jspi

Conversation

@lann

@lann lann commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Closes the last unlit JSPI suspension site: createLoweredImport threw NeedsJspi (message still tagged 'M2 phase 3') on a Promise from a sync-typed host import in every mode, while contracts/embedder-api.md v0.1 documented the cast as permitted. Surfaced by the polymorph-iroh upstream-iroh review: their route puts genuinely-blocking p2 guests (tokio parked in wasi:io/poll) on the consumer roadmap, and that class needs exactly this park.

Contract (embedder-api.md → v0.2, amendment A1; intrinsics.md status note): suspending(fn) is a per-declaration marker — auto-detection evidence, transitive-suspendability contamination (entries promising-wrapped per pin (c)), Suspending wrap. Never inferred: pin (j) makes every marked call pay a continuation hop and pin (c) makes marked imports illegal from start functions, so the costs are opt-in per function. Unmarked imports refuse exactly as before (message now names the marker); sync-only components keep the zero-cost pin.

Mechanics: the park is definitions.py canon_lower line 2286 (thread.wait_until(subtask.resolved)) via blockCurrentActivation — non-cancellable, gate held (#43 hold rule), registered in pendingHostCalls for the deadlock probe. Settle handler records only; result lowering (realloc re-entry!) and deliverResolve run in produce under the resume-time ambient claim (issue-#24 discipline). Wrap decision rides executor state keyed by LoweredIndex — a function-identity brand relay dies at createTrampoline's trap-recording wrapper (measured: Promise coerced to 0 through the unwrapped import).

Pins: tests/embedder/suspending_imports_test.ts — park round trip, resume-time string lowering through guest realloc, WitError rejection over a park → err, unbranded rejection → failure, pin-(c) start trap, marked-sync fast path, both refusal modes, marker mechanics. Plain-mode guard stays in async_lower_test.ts.

Gates: full core matrix green locally; conformance 1254/0 with 95 xfails, 0 unexpected, 0 stale; sched-seeds green; smoke-tls green; websocket-conformance 55/55 (consumer tree verified clean before/after); iroh-exam 4/5 — scenario 3 is the documented pre-existing WebRTC-backend environmental on this machine (findings tracker, IROH-1 disposition); smoke-c0 not runnable here (no experiment-mosh checkout).

Follow-up track (separate PR): the wasi-shims blocking profile — parking-pollable kernel over this mechanism.

…frame (A1)

The M2 jspi flip lit the CM-async builtins' suspension sites but never
the host-lower site: createLoweredImport threw NeedsJspi ('M2 phase 3')
on a Promise from a sync-typed import in EVERY mode, lower-import
trampolines were excluded from the Suspending wrap, and
contracts/embedder-api.md v0.1 nevertheless documented the cast as
permitted. No consumer or suite command ever drove the path (callback-ABI
consumers use async-typed imports; the suite blocks through builtins), so
the residue survived until the polymorph-iroh upstream-iroh review put
genuinely-blocking p2 guests (tokio parked in wasi:io/poll) on the
roadmap.

Design (embedder-api.md v0.2, amendment A1):

- suspending(fn) — a per-declaration marker on the imports record, the
  static counterpart of v0.1's 'visible, deliberate cast'. Marking is
  never inferred: per jspi pin (j) every call through a Suspending
  import pays a continuation hop even on plain-value returns, and per
  pin (c) a marked import reached from a start function traps — costs an
  embedder must opt into per function, not per component. Unmarked
  imports keep the plain convention; sync-only components keep the
  zero-cost pin.
- A marked import is auto-detection evidence (chooseMode), a genuine
  blocker for transitive suspendability (importer's entries get
  promising-wrapped, satisfying pin (c)), and Suspending-wrapped at
  importValue. The wrap decision rides executor state (LoweredIndex
  set) rather than a function brand: createTrampoline's trap-recording
  wrapper replaces the CoreFn identity, which silently killed the
  branded-relay design (measured: the returned Promise coerced to 0
  through the unwrapped import).
- The park is definitions.py canon_lower line 2286
  (thread.wait_until(subtask.resolved)): blockCurrentActivation with
  cancellable=false, gate HELD across the park (the #43 hold rule),
  registered in pendingHostCalls so the deadlock probe counts it as
  externally wakeable. The settle handler only records the outcome; all
  CABI work (onResolve's result lowering — which can re-enter the guest
  through realloc — and deliverResolve) is deferred to produce, running
  under the suspension point's ambient claim per the issue-#24
  attribution discipline. Rejections rethrow from produce and ride the
  builtins' post-resume-trap rail (empirical fact (e)).
- The embedder facade relays the brand across its two wrapper layers
  (dispatch closure, lazy leaf wrapper); #wrapImportFn's existing
  thenable chaining gives suspending imports full result adaptation and
  the WitError-rejection -> result::err conversion unchanged.

Unmarked Promise-returning sync lowers still refuse cleanly in every
mode, now naming the marker; the plain-mode guard pin in
async_lower_test.ts keeps the NeedsJspi shape.

Pinned by tests/embedder/suspending_imports_test.ts: park round trip
(post-resume continuation reaches later imports), resume-time string
lowering through guest realloc, marked-but-sync fast path, unmarked and
jspi:false refusals, unbranded rejection surfacing, WitError rejection
over a park -> err case, pin-(c) start-function trap, marker mechanics.

Gates: full core matrix green locally (build, test-rust, shells,
test-runtime 336/0, test-wasi-shims, test-ct-runner, test-bundle,
conformance 1254/0/95 xfail with 0 unexpected + 0 stale, sched-seeds,
test-ports, test-webrtc); smoke-tls green; websocket-conformance 55/55;
iroh-exam 4/5 (scenario-3 WebRTC backend failure is the documented
pre-existing environmental on this machine, upstream-consumer-findings.md
IROH-1 disposition); smoke-c0 not runnable here (missing experiment-mosh
consumer checkout).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant