ACCEPTED (owner-approved 2026-05-18). Recorded as ADR-013
(SETTLED-DECISIONS.adoc + .machine_readable/6a2/META.a2ml). The
async ABI section is mirrored into the typed-wasm convergence layer
with an Ephapax co-stakeholder review flag (typed-wasm ADR-004 —
tracked separately). Implementation proceeds as the 4-PR plan below,
each behind the 258-case gate.
stdlib/Http.affine exposes one portable surface:
pub fn fetch(req: Request) -> Response / { Net, Async }The Deno-ESM backend lowers this to a direct await (shipped, #226).
The WasmGC backend cannot: an extern call is synchronous and the
wasm boundary is i32-only. The estate’s established async-over-wasm
mechanism (#205) is callback-based — an async extern returns a
Thenable handle; the guest registers a continuation closure via
thenableThen and the host re-enters the guest after settlement,
whereupon thenableResultJson yields the value.
That mechanism is correct but its surface is callback-shaped
(→ Thenable + explicit thenableThen/thenableResultJson). The
owner’s decision (issue #225, Option 2) is to keep one
byte-identical source surface — fetch → Response on both targets —
and make the wasm backend do the continuation plumbing invisibly.
On the WasmGC backend only, functions whose effect row includes
Async are compiled via a selective continuation-passing (CPS)
transform. Pure / non-Async functions are untouched (no codegen or
performance change for them).
For an Async function body, each async boundary — a call to an
extern returning under / Async, or a call to another Async
function — splits the body:
-
Code up to and including the async call is emitted normally; the call yields a
Thenablehandle (the existing #205 host convention). -
Code after the async call becomes a generated continuation function. Its environment is the set of live locals at the split point, captured exactly via the existing #199 closure ABI (
find_free_vars+ an[fnId@0, envPtr@4]heap record reachable through__indirect_function_table). -
The split lowers to
thenableThen(handle, <continuation-closure>); the enclosing function itself returns aThenablehandle representing its own eventual completion. -
The continuation, when re-entered by the host, reconstructs the resolved value (for
http_request: aResponsefrom the settled payload) and resumes.
Because every Async caller is transformed identically, Thenable
handles compose up the call chain transparently; at the host boundary
the outermost Thenable is what the host awaits. The AffineScript
programmer never sees any of this — the effect row is the abstraction,
the backend chooses the mechanism, exactly as on the Deno side.
-
find_free_vars(codegen.ml) already computes capture sets. -
The #199 closure ABI already marshals
[fnId, envPtr]and dispatches through__indirect_function_table. -
The #205
thenableThenprimitive already performs host→guest re-entry after a settledPromise.
The transform is new orchestration over proven primitives, not a new runtime mechanism.
-
Affine/linear capture. A linear (
@linear/own) local captured into a continuation env is used exactly once by the continuation. The borrow checker must treat continuation capture as that single use; double-resumption must be impossible (aThenablesettles once;thenableThenfires its callback once — assert this). -
Effect-row fidelity. The transform triggers iff
Async ∈ fd_eff.Net/other effects ride along unchanged;fd_is_asyncalready exists (Deno side) and is reused as the trigger predicate. -
Value reconstruction.
Responsereconstruction from the settled payload needs structured decode.jsonField(one-level scalar) is insufficient forheaders: [(String,String)]; this slice introduces a minimal typed reader for the fixedResponseshape and defers general decode to #161 (Json). No silent lossiness. -
Gate. Full
dune test --force(currently 258) green at every commit; a wasm e2e parity test mirrorstests/codegen-deno/http_fetch.*.
-
Scope: WasmGC backend
Asyncfunctions only. Deno-ESM unchanged. Non-Asynccode unchanged. -
Not JSPI (explicitly rejected, issue #225). No browser/runtime suspension dependency.
-
Not a general delimited-continuation feature — only the
Asynceffect, only enough to makeThenabletransparent.
Confirmed against bin/main.ml + lib/codegen.ml: the WasmGC backend
does not flatten imports (only the source-to-source backends do). A
cross-module pub fn is emitted as a Wasm import named
<Module>.<fn> — the stdlib AffineScript bodies are not compiled into
the unit; the host supplies them. Verified: a unit using Http.get
imports {module:"Http", name:"get"}, exactly as httpPostJson is a
host import today.
Consequence — the work splits cleanly in two:
-
Host adapter (no compiler-correctness risk).
Http.get/fetch/post/request/is_okon wasm are host imports, implemented in JS exactly like the provenhttpPostJson/#205 surface. This is an adapter, mirroringpackages/affine-vscode/mod.js. It delivers the large majority of the typed-wasm Http target with zero risk to the compiler. -
CPS transform (the hard tail). Needed only for a user
Asyncfunction that interleaves an async call with subsequent local work in the same body (get(u).status,is_ok(get(u)), …). This is the binding-correctness piece and is gated behind the Ephapax ABI review (typed-wasm#31).
The transparent fetch/get → Response source surface (ADR-013’s
goal) is unchanged; it is delivered by the transform sitting on top
of the verified Thenable-surface primitives.
-
This doc → ADR — done (ADR-013).
-
PR 1 — host adapter + verified foundation: generic Http wasm adapter (
packages/affine-http/mod.js) +Thenable-returning lower primitives instdlib/Http.affine(mirroring the provenhttpPostJson/#205 shape) + wasm e2e round-trip with a stubbedfetch. No compiler change; this is the verified base the transform builds on. -
PR 2 — transform base case: WasmGC
Asyncuser fn with a single async boundary and no live-local capture (get(u).status); emits the continuation registration; once-resumption assert. -
PR 3 — live-local capture + composition: #199 env ABI capture (PR3a #236); affine-capture borrow rule (PR3b #237);
Async→Asyncchaining (PR3c #238);Responsetyped reader (PR3d #266); fullhttp_fetch-parity wasm test. DONE. -
PR 4 — joint-close: both targets green ⇒ close #160 and #225. DONE — Deno-ESM (#226) + the typed-wasm CPS line PR1..PR3d all merged; this PR joint-closes #160 + #225. The ADR-013 delivery plan is complete; the transparent
fetch/get → Responsesurface is delivered on both targets. (Two follow-ups, not part of this slice, remain tracked: effect-threaded boundary generalisation #234; estate #199/#205 re-validation #235.)