Skip to content

Commit 15384bb

Browse files
hyperpolymathclaude
andcommitted
docs(design): Async-on-wasm transparent CPS transform — proposal (#225)
Design-before-implementation for the typed-wasm Http target (issue #225, owner-chosen Option 2: one byte-identical fetch->Response surface on both targets). Selective CPS/continuation transform of Async-effect functions on the WasmGC backend only, built on the existing #199 closure ABI + #205 thenableThen re-entry. States the correctness obligations (affine capture / once-resumption / value reconstruction) and a 4-PR incremental plan, each behind the 258 gate. PROPOSED — awaiting owner approval; promotes to ADR + typed-wasm convergence ABI section (Ephapax co-stakeholder) on sign-off. Refs #225 #160. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7018b92 commit 15384bb

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

docs/specs/async-on-wasm-cps.adoc

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
= Async on the WasmGC backend: transparent CPS continuation transform
3+
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4+
:toc:
5+
:icons: font
6+
:status: PROPOSED — awaiting owner approval (issue #225)
7+
8+
== Status
9+
10+
PROPOSED. Design-before-implementation for a high-blast-radius,
11+
cross-stakeholder change (issue #225). On approval this is promoted to
12+
an ADR in `SETTLED-DECISIONS.adoc` + `.machine_readable/6a2/META.a2ml`,
13+
and becomes a section of the typed-wasm convergence ABI (Ephapax is a
14+
co-stakeholder — typed-wasm ADR-004).
15+
16+
== Problem
17+
18+
`stdlib/Http.affine` exposes one portable surface:
19+
20+
[source]
21+
----
22+
pub fn fetch(req: Request) -> Response / { Net, Async }
23+
----
24+
25+
The Deno-ESM backend lowers this to a direct `await` (shipped, #226).
26+
The WasmGC backend cannot: an `extern` call is synchronous and the
27+
wasm boundary is i32-only. The estate's established async-over-wasm
28+
mechanism (#205) is *callback-based* — an async extern returns a
29+
`Thenable` **handle**; the guest registers a continuation closure via
30+
`thenableThen` and the host re-enters the guest after settlement,
31+
whereupon `thenableResultJson` yields the value.
32+
33+
That mechanism is correct but its *surface* is callback-shaped
34+
(`-> Thenable` + explicit `thenableThen`/`thenableResultJson`). The
35+
owner's decision (issue #225, Option 2) is to keep **one
36+
byte-identical source surface** — `fetch -> Response` on both targets —
37+
and make the wasm backend do the continuation plumbing invisibly.
38+
39+
== Decision
40+
41+
On the WasmGC backend only, functions whose effect row includes
42+
`Async` are compiled via a **selective continuation-passing (CPS)
43+
transform**. Pure / non-`Async` functions are untouched (no codegen or
44+
performance change for them).
45+
46+
=== Mechanism
47+
48+
For an `Async` function body, each *async boundary* — a call to an
49+
`extern` returning under `/ Async`, or a call to another `Async`
50+
function — splits the body:
51+
52+
. Code up to and including the async call is emitted normally; the call
53+
yields a `Thenable` handle (the existing #205 host convention).
54+
. Code *after* the async call becomes a generated **continuation
55+
function**. Its environment is the set of live locals at the split
56+
point, captured exactly via the existing #199 closure ABI
57+
(`find_free_vars` + an `[fnId@0, envPtr@4]` heap record reachable
58+
through `__indirect_function_table`).
59+
. The split lowers to `thenableThen(handle, <continuation-closure>)`;
60+
the enclosing function itself returns a `Thenable` handle
61+
representing *its own* eventual completion.
62+
. The continuation, when re-entered by the host, reconstructs the
63+
resolved value (for `http_request`: a `Response` from the settled
64+
payload) and resumes.
65+
66+
Because every `Async` caller is transformed identically, `Thenable`
67+
handles compose up the call chain transparently; at the host boundary
68+
the outermost `Thenable` is what the host awaits. The AffineScript
69+
programmer never sees any of this — the effect row is the abstraction,
70+
the backend chooses the mechanism, exactly as on the Deno side.
71+
72+
=== Why this is sound to build on what exists
73+
74+
* `find_free_vars` (codegen.ml) already computes capture sets.
75+
* The #199 closure ABI already marshals `[fnId, envPtr]` and dispatches
76+
through `__indirect_function_table`.
77+
* The #205 `thenableThen` primitive already performs host→guest
78+
re-entry after a settled `Promise`.
79+
80+
The transform is new orchestration over proven primitives, not a new
81+
runtime mechanism.
82+
83+
== Correctness obligations (must hold before any merge)
84+
85+
. *Affine/linear capture.* A linear (`@linear` / `own`) local captured
86+
into a continuation env is used exactly once *by the continuation*.
87+
The borrow checker must treat continuation capture as that single
88+
use; double-resumption must be impossible (a `Thenable` settles once;
89+
`thenableThen` fires its callback once — assert this).
90+
. *Effect-row fidelity.* The transform triggers iff `Async ∈ fd_eff`.
91+
`Net`/other effects ride along unchanged; `fd_is_async` already
92+
exists (Deno side) and is reused as the trigger predicate.
93+
. *Value reconstruction.* `Response` reconstruction from the settled
94+
payload needs structured decode. `jsonField` (one-level scalar) is
95+
insufficient for `headers: [(String,String)]`; this slice introduces
96+
a minimal typed reader for the fixed `Response` shape and defers
97+
general decode to #161 (Json). No silent lossiness.
98+
. *Gate.* Full `dune test --force` (currently 258) green at every
99+
commit; a wasm e2e parity test mirrors
100+
`tests/codegen-deno/http_fetch.*`.
101+
102+
== Blast radius / non-goals
103+
104+
* Scope: WasmGC backend `Async` functions only. Deno-ESM unchanged.
105+
Non-`Async` code unchanged.
106+
* Not JSPI (explicitly rejected, issue #225). No browser/runtime
107+
suspension dependency.
108+
* Not a general delimited-continuation feature — only the `Async`
109+
effect, only enough to make `Thenable` transparent.
110+
111+
== Delivery plan (incremental, each behind the 258 gate)
112+
113+
. *This doc → ADR* on approval (`SETTLED-DECISIONS.adoc` +
114+
`META.a2ml`); mirror the ABI section into the typed-wasm convergence
115+
spec; flag Ephapax co-stakeholder review.
116+
. *PR 1 — transform skeleton:* single async boundary, no live-local
117+
capture (straight-line `Async` fn ending in one async call). Wasm
118+
e2e: `Http.get` round-trip via stubbed host.
119+
. *PR 2 — live-local capture* via the #199 env ABI; multi-statement
120+
continuations; affine-capture borrow rule + once-resumption assert.
121+
. *PR 3 — composition:* `Async` fn calling another `Async` fn
122+
(chained `Thenable`); `Response` typed reader; full
123+
`http_fetch`-parity wasm test.
124+
. *PR 4 — joint-close:* both targets green ⇒ close #160 and #225.
125+
126+
== References
127+
128+
* Issue #225 (this work), #160 (Http primitive), #226 (Deno-ESM, shipped)
129+
* #199 (function-value closure ABI), #205 (Thenable resolution)
130+
* typed-wasm ADR-004 (convergence / aggregate library; Ephapax)
131+
* `docs/guides/migration-playbook.adoc` `#portable-http`

0 commit comments

Comments
 (0)