Skip to content

runtime: discard host settlements before facade conversion - #336

Merged
lannbot merged 1 commit into
mainfrom
fix/host-import-settlement
Sep 11, 2026
Merged

runtime: discard host settlements before facade conversion#336
lannbot merged 1 commit into
mainfrom
fix/host-import-settlement

Conversation

@lannbot

@lannbot lannbot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #328
Fixes #329

Move host-import facade result conversion behind the boundary's delivery decision. Observe refused Promise settlements while preserving call-site NeedsJspi; discard cancelled/poisoned settlements before resource transfer or producer creation. End facade borrow scopes when cancellation/refusal releases their canonical lenders, retaining settlement cleanup as an idempotent backstop.

Semantic basis

Component Model pin: 7c676115e93cd7d54c1732d95c54c6a3de7c5ae0.

  • CanonicalABI.md, Tasks/FuncInst calling convention: the callee's cancellation callback may resolve with None; the callback is requested at most once while unresolved.
  • definitions.py: Store.invoke wraps the callee's cancellation callback. canon_lower.on_resolve(None) resolves cancellation without lower_flat_values; the value branch alone lowers results. Subtask.resolve rejects a second resolution. canon_subtask_cancel requests cancellation and delivers the terminal event; Subtask.deliver_resolve releases lenders. lower_own, lower_stream, and lower_future add recipient handles, so calling them for discarded delivery is observably different from skipping them.
  • Concurrency.md §Cancellation: cancelled-before-returned means arguments were passed but no values returned, with borrows released.
  • contracts/embedder-api.md §Functions and async / Cancellation and discard and docs/architecture.md §6 choose prompt cancellation on the JS host's behalf. This repair enforces that existing policy before facade ownership conversion, including ComponentException payloads. deferCancel and abortable retain their existing policy; future<T>-typed import returns remain future sources rather than async call completion.
  • contracts/intrinsics.md requires lender unwind even for non-poisoning capability failures. Per-instance poisoning and JS Promise/JSPI capability refusal are embedding policies, not additional spec cancellation states.

Wasmtime comparison

Inspected the repository's Wasmtime pin 4675ee16b703b33948073a5ff6b961367371e7a1:

  • func/host.rs::call_async_lower validates scope exit, then calls lower_raw only for Some(result).
  • concurrent.rs::Instance::first_poll passes None when abort wins and publishes ReturnCancelled. When completion wins, Some(result) lowers normally; this is not a claim of identical JS/native race policy. subtask_cancel requests abort through the host-task join handle; JoinHandle::run supplies the None outcome.
  • Resource Lower implementations reach guest_resource_lower_own through result lowering. Thus the cancelled outcome skips recipient result-handle insertion rather than inserting and disposing. This says nothing about host registrations or producer work performed independently before return. Successful settlement follows Some(result) and lowers normally.

Executed comparison: built the pinned tree with scratch CARGO_TARGET_DIR, then ran cargo test -p component-async-tests --test test_all -- cancel (10 passed) and cargo test -p component-async-tests --test test_all -- borrowing resource (5 passed). These exercise native cancellation/terminal-state and resource/borrowing behavior. The no-registration-on-cancel conclusion is source-backed, not a bespoke native resource-table assertion.

The comparison rests on the explicit cancel-to-None-to-skipped-lowering path, not Rust Future droppability alone. Wasmtime's dropped host future and this runtime's continuing JS operation differ; the existing JS contract chooses to discard delivery. NeedsJspi, Promise adoption, and per-instance poisoning have no exact Wasmtime analogue. Wasmtime is corroboration; the pinned spec and facade contract govern this repair.

Verification

Real-Wasm facade regressions demonstrated the base failures directly: both sync-refusal modes left unhandled rejection or registered an undelivered resource; cancelled/poisoned fulfillment and ComponentException payloads registered resources; discarded stream/nested-future results started both producers. Counters check absence of conversion, not conversion followed by disposal.

Guest-wrapper non-invalidation is source-derived assurance: discard bypasses the carrier's conversion, hence fromHost and guest takeRep. Existing resource tests separately cover takeRep invalidation. A real-Wasm host import returning a locally defined guest resource cannot be expressed through the supported facade: the pinned spec rejects imports depending on exported resource types (Explainer.md external dependency rules; test/validation/external-visibility.wast), and imported-component substitution is outside the facade's supported imports. No host-bound wrapper stand-in is claimed as equivalent coverage.

  • Focused runtime/embedder tests: 100 passed, none ignored, covering immediate/late rejected refusal, own-valued results and err payloads, async and FACT-sync poisoning, producer non-startup, rejection-time argument teardown, cancellation scope cleanup, raw settlement ordering, and existing future/borrow/mark behavior.
  • just fmt-check: passed.
  • just gates < /dev/null: passed end to end after installing the worktree's missing Playwright browsers. Includes all six Chromium/Firefox conformance realms and six OPFS smokes. WAT regeneration matches both committed Wasm fixtures byte-for-byte. Independent production, test/fixture, and authority-reasoning reviews passed.
  • Runtime: 900 passed, 3 existing exclusions; seeded runs: 897 passed, 6 exclusions each. WASI/ct-runner each retain one external-artifact skip. Conformance: 1285 passed commands, 0 unexpected failures, 126 expected failures, 95 pending-runtime and 5 unsupported directives. No expectation changes. Standalone JSC has no arm64 channel; Bun remains findings-only.
  • Consumer statuses unchanged. TLS executed 6/6/5 passing cases with 1/1/2 not-applicable cases across three suites. All four C0 legs passed; Go and websocket legs are translation checks, not execution. No consumer legs skipped.

Auto-merge is armed with a merge commit under repository defaults.

@lannbot
lannbot enabled auto-merge September 11, 2026 03:23
@lannbot
lannbot merged commit 3ba668e into main Sep 11, 2026
3 checks passed
@lannbot
lannbot deleted the fix/host-import-settlement branch September 11, 2026 03:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants