diff --git a/compiler/rustc_hir_analysis/src/check/region.rs b/compiler/rustc_hir_analysis/src/check/region.rs index f5770b7312ddf..1c0f8785c6b1f 100644 --- a/compiler/rustc_hir_analysis/src/check/region.rs +++ b/compiler/rustc_hir_analysis/src/check/region.rs @@ -8,7 +8,7 @@ use std::mem; -use rustc_data_structures::fx::FxHashMap; +use rustc_data_structures::fx::{FxHashMap, FxHashSet}; use rustc_hir as hir; use rustc_hir::def::{CtorKind, DefKind, Res}; use rustc_hir::def_id::DefId; @@ -38,6 +38,14 @@ struct ScopeResolutionVisitor<'tcx> { cx: Context, + /// Tracks [extending] block expressions. This is used in performing lifetime extension on block + /// tail expressions: if we've already extended the temporary scopes of extending borrows within + /// a block's tail when checking a parent `let` statement or block, we don't want to re-extend + /// them to be shorter when checking the block itself. + /// + /// [extending]: https://doc.rust-lang.org/nightly/reference/destructors.html#extending-based-on-expressions + extended_blocks: FxHashSet, + extended_super_lets: FxHashMap>, } @@ -160,6 +168,20 @@ fn resolve_block<'tcx>( .backwards_incompatible_scope .insert(local_id, Scope { local_id, data: ScopeData::Node }); } + // If we haven't already checked for temporary lifetime extension due to a parent `let` + // statement initializer or block, do so. This, e.g., allows `temp()` in `{ &temp() }` + // to outlive the block even when the block itself is not in a `let` statement + // initializer. The same rules for `let` are used here, so non-extending borrows are + // unaffected: `{ f(&temp()) }` drops `temp()` at the end of the block. + // NB: This should be checked even if the block is from Rust 2021 or before. Macro + // expansion can result in nested blocks from different editions, and we always want to + // propagate the outermost extending lifetime to the innermost extending expressions. + if !visitor.extended_blocks.contains(&blk.hir_id.local_id) { + let blk_result_scope = prev_cx.parent.and_then(|blk_parent| { + visitor.scope_tree.default_temporary_scope(blk_parent).0 + }); + record_rvalue_scope_if_borrow_expr(visitor, tail_expr, blk_result_scope); + } resolve_expr(visitor, tail_expr, terminating); } } @@ -467,9 +489,10 @@ fn resolve_local<'tcx>( // A, but the inner rvalues `a()` and `b()` have an extended lifetime // due to rule C. + let mut extend_initializer = true; if let_kind == LetKind::Super { if let Some(scope) = visitor.extended_super_lets.remove(&pat.unwrap().hir_id.local_id) { - // This expression was lifetime-extended by a parent let binding. E.g. + // This expression was lifetime-extended by a parent let binding or block. E.g. // // let a = { // super let b = temp(); @@ -482,7 +505,8 @@ fn resolve_local<'tcx>( // `super let` to its own var_scope. We use that scope. visitor.cx.var_parent = scope; } else { - // This `super let` is not subject to lifetime extension from a parent let binding. E.g. + // This `super let` is not subject to lifetime extension from a parent let binding or + // block. E.g. // // identity({ super let x = temp(); &x }).method(); // @@ -490,17 +514,26 @@ fn resolve_local<'tcx>( // // Iterate up to the enclosing destruction scope to find the same scope that will also // be used for the result of the block itself. - while let Some(s) = visitor.cx.var_parent { - let parent = visitor.scope_tree.parent_map.get(&s).cloned(); - if let Some(Scope { data: ScopeData::Destruction, .. }) = parent { - break; - } - visitor.cx.var_parent = parent; + if let Some(inner_scope) = visitor.cx.var_parent { + (visitor.cx.var_parent, _) = visitor.scope_tree.default_temporary_scope(inner_scope) } + // Don't apply lifetime extension to the initializer of non-extended `super let`. + // This helps ensure that `{ super let x = &$EXPR; x }` is equivalent to `&$EXPR` in + // non-extending contexts: we want to avoid extending temporaries in `$EXPR` past what + // their temporary scopes would otherwise be (#145784). + // Currently, this shouldn't do anything. The discrepancy in #145784 was due to + // `{ super let x = &{ &temp() }; x }` extending `temp()` to outlive its immediately + // enclosing temporary scope (the block tail expression in Rust 2024), whereas in a + // non-extending context, `&{ &temp() }` would drop `temp()` at the end of the block. + // This particular quirk no longer exists: lifetime extension rules are applied to block + // tail expressions, so `temp()` is extended past the block in the latter case as well. + extend_initializer = false; } } - if let Some(expr) = init { + if let Some(expr) = init + && extend_initializer + { record_rvalue_scope_if_borrow_expr(visitor, expr, visitor.cx.var_parent); if let Some(pat) = pat { @@ -592,87 +625,89 @@ fn resolve_local<'tcx>( | PatKind::Err(_) => false, } } +} - /// If `expr` matches the `E&` grammar, then records an extended rvalue scope as appropriate: - /// - /// ```text - /// E& = & ET - /// | StructName { ..., f: E&, ... } - /// | [ ..., E&, ... ] - /// | ( ..., E&, ... ) - /// | {...; E&} - /// | { super let ... = E&; ... } - /// | if _ { ...; E& } else { ...; E& } - /// | match _ { ..., _ => E&, ... } - /// | box E& - /// | E& as ... - /// | ( E& ) - /// ``` - fn record_rvalue_scope_if_borrow_expr<'tcx>( - visitor: &mut ScopeResolutionVisitor<'tcx>, - expr: &hir::Expr<'_>, - blk_id: Option, - ) { - match expr.kind { - hir::ExprKind::AddrOf(_, _, subexpr) => { - record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id); - visitor.scope_tree.record_rvalue_candidate( - subexpr.hir_id, - RvalueCandidate { target: subexpr.hir_id.local_id, lifetime: blk_id }, - ); - } - hir::ExprKind::Struct(_, fields, _) => { - for field in fields { - record_rvalue_scope_if_borrow_expr(visitor, field.expr, blk_id); - } +/// If `expr` matches the `E&` grammar, then records an extended rvalue scope as appropriate: +/// +/// ```text +/// E& = & ET +/// | StructName { ..., f: E&, ... } +/// | Constructor(..., E&, ...) +/// | [ ..., E&, ... ] +/// | ( ..., E&, ... ) +/// | {...; E&} +/// | { super let ... = E&; ... } +/// | if _ { ...; E& } else { ...; E& } +/// | match _ { ..., _ => E&, ... } +/// | E& as ... +/// ``` +fn record_rvalue_scope_if_borrow_expr<'tcx>( + visitor: &mut ScopeResolutionVisitor<'tcx>, + expr: &hir::Expr<'_>, + blk_id: Option, +) { + match expr.kind { + hir::ExprKind::AddrOf(_, _, subexpr) => { + record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id); + visitor.scope_tree.record_rvalue_candidate( + subexpr.hir_id, + RvalueCandidate { target: subexpr.hir_id.local_id, lifetime: blk_id }, + ); + } + hir::ExprKind::Struct(_, fields, _) => { + for field in fields { + record_rvalue_scope_if_borrow_expr(visitor, field.expr, blk_id); } - hir::ExprKind::Array(subexprs) | hir::ExprKind::Tup(subexprs) => { - for subexpr in subexprs { - record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id); - } + } + hir::ExprKind::Array(subexprs) | hir::ExprKind::Tup(subexprs) => { + for subexpr in subexprs { + record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id); } - hir::ExprKind::Cast(subexpr, _) => { - record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id) + } + hir::ExprKind::Cast(subexpr, _) => { + record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id) + } + hir::ExprKind::Block(block, _) => { + // Mark the block as extending, so we know its extending borrows and `super let`s + // have extended scopes when checking the block itself. + visitor.extended_blocks.insert(block.hir_id.local_id); + if let Some(subexpr) = block.expr { + record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id); } - hir::ExprKind::Block(block, _) => { - if let Some(subexpr) = block.expr { - record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id); - } - for stmt in block.stmts { - if let hir::StmtKind::Let(local) = stmt.kind - && let Some(_) = local.super_ - { - visitor.extended_super_lets.insert(local.pat.hir_id.local_id, blk_id); - } + for stmt in block.stmts { + if let hir::StmtKind::Let(local) = stmt.kind + && let Some(_) = local.super_ + { + visitor.extended_super_lets.insert(local.pat.hir_id.local_id, blk_id); } } - hir::ExprKind::If(_, then_block, else_block) => { - record_rvalue_scope_if_borrow_expr(visitor, then_block, blk_id); - if let Some(else_block) = else_block { - record_rvalue_scope_if_borrow_expr(visitor, else_block, blk_id); - } + } + hir::ExprKind::If(_, then_block, else_block) => { + record_rvalue_scope_if_borrow_expr(visitor, then_block, blk_id); + if let Some(else_block) = else_block { + record_rvalue_scope_if_borrow_expr(visitor, else_block, blk_id); } - hir::ExprKind::Match(_, arms, _) => { - for arm in arms { - record_rvalue_scope_if_borrow_expr(visitor, arm.body, blk_id); - } + } + hir::ExprKind::Match(_, arms, _) => { + for arm in arms { + record_rvalue_scope_if_borrow_expr(visitor, arm.body, blk_id); } - hir::ExprKind::Call(func, args) => { - // Recurse into tuple constructors, such as `Some(&temp())`. - // - // That way, there is no difference between `Some(..)` and `Some { 0: .. }`, - // even though the former is syntactically a function call. - if let hir::ExprKind::Path(path) = &func.kind - && let hir::QPath::Resolved(None, path) = path - && let Res::SelfCtor(_) | Res::Def(DefKind::Ctor(_, CtorKind::Fn), _) = path.res - { - for arg in args { - record_rvalue_scope_if_borrow_expr(visitor, arg, blk_id); - } + } + hir::ExprKind::Call(func, args) => { + // Recurse into tuple constructors, such as `Some(&temp())`. + // + // That way, there is no difference between `Some(..)` and `Some { 0: .. }`, + // even though the former is syntactically a function call. + if let hir::ExprKind::Path(path) = &func.kind + && let hir::QPath::Resolved(None, path) = path + && let Res::SelfCtor(_) | Res::Def(DefKind::Ctor(_, CtorKind::Fn), _) = path.res + { + for arg in args { + record_rvalue_scope_if_borrow_expr(visitor, arg, blk_id); } } - _ => {} } + _ => {} } } @@ -813,6 +848,7 @@ pub(crate) fn region_scope_tree(tcx: TyCtxt<'_>, def_id: DefId) -> &ScopeTree { tcx, scope_tree: ScopeTree::default(), cx: Context { parent: None, var_parent: None }, + extended_blocks: Default::default(), extended_super_lets: Default::default(), }; diff --git a/compiler/rustc_middle/src/middle/region.rs b/compiler/rustc_middle/src/middle/region.rs index 857d041224fa9..5367e5edd496a 100644 --- a/compiler/rustc_middle/src/middle/region.rs +++ b/compiler/rustc_middle/src/middle/region.rs @@ -299,4 +299,43 @@ impl ScopeTree { true } + + /// Returns the scope of non-lifetime-extended temporaries within a given scope, as well as + /// whether we've recorded a potential backwards-incompatible change to lint on. + /// Returns `None` when no enclosing temporary scope is found, such as for static items. + pub fn default_temporary_scope(&self, inner: Scope) -> (Option, Option) { + let mut id = inner; + let mut backwards_incompatible = None; + + while let Some(&p) = self.parent_map.get(&id) { + match p.data { + ScopeData::Destruction => { + debug!("temporary_scope({inner:?}) = {id:?} [enclosing]"); + return (Some(id), backwards_incompatible); + } + ScopeData::IfThenRescope | ScopeData::MatchGuard => { + debug!("temporary_scope({inner:?}) = {p:?} [enclosing]"); + return (Some(p), backwards_incompatible); + } + ScopeData::Node + | ScopeData::CallSite + | ScopeData::Arguments + | ScopeData::IfThen + | ScopeData::Remainder(_) => { + // If we haven't already passed through a backwards-incompatible node, + // then check if we are passing through one now and record it if so. + // This is for now only working for cases where a temporary lifetime is + // *shortened*. + if backwards_incompatible.is_none() { + backwards_incompatible = + self.backwards_incompatible_scope.get(&p.local_id).copied(); + } + id = p + } + } + } + + debug!("temporary_scope({inner:?}) = None"); + (None, backwards_incompatible) + } } diff --git a/compiler/rustc_middle/src/ty/rvalue_scopes.rs b/compiler/rustc_middle/src/ty/rvalue_scopes.rs index 7dfe2d280514f..8b92e48ed1a07 100644 --- a/compiler/rustc_middle/src/ty/rvalue_scopes.rs +++ b/compiler/rustc_middle/src/ty/rvalue_scopes.rs @@ -35,41 +35,8 @@ impl RvalueScopes { // if there's one. Static items, for instance, won't // have an enclosing scope, hence no scope will be // returned. - let mut id = Scope { local_id: expr_id, data: ScopeData::Node }; - let mut backwards_incompatible = None; - - while let Some(&p) = region_scope_tree.parent_map.get(&id) { - match p.data { - ScopeData::Destruction => { - debug!("temporary_scope({expr_id:?}) = {id:?} [enclosing]"); - return (Some(id), backwards_incompatible); - } - ScopeData::IfThenRescope | ScopeData::MatchGuard => { - debug!("temporary_scope({expr_id:?}) = {p:?} [enclosing]"); - return (Some(p), backwards_incompatible); - } - ScopeData::Node - | ScopeData::CallSite - | ScopeData::Arguments - | ScopeData::IfThen - | ScopeData::Remainder(_) => { - // If we haven't already passed through a backwards-incompatible node, - // then check if we are passing through one now and record it if so. - // This is for now only working for cases where a temporary lifetime is - // *shortened*. - if backwards_incompatible.is_none() { - backwards_incompatible = region_scope_tree - .backwards_incompatible_scope - .get(&p.local_id) - .copied(); - } - id = p - } - } - } - - debug!("temporary_scope({expr_id:?}) = None"); - (None, backwards_incompatible) + region_scope_tree + .default_temporary_scope(Scope { local_id: expr_id, data: ScopeData::Node }) } /// Make an association between a sub-expression and an extended lifetime diff --git a/tests/mir-opt/pre-codegen/slice_index.slice_index_range.PreCodegen.after.panic-unwind.mir b/tests/mir-opt/pre-codegen/slice_index.slice_index_range.PreCodegen.after.panic-unwind.mir index d4b86b9633acd..044e9e6ad5575 100644 --- a/tests/mir-opt/pre-codegen/slice_index.slice_index_range.PreCodegen.after.panic-unwind.mir +++ b/tests/mir-opt/pre-codegen/slice_index.slice_index_range.PreCodegen.after.panic-unwind.mir @@ -66,8 +66,8 @@ fn slice_index_range(_1: &[u32], _2: std::ops::Range) -> &[u32] { StorageDead(_10); StorageDead(_9); _0 = &(*_12); - StorageDead(_12); StorageDead(_8); + StorageDead(_12); return; } diff --git a/tests/ui/borrowck/format-args-temporary-scopes.e2024.stderr b/tests/ui/borrowck/format-args-temporary-scopes.e2024.stderr new file mode 100644 index 0000000000000..1bff2e2f42637 --- /dev/null +++ b/tests/ui/borrowck/format-args-temporary-scopes.e2024.stderr @@ -0,0 +1,27 @@ +error[E0716]: temporary value dropped while borrowed + --> $DIR/format-args-temporary-scopes.rs:18:48 + | +LL | println!("{:?}", { std::convert::identity(&temp()) }); + | --------------------------^^^^^^--- + | | | | + | | | temporary value is freed at the end of this statement + | | creates a temporary value which is freed while still in use + | borrow later used here + | + = note: consider using a `let` binding to create a longer lived value + +error[E0716]: temporary value dropped while borrowed + --> $DIR/format-args-temporary-scopes.rs:25:52 + | +LL | println!("{:?}{:?}", { std::convert::identity(&temp()) }, ()); + | --------------------------^^^^^^--- + | | | | + | | | temporary value is freed at the end of this statement + | | creates a temporary value which is freed while still in use + | borrow later used here + | + = note: consider using a `let` binding to create a longer lived value + +error: aborting due to 2 previous errors + +For more information about this error, try `rustc --explain E0716`. diff --git a/tests/ui/borrowck/format-args-temporary-scopes.rs b/tests/ui/borrowck/format-args-temporary-scopes.rs new file mode 100644 index 0000000000000..6d8566ae3282b --- /dev/null +++ b/tests/ui/borrowck/format-args-temporary-scopes.rs @@ -0,0 +1,27 @@ +//! Test for #145784 as it relates to format arguments: arguments to macros such as `println!` +//! should obey normal temporary scoping rules. +//@ revisions: e2021 e2024 +//@ [e2021] check-pass +//@ [e2021] edition: 2021 +//@ [e2024] edition: 2024 + +fn temp() {} + +fn main() { + // In Rust 2024, block tail expressions are temporary scopes, but temporary lifetime extension + // rules apply: `&temp()` here is an extending borrow expression, so `temp()`'s lifetime is + // extended past the block. + println!("{:?}", { &temp() }); + + // Arguments to function calls aren't extending expressions, so `temp()` is dropped at the end + // of the block in Rust 2024. + println!("{:?}", { std::convert::identity(&temp()) }); + //[e2024]~^ ERROR: temporary value dropped while borrowed [E0716] + + // In Rust 1.89, `format_args!` had different lifetime extension behavior dependent on how many + // formatting arguments it had (#145880), so let's test that too. + println!("{:?}{:?}", { &temp() }, ()); + + println!("{:?}{:?}", { std::convert::identity(&temp()) }, ()); + //[e2024]~^ ERROR: temporary value dropped while borrowed [E0716] +} diff --git a/tests/ui/drop/destructuring-assignments.rs b/tests/ui/drop/destructuring-assignments.rs new file mode 100644 index 0000000000000..c90dba1855055 --- /dev/null +++ b/tests/ui/drop/destructuring-assignments.rs @@ -0,0 +1,68 @@ +// Test drop order for destructuring assignments against +// other expressions they should be consistent with. +// +// See: +// +// - https://github.com/rust-lang/rust/pull/145838 +// +// Original author: TC +// Date: 2025-08-30 +//@ edition: 2024 +//@ run-pass + +#![allow(unused_must_use)] + +fn main() { + assert_drop_order(1..=3, |e| { + &({ &raw const *&e.log(2) }, drop(e.log(1))); + drop(e.log(3)); + }); + assert_drop_order(1..=3, |e| { + { let _x; _x = &({ &raw const *&e.log(2) }, drop(e.log(1))); } + drop(e.log(3)); + }); + assert_drop_order(1..=3, |e| { + _ = &({ &raw const *&e.log(2) }, drop(e.log(1))); + drop(e.log(3)); + }); + assert_drop_order(1..=3, |e| { + { let _ = &({ &raw const *&e.log(2) }, drop(e.log(1))); } + drop(e.log(3)); + }); + assert_drop_order(1..=3, |e| { + let _x; let _y; + (_x, _y) = ({ &raw const *&e.log(2) }, drop(e.log(1))); + drop(e.log(3)); + }); +} + +// # Test scaffolding... + +use core::cell::RefCell; + +struct DropOrder(RefCell>); +struct LogDrop<'o>(&'o DropOrder, u64); + +impl DropOrder { + fn log(&self, n: u64) -> LogDrop<'_> { + LogDrop(self, n) + } +} + +impl<'o> Drop for LogDrop<'o> { + fn drop(&mut self) { + self.0 .0.borrow_mut().push(self.1); + } +} + +#[track_caller] +fn assert_drop_order( + ex: impl IntoIterator, + f: impl Fn(&DropOrder), +) { + let order = DropOrder(RefCell::new(Vec::new())); + f(&order); + let order = order.0.into_inner(); + let expected: Vec = ex.into_iter().collect(); + assert_eq!(order, expected); +} diff --git a/tests/ui/drop/if-let-super-let.rs b/tests/ui/drop/if-let-super-let.rs new file mode 100644 index 0000000000000..c6543e6d3dc97 --- /dev/null +++ b/tests/ui/drop/if-let-super-let.rs @@ -0,0 +1,112 @@ +//! Test for #145328: ensure the lifetime of a `super let` binding within an `if let` scrutinee is +//! at most the scope of the `if` condition's temporaries. Additionally, test `pin!` since it's +//! implemented in terms of `super let` and exposes this behavior. +//@ run-pass +//@ revisions: e2021 e2024 +//@ [e2021] edition: 2021 +//@ [e2024] edition: 2024 + +#![feature(if_let_guard)] +#![feature(super_let)] +#![expect(irrefutable_let_patterns)] + +use std::cell::RefCell; +use std::pin::pin; + +fn main() { + // The `super let` bindings here should have the same scope as `if let` temporaries. + // In Rust 2021, this means it lives past the end of the `if` expression. + // In Rust 2024, this means it lives to the end of the `if`'s success block. + assert_drop_order(0..=2, |o| { + #[cfg(e2021)] + ( + if let _ = { super let _x = o.log(2); } { o.push(0) }, + o.push(1), + ); + #[cfg(e2024)] + ( + if let _ = { super let _x = o.log(1); } { o.push(0) }, + o.push(2), + ); + }); + assert_drop_order(0..=2, |o| { + #[cfg(e2021)] + ( + if let true = { super let _x = o.log(2); false } {} else { o.push(0) }, + o.push(1), + ); + #[cfg(e2024)] + ( + if let true = { super let _x = o.log(0); false } {} else { o.push(1) }, + o.push(2), + ); + }); + + // `pin!` should behave likewise. + assert_drop_order(0..=2, |o| { + #[cfg(e2021)] (if let _ = pin!(o.log(2)) { o.push(0) }, o.push(1)); + #[cfg(e2024)] (if let _ = pin!(o.log(1)) { o.push(0) }, o.push(2)); + }); + assert_drop_order(0..=2, |o| { + #[cfg(e2021)] + ( + if let None = Some(pin!(o.log(2))) {} else { o.push(0) }, + o.push(1), + ); + #[cfg(e2024)] + ( + if let None = Some(pin!(o.log(0))) {} else { o.push(1) }, + o.push(2), + ); + }); + + // `super let` bindings' scope should also be consistent with `if let` temporaries in guards. + // Here, that means the `super let` binding in the second guard condition operand should be + // dropped before the first operand's temporary. This is consistent across Editions. + assert_drop_order(0..=1, |o| { + match () { + _ if let _ = o.log(1) + && let _ = { super let _x = o.log(0); } => {} + _ => unreachable!(), + } + }); + assert_drop_order(0..=1, |o| { + match () { + _ if let _ = o.log(1) + && let _ = pin!(o.log(0)) => {} + _ => unreachable!(), + } + }); +} + +// # Test scaffolding... + +struct DropOrder(RefCell>); +struct LogDrop<'o>(&'o DropOrder, u64); + +impl DropOrder { + fn log(&self, n: u64) -> LogDrop<'_> { + LogDrop(self, n) + } + fn push(&self, n: u64) { + self.0.borrow_mut().push(n); + } +} + +impl<'o> Drop for LogDrop<'o> { + fn drop(&mut self) { + self.0.push(self.1); + } +} + +#[track_caller] +fn assert_drop_order( + ex: impl IntoIterator, + f: impl Fn(&DropOrder), +) { + let order = DropOrder(RefCell::new(Vec::new())); + f(&order); + let order = order.0.into_inner(); + let expected: Vec = ex.into_iter().collect(); + assert_eq!(order, expected); +} diff --git a/tests/ui/drop/lint-tail-expr-drop-order-borrowck.rs b/tests/ui/drop/lint-tail-expr-drop-order-borrowck.rs index e8368b0a369d8..1ff3a78aa86b0 100644 --- a/tests/ui/drop/lint-tail-expr-drop-order-borrowck.rs +++ b/tests/ui/drop/lint-tail-expr-drop-order-borrowck.rs @@ -28,7 +28,7 @@ fn should_lint_with_unsafe_block() { fn should_lint_with_big_block() { fn f(_: T) {} f({ - &mut || 0 + std::convert::identity(&mut || 0) //~^ ERROR: relative drop order changing //~| WARN: this changes meaning in Rust 2024 //~| NOTE: this temporary value will be dropped at the end of the block @@ -40,7 +40,7 @@ fn should_lint_with_big_block() { fn another_temp_that_is_copy_in_arg() { fn f() {} fn g(_: &()) {} - g({ &f() }); + g({ std::convert::identity(&f()) }); //~^ ERROR: relative drop order changing //~| WARN: this changes meaning in Rust 2024 //~| NOTE: this temporary value will be dropped at the end of the block @@ -48,4 +48,9 @@ fn another_temp_that_is_copy_in_arg() { //~| NOTE: for more information, see } +fn do_not_lint_extending_borrow() { + fn f(_: T) {} + f({ &mut || 0 }); +} + fn main() {} diff --git a/tests/ui/drop/lint-tail-expr-drop-order-borrowck.stderr b/tests/ui/drop/lint-tail-expr-drop-order-borrowck.stderr index 2eeda8ac387fd..106080007784e 100644 --- a/tests/ui/drop/lint-tail-expr-drop-order-borrowck.stderr +++ b/tests/ui/drop/lint-tail-expr-drop-order-borrowck.stderr @@ -26,22 +26,22 @@ LL | f(unsafe { String::new().as_str() }.len()); = note: for more information, see error: relative drop order changing in Rust 2024 - --> $DIR/lint-tail-expr-drop-order-borrowck.rs:31:9 + --> $DIR/lint-tail-expr-drop-order-borrowck.rs:31:32 | -LL | &mut || 0 - | ^^^^^^^^^ - | | - | this temporary value will be dropped at the end of the block +LL | std::convert::identity(&mut || 0) + | -----------------------^^^^^^^^^- + | | | + | | this temporary value will be dropped at the end of the block | borrow later used here | = warning: this changes meaning in Rust 2024 = note: for more information, see error: relative drop order changing in Rust 2024 - --> $DIR/lint-tail-expr-drop-order-borrowck.rs:43:9 + --> $DIR/lint-tail-expr-drop-order-borrowck.rs:43:32 | -LL | g({ &f() }); - | - ^^^^ this temporary value will be dropped at the end of the block +LL | g({ std::convert::identity(&f()) }); + | - ^^^^ this temporary value will be dropped at the end of the block | | | borrow later used by call | diff --git a/tests/ui/drop/super-let-tail-expr-drop-order.rs b/tests/ui/drop/super-let-tail-expr-drop-order.rs new file mode 100644 index 0000000000000..3504bde887067 --- /dev/null +++ b/tests/ui/drop/super-let-tail-expr-drop-order.rs @@ -0,0 +1,155 @@ +//! Test for #145784. This tests three things: +//! +//! - In Rust 2024, temporary lifetime extension applies to block tail expressions, such that +//! extending borrows and `super let`s in block tails are extended to outlive the result of the +//! block. +//! +//! - Since `super let`'s initializer has the same temporary scope as the variable scope of its +//! bindings, this means that lifetime extension can effectively see through `super let`. +//! +//! - In particular, the argument to `pin!` is an extending expression, and the argument of an +//! extending `pin!` has an extended temporary scope. The lifetime of the argument, as well those +//! of extending borrows and `super lets` within it, should match the result of the `pin!`, +//! regardless of whether it itself is extended by a parent expression. +//! +//! For more information on temporary lifetime extension, see +//! https://doc.rust-lang.org/nightly/reference/destructors.html#temporary-lifetime-extension +//! +//! For tests that `super let` initializers aren't temporary drop scopes, and tests for +//! lifetime-extended `super let`, see tests/ui/borrowck/super-let-lifetime-and-drop.rs +//@ run-pass +//@ revisions: e2021 e2024 +//@ [e2021] edition: 2021 +//@ [e2024] edition: 2024 + +#![feature(super_let)] +#![allow(unused_braces)] + +use std::cell::RefCell; +use std::pin::pin; + +fn f(_: LogDrop<'_>, x: T) -> T { x } + +fn main() { + // Test block arguments to non-extending `pin!`. + // In Rust 2021, block tail expressions aren't temporary drop scopes, so their temporaries + // should outlive the `pin!` invocation. + // In Rust 2024, extending borrows within block tail expressions have extended lifetimes to + // outlive result of the block, so the end result is the same in this case. + // By nesting two `pin!` calls, this ensures extending borrows in the inner `pin!` outlive the + // outer `pin!`. + assert_drop_order(1..=3, |o| { + ( + pin!(( + pin!({ &o.log(3) as *const LogDrop<'_> }), + drop(o.log(1)), + )), + drop(o.log(2)), + ); + }); + + // The same holds for `super let` initializers in non-extending expressions. + assert_drop_order(1..=4, |o| { + ( + { + super let _ = { + super let _ = { &o.log(4) as *const LogDrop<'_> }; + drop(o.log(1)) + }; + drop(o.log(2)) + }, + drop(o.log(3)), + ); + }); + + // Within an extending expression, the argument to `pin!` is also an extending expression, + // allowing borrow operands in block tail expressions to have extended lifetimes. + assert_drop_order(1..=2, |o| { + let _ = pin!({ &o.log(2) as *const LogDrop<'_> }); + drop(o.log(1)); + }); + + // The same holds for `super let` initializers in extending expressions. + assert_drop_order(1..=2, |o| { + let _ = { super let _ = { &o.log(2) as *const LogDrop<'_> }; }; + drop(o.log(1)); + }); + + // We have extending borrow expressions within an extending block + // expression (within an extending borrow expression) within a + // non-extending expresion within the initializer expression. + // These two should be the same. + assert_drop_order(1..=3, |e| { + let _v = f(e.log(1), &{ &raw const *&e.log(2) }); + drop(e.log(3)); + }); + assert_drop_order(1..=3, |e| { + let _v = f(e.log(1), { + super let v = &{ &raw const *&e.log(2) }; + v + }); + drop(e.log(3)); + }); + + // We have extending borrow expressions within a non-extending + // expression within the initializer expression. + // + // These two should be the same. + assert_drop_order(1..=3, |e| { + let _v = f(e.log(1), &&raw const *&e.log(2)); + drop(e.log(3)); + }); + assert_drop_order(1..=3, |e| { + let _v = f(e.log(1), { + super let v = &&raw const *&e.log(2); + v + }); + drop(e.log(3)); + }); + + // We have extending borrow expressions within an extending block + // expression (within an extending borrow expression) within the + // initializer expression. + // + // These two should be the same. + assert_drop_order(1..=2, |e| { + let _v = &{ &raw const *&e.log(2) }; + drop(e.log(1)); + }); + assert_drop_order(1..=2, |e| { + let _v = { + super let v = &{ &raw const *&e.log(2) }; + v + }; + drop(e.log(1)); + }); +} + +// # Test scaffolding... + +struct DropOrder(RefCell>); +struct LogDrop<'o>(&'o DropOrder, u64); + +impl DropOrder { + fn log(&self, n: u64) -> LogDrop<'_> { + LogDrop(self, n) + } +} + +impl<'o> Drop for LogDrop<'o> { + fn drop(&mut self) { + self.0.0.borrow_mut().push(self.1); + } +} + +#[track_caller] +fn assert_drop_order( + ex: impl IntoIterator, + f: impl Fn(&DropOrder), +) { + let order = DropOrder(RefCell::new(Vec::new())); + f(&order); + let order = order.0.into_inner(); + let expected: Vec = ex.into_iter().collect(); + assert_eq!(order, expected); +}