Skip to content

Commit 000f915

Browse files
committed
feat(borrow): flow-sensitive escape via outer = &y (CORE-01 pt3 Slice B, Refs #177)
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.
1 parent 660519e commit 000f915

8 files changed

Lines changed: 194 additions & 23 deletions

File tree

.machine_readable/6a2/STATE.a2ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ test-files = 54
7171
# the feature is not enforced on user programs through the CLI pipeline.
7272
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.)"
7373
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."
7575
row-polymorphism = "60% (records + effects rows implemented in typecheck/unify; not fully exercised end-to-end)"
7676
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."
7777
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: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,17 @@ ref-bindings expire at their binder's *last use*, not at block exit
100100
(`compute_last_use_index` pre-pass + per-statement expiry in
101101
`check_block`). Patterns like `let r = &x; print(*r); x = 2` and
102102
`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.
103+
continue to block aliasing assignments. *Part 3 Slice B* (Refs #177):
104+
flow-sensitive escape via re-assignment — `outer = &y` in `StmtAssign`
105+
now pre-releases the held borrow and re-binds the
106+
`(binder -> new_borrow)` ref-graph entry to the freshly-created borrow,
107+
so the NLL last-use + return-escape analyses see the *current* referent
108+
after re-assignment, not the stale original (3 hermetic tests: old
109+
released, new tracked by NLL, anti-regression that new borrow still
110+
protects). *Deferred (Slices C–D):* origin/region variables (Polonius
111+
surface) + loan-live-at-point dataflow across CFG joins for
112+
`ExprHandle`/`ExprTry`/loops; tighter quantity integration for captured
113+
linears; reborrow through indirection (RHS not a direct `&place`).
107114

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

docs/TECH-DEBT.adoc

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,16 +154,25 @@ index that mentions it; after each statement, `check_block` expires any
154154
ref-binding introduced in that block whose binder is now dead, releasing
155155
the underlying borrow. Unblocks the canonical NLL patterns
156156
(`let r = &x; print(*r); x = 2`; `let m = &mut x; let y = *m; x`) while
157-
preserving soundness — the anti-aliasing rules still fire against
158-
statements that *do* use the binder (3 hermetic tests in "E2E Borrow
159-
Graph", two positive + one anti-regression). *Slices B–D residual:*
160-
flow-sensitive escape via assignment to an outer mutable
161-
(`outer = &x`); origin/region variables (Polonius surface) + loan-live-
162-
at-point dataflow across CFG joins; tighter quantity integration for
163-
captured linears. |S1 |pt1 #240 + pt2 return-escape + `&mut` surface +
164-
pt3 Slice A NLL last-use DONE (Refs #177); residual = Slices B–D
165-
(flow-sensitive escape, origin variables, quantity integration) —
166-
issue #177
157+
preserving soundness. *Part 3 Slice B LANDED* (Refs #177): flow-sensitive
158+
escape via re-assignment — `outer = &y` in `StmtAssign` now
159+
pre-releases the held borrow and re-binds the
160+
`(binder -> new_borrow)` ref-graph entry to the freshly-created borrow,
161+
so the NLL last-use + return-escape analyses see the *current* referent
162+
after re-assignment, not the stale original. Pre-release order chosen so
163+
the same-target reborrow (`r = &mut x` while `r` already holds
164+
`&mut x`) does not trip `ConflictingBorrow` on the about-to-be-replaced
165+
exclusive borrow. 3 hermetic tests in "E2E Borrow Graph" (old released,
166+
new tracked by NLL, anti-regression that new borrow still protects its
167+
new target). *Slices C–D residual:* origin/region variables (Polonius
168+
surface) + loan-live-at-point dataflow across CFG joins for
169+
`ExprHandle`/`ExprTry`/loops; tighter quantity integration for captured
170+
linears; reborrow through indirection (`r = some_other_ref_var` RHS does
171+
not yet copy the other binder's graph entry — symmetric let/assign
172+
limitation, parks with the let-graph's same restriction). |S1 |pt1 #240
173+
+ pt2 return-escape + `&mut` surface + pt3 Slices A+B DONE (Refs #177);
174+
residual = Slices C–D (origin variables, quantity integration,
175+
ref-to-ref binding) — issue #177
167176
|CORE-02 |Effect-handler dispatch on WasmGC (was `UnsupportedFeature`;
168177
the chosen path is CPS over the EH proposal). The #225 CPS line closed
169178
the async slice; #234 generalised the boundary recogniser from a

lib/borrow.ml

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,8 +1233,41 @@ and check_stmt (ctx : context) (state : state) (symbols : Symbol.t) (stmt : stmt
12331233
(* Can't assign while borrowed *)
12341234
Error (MoveWhileBorrowed (place, borrow))
12351235
| None ->
1236-
(* Assignment is ok, check the RHS *)
1237-
check_expr ctx state symbols rhs
1236+
(* Slice B (CORE-01 pt3 / #177): flow-sensitive escape via
1237+
`outer = &y`. If LHS is a ref-binder symbol that already
1238+
holds a borrow and RHS is a fresh `&y`/`&mut y` reference,
1239+
the assignment *replaces* the held borrow. We pre-release
1240+
the old held borrow before checking the RHS so the same-
1241+
target reborrow case (`r = &mut x` while `r` already holds
1242+
`&mut x`) does not trip [ConflictingBorrow] on the
1243+
about-to-be-replaced exclusive borrow; we then re-bind the
1244+
ref-graph entry to the freshly-created borrow. Mirrors
1245+
[record_ref_binding]'s let-graph contract for the
1246+
assignment path so the NLL last-use + return-escape
1247+
analyses see the *current* referent after re-assignment,
1248+
not the stale original. *)
1249+
let pre_release =
1250+
match root_var place, ref_target symbols rhs with
1251+
| Some binder_sym, Some _
1252+
when List.mem_assoc binder_sym state.ref_bindings ->
1253+
let old_borrow = List.assoc binder_sym state.ref_bindings in
1254+
end_borrow state old_borrow;
1255+
state.ref_bindings <-
1256+
List.filter (fun (s, _) -> s <> binder_sym) state.ref_bindings;
1257+
Some binder_sym
1258+
| _ -> None
1259+
in
1260+
let* () = check_expr ctx state symbols rhs in
1261+
(match pre_release, ref_target symbols rhs with
1262+
| Some binder_sym, Some new_target ->
1263+
(match List.find_opt (fun b ->
1264+
places_overlap b.b_place new_target) state.borrows with
1265+
| Some new_b ->
1266+
state.ref_bindings <-
1267+
(binder_sym, new_b) :: state.ref_bindings
1268+
| None -> ())
1269+
| _ -> ());
1270+
Ok ()
12381271
end
12391272
| None ->
12401273
(* LHS is not a place (e.g., function call result), check both sides *)
@@ -1341,11 +1374,26 @@ let check_program (symbols : Symbol.t) (program : program) : unit result =
13411374
Outer-block ref-bindings are deliberately preserved — they expire
13421375
only at their own block's exit.
13431376
1344-
Still deferred — region-/flow-sensitive remainder:
1345-
- Flow-sensitive escape via assignment to an outer mutable
1346-
(`outer = &x`): the assignment must update the borrow graph the
1347-
way [let r = &x] already does.
1377+
CORE-01 pt3 Slice B (flow-sensitive escape via re-assignment):
1378+
`outer = &y` now updates the borrow graph the way `let outer = &y`
1379+
does. In StmtAssign, when LHS is a ref-binder symbol with a held
1380+
borrow and RHS is a direct `&p`/`&mut p`, the old borrow is
1381+
pre-released (so a same-target reborrow like `r = &mut x` while
1382+
`r` already holds `&mut x` does not trip ConflictingBorrow on the
1383+
about-to-be-replaced exclusive borrow), then after the RHS check
1384+
the (binder -> new_borrow) ref-graph entry is re-bound to the
1385+
freshly-created borrow. NLL last-use + return-escape now see the
1386+
*current* referent after re-assignment, not the stale original.
1387+
1388+
Still deferred:
1389+
- Reborrow through indirection: `r = some_other_ref_var` (RHS not a
1390+
direct `&place`) does not yet copy the other binder's graph
1391+
entry — the ref-binding stays stale. Same limitation as
1392+
[record_ref_binding] for the let-graph path; would require
1393+
symmetric let/assign handling for ref-to-ref binding.
13481394
- Origin/region variables (Polonius surface) + loan-live-at-point
1349-
dataflow across CFG joins (Slice C).
1350-
- Tighter integration with the quantity checker for captured linears.
1395+
dataflow across CFG joins for `ExprHandle`/`ExprTry`/loops
1396+
(Slice C).
1397+
- Tighter integration with the quantity checker for captured
1398+
linears (Slice D).
13511399
*)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell
3+
//
4+
// CORE-01 pt3 Slice B / #177 anti-regression: after `r = &y`, the
5+
// NEW borrow on `y` must still protect `y` while `r` is live. A
6+
// buggy Slice B that dropped the ref-binding but failed to re-bind
7+
// to the new borrow would silently accept `y = 10` below — the
8+
// borrow-graph would lose track of who holds &y. With correct
9+
// Slice B, `(r, &y)` is in ref_bindings, the borrow on `y` is in
10+
// state.borrows, and the assignment to `y` is rejected as
11+
// MoveWhileBorrowed. Expected: Error MoveWhileBorrowed.
12+
13+
module SliceBNewBorrowStillProtects;
14+
15+
fn still_protected() -> Int {
16+
let mut x = 1;
17+
let mut y = 2;
18+
let mut r = &x;
19+
r = &y;
20+
y = 10;
21+
*r
22+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell
3+
//
4+
// CORE-01 pt3 Slice B / #177: after `r = &y`, the NLL last-use
5+
// machinery must track the NEW borrow (on `y`), not the stale old
6+
// one (on `x`). Without Slice B, the ref-binding entry stays
7+
// `(r, &x)`, so when NLL expires `r` after its last use it ends the
8+
// wrong borrow — leaving the new borrow on `y` alive and
9+
// spuriously rejecting the later `y = 10`. With Slice B, the entry
10+
// is `(r, &y)` post-reassignment, NLL expires the correct borrow,
11+
// and both `x = 5` and `y = 10` succeed. Expected: Ok.
12+
13+
module SliceBNllExpiresNew;
14+
15+
fn nll_after_reassign() -> Int {
16+
let mut x = 1;
17+
let mut y = 2;
18+
let mut r = &x;
19+
r = &y;
20+
let z = *r;
21+
x = 5;
22+
y = 10;
23+
x + y + z
24+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell
3+
//
4+
// CORE-01 pt3 Slice B / #177: flow-sensitive escape via `outer = &y`.
5+
// Pre-Slice-B, the borrow held by `r` on `x` was never released when
6+
// `r` was re-assigned to `&y` — so `x = 10` was rejected as
7+
// MoveWhileBorrowed even though `r` no longer pointed at `x`. With
8+
// Slice B, the assignment `r = &y` ends the held borrow on `x` and
9+
// re-binds `r` in the ref-graph to the freshly-created borrow on `y`,
10+
// so the subsequent write to `x` is legal. Expected: Ok.
11+
12+
module SliceBOuterAssignReleasesOld;
13+
14+
fn outer_reassign() -> Int {
15+
let mut x = 1;
16+
let mut y = 2;
17+
let mut r = &x;
18+
let z = *r;
19+
r = &y;
20+
x = 10;
21+
*r + x + z
22+
}

test/test_e2e.ml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4232,6 +4232,39 @@ let test_borrow_nll_still_rejects_live_borrow () =
42324232
Alcotest.fail "NLL over-expired a still-live borrow — assignment \
42334233
to a borrowed owner was accepted"
42344234

4235+
(* CORE-01 pt3 Slice B / #177 — flow-sensitive escape via `outer = &y`.
4236+
The assignment `r = &y` (where `r` is an existing ref-binder) now
4237+
releases the old held borrow and re-binds `r` in the ref-graph to
4238+
the freshly-created borrow. Three tests pin: (1) the old target is
4239+
freed by the reassignment; (2) NLL last-use then correctly expires
4240+
the NEW borrow; (3) anti-regression — the new borrow still
4241+
protects its new target while the binder is live. *)
4242+
let test_slice_b_outer_assign_releases_old () =
4243+
match borrow_result (fixture "slice_b_outer_assign_releases_old.affine") with
4244+
| Ok () -> ()
4245+
| Error e ->
4246+
Alcotest.fail ("Slice B: `r = &y` did not release the old borrow on \
4247+
`x` — `x = …` after the reassignment was spuriously \
4248+
rejected: " ^ Borrow.format_borrow_error e)
4249+
4250+
let test_slice_b_nll_expires_new () =
4251+
match borrow_result (fixture "slice_b_nll_expires_new.affine") with
4252+
| Ok () -> ()
4253+
| Error e ->
4254+
Alcotest.fail ("Slice B: NLL last-use after `r = &y` did not expire \
4255+
the new borrow on `y`: " ^ Borrow.format_borrow_error e)
4256+
4257+
let test_slice_b_new_borrow_still_protects () =
4258+
match borrow_result (fixture "slice_b_new_borrow_still_protects.affine") with
4259+
| Error (Borrow.MoveWhileBorrowed _) -> ()
4260+
| Error e ->
4261+
Alcotest.fail ("Slice B anti-regression: expected MoveWhileBorrowed \
4262+
on `y = …` (the new borrow must still protect `y`), \
4263+
got: " ^ Borrow.format_borrow_error e)
4264+
| Ok () ->
4265+
Alcotest.fail "Slice B regressed: the new borrow on `y` was not \
4266+
tracked, so the write to `y` was silently accepted"
4267+
42354268
let borrow_tests = [
42364269
Alcotest.test_case "BorrowOutlivesOwner: &local escapes its block"
42374270
`Quick test_borrow_outlives_owner;
@@ -4257,6 +4290,12 @@ let borrow_tests = [
42574290
`Quick test_borrow_nll_read_after_mut_last_use;
42584291
Alcotest.test_case "NLL anti-regression: still-live borrow blocks assign (#177 pt3 Slice A)"
42594292
`Quick test_borrow_nll_still_rejects_live_borrow;
4293+
Alcotest.test_case "Slice B: `r = &y` releases old borrow on `x` (#177 pt3 Slice B)"
4294+
`Quick test_slice_b_outer_assign_releases_old;
4295+
Alcotest.test_case "Slice B: NLL expires the NEW borrow after `r = &y` (#177 pt3 Slice B)"
4296+
`Quick test_slice_b_nll_expires_new;
4297+
Alcotest.test_case "Slice B anti-regression: new borrow still protects `y` (#177 pt3 Slice B)"
4298+
`Quick test_slice_b_new_borrow_still_protects;
42604299
]
42614300

42624301
(* ============================================================================

0 commit comments

Comments
 (0)