You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Until now, `ExprHandle` handler arms and `ExprTry` catch arms were
checked *sequentially* against a shared state — so moves and
borrows from arm i polluted the state seen by arm i+1, even though
only one arm runs at runtime. This produced spurious UseAfterMove
on the second of two catch arms that independently move the same
value, etc.
Slice C-light closes this for handlers and try/catch by mirroring
the snapshot-restore-merge pattern that `ExprMatch` already does
inline. The merge logic — intersection-on-borrows, union-on-
moves, first-error propagation, dedupe against the base — is now
factored into a single helper, `merge_arm_results`, so all CFG-
join sites agree on "what state survives the join":
- Errors: propagate the first error seen, left-to-right.
- Borrows: intersection across arms (a borrow survives the join
only if it survived every arm; branch-local borrows naturally
die at arm exit via `check_block`'s borrows_at_entry restore).
- Moves: union across arms, deduplicated against the base — a
place is reported as moved post-join if any arm moved it
(conservative-sound).
`ExprMatch`'s inlined logic is *deliberately left untouched* — same
semantics as the new helper, but its tests have been stable since
PR #240; refactoring it would add risk without ergonomic upside
on this PR. A future cleanup can converge them.
ExprHandle (`handle body { return(_) => ..., op(p1, p2) => ... }`):
mutually exclusive continuations dispatched on whether the body
returned normally or performed an effect-op. Body runs against
the current state, then each arm runs against the post-body state
independently via the helper.
ExprTry (`try { body } catch { p => arm; ... } finally { ... }`):
body runs first via `check_block` (so its block-local borrows are
cleaned at block exit; its moves persist). The snapshot is taken
*after* check_block — so the catch arms run against a state that
already includes the body's moves (conservatively assuming body
might have moved before throwing). Catch arms merge via the
helper; `finally` then runs deterministically against the merged
post-catch state.
Tests (E2E Borrow Graph, +2):
- `slice_c_catch_arm_isolation.affine` (positive): three catch
arms, two of which independently `drop_int(y)`. Pre-Slice-C
this was rejected — arm 2's `drop_int(y)` saw arm 1's move and
fired UseAfterMove. Post-Slice-C every arm starts from the
post-body state and succeeds independently.
- `slice_c_body_move_persists.affine` (anti-regression): the
try-body moves `y`; the catch arm matches `_` and ignores `y`;
after the `try`, a `read_int(y)` must still fail
UseAfterMove because the body might have moved before throwing.
This pins that the helper's dedupe-against-base correctly
preserves body-side moves through the merge. The current
`state.moved` propagation does this for free (the snapshot
*captures* body's moves into base_moved, and the merge sets
`state.moved <- base_moved @ unique_new`), but the test pins
the invariant so a future refactor cannot drop it.
ExprHandle is implemented in parallel but has no dedicated test
fixture: the AffineScript test corpus has no live `handle`
expressions outside the parser's own grammar tests, so an
end-to-end borrow-graph fixture for it would be the first. The
code path is exercised whenever a real `handle` appears; until
then, soundness is by symmetry-with-ExprTry.
Anti-regression sweep: all existing borrow / linear-arrow /
quantity fixtures audited. The `try_catch_*` and `try_finally`
fixtures already in the gate (e/Try semantics smoke tests) are
single-arm and don't touch moves, so they go through the new path
unchanged. Move-or-borrow scenarios crossing `ExprHandle`/
`ExprTry` are net-new fixtures here.
Docs updated:
- `STATE.a2ml` borrow-checker → "phase-3-parts-1-3-Slices-A-B-C-light-landed"
- `CAPABILITY-MATRIX.adoc` borrow-checker row records Slice C-light
- `TECH-DEBT-alt.adoc` CORE-01 row narrows residual to
C-full (origin variables, ADR-gated), C' (loop soundness),
D (quantity integration), plus ref-to-ref binding.
NOTE: container has no OCaml toolchain; `dune build` / `dune runtest`
not run locally. CI is the source of truth. Mechanically scoped to
two arms of `check_expr` plus one new helper.
Copy file name to clipboardExpand all lines: .machine_readable/6a2/STATE.a2ml
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -71,7 +71,7 @@ test-files = 54
71
71
# the feature is not enforced on user programs through the CLI pipeline.
72
72
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.)"
73
73
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."
74
-
borrow-checker = "phase-3-parts-1-3-Slices-A-and-B-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, call-arg borrows temporary, ref-binding graph tracked. pt2 (gate 271→274 and 278→281) return-escape + &mut e parser surface (zero Menhir conflict delta — exclusive borrow finally expressible from real source). pt3 Slice A (PR #335) 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. Unblocks `let r = &x; print(*r); x = 2` and `let m = &mut x; let y = *m; x` (2 positive + 1 anti-regression in E2E Borrow Graph). pt3 Slice B (this PR) flow-sensitive escape via re-assignment: `outer = &y` in StmtAssign now pre-releases the held borrow and re-binds the (binder → new_borrow) ref-graph entry to the freshly-created borrow, so NLL last-use + return-escape see the *current* referent. 3 hermetic tests (2 positive + 1 anti-regression). Residual (Slices C–D): origin/region variables (Polonius surface) + loan-live-at-point dataflow for CFG joins in ExprHandle/ExprTry/loops; tighter quantity integration for captured linears; reborrow through indirection (RHS that is a ref-typed value rather than a direct &place). Authoritative: docs/CAPABILITY-MATRIX.adoc + docs/TECH-DEBT.adoc CORE-01."
74
+
borrow-checker = "phase-3-parts-1-3-Slices-A-B-C-light-landed (CORE-01, Refs #177, 2026-05-24): pt1 (#240, gate 263/263) borrow-graph validation wired — BorrowOutlivesOwner emitted, shared-XOR-exclusive enforced at use sites, ownership from param type TyOwn/TyRef/TyMut, call-arg borrows temporary, ref-binding graph tracked. pt2 (gate 271→274 and 278→281) return-escape + &mut e parser surface. pt3 Slice A (PR #335) NLL last-use expiry: compute_last_use_index pre-pass; check_block expires in-block ref-bindings once their binder is dead. pt3 Slice B (effectively #354/#355/#356 after #351 procedural close) flow-sensitive escape via re-assignment: `outer = &y` in StmtAssign pre-releases the held borrow and re-binds the ref-graph entry to the freshly-created borrow. pt3 Slice C-light (this PR) CFG-join semantics for non-match join constructs: ExprHandle handler arms and ExprTry catch arms are now isolated via snapshot-restore-merge (factored merge_arm_results helper, mirroring ExprMatch's inlined logic) — moves/borrows from arm i no longer pollute arm i+1. 2 hermetic tests (positive: independent catch-arm moves OK; anti-regression: body-side move persists past the try). Residual (Slices C-full, C', D): true Polonius origin/region variables on TyRef/TyMut with subset constraints + datalog-style solver (architectural; ADR-gated); loop soundness via 2-iter check (coupled to StmtAssign clear-on-rewrite fix); reborrow through indirection (ref-to-ref binding); quantity-checker tightening for captured linears. Authoritative: docs/CAPABILITY-MATRIX.adoc + docs/TECH-DEBT-alt.adoc CORE-01."
75
75
row-polymorphism = "60% (records + effects rows implemented in typecheck/unify; not fully exercised end-to-end)"
76
76
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."
77
77
dependent-types = "parse-only (TRefined AST node exists and refinement predicates parse, but predicates do not reduce; no SMT/decision procedure wired in)"
0 commit comments