Skip to content

Commit 909756f

Browse files
committed
docs(adr): ADR-016 — effect-threaded async-boundary detection (#234 S1)
#234: the CPS transform's async-boundary detector is a hardcoded set `async_primitives=["http_request_thenable"]` — brittle and blind to user-defined `Async` fns. The generalisation ("boundary = any call whose effect row ⊇ Async") touches the typecheck→codegen interface (shared infra) → one-way-door fork, escalated (AskUserQuestion 2026-05-19). Owner chose the **side-table** mechanism over AST annotation / codegen-local re-derivation. This is slice S1 — the decision record only, no code (the discipline for a one-way shared-infra change, mirroring ADR-015): - ADR-016 in docs/specs/SETTLED-DECISIONS.adoc + the META.a2ml `[[adr]]` block (format-matched to ADR-012/014/015). Side-table keyed by a deterministic shared call-site numbering (`lib/effect_sites.ml`, called identically by typecheck & codegen so keys can't drift — no AST shape change, robust to future rebuilds); structural recogniser retained as the sound table-miss fallback. Staged S1..S4 (S2 build+return table unused/gate-neutral; S3 pipeline+codegen switch + user-Async e2e; S4 retire hardcoded set). - TECH-DEBT CORE-02 truthed (#225 line CLOSED; #234 ADR-016 accepted). Gate: `dune test --force` 278/278 (no code; zero regression). Refs #234. Not Closes — staged campaign; owner closes per ISSUE-CLOSURE.
1 parent cba836f commit 909756f

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

.machine_readable/6a2/META.a2ml

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,3 +1016,82 @@ references = [
10161016
"hyperpolymath/typed-wasm (multi-producer ownership-section ABI)",
10171017
"WASI 0.2 / WebAssembly Component Model specification",
10181018
]
1019+
1020+
[[adr]]
1021+
id = "ADR-016"
1022+
status = "accepted"
1023+
date = "2026-05-19"
1024+
title = "Effect-threaded async-boundary detection (side-table; #234)"
1025+
context = """
1026+
The WasmGC CPS transform (ADR-013) detects the async boundary
1027+
*structurally*: a `let` whose RHS calls a primitive in a hardcoded set
1028+
`async_primitives = ["http_request_thenable"]` (lib/codegen.ml). This
1029+
was the deliberately-contained #225 PR3a scope. It is brittle: every
1030+
new async stdlib primitive must be added to the set, and a
1031+
user-defined `Async`-effecting function is invisible to it — blocking
1032+
the fully transparent ADR-013 surface for user async code. #234 asks
1033+
for the principled generalisation: the boundary is any call whose
1034+
effect row ⊇ `Async`. This requires per-call effect information at
1035+
codegen time. The AST (`lib/ast.ml`) carries no location or node id on
1036+
`ExprApp`, and the owner-chosen mechanism (AskUserQuestion
1037+
2026-05-19) is a typecheck→codegen side-table, NOT AST annotation.
1038+
"""
1039+
decision = """
1040+
Thread per-call-site effect rows from typecheck to codegen via a
1041+
*side-table*, keyed by a deterministic shared call-site numbering.
1042+
1043+
- *Keying.* A single shared traversal (new `lib/effect_sites.ml`)
1044+
assigns every `ExprApp` an ordinal in a fixed pre-order walk of the
1045+
program. Both typecheck and codegen obtain ordinals by calling the
1046+
*same* function, so keys cannot drift. No AST shape change (the
1047+
pipeline already threads one `prog` value through resolve →
1048+
typecheck → borrow → quantity → codegen unmodified, but ordinals —
1049+
not physical identity — are the contract, which is robust to any
1050+
future node rebuild).
1051+
- *Producer.* `Typecheck.check_program` populates
1052+
`(int, effect_row) Hashtbl.t` (call-site ordinal → resolved effect
1053+
row, declared *and* inferred) and returns it in its result context.
1054+
- *Pipeline.* `bin/main.ml` threads the table into the codegen entry
1055+
alongside `prog` (one extra argument; the existing source-to-source
1056+
backends ignore it).
1057+
- *Consumer.* The CPS detector's boundary predicate becomes “the
1058+
`let`-RHS call's effect row ⊇ `Async`” via a table lookup, replacing
1059+
`is_async_prim_call`/`async_primitives`.
1060+
- *Fallback / safety.* If the table has no entry for a site (e.g. a
1061+
pre-typecheck embedder path, or a synthesised node), codegen falls
1062+
back to the structural recogniser. The hardcoded set is retired only
1063+
once the table path is proven (final slice); over-conservative
1064+
fallback is always sound (= today's behaviour).
1065+
1066+
Staged (ledger #234; each a gated PR, full `dune test --force` +
1067+
wasm e2e):
1068+
- S1 (this): ADR-016 + plan. No code change.
1069+
- S2: `lib/effect_sites.ml` shared numbering + typecheck builds &
1070+
returns the side-table. NO codegen behaviour change (table built,
1071+
unused) — pure, gate-neutral.
1072+
- S3: pipeline threads the table; codegen boundary predicate switches
1073+
to the table with structural fallback; new e2e proving a
1074+
*user-defined* `Async` fn triggers the transform (the payoff). All
1075+
existing http_cps_* / http_response_reader stay green.
1076+
- S4: retire the hardcoded `async_primitives` set (fallback remains
1077+
for table-miss only); doc truthing.
1078+
"""
1079+
consequences = """
1080+
- Generalises to user-defined `Async` functions; new async primitives
1081+
need no codegen edit (closes the brittleness #234 names).
1082+
- One-way-ish: introduces a typecheck→codegen data channel (shared
1083+
infra). Reversible per-slice (S2/S3 are additive; the structural
1084+
recogniser stays as the sound fallback throughout).
1085+
- Source-to-source backends unaffected (they ignore the table arg).
1086+
- This decision is settled; do not reopen without amending this ADR.
1087+
Ledger #234 / CORE-02 in docs/TECH-DEBT.adoc.
1088+
"""
1089+
references = [
1090+
"https://github.com/hyperpolymath/affinescript/issues/234",
1091+
"https://github.com/hyperpolymath/affinescript/issues/225",
1092+
"docs/specs/async-on-wasm-cps.adoc (ADR-013)",
1093+
"lib/codegen.ml (async_primitives / detect_async_base_case)",
1094+
"lib/typecheck.ml (check_program)",
1095+
"lib/effect_sites.ml (new; shared call-site numbering)",
1096+
"docs/specs/SETTLED-DECISIONS.adoc (ADR-016 section)",
1097+
]

docs/TECH-DEBT.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ analysis. |S1 |pt1 #240 + pt2 return-escape DONE (Refs #177); pt2 residual
102102
parser-gated — issue #177
103103
|CORE-02 |Effect-handler dispatch on WasmGC (currently `UnsupportedFeature`;
104104
EH proposal or CPS). The #225 CPS line closes the async slice. |S2 |partial
105-
(PR3a/b/c merged; #234 generalises the recogniser)
105+
(#225 line CLOSED PR1..PR3d+PR4; #234 generalises the recogniser —
106+
ADR-016 ACCEPTED, side-table typecheck→codegen, staged S1..S4; S1 done)
106107
|CORE-03 |ADR-014: module-qualified type/effect path. Decision settled
107108
(both `.`/`::` accepted, `Pkg::Type` canonical, `.`→`::` for free via the
108109
`::`-fold). Was the estate's dominant parse blocker (525/1177 .affine).

docs/specs/SETTLED-DECISIONS.adoc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,37 @@ default wasm target to component and demote preview1 to a legacy target.
308308
This decision is settled; do not reopen without amending the ADR. Full
309309
ADR in `.machine_readable/6a2/META.a2ml` (ADR-015); ledger INT-03 in
310310
`docs/TECH-DEBT.adoc`; roadmap in `docs/ECOSYSTEM.adoc`.
311+
312+
== Effect-Threaded Async-Boundary Detection (ADR-016)
313+
314+
The WasmGC CPS transform (ADR-013) detects the async boundary
315+
*structurally* — a `let` whose RHS calls a primitive in a hardcoded set
316+
`async_primitives = ["http_request_thenable"]`. That was the contained
317+
#225 PR3a scope; it is brittle (every new async primitive needs a
318+
codegen edit) and blind to user-defined `Async` functions, blocking the
319+
fully transparent ADR-013 surface for user async code. #234 generalises
320+
it: the boundary is *any call whose effect row ⊇ `Async`*.
321+
322+
The AST carries no location/node id on `ExprApp`, and the escalated
323+
one-way-door fork (AskUserQuestion 2026-05-19) was decided in favour of
324+
a *typecheck→codegen side-table*, not AST annotation.
325+
326+
Decision: thread per-call effect rows via a side-table keyed by a
327+
deterministic shared call-site numbering (a single traversal in a new
328+
`lib/effect_sites.ml`, called identically by typecheck and codegen, so
329+
keys cannot drift; no AST shape change). `Typecheck.check_program`
330+
populates `ordinal → effect_row` (declared and inferred) and returns
331+
it; `bin/main.ml` threads it into codegen (source-to-source backends
332+
ignore it); the CPS boundary predicate becomes a table lookup with the
333+
existing structural recogniser as the sound table-miss fallback.
334+
335+
Staged (ledger #234): *S1* this ADR + plan (no code); *S2*
336+
`effect_sites.ml` + typecheck builds/returns the table (built, unused —
337+
gate-neutral); *S3* pipeline threads it, codegen switches to the table
338+
with structural fallback, new e2e proving a user-defined `Async` fn
339+
triggers the transform; *S4* retire the hardcoded set (fallback kept
340+
for table-miss only).
341+
342+
This decision is settled; do not reopen without amending the ADR. Full
343+
ADR in `.machine_readable/6a2/META.a2ml` (ADR-016); ledger #234 /
344+
CORE-02 in `docs/TECH-DEBT.adoc`.

0 commit comments

Comments
 (0)