From afadb7a777da15ba4013344ac834677c7daa4a67 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 11:11:40 +0000 Subject: [PATCH] Do not drop moved-out owned locals at the move site Fixes #41. A closure capturing `&mut` stored in a tuple/struct field made thrust derive a contradictory environment and unsoundly accept programs that can panic. The drop-point analysis treats a local's last use as its drop site. For a value moved into an aggregate (e.g. a closure moved into a tuple), this dropped the moved-out temporary at the move site, prematurely resolving the captured reference's prophecy (final == current) to its construction value. The later call mutating through the closure then contradicted that, making the environment inconsistent and any following assertion vacuously "safe". Owned (non-reference) locals whose ownership is transferred away by a move are now excluded from the drop set: their drop obligation belongs to the destination. Moved references are left untouched since ReborrowVisitor/ RustCallVisitor turn them into reborrows, so the source remains live. https://claude.ai/code/session_01HHar2z2xTNwffns5SF7wRe --- src/analyze/basic_block/drop_point.rs | 45 +++++++++++++++++++++++++++ tests/ui/fail/closure_field_mut.rs | 13 ++++++++ tests/ui/pass/closure_field_mut.rs | 13 ++++++++ 3 files changed, 71 insertions(+) create mode 100644 tests/ui/fail/closure_field_mut.rs create mode 100644 tests/ui/pass/closure_field_mut.rs diff --git a/src/analyze/basic_block/drop_point.rs b/src/analyze/basic_block/drop_point.rs index be08b10..919acb3 100644 --- a/src/analyze/basic_block/drop_point.rs +++ b/src/analyze/basic_block/drop_point.rs @@ -57,6 +57,50 @@ pub struct DropPointsBuilder<'mir, 'tcx> { bb_ins_cache: HashMap>, } +/// Locals whose ownership is fully transferred away by the statement (or +/// terminator) at `statement_index`. Such a local is left uninitialized, so its +/// drop obligation (including resolving any mutable-borrow prophecies it owns) +/// moves to the destination and it must not be dropped at the move site. +/// +/// Only owned (non-reference) operands are reported: `move`d references are +/// turned into reborrows by `ReborrowVisitor`/`RustCallVisitor`, so the source +/// local remains live and must still be dropped. +fn moved_locals<'tcx>( + body: &Body<'tcx>, + bb: BasicBlock, + statement_index: usize, +) -> DenseBitSet { + struct Visitor<'a, 'tcx> { + body: &'a Body<'tcx>, + locals: DenseBitSet, + } + impl<'tcx> mir::visit::Visitor<'tcx> for Visitor<'_, 'tcx> { + fn visit_operand(&mut self, operand: &mir::Operand<'tcx>, _location: mir::Location) { + if let mir::Operand::Move(place) = operand { + if place.projection.is_empty() && !self.body.local_decls[place.local].ty.is_ref() { + self.locals.insert(place.local); + } + } + } + } + let mut visitor = Visitor { + body, + locals: DenseBitSet::new_empty(body.local_decls.len()), + }; + let loc = mir::Location { + statement_index, + block: bb, + }; + let data = &body.basic_blocks[bb]; + use mir::visit::Visitor as _; + if statement_index < data.statements.len() { + visitor.visit_statement(&data.statements[statement_index], loc); + } else if let Some(tmnt) = &data.terminator { + visitor.visit_terminator(tmnt, loc); + } + visitor.locals +} + fn def_local<'tcx>(data: &mir::BasicBlockData<'tcx>, statement_index: usize) -> Option { struct Visitor { local: Option, @@ -135,6 +179,7 @@ impl<'mir, 'tcx> DropPointsBuilder<'mir, 'tcx> { t.insert(def); } t.subtract(&last_live_locals); + t.subtract(&moved_locals(self.body, bb, statement_index)); t }; last_live_locals = live_locals; diff --git a/tests/ui/fail/closure_field_mut.rs b/tests/ui/fail/closure_field_mut.rs new file mode 100644 index 0000000..95a66b1 --- /dev/null +++ b/tests/ui/fail/closure_field_mut.rs @@ -0,0 +1,13 @@ +//@error-in-other-file: Unsat + +fn main() { + let mut call_count = 0; + let mut s = ( + || { + call_count = 1; + }, + 0, + ); + (s.0)(); + assert!(call_count == 0); +} diff --git a/tests/ui/pass/closure_field_mut.rs b/tests/ui/pass/closure_field_mut.rs new file mode 100644 index 0000000..9ae0b3e --- /dev/null +++ b/tests/ui/pass/closure_field_mut.rs @@ -0,0 +1,13 @@ +//@check-pass + +fn main() { + let mut call_count = 0; + let mut s = ( + || { + call_count = 1; + }, + 0, + ); + (s.0)(); + assert!(call_count == 1); +}