Skip to content

Commit a3f8823

Browse files
author
Z User
committed
Merge PR #3892: hy-check(loans) extend outer A2/A3 loan windows across loops and track move-wrapped aliases (fixes #3891)
Author: Burapa-J. Two leak paths let an outlived loan look dead: loop-carried loans were compared against a raw stmt index (l.last) that precedes the loop, so an in-loop conflicting access escaped A2/A3; and alias extension parsed `q := move p` as non-Ident (Expr::Move) and returned early, so p's slot-loan window was never extended to q. loans.rs: - check_a2_conflict + map-slot conflict find: idx <= effective_loan_last(st, l) (loop-extended window) instead of raw l.last - alias source resolution: unwrap Paren AND Move wrappers in a loop before the Ident check (move-wrapped aliases now tracked) - diagnostic note reports eff_last (the real non-lexical window end) Verification (merged main, 3-stage battery): - cargo test --workspace: exit 0, 488 suites, 3126 passed, 0 failed - addr_of_loan standalone: 8/8 (new loop-carried O8/A2 + move-alias fixtures all reject with HYB-E3102/E3101 as specified) - integration files: 479 on disk (extends existing suite, no new file) - corpus harness: 135/135 verdict agreement, tripwire not fired - differential loop FULL: 45/45 accept-side, 90/90 reject-side - trial merge clean (2 files), no in-merge fixture fixups CREDIT: @Burapa-J
2 parents 0e3ed6a + a09fd46 commit a3f8823

2 files changed

Lines changed: 61 additions & 4 deletions

File tree

‎compiler/crates/hy-check/src/loans.rs‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -423,11 +423,12 @@ impl<'a> Checker<'a> {
423423
/// borrow-mode loan on the same handle (or an alias / may-share peer).
424424
fn check_a2_conflict(&mut self, st: &FnState, cr: &CallRef, root: &str, idx: usize) {
425425
let conflict = st.loans.iter().find(|l| {
426+
let eff_last = self.effective_loan_last(st, l);
426427
matches!(
427428
&l.target,
428429
LoanTarget::Handle { handle } if a3_paths_conflict(st, handle, root)
429430
) && l.created <= idx
430-
&& idx <= l.last
431+
&& idx <= eff_last
431432
});
432433
let Some(loan) = conflict else { return };
433434
let msg = format!("mutating call on `{}` while a borrow loan is live", root);
@@ -538,7 +539,11 @@ impl<'a> Checker<'a> {
538539
val: &Expr,
539540
idx: usize,
540541
) {
541-
let Expr::Ident { name: src, .. } = unwrap_parens(val) else {
542+
let mut cur = val;
543+
while let Expr::Paren { inner: e, .. } | Expr::Move { operand: e, .. } = cur {
544+
cur = e;
545+
}
546+
let Expr::Ident { name: src, .. } = cur else {
542547
return;
543548
};
544549
let alias_last = st.last_use.get(alias_name).copied().unwrap_or(idx).max(idx);
@@ -617,15 +622,17 @@ impl<'a> Checker<'a> {
617622
what: &str,
618623
) {
619624
let Some(loan) = st.loans.iter().find(|l| {
625+
let eff_last = self.effective_loan_last(st, l);
620626
matches!(&l.target, LoanTarget::MapSlot { binding, path: p, key: k }
621627
if owner != Some(binding.as_str())
622628
&& a3_paths_conflict(st, p, path)
623629
&& !matches!((k.as_deref(), key), (Some(a), Some(b)) if a != b))
624630
&& l.created <= idx
625-
&& idx <= l.last
631+
&& idx <= eff_last
626632
}) else {
627633
return;
628634
};
635+
let eff_last = self.effective_loan_last(st, loan);
629636
let LoanTarget::MapSlot { binding, .. } = &loan.target else {
630637
unreachable!("filtered above");
631638
};
@@ -636,7 +643,7 @@ impl<'a> Checker<'a> {
636643
let notes = vec![
637644
format!(
638645
"{} ({}:{}) is a unique loan on a slot of `{}`, live from creation to its last use (O8, non-lexical window ending at statement {})",
639-
loan.desc, self.path, loan.span.start, path, loan.last
646+
loan.desc, self.path, loan.span.start, path, eff_last
640647
),
641648
format!(
642649
"the loan lowers to copy-out / store-back, so this access and `{}` would silently overwrite each other — finish using `{}` first, or mutate the slot only through `{}`",

‎compiler/crates/hy-check/tests/addr_of_loan.rs‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,53 @@ fn addr_of_plain_local_is_not_affected() {
9696
r.diagnostics
9797
);
9898
}
99+
100+
#[test]
101+
fn loop_carried_o8_slot_alias_a2_handle_loan_and_move_alias_reject() {
102+
// 1. O8 slot loan created before a loop and used before a second slot loan / field store inside the loop body.
103+
let r_o8_loop = check(
104+
"struct node { v int }\n\
105+
fn main() {\n\
106+
m := map[string]node{\"k\": node{v: 1}}\n\
107+
p := &m[\"k\"]\n\
108+
for i in 0..2 {\n\
109+
_ = i\n\
110+
p.v = 2\n\
111+
q := &m[\"k\"]\n\
112+
q.v = 3\n\
113+
}\n\
114+
}\n",
115+
);
116+
assert_eq!(r_o8_loop.verdict, Verdict::Reject, "got {:?}", r_o8_loop.diagnostics);
117+
assert!(codes(&r_o8_loop).iter().any(|c| c == "HYB-E3102"));
118+
119+
// 2. A2 handle borrow loan created before a loop and read before a mutating handle call inside the loop body.
120+
let r_a2_loop = check(
121+
"import \"rust/serde_json\"\n\
122+
fn main() {\n\
123+
doc := serde_json.parse(`{}`)?\n\
124+
items := doc.array(\"items\")?\n\
125+
for i in 0..2 {\n\
126+
_ = i\n\
127+
fmt.println(items.len())\n\
128+
doc.set(\"k\", 1)?\n\
129+
}\n\
130+
}\n",
131+
);
132+
assert_eq!(r_a2_loop.verdict, Verdict::Reject, "got {:?}", r_a2_loop.diagnostics);
133+
assert!(codes(&r_a2_loop).iter().any(|c| c == "HYB-E3101"));
134+
135+
// 3. `q := move p` extends `p`'s slot-loan window to `q`'s last use so an intervening map insert is rejected.
136+
let r_move_alias = check(
137+
"struct node { v int }\n\
138+
fn main() {\n\
139+
m := map[string]node{\"k\": node{v: 1}}\n\
140+
p := &m[\"k\"]\n\
141+
q := move p\n\
142+
m[\"x\"] = node{v: 2}\n\
143+
q.v = 3\n\
144+
}\n",
145+
);
146+
assert_eq!(r_move_alias.verdict, Verdict::Reject, "got {:?}", r_move_alias.diagnostics);
147+
assert!(codes(&r_move_alias).iter().any(|c| c == "HYB-E3102"));
148+
}

0 commit comments

Comments
 (0)