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
## Summary
CORE-01 Phase 3 **Slice C-light** — CFG-join semantics for `ExprHandle`
handler arms and `ExprTry` catch arms. Both were previously checked
*sequentially* against a shared state, so moves/borrows from arm i
polluted the state seen by arm i+1 — even though only one arm runs at
runtime. This PR mirrors the snapshot-restore-merge pattern that
`ExprMatch` already does inline, and factors the merge logic into a
`merge_arm_results` helper that all CFG-join sites can share.
### Before (sequential)
```affine
fn catch_arm_isolation() -> Unit {
let y = 100;
try { 0 } catch {
0 => drop_int(y),
1 => drop_int(y), // ← spurious UseAfterMove: arm 0 polluted state.moved
_ => ()
}
}
```
### After (isolated)
Each arm runs against the post-body state independently; the merge takes
intersection of borrows and union of moves.
## Mechanics
New helper near `check_expr` in `lib/borrow.ml`:
```ocaml
val merge_arm_results :
state -> borrow list -> move_record list ->
(unit result * borrow list * move_record list) list ->
unit result
```
- Errors: propagates the first error seen.
- Borrows: intersection across arms (branch-local borrows die at
arm-block exit anyway; this just formalises the join).
- Moves: union across arms, deduplicated against the base — a place is
moved post-join if any arm moved it (conservative-sound).
Wired into `ExprHandle` and `ExprTry`. `ExprMatch`'s inlined logic is
**deliberately untouched** — same semantics, but PR-scope discipline
says don't refactor a stable code path here.
For `ExprTry`: `check_block(body)` runs first (its block-local borrows
are cleaned at block exit; moves persist). The snapshot is taken *after*
check_block, so catch arms see the body's moves in `base_moved`. The
merge's dedupe-against-base preserves them through the join. `finally`
runs deterministically after the merge.
## Tests (E2E Borrow Graph, +2)
| Fixture | Direction | Expected |
|---|---|---|
| `slice_c_catch_arm_isolation.affine` | positive | Ok — two arms
independently move `y`, currently rejected with spurious UseAfterMove |
| `slice_c_body_move_persists.affine` | anti-regression | UseAfterMove
on `read_int(y)` after the body moved `y`, even with a `_ =>` catch arm
|
## Scope of this PR
`ExprHandle` is implemented in parallel but has **no dedicated test
fixture** — the AffineScript test corpus has no live `handle`
expressions outside the parser's 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`.
**Deferred (Slices C-full / C' / D):**
- **Slice C-full** — true Polonius origin/region variables on
`TyRef`/`TyMut` with subset constraints + datalog-style
loan-live-at-point solver. Architectural change to the type system;
ADR-gated.
- **Slice C'** — loop soundness via a 2-iteration check on
`StmtWhile`/`StmtFor`. Coupled to a `StmtAssign` clear-on-rewrite fix
(assignment is currently treated as a read of LHS, so the naive 2-iter
spuriously fails on legitimate re-assignment loops).
- **Ref-to-ref binding** — `r = some_other_ref_var` (RHS not a direct
`&place`) still leaves the ref-binding stale. Symmetric let/assign
limitation.
- **Slice D** — tighter quantity-checker integration for captured
linears.
## Docs
- `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 / C' / D +
ref-to-ref binding
## Test plan
- [ ] CI `build` (opam exec -- dune build) green
- [ ] CI `Run tests` (opam exec -- dune runtest) — `E2E Borrow Graph`
count +2
- [ ] Existing `try_catch_*` / `try_finally` fixtures (single-arm Int
expressions, no move/borrow) untouched
**Local-build caveat**: container has no OCaml toolchain; CI is the
source of truth.
Refs #177
---
_Generated by [Claude
Code](https://claude.ai/code/session_01TLERhquTknLCfyjv7eQU8b)_
Co-authored-by: Claude <noreply@anthropic.com>
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