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
Under purely lexical analysis (and even after Slice A's NLL last-use),
re-assigning a ref-binder — `let mut r = &x; r = &y` — left the
*old* held borrow on `x` in `state.borrows` and the *stale*
`(r -> old_borrow)` entry in `state.ref_bindings`. The new borrow
on `y` was added by `check_expr(rhs)` but never wired into the
ref-graph, so:
- `x = 10` after the reassignment was rejected as MoveWhileBorrowed
even though `r` no longer pointed at `x` (the bug this PR fixes).
- NLL last-use on `r` expired the WRONG borrow (the old one on `x`),
leaving the new borrow on `y` hanging — masking valid writes to
`y`.
- `check_return_escape` looked up the stale `(r -> &x)` entry rather
than the actual current referent `&y`.
This is the "flow-sensitive escape via assignment to an outer
mutable" residual called out in the Slice A docstring.
Implementation in `lib/borrow.ml` `StmtAssign`:
When LHS is a ref-binder symbol that already holds a borrow AND RHS
is a direct `&p`/`&mut p`, the code now:
1. *Pre*-releases the old held borrow (via `end_borrow`) and removes
the stale entry from `ref_bindings` BEFORE checking the RHS. The
pre-release order matters for the same-target reborrow case
(`r = &mut x` while `r` already holds `&mut x`): post-release
ordering would trip `ConflictingBorrow` on the about-to-be-
replaced exclusive borrow at `record_borrow` time, which is
user-confusing because the conflict is purely an artefact of
sequential modelling. Pre-release dissolves the conflict.
2. Checks the RHS, which creates the new borrow on `state.borrows`
the usual way (`record_borrow`).
3. After RHS-check, looks up the freshly-created borrow on the new
target place and binds it into `ref_bindings` as
`(binder_sym, new_borrow)` — the symmetric assignment-side of
`record_ref_binding`'s let-graph contract.
Sound: NLL last-use, in-block `BorrowOutlivesOwner`, and
`check_return_escape` now consult the current referent. The new
borrow continues to live on `state.borrows` and continues to be
visible to `find_aliasing_exclusive` / "active borrow of LHS"
detection — see the anti-regression test.
Tests (E2E Borrow Graph, +3):
- `slice_b_outer_assign_releases_old.affine` — `r = &y` then
`x = 10` is now Ok (the old borrow on `x` was released).
- `slice_b_nll_expires_new.affine` — after `r = &y` and `r`'s last
use, NLL expires the NEW borrow (on `y`), so subsequent writes to
BOTH `x` and `y` succeed. Without the re-bind, NLL would expire
the wrong borrow and `y = 10` would fail.
- `slice_b_new_borrow_still_protects.affine` — anti-regression:
after `r = &y`, while `r` is still live, `y = 10` must still
fail (MoveWhileBorrowed). Pins that the new borrow IS tracked.
Existing tests audited and remain green by construction:
- `borrow_return_refparam_ok.affine`: no re-assignment in scope; no
Slice B trigger. Unaffected.
- `borrow_return_escape_{param,local}.affine`: `let r = &x` then
`return r` (no reassignment); same path as before.
- `borrow_nll_still_rejects_live_borrow.affine`: no `&p`-form RHS
in scope; Slice B doesn't fire.
- All existing borrow / quantity / linear-arrow fixtures are
untouched by the new path — the assignment branch only deviates
when LHS is a ref-binder AND RHS is a direct `&p`/`&mut p`.
Deferred (Slices C–D residual):
- Reborrow through indirection: `r = some_other_ref_var` (RHS not
a direct `&place`) still leaves the ref-binding stale. Same
limitation as `record_ref_binding`'s let-graph path; would need
symmetric ref-to-ref binding for both let and assign.
- Origin/region variables (Polonius surface) + loan-live-at-point
dataflow across CFG joins for `ExprHandle`/`ExprTry`/loops.
- Tighter quantity-checker integration for captured linears.
Docs updated: `STATE.a2ml` borrow-checker → "Slices A and B
landed"; `CAPABILITY-MATRIX.adoc` borrow-checker row records
Slice B; `TECH-DEBT.adoc` CORE-01 row records Slice B + narrows
residual to Slices C–D.
NOTE: this container has no OCaml toolchain; `dune build` /
`dune runtest` were not run locally. CI is the source of truth.
Mechanically scoped to one branch of `StmtAssign`'s `Some place →
None`-conflict-on-LHS arm; all other code paths are unchanged.
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-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."
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."
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