Skip to content

Commit 4de794b

Browse files
Merge branch 'main' into claude/lucid-pascal-0xL5X
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
2 parents c17de6e + e8f00fd commit 4de794b

29 files changed

Lines changed: 1128 additions & 102 deletions

.github/workflows/ci.yml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -183,16 +183,22 @@ jobs:
183183
# ReScript source. Picks the existing res-to-affine test fixture
184184
# so any drift in the pinned commit's syntactic surface area
185185
# surfaces here rather than at walker-rule writing time.
186+
#
187+
# NOTE: tree-sitter-cli >= 0.25 repurposed `--paths` to mean "a
188+
# file listing input source paths", not "a directory containing
189+
# a grammar". Grammar lookup is now driven by the current
190+
# working directory (the CLI walks up looking for grammar.js /
191+
# src/parser.c), so we cd into the vendored grammar tree and
192+
# pass an absolute path to the fixture. Without this, the step
193+
# fails with `Failed to read paths file ... Is a directory`.
186194
run: |
187195
shopt -s nullglob
188196
fixtures=(tools/res-to-affine/test/fixtures/*.res)
189197
if [ ${#fixtures[@]} -eq 0 ]; then
190198
echo "no .res fixtures to smoke-parse; skipping"
191199
exit 0
192200
fi
193-
tree-sitter parse \
194-
--quiet \
195-
"${fixtures[0]}" \
196-
--paths tools/vendor/tree-sitter-rescript \
197-
> /dev/null
201+
fixture_abs="$(realpath "${fixtures[0]}")"
202+
( cd tools/vendor/tree-sitter-rescript \
203+
&& tree-sitter parse --quiet "${fixture_abs}" > /dev/null )
198204
echo "smoke-parsed: ${fixtures[0]}"

.machine_readable/6a2/META.a2ml

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,23 +1070,34 @@ Thread per-call-site effect rows from typecheck to codegen via a
10701070
`let`-RHS call's effect row ⊇ `Async`” via a table lookup, replacing
10711071
`is_async_prim_call`/`async_primitives`.
10721072
- *Fallback / safety.* If the table has no entry for a site (e.g. a
1073-
pre-typecheck embedder path, or a synthesised node), codegen falls
1074-
back to the structural recogniser. The hardcoded set is retired only
1075-
once the table path is proven (final slice); over-conservative
1076-
fallback is always sound (= today's behaviour).
1073+
pre-typecheck embedder path, or a synthesised node), or the consumer
1074+
detects a producer/consumer count-mismatch, [Effect_sites.is_async_call]
1075+
returns false ⇒ the CPS transform simply does not fire for that call.
1076+
The pre-S4 plan retained the structural recogniser as the fallback
1077+
*until the table path was proven*; S4 (PR #278) retired the
1078+
hardcoded `async_primitives` set, so the steady-state fallback is
1079+
"no transform" — over-conservative, always sound.
10771080

10781081
Staged (ledger #234; each a gated PR, full `dune test --force` +
10791082
wasm e2e):
1080-
- S1 (this): ADR-016 + plan. No code change.
1081-
- S2: `lib/effect_sites.ml` shared numbering + typecheck builds &
1082-
returns the side-table. NO codegen behaviour change (table built,
1083-
unused) — pure, gate-neutral.
1084-
- S3: pipeline threads the table; codegen boundary predicate switches
1085-
to the table with structural fallback; new e2e proving a
1086-
*user-defined* `Async` fn triggers the transform (the payoff). All
1087-
existing http_cps_* / http_response_reader stay green.
1088-
- S4: retire the hardcoded `async_primitives` set (fallback remains
1089-
for table-miss only); doc truthing.
1083+
- S1 (ADR-016 + plan, PR #270): DONE — no code change.
1084+
- S2a (`lib/effect_sites.ml` shared numbering, PR #275): DONE — pure,
1085+
gate-neutral.
1086+
- S2b (typecheck builds & returns the side-table, PR #276): DONE — no
1087+
codegen behaviour change yet.
1088+
- S3 (pipeline threads the table; codegen boundary predicate switches
1089+
to the table with structural fallback; new e2e
1090+
`tests/codegen/effect_async_boundary.affine` proving a *user-defined*
1091+
`Async` fn triggers the transform — PR #277): DONE. All existing
1092+
http_cps_* / http_response_reader stay green.
1093+
- S4 (retire the hardcoded `async_primitives` set; boundary is now
1094+
exactly `Effect_sites.is_async_call`; fallback remains for table-
1095+
empty / count-mismatch only — PR #278): DONE.
1096+
1097+
*Delivery status:* CLOSED 2026-05-19 end-to-end. Issue #234 closed
1098+
completed (`hyperpolymath/affinescript#234`). The structural name set
1099+
no longer exists; the boundary is single-sourced from the typecheck
1100+
effect side-table via the shared `Effect_sites` ordinal.
10901101
"""
10911102
consequences = """
10921103
- Generalises to user-defined `Async` functions; new async primitives

.machine_readable/6a2/STATE.a2ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ test-files = 54
6868
# the feature is not enforced on user programs through the CLI pipeline.
6969
affine-types = "wired-and-reachable (Track A Manhattan plan complete 2026-04-10. Quantity semiring in lib/quantity.ml; invoked from typecheck.ml:1206 inside the standard CLI pipeline. Surface syntax per ADR-007 hybrid: @linear/@erased/@unrestricted attributes (Option C primary) on let/stmt-let/param/lambda-param, AND :1/:0/:ω numeric sugar (Option B) on let/stmt-let. Scaled Let rule per ADR-002 implemented in lib/quantity.ml ExprLet/StmtLet — closes BUG-001 (ω-let smuggles linear values) and BUG-002 (zero-let erasure). Four regression fixtures in test/e2e/fixtures/ exercise both surface forms. Behavioural enforcement verified via E2E Quantity test suite — 4 new passing tests, 0 regressions.)"
7070
linear-arrows = "enforced (2026-04-11): Three-part fix landed. (1) typecheck.ml lambda synth: |@linear x: T| e now synthesises T -[1]-> U (was always QOmega). (2) typecheck.ml lambda check mode: explicit param quantity annotation validated against expected TArrow quantity; unannotated params inherit context quantity. (3) quantity.ml ExprLambda: added env.errors accumulator; annotated lambda params declared via env_declare so env_use tracks them; usage verified with check_quantity after body walk; violations pushed to env.errors and drained at end of check_function_quantities (step 4). Saved/restored env.quantities entries to prevent scope leakage. Two E2E fixtures + 2 passing tests. 75 tests total, 0 regressions. Commit d2f9b7b pushed."
71-
borrow-checker = "phase-3-part-1-landed (CORE-01, PR #240 Refs #177, 2026-05-19, gate 263/263): borrow-graph validation wired — BorrowOutlivesOwner emitted (&local escaping its block), shared-XOR-exclusive enforced at use sites (UseWhileExclusivelyBorrowed), ownership derived from param type TyOwn/TyRef/TyMut (owned/ref/mut discipline now enforced from real parsed source — closed a latent hole), call-arg borrows temporary, ref-binding graph tracked. Part 2+ deferred: NLL/region inference, flow-sensitive escape via `outer = &x`, tighter quantity integration. Authoritative: docs/CAPABILITY-MATRIX.adoc + docs/TECH-DEBT.adoc CORE-01."
71+
borrow-checker = "phase-3-part-3-slice-A-landed (CORE-01, Refs #177, 2026-05-24): pt1 (#240, gate 263/263) borrow-graph validation wired — BorrowOutlivesOwner emitted (&local escaping its block), shared-XOR-exclusive enforced at use sites (UseWhileExclusivelyBorrowed), ownership derived from param type TyOwn/TyRef/TyMut (owned/ref/mut discipline now enforced from real parsed source — closed a latent hole), call-arg borrows temporary, ref-binding graph tracked. pt2 (gate 271→274 and 278→281) return-escape (return-of-ref-rooted-at-callee-owned-binding caught) + &mut e parser surface (zero Menhir conflict delta — exclusive borrow finally expressible from real source). pt3 Slice A NLL last-use expiry: forward pre-pass compute_last_use_index maps each symbol to its greatest mentioning statement index; check_block expires ref-bindings introduced in-block once their binder is dead, releasing the underlying borrow. Unblocks `let r = &x; print(*r); x = 2` and `let m = &mut x; let y = *m; x` while still rejecting same-block live aliasing (2 positive + 1 anti-regression hermetic tests in E2E Borrow Graph). Residual (Slices B–D): flow-sensitive escape via `outer = &x`, origin/region variables (Polonius surface) + loan-live-at-point dataflow across CFG joins, tighter quantity integration. Authoritative: docs/CAPABILITY-MATRIX.adoc + docs/TECH-DEBT.adoc CORE-01."
7272
row-polymorphism = "60% (records + effects rows implemented in typecheck/unify; not fully exercised end-to-end)"
7373
effects = "interpreter-complete (handler dispatch, PerformEffect propagation, ExprResume, multi-arg ops all wired in interp.ml 2026-04-11). WasmGC: ops registered as unreachable stubs; ExprHandle/ExprResume reject with UnsupportedFeature — full WASM dispatch needs EH proposal or CPS transform."
7474
dependent-types = "parse-only (TRefined AST node exists and refinement predicates parse, but predicates do not reduce; no SMT/decision procedure wired in)"

docs/CAPABILITY-MATRIX.adoc

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,26 @@ enforced. Handlers: *interpreter-complete*; WasmGC dispatch is
8484
`UnsupportedFeature` pending the EH proposal or the CPS transform (the #225
8585
CPS line is closing the async slice of this).
8686

87-
|Borrow checker |*partial — graph validation landed (CORE-01 pt 1)* |
87+
|Borrow checker |*partial — graph validation + NLL last-use landed
88+
(CORE-01 pts 1–3 Slice A)* |
8889
Place/borrow/move infra + use-after-move, conflicting-borrow,
8990
move-while-borrowed, lambda-capture. *CORE-01 part 1* (PR #240, Refs #177,
9091
gate 263/263): borrow-graph validation wired — `BorrowOutlivesOwner` now
9192
emitted (`&local` escaping its block); shared-XOR-exclusive enforced at
9293
*use* sites (`UseWhileExclusivelyBorrowed`); ownership now derived from the
9394
parameter *type* (`TyOwn/TyRef/TyMut`) so the owned/ref/mut discipline is
9495
actually enforced from real parsed source (closed a latent hole); call-arg
95-
borrows made temporary. *Deferred (CORE-01 pt 2+):* NLL/region inference,
96-
flow-sensitive escape via `outer = &x`, tighter quantity integration.
96+
borrows made temporary. *Part 2* (Refs #177): return-escape +
97+
`&mut e` parser surface (the exclusive borrow is finally expressible from
98+
source). *Part 3 Slice A* (Refs #177): non-lexical lifetimes —
99+
ref-bindings expire at their binder's *last use*, not at block exit
100+
(`compute_last_use_index` pre-pass + per-statement expiry in
101+
`check_block`). Patterns like `let r = &x; print(*r); x = 2` and
102+
`let m = &mut x; let y = *m; x` now type-check, while still-live borrows
103+
continue to block aliasing assignments. *Deferred (Slices B–D):*
104+
flow-sensitive escape via `outer = &x`; origin/region variables
105+
(Polonius surface) + loan-live-at-point dataflow across CFG joins;
106+
tighter quantity integration for captured linears.
97107

98108
|Quantity / affine types |*enforced* |QTT semiring in `lib/quantity.ml`,
99109
invoked inside the standard CLI pipeline. `@linear`/`@erased`/`@unrestricted`

docs/ECOSYSTEM.adoc

Lines changed: 11 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.
@@ -234,6 +235,15 @@ byte-level wasm-IR extension (I32Load8U/I32Store8 absent today)
234235
sub-slices: S5 (native clocks/env/argv via preview2), S6b
235236
(`wasi:sockets`), S6c (flip the default wasm target from preview1
236237
→ component). WIT world of record: `wit/affinescript.wit`
238+
bridges to `wasi:clocks`/`wasi:cli`. Real-host main-invoke deferred
239+
to S6 (WIT export-lifting / wasi:cli/run command shape).
240+
**S5 string accessors (env_at/arg_at) DONE: the wasm IR gained
241+
the byte-level load/store family (I32Load8U/I32Store8 + the full
242+
WebAssembly 1.0 §5.4.6 row, opcodes 0x2C..0x35 / 0x3A..0x3E);
243+
accessors lower to on-demand `environ_get`/`args_get` paired with
244+
the existing `*_sizes_get` import (dedup keeps each WASI import
245+
once even when both `*_count` and `*_at` are used).** WIT world
246+
of record: `wit/affinescript.wit`
237247
|INT-04 |Publish compiler + runtime to JSR (then npm) |#181 |runtime
238248
packaging READY (affine-js + affinescript-tea JSR dry-run green;
239249
manual-only `publish-jsr.yml`; docs/PACKAGING.adoc). INT-01 dep

docs/TECH-DEBT.adoc

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,37 @@ typed/codegen'd like `&e` — exclusivity is a static borrow property).
110110
`&`-in-`#{` literals and bare block-statements already parse on `main`;
111111
`-> &T`/`&T` *type* sigil deliberately **not** added — `ref T`/`mut T`
112112
keyword types already express reference types; a `&T` sigil is duplicate
113-
surface (ADR territory, not a soundness gap). *Part 2 residual — now the
114-
analysis, not surface:* NLL/region inference (Polonius) + flow-sensitive
115-
escape via `outer = &x` are now *expressible* (surface unblocked); the
116-
remaining work is the dataflow analysis itself. |S1 |pt1 #240 + pt2
117-
return-escape + `&mut` surface DONE (Refs #177); residual = NLL analysis
118-
— 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)
113+
surface (ADR territory, not a soundness gap). *Part 3 Slice A — NLL
114+
last-use expiry LANDED* (Refs #177): a forward pre-pass
115+
(`compute_last_use_index`) maps each symbol to the greatest statement
116+
index that mentions it; after each statement, `check_block` expires any
117+
ref-binding introduced in that block whose binder is now dead, releasing
118+
the underlying borrow. Unblocks the canonical NLL patterns
119+
(`let r = &x; print(*r); x = 2`; `let m = &mut x; let y = *m; x`) while
120+
preserving soundness — the anti-aliasing rules still fire against
121+
statements that *do* use the binder (3 hermetic tests in "E2E Borrow
122+
Graph", two positive + one anti-regression). *Slices B–D residual:*
123+
flow-sensitive escape via assignment to an outer mutable
124+
(`outer = &x`); origin/region variables (Polonius surface) + loan-live-
125+
at-point dataflow across CFG joins; tighter quantity integration for
126+
captured linears. |S1 |pt1 #240 + pt2 return-escape + `&mut` surface +
127+
pt3 Slice A NLL last-use DONE (Refs #177); residual = Slices B–D
128+
(flow-sensitive escape, origin variables, quantity integration) —
129+
issue #177
130+
|CORE-02 |Effect-handler dispatch on WasmGC (was `UnsupportedFeature`;
131+
the chosen path is CPS over the EH proposal). The #225 CPS line closed
132+
the async slice; #234 generalised the boundary recogniser from a
133+
hardcoded name set to "any call whose effect row ⊇ `Async`". |S2
134+
|*CLOSED 2026-05-19* — #225 line CLOSED PR1..PR3d+PR4; #234 ADR-016
135+
delivered end-to-end (S1 #270 ADR; S2a #275 shared call-site
136+
numbering `lib/effect_sites.ml`; S2b #276 typecheck builds the
137+
ordinal→effect side-table; S3 #277 pipeline threads it + codegen
138+
boundary predicate switched + user-defined-`Async` e2e
139+
`tests/codegen/effect_async_boundary.affine`; S4 #278 hardcoded
140+
`async_primitives` set retired — boundary is now exactly
141+
`Effect_sites.is_async_call`). Issue #234 closed completed. Table-miss
142+
fallback (`Effect_sites.is_async_call` = false ⇒ no transform) remains
143+
as the sound table-empty / count-mismatch path.
123144
|CORE-03 |ADR-014: module-qualified type/effect path. Decision settled
124145
(both `.`/`::` accepted, `Pkg::Type` canonical, `.`→`::` for free via the
125146
`::`-fold). Was the estate's dominant parse blocker (525/1177 .affine).
@@ -159,7 +180,32 @@ complete; convergence ABI shared w/ Ephapax
159180
insert/set/remove/keys/values; AOT-gated (#136). Closes ESC-03 #247 (the
160181
#229 `Dict.t` target); unblocks the additive `Http.affine` headers→Dict
161182
upgrade
162-
|STDLIB-04 |Residual `extern` builtins → real implementations |S3 |open
183+
|STDLIB-04 |Residual `extern` builtins → real implementations (umbrella;
184+
split into 04a–04e 2026-05-24 per per-extern audit, one row per PR) |S3
185+
|open (5 sub-rows)
186+
|STDLIB-04a |Mut effect externs (`make_ref`/`get`/`set`) — *LANDED*
187+
(Refs #328): interp uses `VMut` cell (`lib/interp.ml`); Deno codegen
188+
lowers to `{__cell: x}` single-field object (`lib/codegen_deno.ml`);
189+
3 hermetic e2e tests in "E2E STDLIB-04a Mut #328" (Int + String
190+
round-trips + Deno codegen __cell-shape assertion) |S3 |DONE
191+
2026-05-24 (Refs #328)
192+
|STDLIB-04b |Throws extern `error<T>` — *LANDED* (Refs #329): mirrors
193+
`panic`'s divergent semantics with a polymorphic return (`<T>` unifies
194+
with the call-site expectation, unobservable at runtime). Wired in
195+
interp (`RuntimeError`), Deno codegen (`throw new Error`), resolve
196+
seed, and typecheck as a scheme (`poly1` so each call instantiates a
197+
fresh tyvar). 3 hermetic tests in "E2E STDLIB-04b error #329". |S3
198+
|DONE 2026-05-24 (Refs #329)
199+
|STDLIB-04c |`string_concat` extern — no direct wiring found; `++`
200+
operator independently lowered. Decide: remove (operator-only) or wire
201+
to mirror `++` |S3 |open — issue #330
202+
|STDLIB-04d |IO externs (`print`/`println`/`read_line`/`read_file`/
203+
`write_file`) — wired on all backends but no dedicated hermetic e2e
204+
tests; test-debt, not impl-debt |S3 |open — issue #331
205+
|STDLIB-04e |Pure externs (`int_to_string`/`string_to_int`/
206+
`string_length`) — real + tested (`lib/interp.ml:615-633`,
207+
`lib/codegen_deno.ml:263-272`); bookkeeping to mark DONE |S3 |open —
208+
issue #332
163209
|===
164210

165211
== Section D — INT (ecosystem integration)
@@ -211,6 +257,14 @@ survives, `wasmtime run` exits 0. Real-host main-invoke now
211257
works via `wasmtime run <component>`.** Remaining S6
212258
sub-slices: S5 (native clocks/env/argv), S6b (`wasi:sockets`),
213259
S6c (flip the default wasm target from preview1 → component)
260+
regression proves no collision. **S5 (env_at/arg_at) DONE — wasm
261+
IR extended with the byte-level load/store family
262+
(I32Load8U/I32Store8 and siblings); accessors lower to on-demand
263+
`environ_get`/`args_get` imports paired with the existing
264+
`*_sizes_get` (dedup keeps each WASI import exactly once even when
265+
both `*_count` and `*_at` are used in the same unit); guest
266+
allocates a length-prefixed AS string and byte-copies from the
267+
WASI buffer.** Real-host main-invoke = S6 (WIT export lifting)
214268
|INT-04 |Publish to JSR/npm |S2 |#181 packaging READY (dry-run green,
215269
manual workflow); compiler-binary distribution decided = **ADR-019**
216270
(#260, Releases + thin Deno/JSR shim, staged S1..S4) — S1/S2/S3
@@ -263,7 +317,9 @@ Proven + locked (see INT-02)
263317
|CONV-01 |Estate-wide re-validation of the #199/#205 closure ABI (static →
264318
real wasm engine) |S2 |open #235
265319
|CONV-02 |Effect-threaded async-boundary detection (generalise the
266-
structural-conservative recogniser) |S2 |open #234
320+
structural-conservative recogniser) |S2 |*CLOSED 2026-05-19* — #234
321+
DELIVERED end-to-end (ADR-016 S1..S4; PRs #270/#275/#276/#277/#278).
322+
See CORE-02 above for the resolution narrative.
267323
|CONV-03 |#225/#160 convergence ABI matured to "shared with Ephapax" |S1
268324
|partial (PR3a/b/c merged)
269325
|CONV-04 |Widen emitted-wasm enforcement beyond L7+L10 toward L1–6/L13–16 |S2

0 commit comments

Comments
 (0)