From 1e46f80d3fdebe68608173bfe1e7ffacc4abc930 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 12:34:29 +0000 Subject: [PATCH 1/5] 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/5] 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/5] 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/5] 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 42b206f72c4ba8e3e76f5210392f1d74c82a5674 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 15 Jun 2026 00:58:40 +0000 Subject: [PATCH 5/5] Decide flow decomposition from the type, with enum support Make the 'needs decomposition' decision a separate, type-driven notion from local mutability. mut_local (reassignable/mut-borrowed) remains the box-elaboration trigger per standard Rust semantics; whether to bind with flow decomposition is now derived independently from the type. Replace Type::contains_mut with Env::type_contains_mut, which resolves enum variants through the enum definitions (with a guard against recursive enum types) so a &mut reachable through an enum is detected too, rather than relying on mut_locals' Move rule to mark the enum local mutable. Add pass/fail ui tests for reborrowing a &mut field out of an enum param. https://claude.ai/code/session_01MQbByXbDZ8FxkhRra4nPfN --- src/analyze/basic_block.rs | 4 +- src/refine/env.rs | 48 ++++++++++++++++++- src/rty.rs | 17 ------- .../fail/reborrow_mut_field_of_enum_param.rs | 28 +++++++++++ .../pass/reborrow_mut_field_of_enum_param.rs | 28 +++++++++++ 5 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 tests/ui/fail/reborrow_mut_field_of_enum_param.rs create mode 100644 tests/ui/pass/reborrow_mut_field_of_enum_param.rs diff --git a/src/analyze/basic_block.rs b/src/analyze/basic_block.rs index 0583d708..3a444aad 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) || self.env.type_contains_mut(&rty.ty) { 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() + || self.env.type_contains_mut(¶m_unrefined_rty.ty) { self.env.mut_bind(local, param_unrefined_rty); } else { diff --git a/src/refine/env.rs b/src/refine/env.rs index d9c3dfbc..89742ffb 100644 --- a/src/refine/env.rs +++ b/src/refine/env.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use pretty::{termcolor, Pretty}; use rustc_abi::{FieldIdx, VariantIdx}; @@ -1013,6 +1013,52 @@ where } } + /// Returns `true` if a `&mut` can be reached from a place of this type by + /// following tuple/struct field, pointer, and enum-variant projections, so + /// that a sub-place may be mut-borrowed even when the enclosing local is not + /// reassignable. Such a local must be given flow-decomposed bindings. + /// + /// This is the "needs decomposition" notion, decided purely from the type + /// (resolving enum variants through the enum definitions). It is independent + /// of whether the local is reassignable/mut-borrowed: that is the separate + /// concern handled by box-elaboration before binding. A bare `Box`/`own` is + /// not itself reborrowable, so it only matters here when it wraps a `&mut`. + pub fn type_contains_mut(&self, ty: &rty::Type) -> bool { + self.type_contains_mut_rec(ty, &mut HashSet::new()) + } + + fn type_contains_mut_rec( + &self, + ty: &rty::Type, + visiting: &mut HashSet, + ) -> bool { + match ty { + rty::Type::Pointer(ty) => { + ty.is_mut() || self.type_contains_mut_rec(&ty.elem.ty, visiting) + } + rty::Type::Tuple(ty) => ty + .elems + .iter() + .any(|elem| self.type_contains_mut_rec(&elem.ty, visiting)), + rty::Type::Enum(ty) => { + // Guard against recursive enum types; a back edge reaches no new + // `&mut` (any reachable one is found along a finite path). + if !visiting.insert(ty.symbol.clone()) { + return false; + } + let def = self.enum_defs.enum_def(&ty.symbol); + let found = def.field_tys().any(|field_ty| { + let mut field_ty = rty::RefinedType::unrefined(field_ty.clone().vacuous()); + field_ty.instantiate_ty_params(ty.args.clone()); + self.type_contains_mut_rec(&field_ty.ty, visiting) + }); + visiting.remove(&ty.symbol); + found + } + _ => false, + } + } + fn locate_place(&self, place: Place<'_>) -> Var { let mut var = place.local.into(); 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(), diff --git a/tests/ui/fail/reborrow_mut_field_of_enum_param.rs b/tests/ui/fail/reborrow_mut_field_of_enum_param.rs new file mode 100644 index 00000000..8e5f39ea --- /dev/null +++ b/tests/ui/fail/reborrow_mut_field_of_enum_param.rs @@ -0,0 +1,28 @@ +//@error-in-other-file: Unsat + +// Companion to reborrow_mut_field_of_aggregate_param for enums: a `&mut` +// reachable through an enum variant. Exercises the enum arm of the +// "needs decomposition" check; binding must flow-decompose so the `&mut` +// pulled out of the enum can be reborrowed. + +enum E<'a> { + A(&'a mut i64), + B, +} + +fn bump(r: &mut i64) { + *r = 1; +} + +fn f(w: E) { + match w { + E::A(r) => bump(r), + E::B => {} + } +} + +fn main() { + let mut x = 0_i64; + f(E::A(&mut x)); + assert!(x == 0); +} diff --git a/tests/ui/pass/reborrow_mut_field_of_enum_param.rs b/tests/ui/pass/reborrow_mut_field_of_enum_param.rs new file mode 100644 index 00000000..c3d0c616 --- /dev/null +++ b/tests/ui/pass/reborrow_mut_field_of_enum_param.rs @@ -0,0 +1,28 @@ +//@check-pass + +// Companion to reborrow_mut_field_of_aggregate_param for enums: a `&mut` +// reachable through an enum variant. Exercises the enum arm of the +// "needs decomposition" check; binding must flow-decompose so the `&mut` +// pulled out of the enum can be reborrowed. + +enum E<'a> { + A(&'a mut i64), + B, +} + +fn bump(r: &mut i64) { + *r = 1; +} + +fn f(w: E) { + match w { + E::A(r) => bump(r), + E::B => {} + } +} + +fn main() { + let mut x = 0_i64; + f(E::A(&mut x)); + assert!(x == 1); +}