Skip to content

Commit b2c51c2

Browse files
committed
avoid generating drops for moved operands of calls
Currently, after a CALL terminator is created in MIR, we insert DROP statements for all of its operands -- even though they were just moved shortly before! These spurious drops are later removed, but not before causing borrow check errors. This PR series modifies the drop code to track operands that were moved and avoid creating drops for them. Right now, I'm only using this mechanism for calls, but it seems likely it could be used in more places.
1 parent e356983 commit b2c51c2

File tree

5 files changed

+147
-4
lines changed

5 files changed

+147
-4
lines changed

src/librustc/mir/mod.rs

+9
Original file line numberDiff line numberDiff line change
@@ -1917,6 +1917,15 @@ impl<'tcx> Place<'tcx> {
19171917
}
19181918
}
19191919

1920+
/// If this place represents a local variable like `_X` with no
1921+
/// projections, return `Some(_X)`.
1922+
pub fn as_local(&self) -> Option<Local> {
1923+
match self {
1924+
Place { projection: box [], base: PlaceBase::Local(l) } => Some(*l),
1925+
_ => None,
1926+
}
1927+
}
1928+
19201929
pub fn as_ref(&self) -> PlaceRef<'_, 'tcx> {
19211930
PlaceRef {
19221931
base: &self.base,

src/librustc_mir/build/expr/into.rs

+3
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
244244

245245
let success = this.cfg.start_new_block();
246246
let cleanup = this.diverge_cleanup();
247+
248+
this.record_operands_moved(&args);
249+
247250
this.cfg.terminate(
248251
block,
249252
source_info,

src/librustc_mir/build/scope.rs

+89-4
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ struct Scope {
110110
/// end of the vector (top of the stack) first.
111111
drops: Vec<DropData>,
112112

113+
moved_locals: Vec<Local>,
114+
113115
/// The cache for drop chain on “normal” exit into a particular BasicBlock.
114116
cached_exits: FxHashMap<(BasicBlock, region::Scope), BasicBlock>,
115117

@@ -159,7 +161,7 @@ struct CachedBlock {
159161
generator_drop: Option<BasicBlock>,
160162
}
161163

162-
#[derive(Debug)]
164+
#[derive(Debug, PartialEq, Eq)]
163165
pub(crate) enum DropKind {
164166
Value,
165167
Storage,
@@ -280,6 +282,7 @@ impl<'tcx> Scopes<'tcx> {
280282
region_scope: region_scope.0,
281283
region_scope_span: region_scope.1.span,
282284
drops: vec![],
285+
moved_locals: vec![],
283286
cached_generator_drop: None,
284287
cached_exits: Default::default(),
285288
cached_unwind: CachedBlock::default(),
@@ -484,7 +487,8 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
484487
block,
485488
unwind_to,
486489
self.arg_count,
487-
false,
490+
false, // not generator
491+
false, // not unwind path
488492
));
489493

490494
block.unit()
@@ -576,7 +580,8 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
576580
block,
577581
unwind_to,
578582
self.arg_count,
579-
false,
583+
false, // not generator
584+
false, // not unwind path
580585
));
581586

582587
scope = next_scope;
@@ -626,7 +631,8 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
626631
block,
627632
unwind_to,
628633
self.arg_count,
629-
true,
634+
true, // is generator
635+
true, // is cached path
630636
));
631637
}
632638

@@ -822,6 +828,75 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
822828
span_bug!(span, "region scope {:?} not in scope to drop {:?}", region_scope, local);
823829
}
824830

831+
/// Indicates that the "local operand" stored in `local` is
832+
/// *moved* at some point during execution (see `local_scope` for
833+
/// more information about what a "local operand" is -- in short,
834+
/// it's an intermediate operand created as part of preparing some
835+
/// MIR instruction). We use this information to suppress
836+
/// redundant drops on the non-unwind paths. This results in less
837+
/// MIR, but also avoids spurious borrow check errors
838+
/// (c.f. #64391).
839+
///
840+
/// Example: when compiling the call to `foo` here:
841+
///
842+
/// ```rust
843+
/// foo(bar(), ...)
844+
/// ```
845+
///
846+
/// we would evaluate `bar()` to an operand `_X`. We would also
847+
/// schedule `_X` to be dropped when the expression scope for
848+
/// `foo(bar())` is exited. This is relevant, for example, if the
849+
/// later arguments should unwind (it would ensure that `_X` gets
850+
/// dropped). However, if no unwind occurs, then `_X` will be
851+
/// unconditionally consumed by the `call`:
852+
///
853+
/// ```
854+
/// bb {
855+
/// ...
856+
/// _R = CALL(foo, _X, ...)
857+
/// }
858+
/// ```
859+
///
860+
/// However, `_X` is still registered to be dropped, and so if we
861+
/// do nothing else, we would generate a `DROP(_X)` that occurs
862+
/// after the call. This will later be optimized out by the
863+
/// drop-elaboation code, but in the meantime it can lead to
864+
/// spurious borrow-check errors -- the problem, ironically, is
865+
/// not the `DROP(_X)` itself, but the (spurious) unwind pathways
866+
/// that it creates. See #64391 for an example.
867+
pub fn record_operands_moved(
868+
&mut self,
869+
operands: &[Operand<'tcx>],
870+
) {
871+
let scope = match self.local_scope() {
872+
None => {
873+
// if there is no local scope, operands won't be dropped anyway
874+
return;
875+
}
876+
877+
Some(local_scope) => {
878+
self.scopes.iter_mut().find(|scope| scope.region_scope == local_scope)
879+
.unwrap_or_else(|| bug!("scope {:?} not found in scope list!", local_scope))
880+
}
881+
};
882+
883+
// look for moves of a local variable, like `MOVE(_X)`
884+
let locals_moved = operands.iter().flat_map(|operand| match operand {
885+
Operand::Copy(_) | Operand::Constant(_) => None,
886+
Operand::Move(place) => place.as_local(),
887+
});
888+
889+
for local in locals_moved {
890+
// check if we have a Drop for this operand and -- if so
891+
// -- add it to the list of moved operands. Note that this
892+
// local might not have been an operand created for this
893+
// call, it could come from other places too.
894+
if scope.drops.iter().any(|drop| drop.local == local && drop.kind == DropKind::Value) {
895+
scope.moved_locals.push(local);
896+
}
897+
}
898+
}
899+
825900
// Other
826901
// =====
827902
/// Branch based on a boolean condition.
@@ -1020,6 +1095,7 @@ fn build_scope_drops<'tcx>(
10201095
last_unwind_to: BasicBlock,
10211096
arg_count: usize,
10221097
generator_drop: bool,
1098+
is_cached_path: bool,
10231099
) -> BlockAnd<()> {
10241100
debug!("build_scope_drops({:?} -> {:?})", block, scope);
10251101

@@ -1046,6 +1122,15 @@ fn build_scope_drops<'tcx>(
10461122
let drop_data = &scope.drops[drop_idx];
10471123
let source_info = scope.source_info(drop_data.span);
10481124
let local = drop_data.local;
1125+
1126+
// If the operand has been moved, and we are not on an unwind
1127+
// path, then don't generate the drop. (We only take this into
1128+
// account for non-unwind paths so as not to disturb the
1129+
// caching mechanism.)
1130+
if !is_cached_path && scope.moved_locals.iter().any(|&o| o == local) {
1131+
continue;
1132+
}
1133+
10491134
match drop_data.kind {
10501135
DropKind::Value => {
10511136
let unwind_to = get_unwind_to(scope, is_generator, drop_idx, generator_drop)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Regression test for #64391
2+
//
3+
// As described on the issue, the (spurious) `DROP` inserted for the
4+
// `"".to_string()` value was causing a (spurious) unwind path that
5+
// led us to believe that the future might be dropped after `config`
6+
// had been dropped. This cannot, in fact, happen.
7+
8+
async fn connect() {
9+
let config = 666;
10+
connect2(&config, "".to_string()).await
11+
}
12+
13+
async fn connect2(_config: &u32, _tls: String) {
14+
unimplemented!()
15+
}
16+
17+
fn main() { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Regression test for issue #64433.
2+
//
3+
// See issue-64391-2.rs for more details, as that was fixed by the
4+
// same PR.
5+
//
6+
// check-pass
7+
8+
#[derive(Debug)]
9+
struct A<'a> {
10+
inner: Vec<&'a str>,
11+
}
12+
13+
struct B {}
14+
15+
impl B {
16+
async fn something_with_a(&mut self, a: A<'_>) -> Result<(), String> {
17+
println!("{:?}", a);
18+
Ok(())
19+
}
20+
}
21+
22+
async fn can_error(some_string: &str) -> Result<(), String> {
23+
let a = A { inner: vec![some_string, "foo"] };
24+
let mut b = B {};
25+
Ok(b.something_with_a(a).await.map(|_| ())?)
26+
}
27+
28+
fn main() {
29+
}

0 commit comments

Comments
 (0)