Skip to content

Commit 3ec9df7

Browse files
committed
Add tail_temporaries_shorter_lifetime RFC.
1 parent 0044bb7 commit 3ec9df7

File tree

1 file changed

+189
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)