Skip to content

Commit 31397e1

Browse files
committed
feat(borrow): CFG-join for ExprHandle + ExprTry catch arms (CORE-01 pt3 Slice C-light, Refs #177)
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.
1 parent b89eb1a commit 31397e1

7 files changed

Lines changed: 240 additions & 31 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-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."
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: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,19 @@ now pre-releases the held borrow and re-binds the
107107
so the NLL last-use + return-escape analyses see the *current* referent
108108
after re-assignment, not the stale original (3 hermetic tests: old
109109
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`).
110+
protects). *Part 3 Slice C-light* (Refs #177): CFG-join semantics for
111+
non-match join constructs — `ExprHandle` handler arms and `ExprTry`
112+
catch arms are now isolated via snapshot-restore-merge (factored
113+
`merge_arm_results` helper, mirroring `ExprMatch`'s inlined logic).
114+
Moves/borrows from arm i no longer pollute arm i+1; finally runs
115+
deterministically against the merged post-catch state. 2 hermetic
116+
tests (positive + anti-regression). *Deferred (Slices C-full / C' /
117+
D):* true Polonius origin/region variables on `TyRef`/`TyMut` with
118+
subset constraints + datalog-style loan-live-at-point solver
119+
(ADR-gated); loop soundness via 2-iter check on `StmtWhile`/
120+
`StmtFor` (coupled to a `StmtAssign` clear-on-rewrite fix); reborrow
121+
through indirection (RHS not a direct `&place`); quantity-checker
122+
tightening for captured linears.
114123

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

docs/TECH-DEBT-alt.adoc

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,29 @@ the same-target reborrow (`r = &mut x` while `r` already holds
164164
`&mut x`) does not trip `ConflictingBorrow` on the about-to-be-replaced
165165
exclusive borrow. 3 hermetic tests in "E2E Borrow Graph" (old released,
166166
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
167+
new target). *Part 3 Slice C-light LANDED* (Refs #177): CFG-join
168+
semantics for non-match join constructs — `ExprHandle` handler arms
169+
and `ExprTry` catch arms are now isolated via snapshot-restore-merge,
170+
extracted into a `merge_arm_results` helper that mirrors `ExprMatch`'s
171+
inlined logic (intersection on borrows, union on moves, first-error
172+
propagation). Moves/borrows from arm i no longer pollute arm i+1;
173+
`finally` runs deterministically against the merged post-catch state.
174+
2 hermetic tests in "E2E Borrow Graph" (positive: two catch arms can
175+
independently move the same value; anti-regression: body-side moves
176+
still propagate past the try, preserving conservative correctness).
177+
*Residual (Slices C-full / C' / D):* true Polonius origin/region
178+
variables on `TyRef`/`TyMut` with subset constraints and a datalog-
179+
style loan-live-at-point solver (architectural change to the type
180+
system; ADR-gated); loop soundness via a 2-iteration check on
181+
`StmtWhile`/`StmtFor` (coupled to a `StmtAssign` clear-on-rewrite
182+
fix — assignment is currently treated as a read of LHS, so the
183+
naive 2-iter spuriously fails on legitimate re-assignment loops);
184+
reborrow through indirection (`r = some_other_ref_var` RHS, parks
185+
with the let-graph's same restriction); tighter quantity-checker
186+
integration for captured linears. |S1 |pt1 #240 + pt2 return-escape
187+
+ `&mut` surface + pt3 Slices A+B+C-light DONE (Refs #177); residual
188+
= Slices C-full (origin variables, ADR-gated), C' (loop soundness),
189+
D (quantity integration), plus ref-to-ref binding — issue #177
176190
|CORE-02 |Effect-handler dispatch on WasmGC (was `UnsupportedFeature`;
177191
the chosen path is CPS over the EH proposal). The #225 CPS line closed
178192
the async slice; #234 generalised the boundary recogniser from a

lib/borrow.ml

Lines changed: 119 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,60 @@ let compute_last_use_index (symbols : Symbol.t) (blk : block)
669669
| None -> ());
670670
tbl
671671

672+
(** Merge per-arm post-states after a multi-arm CFG-join construct
673+
([ExprHandle], [ExprTry] catch). Each [arm_result] is a triple
674+
[(result, post_arm_borrows, post_arm_moved)] captured immediately
675+
after running the arm against the snapshotted base state.
676+
677+
- Errors: propagates the first error seen (left-to-right).
678+
- Borrows: intersection across arms — a borrow is alive post-join
679+
only if it is alive at the end of every arm. (Branch-local
680+
borrows naturally die at arm exit because the arm body is
681+
typically a block whose [check_block] clears them; this just
682+
formalises that semantics across the join.)
683+
- Moves: union — a place is moved post-join if any arm moved it,
684+
deduplicated against the base so we don't double-count moves
685+
that already existed before the arms.
686+
687+
Mirrors the inlined logic in [ExprMatch]'s arm-merge so all CFG-
688+
join sites share one notion of "what state survives the join".
689+
CORE-01 pt3 Slice C / #177 (CFG-join semantics for non-match
690+
join constructs). *)
691+
let merge_arm_results (state : state)
692+
(base_borrows : borrow list) (base_moved : move_record list)
693+
(arm_results : (unit result * borrow list * move_record list) list)
694+
: unit result =
695+
let errors = List.filter_map (fun (r, _, _) ->
696+
match r with Error e -> Some e | Ok () -> None) arm_results in
697+
match errors with
698+
| e :: _ -> Error e
699+
| [] ->
700+
let all_borrows = List.map (fun (_, bs, _) -> bs) arm_results in
701+
let merged_borrows = match all_borrows with
702+
| [] -> base_borrows
703+
| first :: rest ->
704+
List.fold_left (fun acc arm_borrows ->
705+
List.filter (fun b ->
706+
List.exists (fun b' -> b.b_id = b'.b_id) arm_borrows
707+
) acc
708+
) first rest
709+
in
710+
let all_moves = List.concat_map (fun (_, _, ms) ->
711+
List.filter (fun mr ->
712+
not (List.exists (fun base_mr ->
713+
places_overlap mr.m_place base_mr.m_place
714+
) base_moved)
715+
) ms
716+
) arm_results in
717+
let unique_moves = List.fold_left (fun acc mr ->
718+
if List.exists (fun mr' -> places_overlap mr.m_place mr'.m_place) acc
719+
then acc
720+
else mr :: acc
721+
) [] all_moves in
722+
state.borrows <- merged_borrows;
723+
state.moved <- base_moved @ unique_moves;
724+
Ok ()
725+
672726
(** Check borrows in an expression *)
673727
let rec check_expr (ctx : context) (state : state) (symbols : Symbol.t) (expr : expr) : unit result =
674728
match expr with
@@ -1046,12 +1100,26 @@ let rec check_expr (ctx : context) (state : state) (symbols : Symbol.t) (expr :
10461100

10471101
| ExprHandle eh ->
10481102
let* () = check_expr ctx state symbols eh.eh_body in
1049-
List.fold_left (fun acc arm ->
1050-
let* () = acc in
1051-
match arm with
1052-
| HandlerReturn (_pat, body) -> check_expr ctx state symbols body
1053-
| HandlerOp (_op, _pats, body) -> check_expr ctx state symbols body
1054-
) (Ok ()) eh.eh_handlers
1103+
(* CFG-join (CORE-01 pt3 Slice C / #177): handler arms are
1104+
mutually exclusive continuations dispatched on whether the
1105+
body returned normally ([HandlerReturn]) or performed an
1106+
effect-op ([HandlerOp]). Each arm runs against the post-body
1107+
state independently; moves/borrows from one arm must NOT
1108+
leak into the next. Mirrors [ExprMatch]'s arm-isolation
1109+
pattern via [merge_arm_results]. *)
1110+
let base_borrows = state.borrows in
1111+
let base_moved = state.moved in
1112+
let arm_results = List.map (fun arm ->
1113+
state.borrows <- base_borrows;
1114+
state.moved <- base_moved;
1115+
let body = match arm with
1116+
| HandlerReturn (_pat, b) -> b
1117+
| HandlerOp (_op, _pats, b) -> b
1118+
in
1119+
let r = check_expr ctx state symbols body in
1120+
(r, state.borrows, state.moved)
1121+
) eh.eh_handlers in
1122+
merge_arm_results state base_borrows base_moved arm_results
10551123

10561124
| ExprResume e_opt ->
10571125
begin match e_opt with
@@ -1060,18 +1128,32 @@ let rec check_expr (ctx : context) (state : state) (symbols : Symbol.t) (expr :
10601128
end
10611129

10621130
| ExprTry et ->
1131+
(* CFG-join (CORE-01 pt3 Slice C / #177): body runs first, then
1132+
either succeeds (no-exception path → post-body state
1133+
propagates) or raises (one catch arm runs). Catch arms are
1134+
alternative continuations from the post-body state, so each
1135+
arm runs against that state independently — moves/borrows
1136+
must not leak between catch arms. Finally runs
1137+
deterministically against the merged post-catch state. *)
10631138
let* () = check_block ctx state symbols et.et_body in
1139+
let base_borrows = state.borrows in
1140+
let base_moved = state.moved in
10641141
let* () = match et.et_catch with
1142+
| None -> Ok ()
10651143
| Some arms ->
1066-
List.fold_left (fun acc arm ->
1067-
let* () = acc in
1068-
let* () = match arm.ma_guard with
1069-
| Some g -> check_expr ctx state symbols g
1070-
| None -> Ok ()
1144+
let arm_results = List.map (fun arm ->
1145+
state.borrows <- base_borrows;
1146+
state.moved <- base_moved;
1147+
let r =
1148+
let open Result in
1149+
bind (match arm.ma_guard with
1150+
| Some g -> check_expr ctx state symbols g
1151+
| None -> Ok ())
1152+
(fun () -> check_expr ctx state symbols arm.ma_body)
10711153
in
1072-
check_expr ctx state symbols arm.ma_body
1073-
) (Ok ()) arms
1074-
| None -> Ok ()
1154+
(r, state.borrows, state.moved)
1155+
) arms in
1156+
merge_arm_results state base_borrows base_moved arm_results
10751157
in
10761158
begin match et.et_finally with
10771159
| Some blk -> check_block ctx state symbols blk
@@ -1385,15 +1467,35 @@ let check_program (symbols : Symbol.t) (program : program) : unit result =
13851467
freshly-created borrow. NLL last-use + return-escape now see the
13861468
*current* referent after re-assignment, not the stale original.
13871469
1470+
CORE-01 pt3 Slice C (CFG-join semantics for non-match join
1471+
constructs): [ExprHandle] handler arms and [ExprTry] catch arms
1472+
are mutually exclusive continuations from the post-body state.
1473+
Previously both were checked sequentially against a shared
1474+
state, so moves/borrows from arm i polluted arm i+1 — a
1475+
spurious UseAfterMove on the second of two arms that
1476+
independently move the same value, etc. Both now snapshot the
1477+
post-body state, run each arm independently, and merge via
1478+
[merge_arm_results] (same intersection-borrows / union-moves
1479+
semantics already used inline by [ExprMatch], now extracted to
1480+
one helper so all CFG-join sites agree). Finally runs after
1481+
the merge, deterministically, against the merged state.
1482+
13881483
Still deferred:
13891484
- Reborrow through indirection: `r = some_other_ref_var` (RHS not a
13901485
direct `&place`) does not yet copy the other binder's graph
13911486
entry — the ref-binding stays stale. Same limitation as
13921487
[record_ref_binding] for the let-graph path; would require
13931488
symmetric let/assign handling for ref-to-ref binding.
1394-
- Origin/region variables (Polonius surface) + loan-live-at-point
1395-
dataflow across CFG joins for `ExprHandle`/`ExprTry`/loops
1396-
(Slice C).
1489+
- Origin/region variables (true Polonius surface) — a region
1490+
var on each [TyRef]/[TyMut] with subset constraints and a
1491+
proper datalog-style loan-live-at-point solver. Architectural
1492+
change to the type system; ADR-gated.
1493+
- Loop soundness (`StmtWhile`/`StmtFor`): a single body pass
1494+
misses multi-iteration move conflicts. A 2-iteration check
1495+
would catch them but requires fixing the assignment-clears-
1496+
move imprecision first (assignment is currently treated as a
1497+
read of LHS, so `x = …` after a prior move spuriously fails).
1498+
Couple Slice C' with the StmtAssign clear-on-rewrite fix.
13971499
- Tighter integration with the quantity checker for captured
13981500
linears (Slice D).
13991501
*)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell
3+
//
4+
// CORE-01 pt3 Slice C / #177 anti-regression: a move performed in
5+
// the try-body must still be visible after the try/catch (this is
6+
// the conservative-correct path — body might have moved before
7+
// throwing, so the post-try state must reflect that the move
8+
// possibly happened). The Slice C arm-isolation refactor must not
9+
// drop body-side moves. Reading `y` after the try should error
10+
// UseAfterMove regardless of what the catch arm does.
11+
// Expected: Error UseAfterMove.
12+
13+
module SliceCBodyMovePersists;
14+
15+
fn drop_int(x: own Int) -> Unit = ();
16+
fn read_int(x: Int) -> Int = 0;
17+
18+
fn body_move_persists() -> Int {
19+
let y = 100;
20+
let r = try {
21+
drop_int(y);
22+
0
23+
} catch {
24+
_ => 0
25+
};
26+
read_int(y) + r
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// Copyright (c) 2026 Jonathan D.A. Jewell
3+
//
4+
// CORE-01 pt3 Slice C / #177: try/catch arm isolation. Each catch
5+
// arm is an alternative continuation — only one runs at runtime —
6+
// so moves performed in one arm must not pollute the state seen by
7+
// the next arm. Without Slice C, the second arm's `drop_int(y)`
8+
// would see `y` as already moved (by the first arm) and error
9+
// UseAfterMove. With Slice C, each arm snapshots the post-body
10+
// state and runs independently. Expected: Ok.
11+
12+
module SliceCCatchArmIsolation;
13+
14+
fn drop_int(x: own Int) -> Unit = ();
15+
16+
fn catch_arm_isolation(flag: Int) -> Unit {
17+
let y = 100;
18+
try {
19+
flag
20+
} catch {
21+
0 => drop_int(y),
22+
1 => drop_int(y),
23+
_ => ()
24+
}
25+
}

0 commit comments

Comments
 (0)