Skip to content

Commit c6fa8ec

Browse files
hyperpolymathclaude
andcommitted
docs(adr): promote Async-on-wasm CPS transform to ADR-013 (#225)
Owner-approved 2026-05-18. SETTLED-DECISIONS.adoc + META.a2ml ADR-013 entry; design doc status PROPOSED -> ACCEPTED. typed-wasm convergence ABI section + Ephapax co-stakeholder review tracked separately. Refs #225 #160. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 15384bb commit c6fa8ec

3 files changed

Lines changed: 109 additions & 6 deletions

File tree

.machine_readable/6a2/META.a2ml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,3 +802,73 @@ references = [
802802
"justfile (build / build-loud recipes)",
803803
"lib/parser.mly (expr_assign return/resume; record #{ )",
804804
]
805+
806+
[[adr]]
807+
id = "ADR-013"
808+
status = "accepted"
809+
date = "2026-05-18"
810+
title = "Async on the WasmGC backend: transparent CPS continuation transform"
811+
context = """
812+
stdlib/Http.affine exposes one portable surface
813+
`fetch(req) -> Response / { Net, Async }`. The Deno-ESM backend lowers
814+
it to a direct `await` (#226, shipped). The WasmGC backend cannot:
815+
extern calls are synchronous, the boundary is i32-only, and the
816+
estate's async-over-wasm mechanism (#205) is callback-shaped
817+
(`-> Thenable` + thenableThen/thenableResultJson). Issue #225 Option 2
818+
(owner-chosen) requires one BYTE-IDENTICAL source surface on both
819+
targets, so the wasm backend must hide the continuation plumbing.
820+
typed-wasm is the shared convergence ABI (ADR-004); Ephapax is a
821+
co-stakeholder for the async ABI.
822+
"""
823+
decision = """
824+
On the WasmGC backend ONLY, functions whose effect row includes `Async`
825+
are compiled via a selective continuation-passing (CPS) transform.
826+
Pure / non-Async functions are untouched (no codegen or perf change).
827+
828+
- Each async boundary (call to an extern returning under /Async, or to
829+
another Async function) splits the body; code after it becomes a
830+
generated continuation function whose env is the live-local capture
831+
set, marshalled via the EXISTING #199 closure ABI
832+
([fnId@0,envPtr@4] through __indirect_function_table).
833+
- The split lowers to thenableThen(handle, <continuation-closure>) (the
834+
EXISTING #205 host->guest re-entry); the enclosing Async function
835+
itself returns a Thenable handle, so Thenable composes transparently
836+
up the call chain. At the host boundary the outermost Thenable is
837+
awaited. The programmer never sees Thenable — effect row is the
838+
abstraction, backend chooses the mechanism (as on the Deno side).
839+
- New orchestration over three proven primitives (find_free_vars, #199
840+
closure ABI, #205 re-entry); NOT a new runtime, NOT JSPI (rejected),
841+
NOT a general delimited-continuation feature.
842+
843+
Correctness obligations (binding, pre-merge):
844+
1. A linear/own local captured into a continuation is the borrow
845+
checker's single use; double-resumption impossible (Thenable settles
846+
once; thenableThen fires once — asserted).
847+
2. Transform triggers iff Async in fd_eff; Net/other effects ride
848+
along; fd_is_async reused as predicate.
849+
3. Response reconstruction uses a minimal typed reader (jsonField is
850+
insufficient for headers: [(String,String)]); general decode
851+
deferred to #161. No silent lossiness.
852+
4. Full `dune test --force` (258) green at every commit; wasm e2e
853+
parity test mirrors tests/codegen-deno/http_fetch.*.
854+
"""
855+
consequences = """
856+
- Single portable Http surface across Deno-ESM and WasmGC; #160 closes
857+
only when both targets are green (joint-close with #225).
858+
- The Thenable-handle + thenableThen continuation protocol is the
859+
agreed async ABI for the typed-wasm convergence layer; Ephapax
860+
co-stakeholder review is required for the convergence-spec section.
861+
- Delivered incrementally (4 PRs, each behind the 258 gate); scope is
862+
WasmGC Async functions only.
863+
- This decision is settled; do not reopen without amending this ADR.
864+
"""
865+
references = [
866+
"https://github.com/hyperpolymath/affinescript/issues/225",
867+
"https://github.com/hyperpolymath/affinescript/issues/160",
868+
"https://github.com/hyperpolymath/affinescript/pull/226",
869+
"docs/specs/async-on-wasm-cps.adoc (full design)",
870+
"docs/specs/SETTLED-DECISIONS.adoc (ADR-013 section)",
871+
"lib/codegen.ml (find_free_vars; #199 closure ABI)",
872+
"stdlib/Vscode.affine + packages/affine-vscode/mod.js (#205 thenableThen)",
873+
"typed-wasm ADR-004 (convergence / aggregate library; Ephapax)",
874+
]

docs/specs/SETTLED-DECISIONS.adoc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,35 @@ build is byte-for-byte unchanged and fully transparent on demand.
211211

212212
Settles the disposition of issue #215 residual families. Full ADR in
213213
`.machine_readable/6a2/META.a2ml` (ADR-012).
214+
215+
== Async on WasmGC: Transparent CPS Continuation Transform (ADR-013)
216+
217+
`stdlib/Http.affine` exposes one portable surface,
218+
`fetch(req) -> Response / { Net, Async }`. The Deno-ESM backend lowers
219+
it to a direct `await` (#226). The WasmGC backend cannot — extern calls
220+
are synchronous, the boundary is i32-only, and the estate's
221+
async-over-wasm mechanism (#205) is callback-shaped. Issue #225
222+
(Option 2, owner-chosen) requires *one byte-identical source surface on
223+
both targets*: the wasm backend hides the continuation plumbing.
224+
225+
Decision: on the WasmGC backend only, functions whose effect row
226+
includes `Async` are compiled via a *selective CPS transform*. Each
227+
async boundary splits the body; the post-split code becomes a generated
228+
continuation captured via the existing #199 closure ABI and registered
229+
via the existing #205 `thenableThen` host→guest re-entry. The enclosing
230+
`Async` function returns a `Thenable` handle, so it composes
231+
transparently up the call chain. Pure / non-`Async` code is untouched;
232+
this is *not* JSPI and *not* a general continuation feature — new
233+
orchestration over three proven primitives only.
234+
235+
Binding correctness obligations: affine/linear capture is the borrow
236+
checker's single use with double-resumption impossible; the transform
237+
triggers iff `Async ∈ fd_eff`; `Response` reconstruction is a typed
238+
reader with no silent lossiness (general decode deferred to #161); the
239+
258-case gate is green at every commit with a wasm e2e parity test.
240+
241+
The `Thenable`-handle + `thenableThen` continuation protocol is the
242+
agreed async ABI for the typed-wasm convergence layer; Ephapax is a
243+
co-stakeholder (typed-wasm ADR-004). Delivered as 4 incremental,
244+
gated PRs. Full design in `docs/specs/async-on-wasm-cps.adoc`; full ADR
245+
in `.machine_readable/6a2/META.a2ml` (ADR-013).

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
44
:toc:
55
:icons: font
6-
:status: PROPOSED — awaiting owner approval (issue #225)
6+
:status: ACCEPTED 2026-05-18 — ADR-013 (issue #225)
77

88
== Status
99

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).
10+
ACCEPTED (owner-approved 2026-05-18). Recorded as ADR-013
11+
(`SETTLED-DECISIONS.adoc` + `.machine_readable/6a2/META.a2ml`). The
12+
async ABI section is mirrored into the typed-wasm convergence layer
13+
with an Ephapax co-stakeholder review flag (typed-wasm ADR-004 —
14+
tracked separately). Implementation proceeds as the 4-PR plan below,
15+
each behind the 258-case gate.
1516

1617
== Problem
1718

0 commit comments

Comments
 (0)