Skip to content

Commit 95e7a42

Browse files
committed
Merge RFC 3606: Shorter temp lifetimes in tail exprs
The FCP for RFC 3606 completed on 2024-04-14 with a disposition to merge. Let's merge it.
2 parents 0d835b1 + 929c584 commit 95e7a42

File tree

1 file changed

+184
-0
lines changed

1 file changed

+184
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# Shorter temporary lifetimes in tail expressions
2+
3+
- Feature Name: `shorter_tail_lifetimes`
4+
- Start Date: 2023-05-04
5+
- RFC PR: [rust-lang/rfcs#3606](https://github.com/rust-lang/rfcs/pull/3606)
6+
- Tracking Issue: [rust-lang/rust#123739](https://github.com/rust-lang/rust/issues/123739)
7+
8+
# Summary
9+
10+
In the next edition, drop temporaries in tail expressions *before* dropping locals, rather than after.
11+
12+
# Motivation
13+
14+
Temporaries in the tail expression in a block live longer than the block itself,
15+
so that e.g. `{expr;}` and `{expr}` can behave very differently.
16+
17+
For example, this fails to compile:
18+
19+
```rust
20+
// This fails to compile!
21+
fn f() -> usize {
22+
let c = RefCell::new("..");
23+
c.borrow().len() // ERROR!!!
24+
}
25+
```
26+
27+
The temporary `std::cell::Ref` created in the tail expression will be dropped
28+
after the local `RefCell` is dropped, resulting in a lifetime error.
29+
30+
This leads to having to add seemingly unnecessary extra `let` statements
31+
or having to add seemingly unnecessary semicolons:
32+
33+
```rust
34+
fn main() {
35+
let c = std::cell::RefCell::new(123);
36+
37+
if let Ok(mut b) = c.try_borrow_mut() {
38+
*b = 321;
39+
}; // <-- Error if you remove the semicolon!
40+
}
41+
```
42+
43+
Both of these examples will compile fine after the proposed change.
44+
45+
# Guide-level explanation
46+
47+
Temporaries are normally dropped at the end of the statement.
48+
49+
The tail expression of a block
50+
(such as a function body, if/else body, match arm, block expression, etc.)
51+
is not a statement, so has its own rule:
52+
53+
- Starting in Rust 2024,
54+
temporaries in tail expressions are dropped after evaluating the tail expression,
55+
but before dropping any local variables of the block.
56+
57+
For example:
58+
59+
```rust
60+
fn f() -> usize {
61+
let c = RefCell::new("..");
62+
c.borrow().len() // Ok in Rust 2024
63+
}
64+
```
65+
66+
The `.borrow()` method returns a (temporary) `Ref` object that borrows `c`.
67+
Starting in Rust 2024, this will compile fine,
68+
because the temporary `Ref` is dropped before dropping local variable `c`.
69+
70+
# Reference-level explanation
71+
72+
For blocks/bodies/arms whose `{}` tokens come from Rust 2024 code,
73+
temporaries in the tail expression will be dropped *before* the locals of the block are dropped.
74+
75+
# Breakage
76+
77+
It is tricky to come up with examples that will stop compiling.
78+
79+
For tail expressions of a function body, such code will involve a tail
80+
expression that injects a borrow to a temporary
81+
into an already existing local variable that borrows it on drop.
82+
83+
For example:
84+
85+
```rust
86+
fn why_would_you_do_this() -> bool {
87+
let mut x = None;
88+
// Make a temporary `RefCell` and put a `Ref` that borrows it in `x`.
89+
x.replace(RefCell::new(123).borrow()).is_some()
90+
}
91+
```
92+
93+
We expect such patterns to be very rare in real world code.
94+
95+
For tail expressions of block expressions (and if/else bodies and match arms),
96+
the block could be a subexpression of a larger expression.
97+
In that case, dropping the (not lifetime extended) temporaries at the end of
98+
the block (rather than at the end of the statement) can cause subtle breakage.
99+
For example:
100+
101+
```rust
102+
let zero = { String::new().as_str() }.len();
103+
```
104+
105+
This example compiles if the temporary `String` is kept alive until the end of
106+
the statement, which is what happens today without the proposed changes.
107+
However, it will no longer compile with the proposed changes in the next edition,
108+
since the temporary `String` will be dropped at the end of the block expression,
109+
before `.len()` is executed on the `&str` that borrows the `String`.
110+
111+
(In this specific case, possible fixes are: removing the `{}`,
112+
using `()` instead of `{}`, moving the `.len()` call inside the block, or removing `.as_str()`.)
113+
114+
Such situations are less rare than the first breakage example, but likely still uncommon.
115+
116+
The other kind of breakage to consider is code that will still compile, but behave differently.
117+
However, we also expect code for which it the current drop order is critical is very rare,
118+
as it will involve a Drop implementation with side effects.
119+
120+
For example:
121+
122+
```rust
123+
fn f(m: &Mutex<i32>) -> i32 {
124+
let _x = PanicOnDrop;
125+
*m.lock().unwrap()
126+
}
127+
```
128+
129+
This function will always panic, but will today poison the `Mutex`.
130+
After the proposed change, this code will still panic, but leave the mutex unpoisoned.
131+
(Because the mutex is unlocked *before* dropping the `PanicOnDrop`,
132+
which probably better matches expectations.)
133+
134+
# Edition migration
135+
136+
Since this is a breaking change, this should be an edition change,
137+
even though we expect the impact to be minimal.
138+
139+
We need to investigate any real world cases where this change results in an observable difference.
140+
Depending on this investigation, we can either:
141+
142+
- Not have any migration lint at all, or
143+
- Have a migration lint that warns but does not suggest new code, or
144+
- Have a migration lint that suggests new code for the most basic common cases (e.g. replacing `{}` by `()`), or
145+
- Have a migration lint that suggests new code for all cases (e.g. using explicit `let` and `drop()` statements).
146+
147+
We highly doubt the last option is necessary.
148+
If it turns out to be necessary, that might be a reason to not continue with this change.
149+
150+
# Drawbacks
151+
152+
- It introduces another subtle difference between editions.
153+
(That's kind of the point of editions, though.)
154+
155+
- There's a very small chance this breaks existing code in a very subtle way. However, we can detect these cases and issue warnings.
156+
157+
# Prior art
158+
159+
- There has been an earlier attempt at changing temporary lifetimes with [RFC 66](https://rust.tf/rfc66).
160+
However, it turned out to be too complicated to resolve types prematurely and
161+
it introduced inconsistency when generics are involved.
162+
163+
# Unresolved questions
164+
165+
- How uncommon are the situations where this change could affect existing code?
166+
- How advanced should the edition lint and migration be?
167+
- Can we make sure a lint catches the cases with unsafe code that could result in undefined behaviour?
168+
169+
# Future possibilities
170+
171+
- Not really "future" but more "recent past":
172+
Making temporary lifetime extension consistent between block expressions and
173+
if/else blocks and match arms. This has already been implemented and approved:
174+
https://github.com/rust-lang/rust/pull/121346
175+
176+
- Dropping temporaries in a match scrutinee *before* the arms are evaluated,
177+
rather than after, to prevent deadlocks.
178+
This has been explored in depth as part of the
179+
[temporary lifetimes effort](https://rust-lang.zulipchat.com/#narrow/stream/403629-t-lang.2Ftemporary-lifetimes-2024),
180+
but our initial approaches didn't work out.
181+
This requires more research and design.
182+
183+
- An explicit way to make use of temporary lifetime extension. (`super let`)
184+
This does not require an edition change and will be part of a separate RFC.

0 commit comments

Comments
 (0)