Skip to content

Commit 4116a1a

Browse files
committed
docs(adr): CORE-02 / #234 / ADR-016 — truth ledger to "DELIVERED"
The effect-threaded async-boundary work (issue #234, ADR-016) shipped end-to-end on 2026-05-19 via PRs #270/#275/#276/#277/#278, but the ledger entries and several in-source comments were still describing intermediate states ("S1 done", "Built, not yet consumed", "falls back to the structural recogniser"). Bring them into agreement with the realised end state: - TECH-DEBT.adoc: CORE-02 + CONV-02 marked CLOSED 2026-05-19 with the full S1..S4 / PR cross-reference and the steady-state miss path. - SETTLED-DECISIONS.adoc + META.a2ml ADR-016: staging block records per-slice PR numbers and DONE status; the "Fallback / safety" paragraph rewritten so the steady-state miss path is "no transform" (S4 retired the structural recogniser). - ECOSYSTEM.adoc + async-on-wasm-cps.adoc: #234 marked DELIVERED with PR list, removing the "follow-up, still tracked" framing. - lib/{codegen,effect_sites,typecheck}.ml: stale docstrings/comments refreshed — the producer/consumer pair is wired, the hardcoded async_primitives set is retired, the table-miss path is no-transform. - test/test_main.ml: rename "Effect-sites (#234 S2a)" suite label to "Effect-sites (#234, ADR-016)" now that the whole campaign is in. No behavioural change to the compiler — pure documentation truthing of work already merged. https://claude.ai/code/session_01HZ3i2wX5R5rbY8Ycmug4Ao
1 parent 8137236 commit 4116a1a

9 files changed

Lines changed: 102 additions & 68 deletions

File tree

.machine_readable/6a2/META.a2ml

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,23 +1058,34 @@ Thread per-call-site effect rows from typecheck to codegen via a
10581058
`let`-RHS call's effect row ⊇ `Async`” via a table lookup, replacing
10591059
`is_async_prim_call`/`async_primitives`.
10601060
- *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).
1061+
pre-typecheck embedder path, or a synthesised node), or the consumer
1062+
detects a producer/consumer count-mismatch, [Effect_sites.is_async_call]
1063+
returns false ⇒ the CPS transform simply does not fire for that call.
1064+
The pre-S4 plan retained the structural recogniser as the fallback
1065+
*until the table path was proven*; S4 (PR #278) retired the
1066+
hardcoded `async_primitives` set, so the steady-state fallback is
1067+
"no transform" — over-conservative, always sound.
10651068

10661069
Staged (ledger #234; each a gated PR, full `dune test --force` +
10671070
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.
1071+
- S1 (ADR-016 + plan, PR #270): DONE — no code change.
1072+
- S2a (`lib/effect_sites.ml` shared numbering, PR #275): DONE — pure,
1073+
gate-neutral.
1074+
- S2b (typecheck builds & returns the side-table, PR #276): DONE — no
1075+
codegen behaviour change yet.
1076+
- S3 (pipeline threads the table; codegen boundary predicate switches
1077+
to the table with structural fallback; new e2e
1078+
`tests/codegen/effect_async_boundary.affine` proving a *user-defined*
1079+
`Async` fn triggers the transform — PR #277): DONE. All existing
1080+
http_cps_* / http_response_reader stay green.
1081+
- S4 (retire the hardcoded `async_primitives` set; boundary is now
1082+
exactly `Effect_sites.is_async_call`; fallback remains for table-
1083+
empty / count-mismatch only — PR #278): DONE.
1084+
1085+
*Delivery status:* CLOSED 2026-05-19 end-to-end. Issue #234 closed
1086+
completed (`hyperpolymath/affinescript#234`). The structural name set
1087+
no longer exists; the boundary is single-sourced from the typecheck
1088+
effect side-table via the shared `Effect_sites` ordinal.
10781089
"""
10791090
consequences = """
10801091
- Generalises to user-defined `Async` functions; new async primitives

docs/ECOSYSTEM.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ made real; #228/ADR-014 module-qualified paths (estate port unblocker). |
8080
|*E* |typed-wasm convergence hardening (the transition runway): the
8181
AffineScript↔typed-wasm contract widened from L7+L10 toward full
8282
L1–6/L13–16 emitted-wasm enforcement; estate-wide re-validation (#235);
83-
effect-threaded async-boundary recogniser (#234); the #225/#160 convergence
83+
effect-threaded async-boundary recogniser (#234, DELIVERED 2026-05-19 —
84+
ADR-016, S1..S4 / PRs #270/#275/#276/#277/#278); the #225/#160 convergence
8485
ABI matured to "shared with Ephapax". |*planned* |Begins when D's substrate
8586
(INT-01..04, CORE-01) is closed; ends at a stable, multi-producer
8687
typed-wasm convergence ABI.

docs/TECH-DEBT.adoc

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,20 @@ escape via `outer = &x` are now *expressible* (surface unblocked); the
116116
remaining work is the dataflow analysis itself. |S1 |pt1 #240 + pt2
117117
return-escape + `&mut` surface DONE (Refs #177); residual = NLL analysis
118118
— issue #177
119-
|CORE-02 |Effect-handler dispatch on WasmGC (currently `UnsupportedFeature`;
120-
EH proposal or CPS). The #225 CPS line closes the async slice. |S2 |partial
121-
(#225 line CLOSED PR1..PR3d+PR4; #234 generalises the recogniser —
122-
ADR-016 ACCEPTED, side-table typecheck→codegen, staged S1..S4; S1 done)
119+
|CORE-02 |Effect-handler dispatch on WasmGC (was `UnsupportedFeature`;
120+
the chosen path is CPS over the EH proposal). The #225 CPS line closed
121+
the async slice; #234 generalised the boundary recogniser from a
122+
hardcoded name set to "any call whose effect row ⊇ `Async`". |S2
123+
|*CLOSED 2026-05-19* — #225 line CLOSED PR1..PR3d+PR4; #234 ADR-016
124+
delivered end-to-end (S1 #270 ADR; S2a #275 shared call-site
125+
numbering `lib/effect_sites.ml`; S2b #276 typecheck builds the
126+
ordinal→effect side-table; S3 #277 pipeline threads it + codegen
127+
boundary predicate switched + user-defined-`Async` e2e
128+
`tests/codegen/effect_async_boundary.affine`; S4 #278 hardcoded
129+
`async_primitives` set retired — boundary is now exactly
130+
`Effect_sites.is_async_call`). Issue #234 closed completed. Table-miss
131+
fallback (`Effect_sites.is_async_call` = false ⇒ no transform) remains
132+
as the sound table-empty / count-mismatch path.
123133
|CORE-03 |ADR-014: module-qualified type/effect path. Decision settled
124134
(both `.`/`::` accepted, `Pkg::Type` canonical, `.`→`::` for free via the
125135
`::`-fold). Was the estate's dominant parse blocker (525/1177 .affine).
@@ -254,7 +264,9 @@ Proven + locked (see INT-02)
254264
|CONV-01 |Estate-wide re-validation of the #199/#205 closure ABI (static →
255265
real wasm engine) |S2 |open #235
256266
|CONV-02 |Effect-threaded async-boundary detection (generalise the
257-
structural-conservative recogniser) |S2 |open #234
267+
structural-conservative recogniser) |S2 |*CLOSED 2026-05-19* — #234
268+
DELIVERED end-to-end (ADR-016 S1..S4; PRs #270/#275/#276/#277/#278).
269+
See CORE-02 above for the resolution narrative.
258270
|CONV-03 |#225/#160 convergence ABI matured to "shared with Ephapax" |S1
259271
|partial (PR3a/b/c merged)
260272
|CONV-04 |Widen emitted-wasm enforcement beyond L7+L10 toward L1–6/L13–16 |S2

docs/specs/SETTLED-DECISIONS.adoc

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -329,15 +329,23 @@ deterministic shared call-site numbering (a single traversal in a new
329329
keys cannot drift; no AST shape change). `Typecheck.check_program`
330330
populates `ordinal → effect_row` (declared and inferred) and returns
331331
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).
332+
ignore it); the CPS boundary predicate becomes a table lookup. The
333+
pre-S4 plan retained the existing structural recogniser as the
334+
table-miss fallback; S4 (#278) retired the hardcoded set, so the
335+
steady-state miss path is "no transform" — over-conservative, always
336+
sound.
337+
338+
Staged (ledger #234): *S1* this ADR + plan (no code, PR #270); *S2a*
339+
`effect_sites.ml` shared call-site numbering (PR #275); *S2b* typecheck
340+
builds/returns the table (PR #276); *S3* pipeline threads it, codegen
341+
switches to the table with structural fallback, new e2e
342+
`tests/codegen/effect_async_boundary.affine` proving a user-defined
343+
`Async` fn triggers the transform (PR #277); *S4* retire the hardcoded
344+
`async_primitives` name set, the boundary is now exactly
345+
`Effect_sites.is_async_call` — table-empty / count-mismatch fallback
346+
([is_async_call] = false ⇒ no transform) is the sound miss path (PR
347+
#278). *Status:* DELIVERED 2026-05-19 end-to-end; issue #234 closed
348+
completed.
341349

342350
This decision is settled; do not reopen without amending the ADR. Full
343351
ADR in `.machine_readable/6a2/META.a2ml` (ADR-016); ledger #234 /

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,12 @@ of* the verified Thenable-surface primitives.
157157
*DONE* — Deno-ESM (#226) + the typed-wasm CPS line PR1..PR3d all
158158
merged; this PR joint-closes #160 + #225. The ADR-013 delivery plan
159159
is complete; the transparent `fetch/get -> Response` surface is
160-
delivered on both targets. (Two follow-ups, *not* part of this
161-
slice, remain tracked: effect-threaded boundary generalisation
162-
#234; estate #199/#205 re-validation #235.)
160+
delivered on both targets. Follow-ups: the effect-threaded boundary
161+
generalisation (#234, ADR-016) has since landed (S1..S4, PRs
162+
#270/#275/#276/#277/#278, closed 2026-05-19 — the boundary is now
163+
`Effect_sites.is_async_call` from the typecheck side-table, the
164+
hardcoded `async_primitives` name set is retired); estate #199/#205
165+
re-validation #235 remains tracked.
163166

164167
== References
165168

lib/codegen.ml

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,20 +1974,15 @@ let simple_pat_name (p : pattern) : string option =
19741974
| PatVar id -> Some id.name
19751975
| _ -> None
19761976

1977-
(** Recognised async-primitive calls (ADR-013 #225 PR3a, owner-chosen
1978-
structural-conservative async-boundary identification — see
1979-
affinescript#234 for the effect-threaded generalisation). The async
1980-
boundary is a `let` whose RHS is a call to one of these names. Extend
1981-
this list as wasm-path async stdlib primitives are added. *)
1982-
(* ADR-016 / #234 S4: the hardcoded `async_primitives` name set is
1983-
RETIRED. The async boundary is exactly "a call whose effect row ⊇
1984-
Async", decided from the typecheck side-table keyed by the shared
1985-
[Effect_sites] ordinal ([Effect_sites.is_async_call]). The ADR's
1986-
"table-miss fallback" is the oracle being empty / count-mismatched
1987-
(⇒ [is_async_call] is always false ⇒ no transform = exact pre-#234
1988-
behaviour), NOT a name list — so no structural name disjunct
1989-
remains. `http_request_thenable` is still detected because
1990-
stdlib/Http.affine declares it `/ { Net, Async }`. *)
1977+
(** Async-boundary predicate (ADR-016 / #234, S4 #278). True iff this
1978+
call's resolved effect row ⊇ `Async`, looked up via the typecheck
1979+
side-table keyed by the shared [Effect_sites] ordinal. The legacy
1980+
hardcoded `async_primitives = ["http_request_thenable"]` name set
1981+
(ADR-013 #225 PR3a) is retired — `http_request_thenable` is still
1982+
detected because stdlib/Http.affine declares it `/ { Net, Async }`,
1983+
and any user-defined `/ { Async }` callee triggers the transform
1984+
identically. Table-empty / count-mismatch ⇒ [is_async_call] = false
1985+
⇒ no transform (the sound miss path). *)
19911986
let is_async_prim_call (e : expr) : bool =
19921987
Effect_sites.is_async_call e
19931988

@@ -2515,8 +2510,9 @@ let generate_module ?loader (prog : program) : wasm_module result =
25152510
(post-optimizer) program's nodes. Ordinals are stable across the
25162511
constant-folding optimizer (it never adds/removes/reorders calls),
25172512
so the producer's ordinal→has-Async map keys correctly here. A
2518-
count-mismatch makes [bind_consumer] a no-op ⇒ structural
2519-
fallback. Safe if the producer never ran (empty ⇒ no-op). *)
2513+
count-mismatch makes [bind_consumer] a no-op ⇒ [is_async_call] is
2514+
always false ⇒ no CPS transform (the sound S4 #278 miss path).
2515+
Safe if the producer never ran (empty ⇒ no-op). *)
25202516
Effect_sites.bind_consumer prog;
25212517
let loader = match loader with
25222518
| Some l -> l

lib/effect_sites.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,8 +183,8 @@ let iter (f : int -> expr -> unit) (prog : program) : unit =
183183
[bind_consumer prog] renumbers the (possibly post-opt) [prog] with the
184184
SAME traversal and materialises a physical-identity predicate over
185185
*that* prog's own nodes. Default empty ⇒ [is_async_call] is always
186-
false ⇒ codegen falls back to the structural recogniser (the exact
187-
pre-#234 behaviour; zero regression if the producer never ran or the
186+
false ⇒ codegen emits no CPS transform (the sound table-miss path
187+
under S4 #278; zero regression if the producer never ran or the
188188
counts disagree). *)
189189

190190
let async_by_ord : (int, bool) Hashtbl.t = Hashtbl.create 64

lib/typecheck.ml

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,11 @@ type context = {
193193
(** ADR-016 / #234 S2b: per-call-site effect rows, keyed by the
194194
shared [Effect_sites] ordinal. Populated after a successful check
195195
pass; the value is the callee's declared effect row (EPure when
196-
the callee is not a statically-named function). Built here but
197-
NOT yet consulted by codegen — S3 threads & switches the WasmGC
198-
CPS boundary predicate onto it (the structural recogniser remains
199-
the sound table-miss fallback). *)
196+
the callee is not a statically-named function). Built here and
197+
published to [Effect_sites] for the WasmGC CPS boundary predicate
198+
(S3, #277); the hardcoded name set is retired (S4, #278). A
199+
table-miss / count-mismatch still falls back to "no transform"
200+
([Effect_sites.is_async_call] = false), which is sound. *)
200201
}
201202

202203
type 'a result = ('a, type_error) Result.t
@@ -1778,17 +1779,18 @@ let check_decl (ctx : context) (decl : top_level) : (unit, type_error) Result.t
17781779
[Resolve.resolve_program_with_loader] so that [ExprApp (ExprVar f, ...)]
17791780
can resolve [f] to the imported function's scheme even though [f] does
17801781
not appear in [prog.prog_decls]. *)
1781-
(* ADR-016 / #234 S2b: build the per-call-site effect side-table.
1782-
Keyed by the shared [Effect_sites] ordinal so the (future) codegen
1783-
consumer agrees with this producer. A call's effect row is the
1784-
callee's *declared* row when the callee is a statically-named
1785-
function (`f(..)`, `m.f(..)`, through `ExprSpan`); otherwise EPure
1786-
(sound: S3's predicate is "row ⊇ Async ⇒ boundary", and an
1787-
over-conservative EPure just defers to the structural fallback —
1788-
exactly today's behaviour). Extern fns parse as [TopFn] with
1789-
[FnExtern]/[fd_eff] (parser.mly), so this covers the stdlib async
1790-
primitives and user `Async` fns uniformly. Pure traversal; built,
1791-
not yet consumed. *)
1782+
(* ADR-016 / #234: build the per-call-site effect side-table. Keyed by
1783+
the shared [Effect_sites] ordinal so the codegen consumer agrees with
1784+
this producer. A call's effect row is the callee's *declared* row
1785+
when the callee is a statically-named function (`f(..)`, `m.f(..)`,
1786+
through `ExprSpan`); otherwise EPure (sound: the codegen predicate is
1787+
"row ⊇ Async ⇒ boundary", and an over-conservative EPure just means
1788+
"no transform" — the same as today's S4 table-miss path). Extern fns
1789+
parse as [TopFn] with [FnExtern]/[fd_eff] (parser.mly), so this
1790+
covers the stdlib async primitives and user `Async` fns uniformly.
1791+
After population the ordinal→has-Async projection is published via
1792+
[Effect_sites.set_async_by_ord] for the codegen consumer (S3 #277,
1793+
S4 #278). *)
17921794
let populate_call_effects (ctx : context) (prog : Ast.program) : unit =
17931795
let fn_eff : (string, eff) Hashtbl.t = Hashtbl.create 64 in
17941796
List.iter
@@ -1911,8 +1913,9 @@ let check_program ?(import_types : (string, scheme) Hashtbl.t option)
19111913
errors first (they are more fundamental). *)
19121914
begin match Quantity.check_program_quantities prog with
19131915
| Ok () ->
1914-
(* ADR-016 / #234 S2b: build the per-call-site effect side-table
1915-
on the fully-checked program. Built, not yet consumed. *)
1916+
(* ADR-016 / #234: build the per-call-site effect side-table on
1917+
the fully-checked program, and publish it to [Effect_sites] for
1918+
the codegen async-boundary predicate (S3/S4 — consumed). *)
19161919
populate_call_effects ctx prog;
19171920
Ok ctx
19181921
| Error (qerr, span) ->

test/test_main.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ let () =
1111
("Golden", Test_golden.tests);
1212
("Examples", Test_golden.example_tests);
1313
("Effects (#59)", Test_effects.tests);
14-
("Effect-sites (#234 S2a)", Test_effect_sites.tests);
14+
("Effect-sites (#234, ADR-016)", Test_effect_sites.tests);
1515
("TW L13 isolation (#10)", Test_tw_isolation.tests);
1616
] @ Test_e2e.tests @ Test_stdlib_aot.tests)

0 commit comments

Comments
 (0)