Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

record fewer adjustment types in generator witnesses, avoid spurious drops in MIR construction #64584

Merged
9 changes: 9 additions & 0 deletions src/librustc/mir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,15 @@ impl<'tcx> Place<'tcx> {
}
}

/// If this place represents a local variable like `_X` with no
/// projections, return `Some(_X)`.
pub fn as_local(&self) -> Option<Local> {
nikomatsakis marked this conversation as resolved.
Show resolved Hide resolved
match self {
Place { projection: box [], base: PlaceBase::Local(l) } => Some(*l),
_ => None,
}
}

pub fn as_ref(&self) -> PlaceRef<'_, 'tcx> {
PlaceRef {
base: &self.base,
Expand Down
3 changes: 3 additions & 0 deletions src/librustc_mir/build/expr/into.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {

let success = this.cfg.start_new_block();
let cleanup = this.diverge_cleanup();

this.record_operands_moved(&args);

this.cfg.terminate(
block,
source_info,
Expand Down
135 changes: 110 additions & 25 deletions src/librustc_mir/build/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,25 +104,14 @@ struct Scope {
/// the span of that region_scope
region_scope_span: Span,

/// Whether there's anything to do for the cleanup path, that is,
/// when unwinding through this scope. This includes destructors,
/// but not StorageDead statements, which don't get emitted at all
/// for unwinding, for several reasons:
/// * clang doesn't emit llvm.lifetime.end for C++ unwinding
/// * LLVM's memory dependency analysis can't handle it atm
/// * polluting the cleanup MIR with StorageDead creates
/// landing pads even though there's no actual destructors
/// * freeing up stack space has no effect during unwinding
/// Note that for generators we do emit StorageDeads, for the
/// use of optimizations in the MIR generator transform.
needs_cleanup: bool,

/// set of places to drop when exiting this scope. This starts
/// out empty but grows as variables are declared during the
/// building process. This is a stack, so we always drop from the
/// end of the vector (top of the stack) first.
drops: Vec<DropData>,

moved_locals: Vec<Local>,

/// The cache for drop chain on “normal” exit into a particular BasicBlock.
cached_exits: FxHashMap<(BasicBlock, region::Scope), BasicBlock>,

Expand Down Expand Up @@ -172,7 +161,7 @@ struct CachedBlock {
generator_drop: Option<BasicBlock>,
}

#[derive(Debug)]
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum DropKind {
Value,
Storage,
Expand Down Expand Up @@ -202,8 +191,7 @@ pub enum BreakableTarget {

impl CachedBlock {
fn invalidate(&mut self) {
self.generator_drop = None;
self.unwind = None;
*self = CachedBlock::default();
}

fn get(&self, generator_drop: bool) -> Option<BasicBlock> {
Expand Down Expand Up @@ -261,6 +249,25 @@ impl Scope {
scope: self.source_scope
}
}


/// Whether there's anything to do for the cleanup path, that is,
/// when unwinding through this scope. This includes destructors,
/// but not StorageDead statements, which don't get emitted at all
/// for unwinding, for several reasons:
/// * clang doesn't emit llvm.lifetime.end for C++ unwinding
/// * LLVM's memory dependency analysis can't handle it atm
/// * polluting the cleanup MIR with StorageDead creates
/// landing pads even though there's no actual destructors
/// * freeing up stack space has no effect during unwinding
/// Note that for generators we do emit StorageDeads, for the
/// use of optimizations in the MIR generator transform.
fn needs_cleanup(&self) -> bool {
self.drops.iter().any(|drop| match drop.kind {
DropKind::Value => true,
DropKind::Storage => false,
})
}
}

impl<'tcx> Scopes<'tcx> {
Expand All @@ -274,8 +281,8 @@ impl<'tcx> Scopes<'tcx> {
source_scope: vis_scope,
region_scope: region_scope.0,
region_scope_span: region_scope.1.span,
needs_cleanup: false,
drops: vec![],
moved_locals: vec![],
cached_generator_drop: None,
cached_exits: Default::default(),
cached_unwind: CachedBlock::default(),
Expand All @@ -295,7 +302,7 @@ impl<'tcx> Scopes<'tcx> {

fn may_panic(&self, scope_count: usize) -> bool {
let len = self.len();
self.scopes[(len - scope_count)..].iter().any(|s| s.needs_cleanup)
self.scopes[(len - scope_count)..].iter().any(|s| s.needs_cleanup())
}

/// Finds the breakable scope for a given label. This is used for
Expand Down Expand Up @@ -480,7 +487,8 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
block,
unwind_to,
self.arg_count,
false,
false, // not generator
false, // not unwind path
));

block.unit()
Expand Down Expand Up @@ -572,7 +580,8 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
block,
unwind_to,
self.arg_count,
false,
false, // not generator
false, // not unwind path
));

scope = next_scope;
Expand Down Expand Up @@ -622,7 +631,8 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
block,
unwind_to,
self.arg_count,
true,
true, // is generator
true, // is cached path
));
}

Expand Down Expand Up @@ -801,10 +811,6 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
// cache of outer scope stays intact.
scope.invalidate_cache(!needs_drop, self.is_generator, this_scope);
if this_scope {
if let DropKind::Value = drop_kind {
scope.needs_cleanup = true;
}

let region_scope_span = region_scope.span(self.hir.tcx(),
&self.hir.region_scope_tree);
// Attribute scope exit drops to scope's closing brace.
Expand All @@ -822,6 +828,75 @@ impl<'a, 'tcx> Builder<'a, 'tcx> {
span_bug!(span, "region scope {:?} not in scope to drop {:?}", region_scope, local);
}

/// Indicates that the "local operand" stored in `local` 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
/// MIR instruction). We use this information to suppress
/// redundant drops on the non-unwind paths. This results in less
/// MIR, but also avoids spurious borrow check errors
/// (c.f. #64391).
///
/// Example: when compiling the call to `foo` here:
///
/// ```rust
/// foo(bar(), ...)
/// ```
///
/// we would evaluate `bar()` to an operand `_X`. We would also
/// schedule `_X` to be dropped when the expression scope for
/// `foo(bar())` is exited. This is relevant, for example, if the
/// later arguments should unwind (it would ensure that `_X` gets
/// dropped). However, if no unwind occurs, then `_X` will be
/// unconditionally consumed by the `call`:
///
/// ```
/// bb {
/// ...
/// _R = CALL(foo, _X, ...)
/// }
/// ```
///
/// However, `_X` is still registered to be dropped, and so if we
/// do nothing else, we would generate a `DROP(_X)` that occurs
/// after the call. This will later be optimized out by the
/// drop-elaboation code, but in the meantime it can lead to
/// 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.
nikomatsakis marked this conversation as resolved.
Show resolved Hide resolved
pub fn record_operands_moved(
&mut self,
operands: &[Operand<'tcx>],
) {
let scope = match self.local_scope() {
None => {
// if there is no local scope, operands won't be dropped anyway
return;
}

Some(local_scope) => {
self.scopes.iter_mut().find(|scope| scope.region_scope == local_scope)
.unwrap_or_else(|| bug!("scope {:?} not found in scope list!", local_scope))
}
};

// look for moves of a local variable, like `MOVE(_X)`
let locals_moved = operands.iter().flat_map(|operand| match operand {
Operand::Copy(_) | Operand::Constant(_) => None,
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);
}
}
}

// Other
// =====
/// Branch based on a boolean condition.
Expand Down Expand Up @@ -1020,6 +1095,7 @@ fn build_scope_drops<'tcx>(
last_unwind_to: BasicBlock,
arg_count: usize,
generator_drop: bool,
is_cached_path: bool,
) -> BlockAnd<()> {
debug!("build_scope_drops({:?} -> {:?})", block, scope);

Expand All @@ -1046,6 +1122,15 @@ fn build_scope_drops<'tcx>(
let drop_data = &scope.drops[drop_idx];
let source_info = scope.source_info(drop_data.span);
let local = drop_data.local;

// If the operand has been moved, and we are not on an unwind
// 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 !is_cached_path && scope.moved_locals.iter().any(|&o| o == local) {
continue;
}

match drop_data.kind {
DropKind::Value => {
let unwind_to = get_unwind_to(scope, is_generator, drop_idx, generator_drop)
Expand Down
33 changes: 27 additions & 6 deletions src/librustc_typeck/check/generator_interior.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,34 @@ impl<'a, 'tcx> Visitor<'tcx> for InteriorVisitor<'a, 'tcx> {

let scope = self.region_scope_tree.temporary_scope(expr.hir_id.local_id);

// Record the unadjusted type
// If there are adjustments, then record the final type --
// this is the actual value that is being produced.
if let Some(adjusted_ty) = self.fcx.tables.borrow().expr_ty_adjusted_opt(expr) {
self.record(adjusted_ty, scope, Some(expr), expr.span);
}

// Also record the unadjusted type (which is the only type if
// there are no adjustments). The reason for this is that the
// unadjusted value is sometimes a "temporary" that would wind
// up in a MIR temporary.
//
// As an example, consider an expression like `vec![].push()`.
// Here, the `vec![]` would wind up MIR stored into a
// temporary variable `t` which we can borrow to invoke
// `<Vec<_>>::push(&mut t)`.
//
// Note that an expression can have many adjustments, and we
// are just ignoring those intermediate types. This is because
// those intermediate values are always linearly "consumed" by
// the other adjustments, and hence would never be directly
// captured in the MIR.
//
// (Note that this partly relies on the fact that the `Deref`
// traits always return references, which means their content
// can be reborrowed without needing to spill to a temporary.
// If this were not the case, then we could conceivably have
// to create intermediate temporaries.)
Copy link
Contributor

Choose a reason for hiding this comment

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

I ❤️ these comments!

let ty = self.fcx.tables.borrow().expr_ty(expr);
self.record(ty, scope, Some(expr), expr.span);

// Also include the adjusted types, since these can result in MIR locals
for adjustment in self.fcx.tables.borrow().expr_adjustments(expr) {
self.record(adjustment.target, scope, Some(expr), expr.span);
}
nikomatsakis marked this conversation as resolved.
Show resolved Hide resolved
}
}
17 changes: 17 additions & 0 deletions src/test/ui/async-await/issues/issue-64391-2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Regression test for #64391
//
// As described on the issue, the (spurious) `DROP` inserted for the
// `"".to_string()` value was causing a (spurious) unwind path that
// led us to believe that the future might be dropped after `config`
// had been dropped. This cannot, in fact, happen.

async fn connect() {
let config = 666;
connect2(&config, "".to_string()).await
}

async fn connect2(_config: &u32, _tls: String) {
unimplemented!()
}

fn main() { }
29 changes: 29 additions & 0 deletions src/test/ui/async-await/issues/issue-64433.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Regression test for issue #64433.
//
// See issue-64391-2.rs for more details, as that was fixed by the
// same PR.
//
// check-pass

#[derive(Debug)]
struct A<'a> {
inner: Vec<&'a str>,
}

struct B {}

impl B {
async fn something_with_a(&mut self, a: A<'_>) -> Result<(), String> {
println!("{:?}", a);
Ok(())
}
}

async fn can_error(some_string: &str) -> Result<(), String> {
let a = A { inner: vec![some_string, "foo"] };
let mut b = B {};
Ok(b.something_with_a(a).await.map(|_| ())?)
}

fn main() {
}
20 changes: 20 additions & 0 deletions src/test/ui/async-await/issues/issue-64477.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Regression test for #64477.
//
// We were incorrectly claiming that the `f(x).await` future captured
// a value of type `T`, and hence that `T: Send` would have to hold.
//
// check-pass
// edition:2018

use std::future::Future;
use std::pin::Pin;

fn f<T>(_: &T) -> Pin<Box<dyn Future<Output = ()> + Send>> {
unimplemented!()
}

pub fn g<T: Sync>(x: &'static T) -> impl Future<Output = ()> + Send {
async move { f(x).await }
}

fn main() { }