From 1e46f80d3fdebe68608173bfe1e7ffacc4abc930 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 12:34:29 +0000 Subject: [PATCH 1/6] Bind aggregate params containing &mut with flow bindings Reborrowing a `&mut`-typed field out of an aggregate (tuple/struct) parameter panicked with "deref unbound var" in `Env::locate_place`. Such an aggregate parameter is an immutable local whose type is a tuple, so it took the `immut_bind` path and was stored without flow bindings. The reborrow elaboration then failed to resolve the field projection to the inner `&mut`. Detect mutable references reachable through aggregate and pointer projections via a new `Type::contains_mut` and route such parameters through the flow-decomposing `mut_bind` path, matching how a top-level `&mut` parameter is already handled. Fixes #125 https://claude.ai/code/session_01MQbByXbDZ8FxkhRra4nPfN --- src/analyze/basic_block.rs | 4 +-- src/rty.rs | 14 ++++++++++ .../reborrow_mut_field_of_aggregate_param.rs | 26 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs diff --git a/src/analyze/basic_block.rs b/src/analyze/basic_block.rs index 835a6623..0583d708 100644 --- a/src/analyze/basic_block.rs +++ b/src/analyze/basic_block.rs @@ -184,7 +184,7 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { } else { rty }; - if self.is_mut_local(local) || rty.ty.is_mut() { + if self.is_mut_local(local) || rty.ty.contains_mut() { self.env.mut_bind(local, rty); } else { self.env.immut_bind(local, rty); @@ -1278,7 +1278,7 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { match bb_ty.param_kind(param_idx) { BasicBlockTypeParamKind::Local(local, _) => { if bb_ty.mutbl_of_param(param_idx).unwrap().is_mut() - || param_unrefined_rty.ty.is_mut() + || param_unrefined_rty.ty.contains_mut() { self.env.mut_bind(local, param_unrefined_rty); } else { diff --git a/src/rty.rs b/src/rty.rs index 7311c4d7..430b90ee 100644 --- a/src/rty.rs +++ b/src/rty.rs @@ -1093,6 +1093,20 @@ impl Type { } } + /// Returns `true` if this type is, or transitively contains through + /// aggregate (tuple/struct) and pointer projections, a mutable reference. + /// + /// Such types need flow-decomposed bindings (see `Env::bind_*`) so that + /// projections reaching the `&mut` can be located and reborrowed, even when + /// the enclosing local is itself immutable. + pub fn contains_mut(&self) -> bool { + match self { + Type::Pointer(ty) => ty.is_mut() || ty.elem.ty.contains_mut(), + Type::Tuple(ty) => ty.elems.iter().any(|elem| elem.ty.contains_mut()), + _ => false, + } + } + pub fn is_own(&self) -> bool { match self { Type::Pointer(ty) => ty.is_own(), diff --git a/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs b/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs new file mode 100644 index 00000000..f024cab8 --- /dev/null +++ b/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs @@ -0,0 +1,26 @@ +//@check-pass +//@compile-flags: -Adead_code -C debug-assertions=off + +// Regression test for #125: reborrowing a `&mut`-typed field out of an +// aggregate (tuple/struct) parameter used to panic with "deref unbound var" +// because the aggregate parameter was bound without flow bindings. + +fn bump(r: &mut i64) { + *r = 1; +} + +// tuple parameter +fn f(w: (&mut i64,)) { + bump(w.0); +} + +struct Wrap<'a> { + r: &'a mut i64, +} + +// struct parameter +fn g(w: Wrap) { + bump(w.r); +} + +fn main() {} From fc37ba1ed2fd511b61ddc7854feec24c818c2fcc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 12:41:04 +0000 Subject: [PATCH 2/6] Pair reborrow_mut_field_of_aggregate_param ui test (pass/fail) Follow the repo convention of pairing each ui test in pass/ and fail/. Drive the test from main so verification has a concrete assertion to check, and add the fail counterpart asserting the negated condition. --- .../reborrow_mut_field_of_aggregate_param.rs | 19 +++++++++++++++++++ .../reborrow_mut_field_of_aggregate_param.rs | 15 ++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 tests/ui/fail/reborrow_mut_field_of_aggregate_param.rs diff --git a/tests/ui/fail/reborrow_mut_field_of_aggregate_param.rs b/tests/ui/fail/reborrow_mut_field_of_aggregate_param.rs new file mode 100644 index 00000000..01e88f73 --- /dev/null +++ b/tests/ui/fail/reborrow_mut_field_of_aggregate_param.rs @@ -0,0 +1,19 @@ +//@error-in-other-file: Unsat + +// Regression test for #125: reborrowing a `&mut`-typed field out of an +// aggregate (tuple/struct) parameter used to panic with "deref unbound var" +// because the aggregate parameter was bound without flow bindings. + +fn bump(r: &mut i64) { + *r = 1; +} + +fn f(w: (&mut i64,)) { + bump(w.0); +} + +fn main() { + let mut x = 0_i64; + f((&mut x,)); + assert!(x == 0); +} diff --git a/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs b/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs index f024cab8..910a9ba6 100644 --- a/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs +++ b/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs @@ -1,5 +1,4 @@ //@check-pass -//@compile-flags: -Adead_code -C debug-assertions=off // Regression test for #125: reborrowing a `&mut`-typed field out of an // aggregate (tuple/struct) parameter used to panic with "deref unbound var" @@ -9,18 +8,12 @@ fn bump(r: &mut i64) { *r = 1; } -// tuple parameter fn f(w: (&mut i64,)) { bump(w.0); } -struct Wrap<'a> { - r: &'a mut i64, +fn main() { + let mut x = 0_i64; + f((&mut x,)); + assert!(x == 1); } - -// struct parameter -fn g(w: Wrap) { - bump(w.r); -} - -fn main() {} From eff90c61d883a16d1cfb808dba7f4fbeaed4cc1a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 14:02:43 +0000 Subject: [PATCH 3/6] Trim contains_mut doc comment --- src/rty.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/rty.rs b/src/rty.rs index 430b90ee..b6cb6499 100644 --- a/src/rty.rs +++ b/src/rty.rs @@ -1095,10 +1095,6 @@ impl Type { /// Returns `true` if this type is, or transitively contains through /// aggregate (tuple/struct) and pointer projections, a mutable reference. - /// - /// Such types need flow-decomposed bindings (see `Env::bind_*`) so that - /// projections reaching the `&mut` can be located and reborrowed, even when - /// the enclosing local is itself immutable. pub fn contains_mut(&self) -> bool { match self { Type::Pointer(ty) => ty.is_mut() || ty.elem.ty.contains_mut(), From bc72f58e17e585b4abb27232ef2725c285bb4214 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 14:27:40 +0000 Subject: [PATCH 4/6] Clarify contains_mut doc: scope and enum rationale --- src/rty.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/rty.rs b/src/rty.rs index b6cb6499..8a2c9cd4 100644 --- a/src/rty.rs +++ b/src/rty.rs @@ -1093,8 +1093,15 @@ impl Type { } } - /// Returns `true` if this type is, or transitively contains through - /// aggregate (tuple/struct) and pointer projections, a mutable reference. + /// Returns `true` if a `&mut` can be reached from a place of this type by + /// following tuple/struct field and pointer projections, so that a sub-place + /// may be mut-borrowed even when the enclosing local is not marked mutable. + /// Such a local must be given flow-decomposed bindings. + /// + /// This intentionally does not descend into enums: reaching an enum's `&mut` + /// field requires a pattern match that moves the reference out, which already + /// marks the enum local mutable (see `mut_locals`' `Move` rule), routing it + /// through the flow-decomposing bind path regardless of this check. pub fn contains_mut(&self) -> bool { match self { Type::Pointer(ty) => ty.is_mut() || ty.elem.ty.contains_mut(), From 29fe11721f69af8df175dd38a7dcaf6e10989315 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 09:44:54 +0000 Subject: [PATCH 5/6] Capture partial reborrows of &mut fields in mut_locals #125 panicked ("deref unbound var") when reborrowing a &mut field out of an aggregate parameter. Root cause: the partial reborrow of `w.0` was never detected, so the base local was neither box-elaborated nor flow- decomposed, and resolving the field projection during reborrow failed. A &mut field read out of an aggregate appears as `Operand::Copy(_1.0)` (the pointer value is copied), but `mut_locals`' operand rule only matched `Operand::Move`, so the base local `_1` was never marked. ReborrowVisitor reborrows any &mut-typed operand place regardless of Copy/Move, so the two were inconsistent. Match Copy as well, narrowing the type test from `is_mutable_ptr()` to a &mut reference: `is_mutable_ptr()` also covers `*mut` raw pointers, which are Copy and are never reborrowed, so widening to Copy without the reference restriction would over-mark every raw-pointer copy. This makes the type-based `contains_mut` workaround unnecessary; revert it and remove the helper. Keep the pass/fail regression tests. Fixes #125 https://claude.ai/code/session_01MQbByXbDZ8FxkhRra4nPfN --- src/analyze/basic_block.rs | 4 ++-- src/analyze/local_def.rs | 21 ++++++++++++++------- src/rty.rs | 17 ----------------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/analyze/basic_block.rs b/src/analyze/basic_block.rs index 0583d708..835a6623 100644 --- a/src/analyze/basic_block.rs +++ b/src/analyze/basic_block.rs @@ -184,7 +184,7 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { } else { rty }; - if self.is_mut_local(local) || rty.ty.contains_mut() { + if self.is_mut_local(local) || rty.ty.is_mut() { self.env.mut_bind(local, rty); } else { self.env.immut_bind(local, rty); @@ -1278,7 +1278,7 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { match bb_ty.param_kind(param_idx) { BasicBlockTypeParamKind::Local(local, _) => { if bb_ty.mutbl_of_param(param_idx).unwrap().is_mut() - || param_unrefined_rty.ty.contains_mut() + || param_unrefined_rty.ty.is_mut() { self.env.mut_bind(local, param_unrefined_rty); } else { diff --git a/src/analyze/local_def.rs b/src/analyze/local_def.rs index f1f5bdd2..4f194abc 100644 --- a/src/analyze/local_def.rs +++ b/src/analyze/local_def.rs @@ -649,13 +649,20 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { } fn visit_operand(&mut self, operand: &mir::Operand<'tcx>, location: mir::Location) { - if let mir::Operand::Move(place) = operand { - // to be reborrowed; see analyze::basic_block::visitor - if place - .ty(&self.body.local_decls, self.tcx) - .ty - .is_mutable_ptr() - { + // to be reborrowed; see analyze::basic_block::visitor. The reborrow + // pass reborrows any `&mut`-typed operand place — Copy or Move — + // including a `&mut` field copied out of an aggregate + // (`copy (_1.0)`), which is a *partial* reborrow of the base local + // `_1`. Mark the base local to match it. + // + // The type test is `&mut` reference specifically, not + // `is_mutable_ptr()`: the latter also matches `*mut` raw pointers, + // which are `Copy` and are never reborrowed. + if let mir::Operand::Move(place) | mir::Operand::Copy(place) = operand { + if matches!( + place.ty(&self.body.local_decls, self.tcx).ty.kind(), + mir_ty::TyKind::Ref(_, _, m) if m.is_mut() + ) { self.locals.insert(place.local); } } diff --git a/src/rty.rs b/src/rty.rs index 8a2c9cd4..7311c4d7 100644 --- a/src/rty.rs +++ b/src/rty.rs @@ -1093,23 +1093,6 @@ impl Type { } } - /// Returns `true` if a `&mut` can be reached from a place of this type by - /// following tuple/struct field and pointer projections, so that a sub-place - /// may be mut-borrowed even when the enclosing local is not marked mutable. - /// Such a local must be given flow-decomposed bindings. - /// - /// This intentionally does not descend into enums: reaching an enum's `&mut` - /// field requires a pattern match that moves the reference out, which already - /// marks the enum local mutable (see `mut_locals`' `Move` rule), routing it - /// through the flow-decomposing bind path regardless of this check. - pub fn contains_mut(&self) -> bool { - match self { - Type::Pointer(ty) => ty.is_mut() || ty.elem.ty.contains_mut(), - Type::Tuple(ty) => ty.elems.iter().any(|elem| elem.ty.contains_mut()), - _ => false, - } - } - pub fn is_own(&self) -> bool { match self { Type::Pointer(ty) => ty.is_own(), From 4bfb82ff62d693e68fe0d27993a08479b79e0d67 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 13:02:40 +0000 Subject: [PATCH 6/6] Trim verbose comments --- src/analyze/local_def.rs | 13 ++++--------- .../fail/reborrow_mut_field_of_aggregate_param.rs | 5 ++--- .../pass/reborrow_mut_field_of_aggregate_param.rs | 5 ++--- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/analyze/local_def.rs b/src/analyze/local_def.rs index 4f194abc..cf5eaf63 100644 --- a/src/analyze/local_def.rs +++ b/src/analyze/local_def.rs @@ -649,15 +649,10 @@ impl<'tcx, 'ctx> Analyzer<'tcx, 'ctx> { } fn visit_operand(&mut self, operand: &mir::Operand<'tcx>, location: mir::Location) { - // to be reborrowed; see analyze::basic_block::visitor. The reborrow - // pass reborrows any `&mut`-typed operand place — Copy or Move — - // including a `&mut` field copied out of an aggregate - // (`copy (_1.0)`), which is a *partial* reborrow of the base local - // `_1`. Mark the base local to match it. - // - // The type test is `&mut` reference specifically, not - // `is_mutable_ptr()`: the latter also matches `*mut` raw pointers, - // which are `Copy` and are never reborrowed. + // to be reborrowed; see analyze::basic_block::visitor. A `&mut` + // field read out of an aggregate is `copy (_1.0)`, so match Copy + // too to mark the base local. Reference only: `is_mutable_ptr()` + // also covers `*mut`, which is Copy but never reborrowed. if let mir::Operand::Move(place) | mir::Operand::Copy(place) = operand { if matches!( place.ty(&self.body.local_decls, self.tcx).ty.kind(), diff --git a/tests/ui/fail/reborrow_mut_field_of_aggregate_param.rs b/tests/ui/fail/reborrow_mut_field_of_aggregate_param.rs index 01e88f73..d5451ea6 100644 --- a/tests/ui/fail/reborrow_mut_field_of_aggregate_param.rs +++ b/tests/ui/fail/reborrow_mut_field_of_aggregate_param.rs @@ -1,8 +1,7 @@ //@error-in-other-file: Unsat -// Regression test for #125: reborrowing a `&mut`-typed field out of an -// aggregate (tuple/struct) parameter used to panic with "deref unbound var" -// because the aggregate parameter was bound without flow bindings. +// Regression test for #125: reborrowing a `&mut` field out of an aggregate +// parameter used to panic with "deref unbound var". fn bump(r: &mut i64) { *r = 1; diff --git a/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs b/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs index 910a9ba6..89a855d6 100644 --- a/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs +++ b/tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs @@ -1,8 +1,7 @@ //@check-pass -// Regression test for #125: reborrowing a `&mut`-typed field out of an -// aggregate (tuple/struct) parameter used to panic with "deref unbound var" -// because the aggregate parameter was bound without flow bindings. +// Regression test for #125: reborrowing a `&mut` field out of an aggregate +// parameter used to panic with "deref unbound var". fn bump(r: &mut i64) { *r = 1;