Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 25 additions & 24 deletions compiler/rustc_middle/src/mir/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,42 +732,40 @@ pub enum TerminatorKind<'tcx> {

/// The behavior of this statement differs significantly before and after drop elaboration.
///
/// After drop elaboration: `Drop` terminators are a complete nop for types that have no drop
/// After drop elaboration, `Drop` terminators are a complete nop for types that have no drop
/// glue. For other types, `Drop` terminators behave exactly like a call to
/// `core::mem::drop_glue` with a reference to the given place.
///
/// `Drop` before drop elaboration is a *conditional* execution of the drop glue. Specifically,
/// the `Drop` will be executed if...
/// Before drop elaboration, `Drop` behave as a *conditional* execution of the drop glue.
/// Specifically, the drop glue is executed if, among all statements executed within this
/// `Body`, an assignment to the place occurred more recently than a move out of it.
/// If a place is partially assigned-to or partially moved-from, the drop glue is only executed
/// on the assigned-to part.
///
/// **Needs clarification**: End of that sentence. This in effect should document the exact
/// behavior of drop elaboration. The following sounds vaguely right, but I'm not quite sure:
/// This considers the contents of a `Box` to be a sub-place, but does not consider indirect
/// assignments through references or pointers.
///
/// > The drop glue is executed if, among all statements executed within this `Body`, an assignment to
/// > the place or one of its "parents" occurred more recently than a move out of it. This does not
/// > consider indirect assignments.
/// **Async drop processing**:
/// MIR building detects possible async drops, and constructs a complete CFG. To correctly
/// handle the coroutine being dropped while itself drops, we need a 'drop' target

@RalfJung RalfJung Aug 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"while itself drops"? I can't quite parse this.

View changes since the review

/// similar to `Yield` terminator.

@RalfJung RalfJung Aug 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// similar to `Yield` terminator.
/// similar to `Yield` terminator. That's the `drop` field.

View changes since the review

///
/// The `replace` flag indicates whether this terminator was created as part of an assignment.
/// This should only be used for diagnostic purposes, and does not have any operational
/// meaning.
/// Drop elaboration later refines the set of useful async drops. If there is no need for an
/// async drop, it is downgraded to a sync drop by setting `drop` to `None` If this is an
/// actual async drop, it is expanded to an `await` loop over the `async_drop_in_place` or
/// `AsyncDrop::drop` coroutine.
///
/// Async drop processing:
/// MIR building detects possible async drops, and constructs a complete CFG. To correctly
/// handle the coroutine being dropped while itself drops, we need a 'drop' target
/// similar to `Yield` terminator (see `drops.build_mir::<CoroutineDrop>`).
///
/// Drop elaboration later refines the set of useful async drops. If there is no need for an
/// async drop, it is downgraded to a sync drop by setting `drop` to `None` If this is an
/// actual async drop, it is expanded to an `await` loop over the `async_drop_in_place` or
/// `AsyncDrop::drop` coroutine.
///
/// When a coroutine has any internal async drop, the coroutine drop function will be async
/// (generated by `create_coroutine_drop_shim_async`, not `create_coroutine_drop_shim`).
/// When a coroutine has any internal async drop, the coroutine drop function will be async
/// (generated by `create_coroutine_drop_shim_async`, not `create_coroutine_drop_shim`).
Drop {
place: Place<'tcx>,
target: BasicBlock,
unwind: UnwindAction,
/// The `replace` flag indicates whether this terminator was created as part of an
/// assignment. This should only be used for diagnostic purposes, and does not have any
/// operational meaning.
replace: bool,
/// Cleanup to be done if the coroutine is dropped at this suspend point (for async drop).
/// Cleanup to be done if the coroutine is dropped at this suspend point, for async drop.

@RalfJung RalfJung Aug 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Cleanup to be done if the coroutine is dropped at this suspend point, for async drop.
/// Cleanup to be done if the coroutine is dropped at this suspend point, for async drop.
/// Is always `None` after state machine lowering.

Is this right? The interpreter asserts it.

View changes since the review

drop: Option<BasicBlock>,
},

Expand Down Expand Up @@ -1288,6 +1286,9 @@ pub enum Operand<'tcx> {

/// Creates a value by performing loading the place, just like the `Copy` operand.
///
/// During MIR analyzes, it overwrites the place with `uninit` bytes and unschedules drops on
/// the given place.
Comment on lines +1289 to +1290

@RalfJung RalfJung Aug 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// During MIR analyzes, it overwrites the place with `uninit` bytes and unschedules drops on
/// the given place.
/// Before drop elaboration, this unschedules drops on the given place.

There's no overwriting happening, or at least it's unclear -- that's the point of the next paragraph.

View changes since the review

///
/// This *may* additionally overwrite the place with `uninit` bytes, depending on how we decide
/// in [UCG#188]. You should not emit MIR that may attempt a subsequent second load of this
/// place without first re-initializing it.
Expand Down
15 changes: 13 additions & 2 deletions compiler/rustc_mir_build/src/builder/expr/as_rvalue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use rustc_middle::ty::adjustment::PointerCoercion;
use rustc_middle::ty::cast::{CastTy, mir_cast_kind};
use rustc_middle::ty::util::IntTypeExt;
use rustc_middle::ty::{self, Ty, UpvarArgs};
use rustc_span::{DUMMY_SP, Span, Spanned};
use rustc_span::Span;
use tracing::debug;

use crate::builder::expr::as_place::PlaceBase;
Expand Down Expand Up @@ -74,6 +74,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
NeedsTemporary::No
)
);
this.record_operand_moved(&value_operand);
block.and(Rvalue::Repeat(value_operand, count))
}
}
Expand Down Expand Up @@ -219,6 +220,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
})
.collect();

for operand in fields.iter() {
this.record_operand_moved(operand);
}
block.and(Rvalue::Aggregate(Box::new(AggregateKind::Array(el_ty)), fields))
}
ExprKind::Tuple { ref fields } => {
Expand All @@ -240,6 +244,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
})
.collect();

for operand in fields.iter() {
this.record_operand_moved(operand);
}
block.and(Rvalue::Aggregate(Box::new(AggregateKind::Tuple), fields))
}
ExprKind::Closure(ClosureExpr {
Expand Down Expand Up @@ -342,6 +349,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
Box::new(AggregateKind::CoroutineClosure(closure_id.to_def_id(), args))
}
};
for operand in operands.iter() {
this.record_operand_moved(operand);
}
block.and(Rvalue::Aggregate(result, operands))
}
ExprKind::Assign { .. } | ExprKind::AssignOp { .. } => {
Expand Down Expand Up @@ -424,6 +434,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
NeedsTemporary::No,
)
);
this.record_operand_moved(&operand);
block.and(Rvalue::Use(operand, WithRetag::Yes))
}

Expand Down Expand Up @@ -647,7 +658,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
this.diverge_from(block);
block = success;
}
this.record_operands_moved(&[Spanned { node: value_operand, span: DUMMY_SP }]);
this.record_operand_moved(&value_operand);
}
block.and(Rvalue::Aggregate(Box::new(AggregateKind::Array(elem_ty)), IndexVec::new()))
}
Expand Down
15 changes: 12 additions & 3 deletions compiler/rustc_mir_build/src/builder/expr/into.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use rustc_data_structures::fx::FxHashMap;
use rustc_data_structures::stack::ensure_sufficient_stack;
use rustc_hir as hir;
use rustc_hir::lang_items::LangItem;
use rustc_index::IndexVec;
use rustc_middle::mir::*;
use rustc_middle::span_bug;
use rustc_middle::thir::*;
Expand Down Expand Up @@ -491,7 +492,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {

let success = this.cfg.start_new_block();

this.record_operands_moved(&args);
for operand in args.iter() {
this.record_operand_moved(&operand.node);
}

debug!("expr_into_dest: fn_span={:?}", fn_span);

Expand Down Expand Up @@ -631,7 +634,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
let variant = adt_def.variant(variant_index);
let field_names = variant.fields.indices();

let fields = match base {
let fields: IndexVec<_, _> = match base {
AdtExprBase::None => {
field_names.filter_map(|n| fields_map.get(&n).cloned()).collect()
}
Expand Down Expand Up @@ -696,6 +699,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
user_ty,
active_field_index,
));
for operand in fields.iter() {
this.record_operand_moved(operand);
}
this.cfg.push_assign(
block,
source_info,
Expand Down Expand Up @@ -844,7 +850,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
debug_assert!(Category::of(&expr.kind) == Some(Category::Place));

let place = unpack!(block = this.as_place(block, expr_id));
let rvalue = Rvalue::Use(this.consume_by_copy_or_move(place), WithRetag::Yes);
let operand = this.consume_by_copy_or_move(place);
this.record_operand_moved(&operand);
let rvalue = Rvalue::Use(operand, WithRetag::Yes);
this.cfg.push_assign(block, source_info, destination, rvalue);
block.unit()
}
Expand All @@ -870,6 +878,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
block =
this.as_operand(block, scope, value, LocalInfo::Boring, NeedsTemporary::No)
);
this.record_operand_moved(&value);
let resume = this.cfg.start_new_block();
this.cfg.terminate(
block,
Expand Down
4 changes: 3 additions & 1 deletion compiler/rustc_mir_build/src/builder/expr/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
})
.collect();

this.record_operands_moved(&args);
for operand in args.iter() {
this.record_operand_moved(&operand.node);
}

debug!("expr_into_dest: fn_span={:?}", fn_span);

Expand Down
35 changes: 16 additions & 19 deletions compiler/rustc_mir_build/src/builder/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ use std::mem;
use interpret::ErrorHandled;
use rustc_data_structures::fx::FxHashMap;
use rustc_hir::HirId;
use rustc_index::bit_set::GrowableBitSet;
use rustc_index::{IndexSlice, IndexVec};
use rustc_middle::middle::region;
use rustc_middle::mir::{self, *};
Expand Down Expand Up @@ -137,7 +138,7 @@ struct Scope {
/// end of the vector (top of the stack) first.
drops: Vec<DropData>,

moved_locals: Vec<Local>,
moved_locals: GrowableBitSet<Local>,

/// The drop index that will drop everything in and below this scope on an
/// unwind path.
Expand Down Expand Up @@ -494,7 +495,7 @@ impl<'tcx> Scopes<'tcx> {
source_scope: vis_scope,
region_scope,
drops: vec![],
moved_locals: vec![],
moved_locals: GrowableBitSet::new_empty(),
cached_unwind_block: None,
cached_coroutine_drop_block: None,
});
Expand Down Expand Up @@ -1522,7 +1523,7 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
self.schedule_drop(span, region_scope, local, DropKind::ForLint);
}

/// Indicates that the "local operand" stored in `local` is
/// Indicates that the "local operand" stored in `operand` is
/// *moved* at some point during execution (see `local_scope` for
/// more information about what a "local operand" is -- in short,
/// it's an intermediate operand created as part of preparing some
Expand Down Expand Up @@ -1558,26 +1559,22 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
/// spurious borrow-check errors -- the problem, ironically, is
/// not the `DROP(_X)` itself, but the (spurious) unwind pathways
/// that it creates. See #64391 for an example.
pub(crate) fn record_operands_moved(&mut self, operands: &[Spanned<Operand<'tcx>>]) {
#[instrument(level = "debug", skip(self))]
pub(crate) fn record_operand_moved(&mut self, operand: &Operand<'tcx>) {
let local_scope = self.local_scope();
let scope = self.scopes.scopes.last_mut().unwrap();

assert_eq!(scope.region_scope, local_scope, "local scope is not the topmost scope!",);
assert_eq!(scope.region_scope, local_scope, "local scope is not the topmost scope!");

// look for moves of a local variable, like `MOVE(_X)`
let locals_moved = operands.iter().flat_map(|operand| match operand.node {
Operand::Copy(_) | Operand::Constant(_) | Operand::RuntimeChecks(_) => None,
let local_moved = match operand {
Operand::Copy(_) | Operand::Constant(_) | Operand::RuntimeChecks(_) => return,
Operand::Move(place) => place.as_local(),
});
};

for local in locals_moved {
// check if we have a Drop for this operand and -- if so
// -- add it to the list of moved operands. Note that this
// local might not have been an operand created for this
// call, it could come from other places too.
if scope.drops.iter().any(|drop| drop.local == local && drop.kind == DropKind::Value) {
scope.moved_locals.push(local);
}
// We have a move of a local. Mark its drop to be skipped when leaving top scope.
// If `local` is not dropped by the topmost scope, this is a no-op.
if let Some(local) = local_moved {
scope.moved_locals.insert(local);
}
}

Expand Down Expand Up @@ -1878,7 +1875,7 @@ where
// path, then don't generate the drop. (We only take this into
// account for non-unwind paths so as not to disturb the
// caching mechanism.)
if scope.moved_locals.contains(&local) {
if scope.moved_locals.contains(local) {
continue;
}

Expand Down Expand Up @@ -1922,7 +1919,7 @@ where
// path, then don't generate the drop. (We only take this into
// account for non-unwind paths so as not to disturb the
// caching mechanism.)
if scope.moved_locals.contains(&local) {
if scope.moved_locals.contains(local) {
continue;
}

Expand Down
58 changes: 24 additions & 34 deletions tests/mir-opt/box_partial_move.maybe_move.ElaborateDrops.diff
Original file line number Diff line number Diff line change
Expand Up @@ -19,72 +19,62 @@
+ _5 = const true;
StorageLive(_3);
_3 = copy _1;
switchInt(move _3) -> [0: bb3, otherwise: bb1];
switchInt(move _3) -> [0: bb2, otherwise: bb1];
}

bb1: {
StorageLive(_4);
+ _5 = const false;
_4 = move (*_2);
_0 = Option::<String>::Some(move _4);
- drop(_4) -> [return: bb2, unwind: bb6];
+ goto -> bb2;
}

bb2: {
StorageDead(_4);
goto -> bb4;
goto -> bb3;
}

bb3: {
bb2: {
_0 = Option::<String>::None;
goto -> bb4;
goto -> bb3;
}

bb4: {
bb3: {
StorageDead(_3);
- drop(_2) -> [return: bb5, unwind continue];
+ goto -> bb13;
- drop(_2) -> [return: bb4, unwind continue];
+ goto -> bb11;
}

bb5: {
bb4: {
return;
}

bb6 (cleanup): {
- drop(_2) -> [return: bb7, unwind terminate(cleanup)];
+ goto -> bb7;
}

bb7 (cleanup): {
resume;
+ }
+
+ bb8: {
+ bb5 (cleanup): {
+ resume;
+ }
+
+ bb6: {
+ _6 = &mut _2;
+ _7 = <Box<String> as Drop>::drop(move _6) -> [return: bb5, unwind: bb7];
+ _7 = <Box<String> as Drop>::drop(move _6) -> [return: bb4, unwind: bb5];
+ }
+
+ bb9 (cleanup): {
+ bb7 (cleanup): {
+ _8 = &mut _2;
+ _9 = <Box<String> as Drop>::drop(move _8) -> [return: bb7, unwind terminate(cleanup)];
+ _9 = <Box<String> as Drop>::drop(move _8) -> [return: bb5, unwind terminate(cleanup)];
+ }
+
+ bb10: {
+ goto -> bb12;
+ bb8: {
+ goto -> bb10;
+ }
+
+ bb11: {
+ drop((*_10)) -> [return: bb8, unwind: bb9];
+ bb9: {
+ drop((*_10)) -> [return: bb6, unwind: bb7];
+ }
+
+ bb12: {
+ switchInt(copy _5) -> [0: bb8, otherwise: bb11];
+ bb10: {
+ switchInt(copy _5) -> [0: bb6, otherwise: bb9];
+ }
+
+ bb13: {
+ bb11: {
+ _10 = copy ((_2.0: std::ptr::Unique<std::string::String>).0: std::ptr::NonNull<std::string::String>) as *const std::string::String (Transmute);
+ goto -> bb10;
+ goto -> bb8;
}
}

Loading
Loading