---
title: "Temporary lifetimes draft RFC v2"
tags: ["draft-RFC"]
date: 2024-02-08
discussion: https://rust-lang.zulipchat.com/#narrow/stream/403629-t-lang.2Ftemporary-lifetimes-2024/topic/RFC.20status
url: https://hackmd.io/2x6WhpuiTvqDq2MUpRLCeg
---
# Temporary lifetimes draft RFC (v2)
- Feature Name: `2024_temp_lifetime`, `super_let`
- Start Date: 2023-05-04
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
# Summary
Motivation:
* Two particular aspects of today's temporary rules are a common source of bugs and confusion (e.g., see this [example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e3d3747f371b62750939c47591705d74) for an illustration of both issues):
* Temporaries in match expressions last until the end of the match, which commonly leads to deadlocks.
* Temporaries in the tail expression in a block live longer than the block itself, so that e.g. `{expr;}` and `{expr}` can behave very differently.
* Rust's current temporary rules give no way to create a named temporary value whose lifetime escapes the current block. This in turn creates ergonomic issues:
* There is no way to write macros that introduce temporaries that work in all positions, so e.g. the result of the `format_args!` macro cannot always be stored in a let ([example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=85efb70b0d999626cc030d4ff0912550)).
* When writing blocks in a nested expression, you can't have references to `let` values that escape ([example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b0708ed68979532534c226036e84951e)).
Solution:
* Introduce `super let`, which lets you introduce (and name) temporaries at the enclosing block.
* The expression `& $expr` is equivalent to `{ super let tmp = & $expr; tmp }`.
* In Rust 2024, adjust temporary rules to remove known footguns:
* Temporaries in the tail expression of a block are freed before the end of the block.
* (Optionally) Temporaries in match expressions are freed before testing patterns, rather than at the end of the match.
# Motivation
> Why are we doing this? What use cases does it support? What is the expected outcome?
Rust programs sometimes need to introduce temporary locations so that they can create references. As a simple example, the expression `&foo()` needs to invoke `foo()`, store the result into a temporary location `t`, and then take a reference `&t`; temporaries can also be introduced when invoking `&self` or `&mut self` methods, e.g. with a call like `return_mutex().lock()`, which must store the result of `return_mutex()` into a temporary to invoke `lock`, as `lock` is an `&self` method. Whenever a temporary value is created, it raises the question of when that temporary location will be freed. Given that freeing a value also causes its destructor to run, this choice can affect important questions like when a lock will be released.
The current rules pre-date the RFC process, but they were [motivated in this blog post from 2023](https://smallcultfollowing.com/babysteps/blog/2023/03/15/temporary-lifetimes/). There are two core principles:
* By default, temporaries are dropped at the end of the innermost enclosing statement. This is generally "long enough" to allow method chains and other interesting expressions to work, but not so long that users are surprised by e.g. a lock that is not freed.
* Example: given `let value = foo(&String::new())`, the string returned by `String::new()` will be freed at the end of the `let` statement.
* But sometimes we can see syntactically that a reference to a temporary will be stored in a `let` binding; in that case, we give the temporary an *extended* lifetime, so that it lasts until the end of the block.
* Example: given `let value = &String::new()`, the string returned by `String::new()` will be freed at the end of the enclosing block.
Over time, two problems have been identified with the rules as formulated.
## There is no explicit way to declare a temporary, which is hostile to macros
In blocks today the `let` keyword introduces named storage that is freed at the end of the block:
```rust
{
let value1 = String::new();
let value2 = String::new();
...
let pair = (&value1, &value2);
...
// `value1` and `value2` are freed here
}
```
References to `let`-bound variables must only be used within a block and you will get a compilation error if those references escape:
```rust
let pair = {
let value1 = String::new();
let value2 = String::new();
...
(&value1, &value2)
//~^ ERROR because `value1` and `value2` are freed before the `let` ends
};
```
One way to resolve this is to declare local variables in an outer scope:
```rust
let (value1, value);
let pair = {
value1 = String::new();
value2 = String::new();
(&value1, &value2)
}; // OK
```
This pattern is very flexible, but it is non-obvious.
The other problem with this pattern it is *non-local*, meaning that it requires changes outside of the block itself. As a result, it cannot be used as part of a macro definition.
Today, it is not always possible to write a macro `foo!($expr)` that can be used wherever `$expr` is valid. The `pin!` macro is one example where we were able to do so, but only by exposing a private field as public, using the unstable `#[unstable]` attribute to pretend it's still "private":
```rust
let a = pin!(temp());
// Expands to:
let a = Pin { pinned: &mut { temp() } };
// The temporary resulting from `temp()` is dropped at end of surrounding scope,
// rather than at the end of the `let` statement, because of the syntax rules of
// temporary lifetime extension.
```
If `pin!()` had expanded to a proper call to `Pin::new`, then `temp()` would have been dropped at the end of the `let` statement.
The `format_args!` macro is an example where we didn't find a workaround. As a result, attempting to store the result of `format_args!` into a let binding gets surprising errors:
```rust!
fn main() {
// This is OK. A temporary is created to store the result of
// `String::new()` and that temporary is freed at the end of the statement:
println!("{}", format_args!("{}", String::new()));
// This fails to compile. As before, a temporary is created to
// store the result of `String::new` and that temporary is freed at the end
// of the statement (i.e., after the `let`), but that means that `f`
// contains a reference to that freed temporary...
let f = format_args!("{}", String::new());
// ...and so we get an error here.
println!("{}", f);
}
```
## Temporaries live too long in block tail expressions
Temporaries in the tail expression in a block live longer than the block itself, so that e.g. `{expr;}` and `{expr}` can behave very differently.
For example, the following will not compile:
```rust
fn f() -> usize {
let c = RefCell::new("..");
c.borrow().len()
}
```
The temporary `std::cell::Ref` created in the tail expression will be dropped after the local `RefCell` is dropped, resulting in a lifetime error.
## Temporaries live too long in match expressions
Temporaries in match expressions last until the end of the match, which commonly leads to deadlocks.
For example:
```rust
fn f(v: &Mutex<Vec<i32>>) {
match v.lock().unwrap().pop() {
_ => v.lock().unwrap().push(123), // deadlock!
}
}
```
The temporary `MutexGuard` created in the match scrutinee is not dropped until the end of the `match` (even though it could have been dropped right after `pop()`), resulting in an unexpected deadlock.
This research found these issues to be the dominant cause of real-world bugs in the code they examined:
- [Paper](https://cseweb.ucsd.edu/~yiying/RustStudy-PLDI20.pdf) and [Archived](https://web.archive.org/web/20200505025635/https://cseweb.ucsd.edu/~yiying/RustStudy-PLDI20.pdf)
# Guide-level explanation
This section gives an "intuitive" definition of the RFC changes. It is written to target someone who knows Rust's existing rules fairly well. In the [reference-level explanation][reference-level-explanation] section, we explain the rules in detail.
This RFC will put forth four proposals on changes in treatment of temporary lifetimes. Each can be assesed potentially individually, and all together, we believe, can provide a conherent answer to the temporary lifetime issues in general.
## Short-lived and long-lived temporaries
In a `let` statement, temporaries either live until the end of the statement, or until the end of the surrounding block.
For example:
```rust
let a = (&temp1(), temp2().f());
```
Here, the first temporary (`temp1()`) is long-lived; it lives until the end of the surrounding block, while the second temporary (`temp2()`) is short-lived; dropped at the end of the expression.
The decision on whether a temporary is long-lived or short-lived is purely based on syntax. Only when the *just the syntax* implies the necessity for a temporary to be long-lived, will it be long lived.
In the example above, the first temporary is long-lived, because a reference to it is stored in `a`. The second temporary is not long-lived, however, because the syntax does not demand it: whether we store a reference to it depends on the signature of `f`. The rules err on the short side.
The concept of short-lived and long-lived temporaries only applies to `let` statements. In all other contexts, all temporaries live until the end of the statement.
## Super let
Before this RFC, a long-lived temporary cannot be introduced in most places other than a few very specific syntactic elements (braced struct expressions, array expressions, etc.). You cannot introduce a long-lived temporary inside an argument of a function call, for example:
```rust
let a = &temp(); // long-lived
let b = identity(&temp()); // short-lived
```
This RFC adds `super let` as a way to introduce a long-lived temporary that can be used in any expression.
A `super let` statement can only appear within a block expression (and if/else and match arms) and will introduce a binding to a temporary with a lifetime as if that temporary was written as `& $expr` in place of the block.
For example:
```rust
let a = &temp(); // long-lived
let a = { super let t = temp(); &t }; // equivalent
```
This can be used to make along-lived temporary that's used in any other expression, such as a function-call:
```rust
let a = identity(&temp()); // short-lived temporary (error)
let b = { super let t = temp(); identity(&t) }; // long-lived temporary (compiles)
```
The temporary `temp()` in the `let b` statement will live exactly as long as it would if we had written:
```rust
let b = &temp(); // long-lived
```
### Equivalences
1. In any context, the following two expressions are equivalent:
- `& $expr`
- `{ super let t = & $expr; t }`
(Note that `$expr` could be a `!Sized` place, such as `v[..]`.)
2. Similarly, in any context, the following two expressions are equivalent:
- `& { $expr }`
- `{ super let t = $expr; & t }`
(Note that `{ $expr }` moves the value, which means it must be `Sized`.)
### Using `super let` to define a transparent `pin!` macro
Using `super let`, we can define the `pin!` macro without any tricks and without relying on any internal details only available to the standard library. E.g.:
```rust
let x = pin!(f());
// expands to:
let x = {
super let mut pinned = f();
unsafe { Pin::new_unchecked(&mut pinned) }
};
```
Note that, when defined this way, the `unsafe` block does not encompass too much, i.e., the call to `f()` is outside of the `unsafe` block.
The `pinned` value above will live exactly as long as it would if the pin macro had expanded to a simple struct expression:
```rust
// "idealized" expansion
let x = Pin {
pinned: &mut f(), // long-lived temporary
};
```
### Using `super let` to define a transparent `format_args!` macro
Using `super let`, the `format_args!` macro can be defined in a way that allows it to work in any position:
```rust
let x = format_args!("Hello {name}!");
// expands to:
let x = {
super let args = [fmt::Argument::new_display(&name)];
fmt::Arguments::new(&["Hello ", "!"], &args)
};
```
Such that the temporary `args` array will live exactly as long as it would if `format_args!()` had expanded to a simple struct expression instead of a function call:
```rust
// "idealized" expansion
let x = fmt::Arguments {
strings: &["Hello ", "!"],
args: &[fmt::Argument::new_display(&name)], // long-lived temporary
};
```
### Using `super let` for escaping bindings
The following code results in an error, as `file` will not live long enough"
```rust
let output: Option<&mut dyn Write> = if verbose {
let mut file = File::create("log")?;
Some(&mut file)
} else {
None
};
```
With `super let`, the diagnostic will suggest inserting `super` in the right place to make it compile:
```
error[E0597]: `file` does not live long enough
--> src/main.rs:16:14
|
15 | let mut file = std::fs::File::create("log")?;
| --------
|
help: try using `super let`
|
15 | super let mut file = std::fs::File::create("log")?;
| +++++
```
Note how this is a simple purely local change; this doesn't involve any non-local changes such as inserting a `let` statement before the first line, making it easy to suggest in a diagnostic.
This results in:
```rust
let output: Option<&mut dyn Write> = if verbose {
super let mut file = File::create("log")?;
Some(&mut file)
} else {
None
};
```
Now, `file` will live as long as `output`, exactly as long as it would if we had written:
```rust
let output = &mut File::create("log"); // long-lived temporary
```
### Diagnostics
- A `super let` in any other block than a block expression, if(-let)/else body, or match arm results in a hard error.
- A `super let` where a `let` would have resulted in the exact same behaviour (e.g. in places where there is no difference of short-lived and long-lived temporaries), a lint diagnostic will suggest removing the `super` keyword.
- A `let` that's part of a lifetime error that would be fixed by using a `super let` instead will add a suggestion to add the `super` keyword.
The result is that (in code without warnings or disabled lints), one will only see `super let` when it actually causes something to live longer than a regular `let` would have.
## Consistency between block expressions, if/else bodies, and match arms
A long-lived temporary can be introduced in the tail expression of a block expression:
```rust
let a = {
..;
&temp()
}; // long-lived temporary
```
This RFC makes if/else bodies and match arms work the same way:
```rust
let a = if true {
..;
&temp() // long-lived temporary
} else {
..
};
```
```rust
let a = match () {
_ => &temp(), // long-lived temporary
};
```
This change does not break any existing code; it is fully backwards compatible.
*This particular change has already been accepted through lang-team FCP: https://github.com/rust-lang/rust/pull/121346*
## Shortening temporary lifetimes in block tail expressions
With this RFC, in Rust 2024, temporaries in the tail expression of a block are will be dropped *before* locals are dropped, rather than after.
This means that this code will be accepted:
```rust
fn f() -> usize {
let c = RefCell::new("..");
c.borrow().len() // No longer errors!
}
```
The temporary `cell::Ref` will be dropped *before* the `RefCell` is dropped, making the snippet compile.
### Edition migration
// TODO: Can this break any existing code? Examples?
## Shortening temporary lifetimes in `match` scrutinee expressions
This RFC *optionally* proposes shortening the lifetime of temporaries in `match` scrutinee expressions in Rust 2024.
- This change is somewhat controversial, so it is *optional*. This means that whether this change should happen will remain an unresolved question on the tracking issue, which is to be resolved after gathering more data and experience.
- This change applies equivalently to `if let`, `while let`, and `for`, as they "desugar" to `match`.
Before this RFC, temporaries in a `match` scrutinee expression outlive the match arms.
To prevent unnecessary deadlocks, this RFC (optionally) reduces the lifetime of such temporaries by dropping them before entering the match arms.
The concept of short-lived and long-lived temporaries, based on syntax, as used for `let` statements, will be applied here as well. Short-lived temporaries are dropped before entering the match arms, and long-lived temporaries are dropped after the match arms.
Examples:
```rust
match mutex.lock().unwrap().pop() {
_ => {
// changed: mutex will have been unlocked here!
..
}
}
```
```rust
match &temp() {
_ => {
// unchanged: temporary will still live here
..
}
}
```
Note that the long-lived scope ends at the end of the match expression, which can still be shorter than before:
```rust!
let a = (
match mutex.lock() { .. },
match mutex.lock() { .. }, // changed: no deadlock!
);
```
Before this RFC, the temporary `MutexGuard`s in the example above are dropped only at the end of the `let` statement. With this (optional part of this) RFC, the `MutexGuard`s are dropped at the end of the corresponding `match` expression.
### Match equivalence
With these new rules, the following two will be equivalent:
```rust
match $expr {
$pat => $body
}
```
and
```rust
{
let $pat = $expr;
$body
}
```
### Rewriting your match expression
Dropping tempraries in match scrutinee expressions earlier can break some existing code. For example:
```rust
{
let rc = RefCell::new(vec![1, 2, 3]);
match &rc.borrow()[1..] {
// ^~~~~~~~~~~ temporary value is freed at the end of this *match scrutinee expression*
// the `Ref<'_>` guard is dropped before entering any match arm,
// because the borrow is on the subslice [1..] and not on `rc.borrow()`.
slice => { .. }
}
}
```
The solution would be to introduce an additional `let` binding:
```rust
{
let rc = RefCell::new(vec![1, 2, 3]);
let b = rc.borrow();
match &b[1..] {
slice => { .. }
}
//drop(b); // optionally
}
```
Or, less readably but more locally, by using `super let`:
```rust
{
let rc = RefCell::new(vec![1, 2, 3]);
match { super let b = rc.borrow(); &b[1..] } {
slice => { .. }
}
}
```
Or, using one of the future possibilities mentioned at the end of this RFC, `.let` syntax:
```rust
{
let rc = RefCell::new(vec![1, 2, 3]);
match &rc.borrow().let.[1..] {
slice => { .. }
}
}
```
### Rewriting your macros
It's a well known trick to use a `match` expression to give a name to temporaries in a macro expansion. For example:
```rust
macro_rules! my_assert_eq {
($left:expr, $right:expr) => {
match (&$left, &$right) {
(lhs, rhs) => {
if *lhs != *rhs {
panic!("oh no, {lhs:?} != {rhs:?}")
}
}
}
};
}
```
With the (optional) change to temporaries in `match` scrutinees, one should use... uhh
```rust
macro_rules! my_assert_eq {
($left:expr, $right:expr) => {
let () = match { super let pair = ($left, $right); &pair } {
(lhs, rhs) if *lhs != *rhs {
panic!("oh no, {lhs:?} != {rhs:?}")
}
_ => {}
};
};
}
```
// TODO: `super let` isn't equivalent.
// NOTE(@wieDasDing): I think this works, but I have to admit that this is quirky. It is kind of working still in a limited sense because the "lifetime barrier" is kind of "hard".
### Edition migration
// TODO ...
# Reference-level explanation
[Reference-level-explanation]: #reference-level-explanation
- Every (sub)expression has a *short* and a *long* temporary scope, defined as follows:
- In an expression statement:
- The short and long temporary scopes both last until the end of the statement.
- In a `let` statement:
- The short temporary scope lasts until the end of the statement.
- The long temporary scope lasts until the end of the enclosing block.
- (NEW) In a `super let` statement:
- The temporary scopes are identical to those of the enclosing block (which could be a block expression, match arm, or if/else body).
- (NEW, OPTIONAL) For the scrutinee of a `match`, `if let`, `while let`, or `for`:
- The short temporary scope ends after evaluating the scrutinee, before the body of the match/if/while/for.
- The long temporary scope ends after the body of the match/if/while/for.
- For the condition of an `if` (not `if let`) expression:
- The short and long temporary scopes both end after evaluating the condition, before the body of the `if`.
- For the following subexpressions, the temporary scopes are identical to those of the enclosing expression:
- Operand of a borrow expression.
- Operand of a cast expression.
- Element initializer in a array or tuple expression.
- Field initializer in a braced struct expression.
- (NEW) Arm of a match expression or body of an if(-let)/else expression.
- For the tail expression of a block (block expression, match arm, if/else body, etc.):
- (NEW) The short temporary scope ends before the locals of the block are dropped (after evaluating the tail expression).
- The long temporary scope is the same as that of the block.
- For any other subexpression:
- The short temporary scope is that of its enclosing expression.
- The long temporary scope is the short temporary scope of its enclosing expression.
- A temporary result lives until the end of the *long* temporary scope if:
- it is the (projected) operand of a (`&` or `&mut`) borrow expression (e.g. `&temp().field.0[1]`), or
- it is the (projected) init-expression or scrutinee of a `let`, `match`, `if let` or `while let` with a borrowing pattern (e.g. `let ref a = temp()[..];`).
Any other temporary result lives until the end of the *short* temporary scope.
Below is a visual summary of the of the temporary lifetime rules before and after this RFC (in Rust 2024), with the differences highlighted in pink:

# Drawbacks
[drawbacks]: #drawbacks
- `super let`:
- This introduces a subtly different kind of `let`, which makes the langauge more complicated.
- However, it makes usage of many macros less surprising (e.g. assigning `format_args` to a `let`), making things less complicated.
- Also, there will be diagnostics that will suggest adding or removing `super` when (un)necessary.
- making if/else bodies and match arms consistent with block expressions:
- This has basically no downsides. It is backwards compatible, and already approved through lang FCP.
- shortening lifetime of temporaries in tail expressions:
- TODO (almost no downsides?)
- shortening lifetime of temporaries in match scrutinee:
- This can result in breakage and a reduction of ergonomics in some cases, which is why this change is left as an unresolved question.
# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives
## The `super` keyword
None of the [existing keywords](https://doc.rust-lang.org/reference/keywords.html) other than `super` seems even close to suitable for this use case. So, the alternative would be to introduce a new keyword, but that would restrict this feature to the new edition.
The `super` keyword is currently used to refer to enclosing modules (e.g., `use super::foo` or `pub(super)`). The use in `let` is "similar but different" in that it refers to the enclosing expression in which a block appears.
Another interpretation is that `super let` is a `let` with super powers. 🦸♀️
We expect this to be a niche feature mostly used by macro authors, and a dedicated keyword seems not worth the price.
## `let super`
We placed `super` as a modifier on `let`, not on a pattern (as in `let (super a, b) = ..`), because it is a property of the complete initializer expression, not just to an individual binding.
## `super {}` blocks
We considered an alternative, `super { $expr }`, but with such syntax it was non-obvious which block would be used for the temporary lifetimes. When not used as `let x = super { .. };`, but at a deeper level, the effects can become very non-obvious. A macro that expands only to `super { .. }` witout a surrounding `let` statement can have very surprising non-local effects.
Since a `super let` statement always appears directly inside a block, it is immediately obvious which block is used for the temporary lifetimes.
# Prior art
[prior-art]: #prior-art
There has been an earlier attempt with RFC 66. However, during the experiment with a trial implementation, we found a great burden has fallen on the experimental compiler to resolve types prematurely while it introduces problems of inconsistency when generics are concerned. This experiment has, therefore, enlightened the principle to rely solely on syntactical structure of a Rust program to assign temporary lifetimes, as opposed to relying on typing suggested by RFC 66.
# Unresolved questions
[unresolved-questions]: #unresolved-questions
- Should we change the lifetime of tempraries in a match scrutinee? (See the optional part of the RFC above.)
# Future possibilities
[future-possibilities]: #future-possibilities
A future possibility is to add `.let` postfix syntax to make a temporary long-lived with minimal syntax:
```rust
let a = temp().let.borrow(); // temp() will be long-lived.
```
Or in a `match` scrutinee, if we apply the optional change described earlier:
```rust
match mutex.lock().unwrap().let.first() {
x => {
// mutex still locked here!
println!("{x}");
}
}
```