Skip to content

Commit 78995b7

Browse files
committed
lifetime-extend extending borrows in block tail expressions
1 parent 0db1276 commit 78995b7

File tree

7 files changed

+109
-102
lines changed

7 files changed

+109
-102
lines changed

compiler/rustc_hir_analysis/src/check/region.rs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
99
use std::mem;
1010

11-
use rustc_data_structures::fx::FxHashMap;
11+
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
1212
use rustc_hir as hir;
1313
use rustc_hir::def::{CtorKind, DefKind, Res};
1414
use rustc_hir::def_id::DefId;
@@ -38,6 +38,14 @@ struct ScopeResolutionVisitor<'tcx> {
3838

3939
cx: Context,
4040

41+
/// Tracks [extending] block expressions. This is used in performing lifetime extension on block
42+
/// tail expressions: if we've already extended the temporary scopes of extending borrows within
43+
/// a block's tail when checking a parent `let` statement or block, we don't want to re-extend
44+
/// them to be shorter when checking the block itself.
45+
///
46+
/// [extending]: https://doc.rust-lang.org/nightly/reference/destructors.html#extending-based-on-expressions
47+
extended_blocks: FxHashSet<hir::ItemLocalId>,
48+
4149
extended_super_lets: FxHashMap<hir::ItemLocalId, Option<Scope>>,
4250
}
4351

@@ -160,6 +168,20 @@ fn resolve_block<'tcx>(
160168
.backwards_incompatible_scope
161169
.insert(local_id, Scope { local_id, data: ScopeData::Node });
162170
}
171+
// If we haven't already checked for temporary lifetime extension due to a parent `let`
172+
// statement initializer or block, do so. This, e.g., allows `temp()` in `{ &temp() }`
173+
// to outlive the block even when the block itself is not in a `let` statement
174+
// initializer. The same rules for `let` are used here, so non-extending borrows are
175+
// unaffected: `{ f(&temp()) }` drops `temp()` at the end of the block.
176+
// NB: This should be checked even if the block is from Rust 2021 or before. Macro
177+
// expansion can result in nested blocks from different editions, and we always want to
178+
// propagate the outermost extending lifetime to the innermost extending expressions.
179+
if !visitor.extended_blocks.contains(&blk.hir_id.local_id) {
180+
let blk_result_scope = prev_cx.parent.and_then(|blk_parent| {
181+
visitor.scope_tree.default_temporary_scope(blk_parent).0
182+
});
183+
record_rvalue_scope_if_borrow_expr(visitor, tail_expr, blk_result_scope);
184+
}
163185
resolve_expr(visitor, tail_expr, terminating);
164186
}
165187
}
@@ -470,7 +492,7 @@ fn resolve_local<'tcx>(
470492
let mut extend_initializer = true;
471493
if let_kind == LetKind::Super {
472494
if let Some(scope) = visitor.extended_super_lets.remove(&pat.unwrap().hir_id.local_id) {
473-
// This expression was lifetime-extended by a parent let binding. E.g.
495+
// This expression was lifetime-extended by a parent let binding or block. E.g.
474496
//
475497
// let a = {
476498
// super let b = temp();
@@ -483,7 +505,8 @@ fn resolve_local<'tcx>(
483505
// `super let` to its own var_scope. We use that scope.
484506
visitor.cx.var_parent = scope;
485507
} else {
486-
// This `super let` is not subject to lifetime extension from a parent let binding. E.g.
508+
// This `super let` is not subject to lifetime extension from a parent let binding or
509+
// block. E.g.
487510
//
488511
// identity({ super let x = temp(); &x }).method();
489512
//
@@ -494,10 +517,16 @@ fn resolve_local<'tcx>(
494517
if let Some(inner_scope) = visitor.cx.var_parent {
495518
(visitor.cx.var_parent, _) = visitor.scope_tree.default_temporary_scope(inner_scope)
496519
}
497-
// Don't lifetime-extend child `super let`s or block tail expressions' temporaries in
498-
// the initializer when this `super let` is not itself extended by a parent `let`
499-
// (#145784). Block tail expressions are temporary drop scopes in Editions 2024 and
500-
// later, their temps shouldn't outlive the block in e.g. `f(pin!({ &temp() }))`.
520+
// Don't apply lifetime extension to the initializer of non-extended `super let`.
521+
// This helps ensure that `{ super let x = &$EXPR; x }` is equivalent to `&$EXPR` in
522+
// non-extending contexts: we want to avoid extending temporaries in `$EXPR` past what
523+
// their temporary scopes would otherwise be (#145784).
524+
// Currently, this shouldn't do anything. The discrepancy in #145784 was due to
525+
// `{ super let x = &{ &temp() }; x }` extending `temp()` to outlive its immediately
526+
// enclosing temporary scope (the block tail expresssion in Rust 2024), whereas in a
527+
// non-extending context, `&{ &temp() }` would drop `temp()` at the end of the block.
528+
// This particular quirk no longer exists: lifetime extension rules are applied to block
529+
// tail expressions, so `temp()` is extended past the block in the latter case as well.
501530
extend_initializer = false;
502531
}
503532
}
@@ -639,6 +668,9 @@ fn record_rvalue_scope_if_borrow_expr<'tcx>(
639668
record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id)
640669
}
641670
hir::ExprKind::Block(block, _) => {
671+
// Mark the block as extending, so we know its extending borrows and `super let`s
672+
// have extended scopes when checking the block itelf.
673+
visitor.extended_blocks.insert(block.hir_id.local_id);
642674
if let Some(subexpr) = block.expr {
643675
record_rvalue_scope_if_borrow_expr(visitor, subexpr, blk_id);
644676
}
@@ -816,6 +848,7 @@ pub(crate) fn region_scope_tree(tcx: TyCtxt<'_>, def_id: DefId) -> &ScopeTree {
816848
tcx,
817849
scope_tree: ScopeTree::default(),
818850
cx: Context { parent: None, var_parent: None },
851+
extended_blocks: Default::default(),
819852
extended_super_lets: Default::default(),
820853
};
821854

tests/ui/borrowck/format-args-temporary-scopes.e2024.stderr

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
error[E0716]: temporary value dropped while borrowed
2-
--> $DIR/format-args-temporary-scopes.rs:13:25
2+
--> $DIR/format-args-temporary-scopes.rs:18:48
33
|
4-
LL | println!("{:?}", { &temp() });
5-
| ---^^^^^---
6-
| | | |
7-
| | | temporary value is freed at the end of this statement
8-
| | creates a temporary value which is freed while still in use
4+
LL | println!("{:?}", { std::convert::identity(&temp()) });
5+
| --------------------------^^^^^^---
6+
| | | |
7+
| | | temporary value is freed at the end of this statement
8+
| | creates a temporary value which is freed while still in use
99
| borrow later used here
1010
|
1111
= note: consider using a `let` binding to create a longer lived value
1212

1313
error[E0716]: temporary value dropped while borrowed
14-
--> $DIR/format-args-temporary-scopes.rs:19:29
14+
--> $DIR/format-args-temporary-scopes.rs:25:52
1515
|
16-
LL | println!("{:?}{:?}", { &temp() }, ());
17-
| ---^^^^^---
18-
| | | |
19-
| | | temporary value is freed at the end of this statement
20-
| | creates a temporary value which is freed while still in use
16+
LL | println!("{:?}{:?}", { std::convert::identity(&temp()) }, ());
17+
| --------------------------^^^^^^---
18+
| | | |
19+
| | | temporary value is freed at the end of this statement
20+
| | creates a temporary value which is freed while still in use
2121
| borrow later used here
2222
|
2323
= note: consider using a `let` binding to create a longer lived value

tests/ui/borrowck/format-args-temporary-scopes.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@
88
fn temp() {}
99

1010
fn main() {
11-
// In Rust 2024, block tail expressions are temporary scopes, so the result of `temp()` is
12-
// dropped after evaluating `&temp()`.
11+
// In Rust 2024, block tail expressions are temporary scopes, but temporary lifetime extension
12+
// rules apply: `&temp()` here is an extending borrow expression, so `temp()`'s lifetime is
13+
// extended past the block.
1314
println!("{:?}", { &temp() });
15+
16+
// Arguments to function calls aren't extending expressions, so `temp()` is dropped at the end
17+
// of the block in Rust 2024.
18+
println!("{:?}", { std::convert::identity(&temp()) });
1419
//[e2024]~^ ERROR: temporary value dropped while borrowed [E0716]
1520

16-
// In Rust 1.89, `format_args!` extended the lifetime of all extending expressions in its
17-
// arguments when provided with two or more arguments. This caused the result of `temp()` to
18-
// outlive the result of the block, making this compile.
21+
// In Rust 1.89, `format_args!` had different lifetime extension behavior dependent on how many
22+
// formatting arguments it had (#145880), so let's test that too.
1923
println!("{:?}{:?}", { &temp() }, ());
24+
25+
println!("{:?}{:?}", { std::convert::identity(&temp()) }, ());
2026
//[e2024]~^ ERROR: temporary value dropped while borrowed [E0716]
2127
}

tests/ui/drop/destructuring-assignments.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414

1515
fn main() {
1616
assert_drop_order(1..=3, |e| {
17-
&({ &raw const *&e.log(1) }, drop(e.log(2)));
17+
&({ &raw const *&e.log(2) }, drop(e.log(1)));
1818
drop(e.log(3));
1919
});
2020
assert_drop_order(1..=3, |e| {
21-
{ let _x; _x = &({ &raw const *&e.log(1) }, drop(e.log(2))); }
21+
{ let _x; _x = &({ &raw const *&e.log(2) }, drop(e.log(1))); }
2222
drop(e.log(3));
2323
});
2424
assert_drop_order(1..=3, |e| {

tests/ui/drop/lint-tail-expr-drop-order-borrowck.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ fn should_lint_with_unsafe_block() {
2828
fn should_lint_with_big_block() {
2929
fn f<T>(_: T) {}
3030
f({
31-
&mut || 0
31+
std::convert::identity(&mut || 0)
3232
//~^ ERROR: relative drop order changing
3333
//~| WARN: this changes meaning in Rust 2024
3434
//~| NOTE: this temporary value will be dropped at the end of the block
@@ -40,12 +40,17 @@ fn should_lint_with_big_block() {
4040
fn another_temp_that_is_copy_in_arg() {
4141
fn f() {}
4242
fn g(_: &()) {}
43-
g({ &f() });
43+
g({ std::convert::identity(&f()) });
4444
//~^ ERROR: relative drop order changing
4545
//~| WARN: this changes meaning in Rust 2024
4646
//~| NOTE: this temporary value will be dropped at the end of the block
4747
//~| NOTE: borrow later used by call
4848
//~| NOTE: for more information, see
4949
}
5050

51+
fn do_not_lint_extending_borrow() {
52+
fn f<T>(_: T) {}
53+
f({ &mut || 0 });
54+
}
55+
5156
fn main() {}

tests/ui/drop/lint-tail-expr-drop-order-borrowck.stderr

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,22 @@ LL | f(unsafe { String::new().as_str() }.len());
2626
= note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/temporary-tail-expr-scope.html>
2727

2828
error: relative drop order changing in Rust 2024
29-
--> $DIR/lint-tail-expr-drop-order-borrowck.rs:31:9
29+
--> $DIR/lint-tail-expr-drop-order-borrowck.rs:31:32
3030
|
31-
LL | &mut || 0
32-
| ^^^^^^^^^
33-
| |
34-
| this temporary value will be dropped at the end of the block
31+
LL | std::convert::identity(&mut || 0)
32+
| -----------------------^^^^^^^^^-
33+
| | |
34+
| | this temporary value will be dropped at the end of the block
3535
| borrow later used here
3636
|
3737
= warning: this changes meaning in Rust 2024
3838
= note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/temporary-tail-expr-scope.html>
3939

4040
error: relative drop order changing in Rust 2024
41-
--> $DIR/lint-tail-expr-drop-order-borrowck.rs:43:9
41+
--> $DIR/lint-tail-expr-drop-order-borrowck.rs:43:32
4242
|
43-
LL | g({ &f() });
44-
| - ^^^^ this temporary value will be dropped at the end of the block
43+
LL | g({ std::convert::identity(&f()) });
44+
| - ^^^^ this temporary value will be dropped at the end of the block
4545
| |
4646
| borrow later used by call
4747
|

tests/ui/drop/super-let-tail-expr-drop-order.rs

Lines changed: 29 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
//! Test for #145784: the argument to `pin!` should be treated as an extending expression if and
2-
//! only if the whole `pin!` invocation is an extending expression. Likewise, since `pin!` is
3-
//! implemented in terms of `super let`, test the same for `super let` initializers. Since the
4-
//! argument to `pin!` and the initializer of `super let` are not temporary drop scopes, this only
5-
//! affects lifetimes in two cases:
1+
//! Test for #145784. This tests three things:
62
//!
7-
//! - Block tail expressions in Rust 2024, which are both extending expressions and temporary drop
8-
//! scopes; treating them as extending expressions within a non-extending `pin!` resulted in borrow
9-
//! expression operands living past the end of the block.
3+
//! - In Rust 2024, temporary lifetime extension applies to block tail expressions, such that
4+
//! extending borrows and `super let`s in block tails are extended to outlive the result of the
5+
//! block.
106
//!
11-
//! - Nested `super let` statements, which can have their binding and temporary lifetimes extended
12-
//! when the block they're in is an extending expression.
7+
//! - Since `super let`'s initializer has the same temporary scope as the variable scope of its
8+
//! bindings, this means that lifetime extension can effectively see through `super let`.
139
//!
14-
//! For more information on extending expressions, see
15-
//! https://doc.rust-lang.org/reference/destructors.html#extending-based-on-expressions
10+
//! - In particular, the argument to `pin!` is an extending expression, and the argument of an
11+
//! extending `pin!` has an extended temporary scope. The lifetime of the argument, as well those
12+
//! of extending borrows and `super lets` within it, should match the result of the `pin!`,
13+
//! regardless of whether it itself is extended by a parent expression.
14+
//!
15+
//! For more information on temporary lifetime extension, see
16+
//! https://doc.rust-lang.org/nightly/reference/destructors.html#temporary-lifetime-extension
1617
//!
1718
//! For tests that `super let` initializers aren't temporary drop scopes, and tests for
1819
//! lifetime-extended `super let`, see tests/ui/borrowck/super-let-lifetime-and-drop.rs
@@ -30,34 +31,25 @@ use std::pin::pin;
3031
fn f<T>(_: LogDrop<'_>, x: T) -> T { x }
3132

3233
fn main() {
33-
// Test block arguments to `pin!` in non-extending expressions.
34+
// Test block arguments to non-extending `pin!`.
3435
// In Rust 2021, block tail expressions aren't temporary drop scopes, so their temporaries
3536
// should outlive the `pin!` invocation.
36-
// In Rust 2024, block tail expressions are temporary drop scopes, so their temporaries should
37-
// be dropped after evaluating the tail expression within the `pin!` invocation.
38-
// By nesting two `pin!` calls, this ensures non-extended `pin!` doesn't extend an inner `pin!`.
37+
// In Rust 2024, extending borrows within block tail expressions have extended lifetimes to
38+
// outlive result of the block, so the end result is the same in this case.
39+
// By nesting two `pin!` calls, this ensures extending borrows in the inner `pin!` outlive the
40+
// outer `pin!`.
3941
assert_drop_order(1..=3, |o| {
40-
#[cfg(e2021)]
4142
(
4243
pin!((
4344
pin!({ &o.log(3) as *const LogDrop<'_> }),
4445
drop(o.log(1)),
4546
)),
4647
drop(o.log(2)),
4748
);
48-
#[cfg(e2024)]
49-
(
50-
pin!((
51-
pin!({ &o.log(1) as *const LogDrop<'_> }),
52-
drop(o.log(2)),
53-
)),
54-
drop(o.log(3)),
55-
);
5649
});
5750

5851
// The same holds for `super let` initializers in non-extending expressions.
5952
assert_drop_order(1..=4, |o| {
60-
#[cfg(e2021)]
6153
(
6254
{
6355
super let _ = {
@@ -68,17 +60,6 @@ fn main() {
6860
},
6961
drop(o.log(3)),
7062
);
71-
#[cfg(e2024)]
72-
(
73-
{
74-
super let _ = {
75-
super let _ = { &o.log(1) as *const LogDrop<'_> };
76-
drop(o.log(2))
77-
};
78-
drop(o.log(3))
79-
},
80-
drop(o.log(4)),
81-
);
8263
});
8364

8465
// Within an extending expression, the argument to `pin!` is also an extending expression,
@@ -97,36 +78,18 @@ fn main() {
9778
// We have extending borrow expressions within an extending block
9879
// expression (within an extending borrow expression) within a
9980
// non-extending expresion within the initializer expression.
100-
#[cfg(e2021)]
101-
{
102-
// These two should be the same.
103-
assert_drop_order(1..=3, |e| {
104-
let _v = f(e.log(1), &{ &raw const *&e.log(2) });
105-
drop(e.log(3));
106-
});
107-
assert_drop_order(1..=3, |e| {
108-
let _v = f(e.log(1), {
109-
super let v = &{ &raw const *&e.log(2) };
110-
v
111-
});
112-
drop(e.log(3));
113-
});
114-
}
115-
#[cfg(e2024)]
116-
{
117-
// These two should be the same.
118-
assert_drop_order(1..=3, |e| {
119-
let _v = f(e.log(2), &{ &raw const *&e.log(1) });
120-
drop(e.log(3));
121-
});
122-
assert_drop_order(1..=3, |e| {
123-
let _v = f(e.log(2), {
124-
super let v = &{ &raw const *&e.log(1) };
125-
v
126-
});
127-
drop(e.log(3));
81+
// These two should be the same.
82+
assert_drop_order(1..=3, |e| {
83+
let _v = f(e.log(1), &{ &raw const *&e.log(2) });
84+
drop(e.log(3));
85+
});
86+
assert_drop_order(1..=3, |e| {
87+
let _v = f(e.log(1), {
88+
super let v = &{ &raw const *&e.log(2) };
89+
v
12890
});
129-
}
91+
drop(e.log(3));
92+
});
13093

13194
// We have extending borrow expressions within a non-extending
13295
// expression within the initializer expression.

0 commit comments

Comments
 (0)