# Redesigning `super let`
## Abstract
`super let` is an experimental language feature that allows for explicitly extending the lifetimes of temporary values. This document describes what properties are needed from such a feature and analyzes possible new designs for it.
## Background
When an expression representing a value is used in a context expecting a place, the result of evaluating the expression is stored in an unnamed place, usually called a *temporary*. For example, this occurs when taking a reference to a value expression, such as in `&temp()`; in order to treat the result of `temp()` as a place, it is first implicitly stored in a temporary. Similar to named variables, temporaries have scopes: when program execution leaves a temporary's scope, the temporary is dropped[^static-extension]; its destructor is run if applicable and attempting to access it thereafter will result in a compilation error[^temp-promotion]. The scope of a temporary created in this way is determined by the [temporary scope](https://doc.rust-lang.org/reference/destructors.html#temporary-scopes) associated with the context in which its expression appears.
In the most common case, the temporary scope of an expression extends to the end of the enclosing statement or block. For example, this enables method chaining:
```rust
fn get_string() -> String { /* ... */ }
let trimmed_len = get_string().trim().len();
```
There, the result of `get_string()` is stored in a temporary which lives until the end of the statement, meaning the slice reference returned by [`str::trim`](https://doc.rust-lang.org/core/primitive.str.html#method.trim) can be read from by the call to [`str::len`](https://doc.rust-lang.org/core/primitive.str.html#method.len). However, if we wanted to store the result of `get_string().trim()` in a variable using a `let` statement, we encounter a problem:
```rust
let trimmed_str = get_string().trim();
//~^ ERROR: temporary value dropped while borrowed
let trimmed_len = trimmed_str.len();
```
The temporary scope of the `get_string()` still only encompasses the first statement, so the temporary it represents is dropped at the end of that statement, invalidating `trimmed_str`'s borrow of it. The conventional way to fix this in hand-written code is to introduce a new variable binding in the same scope as `trimmed_str`:
```rust
let string = get_string();
let trimmed_str = string.trim();
let trimmed_len = trimmed_str.len();
```
But since this requires modifying the surrounding program, it's not an option for macros like [`format_args!`](https://doc.rust-lang.org/core/macro.format_args.html). `format_args!` borrows its arguments, which may create temporaries. In order for the result of `format_args!` to be assigned to a variable, those temporaries must live past where the variable is used:
```rust
// This stores the result of `get_string()` in a temporary and borrows it:
let args = format_args!("{}", get_string());
// The scope of the borrowed temporary is extended, allowing later use:
println!("{}", args);
```
This is an instance of [temporary lifetime extension](https://doc.rust-lang.org/reference/destructors.html#temporary-lifetime-extension): when a borrow is syntactically known to appear in the final value assigned by a `let` statement, the temporary scope of the borrowed expression is extended to the scope of the `let` statement's bindings. `super let` is used in the implementation of `format_args!` to signal that temporaries' scopes should be extended when the `format_args!` invocation appears in an [extending context](https://doc.rust-lang.org/reference/destructors.html#extending-based-on-expressions), such as the initializer of a `let` statement. This is in contrast with `get_string().trim()`, where the result of calling `trim` is not syntactically known to reference the temporary, so it's never extended.
[^static-extension]: Some temporaries referenced in statics and constants may have their scopes [extended](https://doc.rust-lang.org/reference/destructors.html#temporary-lifetime-extension) to encompass the entire program. These temporaries never go out of scope and are never dropped.
[^temp-promotion]: There is an exception to this: some temporaries without destructors may implicitly be [promoted](https://doc.rust-lang.org/reference/destructors.html#constant-promotion); in this case, they never become inaccessible. For example, the referent of `&0` will never go out of scope.
## Design goals
- `super let` should enable the creation of APIs that act like Rust syntax by permitting temporary lifetime extension. Since it fills an expressiveness gap, this is the top priority.
- It should be possible to borrow temporaries within a macro expansion that act as if they're borrowed from the macro invocation site. For example, to enforce the pinning invariant, [`pin!`](https://doc.rust-lang.org/core/pin/macro.pin.html) evaluates its argument into a temporary, which it then borrows. This temporary's lifetime may be extended:
```rust
let x = pin!(get_string());
x;
```
Outside of extending contexts, the temporary is dropped at the end of the temporary scope enclosing the `pin!` invocation:
```rust
let x = core::convert::identity(pin!(get_string()));
//~^ ERROR: temporary value dropped while borrowed
x;
```
The Rust Reference's section on [super macros](https://doc.rust-lang.org/reference/expressions.html#super-macros) describes temporaries with this quality as *super temporaries*.
- When a macro argument will appear in the final result of a macro expansion, it should be possible to propagate temporary lifetime extension to it, since temporaries borrowed by it will be in the final result as well. For example, the `get_string()` temporary here is extended since the pinned value is a reference to it:
```rust
let x = pin!(&get_string());
x;
```
The Rust Reference describes macro arguments with this quality as *super operands*.
- The more expressive, the better, generally, though expressiveness isn't a total order. Being able to implement `pin!` and `format_args!` is a requirement, but different designs have their own strengths and weaknesses in terms of what they can express.
- `super let` should have clear semantics. It should have a simple and ideally intuitive mental model, and its behavior should be minimally surprising, as long that doesn't limit its ability to express super macros.
- `super let` should itself fit into Rust's model of temporary scoping and compose well with other language features, as long as that doesn't compromise the previous goals. This can be observed through semantic equivalences; for example, the experimental version of `super let` in the compiler has the property that for all value expressions `$vexpr`, `{ super let x = $vexpr; x }` is equivalent to `$vexpr` and `{ super let x = $vexpr; &x }` is equivalent to `&$vexpr` in expression contexts other than lifetime-extended place operands[^place-context-issue].
- Code using `super let` to express super operands and super temporaries should be readable and writable. Ideally, `super let` has minimal syntactic and conceptual footprints, so code written with it is understandable at a glance. Likewise, using `super let` to extend a temporary's scope should ideally require minimal refactoring.
- Making `super let` easy to use for general temporary lifetime extension in end-user code is an explicit non-goal of this document; see the [section on ergonomics] for more details. Designing for that use case can be considered potential future work.
[^place-context-issue]: `let _ = &*&get_string();` lifetime-extends the temporary holding the output of `get_string()`. However, currently `let _ = &*{ super let x = get_string(); &x };`, which is expected to be equivalent, does not extend `x`. `super let` is designed so that the temporaries defined can be referenced in the result value of the block, so lifetime-extending the temporary holding the result of the block should reasonably lifetime-extend the `super let`. As such, this could be seen as a bug. Only extending the binding, however, would not work in a case like `&*{ super let y = &get_string(); &y };`; without lifetime-extending the `get_string()` temporary, that would not be equivalent to `let _ = &*&&get_string();`. But treating the `super let` initializer as an extending context in these cases would also break `super let`'s semantic equivalences: `let _ = &*{ super let z = (&get_string(),); &z };` would extend the `get_string()` whereas `let _ = &*&(&get_string(),);` wouldn't. Making Rust's temporary scoping more amenable to algebraic reasoning is ongoing work.
## `super let` statements, as currently implemented
`super let` is currently a form of `let` statement, written `super let $pat = $init;`. This syntax is based on the intuition that it acts like a `let` statement in a parent scope. When executed, it functions like a normal `let` statement: the initializer is matched against the pattern, which may bind variables. The difference from `let` lies in how its bindings and initializer are scoped. To see how, we'll look at one of our recurring examples, `pin!`:
```rust
pub macro pin($value:expr $(,)?) {
{
super let mut pinned = $value;
// SAFETY: The value is pinned: it is the local above which cannot be named outside this macro.
unsafe { $crate::pin::Pin::new_unchecked(&mut pinned) }
}
}
```
If `$vexpr` is a value expression, we expect `pin!($vexpr)` to determine temporary scoping for `$vexpr` the same as writing `&mut $vexpr` would. First, this means `pinned` must be a super temporary: when `pin!(temp())` is used in an extending context, the result of `temp()`, bound to `pinned`, should be lifetime-extended. This is necessary for the reference to it in the result of `pin!(temp())` to be live for the rest of the block. Inversely, when `pin!(temp())` is not extending, `pinned` should be dropped in the temporary scope enclosing the `pin!` invocation. In both cases, the scope of `pinned` can be interpreted as a parent scope of the block, so the intuition that `super let` is like a `let` statement in a parent scope holds. In paritcular, when `pinned` is lifetime-extended, it's scoped as though it's a `let` binding in a parent block.
Following similar logic, to scope `pin!(get_string().trim())` like `&mut get_string().trim()`, the `get_string()` temporary must always be scoped to the temporary scope enclosing the `pin!` invocation. Thus, since the argument to `pin!` expands to the initializer of a `super let` statement, temporaries are not dropped at the end of a `super let`, nor at the end of its block. Similarly unlike `let`, `super let` does not always treat its initializer as extending: in `pin!(&temp())`, `temp()` should only be lifetime-extended if the `pin!` invocation is extending[^init-extend-issue]. These discrepancies suggest it's not really like a `let` statement in a parent scope, going against our principle that `super let` should be unsurprising. As such, a `let` statement may not be the clearest syntax for these semantics.
For more information on the current design of `super let` in the compiler, see the [Rust 2024 temporary scoping design document][temps-2024], [Mara Bos's blog post][mara-blog], and its [implementation history](https://github.com/rust-lang/rust/issues/139076).
[mara-blog]: https://blog.m-ou.se/super-let/
[temps-2024]: https://hackmd.io/LBCK4dQlT8ipGCNA6yM_Nw?view
[^init-extend-issue]: See Rust issue [#145784](https://github.com/rust-lang/rust/issues/145784) for more information.
## `let _ = _ in _`
Since `super let` statements were scoped more like expressions than statements, one proposal has been to write them as expressions instead. The starting point for this is `let` expressions, written `let $pat = $init in $body`. Semantically, this is meant to work just like `{ super let $pat = $init; $body }` did, except without the block tail temporary scope around the body expression. However, this still runs into issues, both with the intuition of what a `let` expression should do and with expressiveness.
### What should a `let` expression be?
If `let` expressions are meant to express `super let`, they can't just be sugar for `{ let $pat = $init; $body }`. As with `super let` statements above, let's analyze the `pin!` macro to see what's needed of them:
```rust
pub macro pin($value:expr $(,)?) {
// SAFETY: The value is pinned: it is the local below which cannot be named outside this macro.
let mut pinned = $value in
unsafe { $crate::pin::Pin::new_unchecked(&mut pinned) }
}
```
This time, the lifetime of `pinned` may be surprising: `pinned` outlives the execution of the `let` expression. This can be seen in `pin!(temp())`, where `pinned` must be live after after the `let` expression to use the result of the macro at all. Unlike with `super let` statements, which were backed by an intuition that suggests bindings should outlive their lexical scope, `let in` appears on the surface to be a general-purpose binding expression. Under this interpretation, a `let in` expression would most likely drop its bindings at the end of the body, like other binding constructs. This isn't an issue inherent to `super let` being a binding expression; `let in` could be replaced with syntax emphasizing that the bindings are meant to work like temporaries.
Likewise, it would need to be possible to lifetime-extend temporaries borrowed in the initializer of a `let in` expression. This is what allows `pin!(&temp())` to be assigned to a variable in a `let` statement. However, this also goes against expectations for a general-purpose binding construct. The result of the initializer isn't necessarily be expected to appear or be referenced in the final value of a `let in` expression, and if it's not, there's no reason to lifetime-extend its borrows. Typically, we only make expressions extending when we're almost certain from syntactic reasoning that it's necessary for borrow-checking to succeed. As above, the syntax could be changed to reflect that this feature serves a specific purpose.
### Evaluation order restrictions
There is a more fundamental issue with `let in`-style `super let`, also present in the original `super let` statements. In order to fit into Rust's temporary scoping model, the context a `let in` expression appears in determines how its bindings and initializer are scoped: its bindings and its initializer's temporaries will only be lifetime-extended if its context is extending. Therefore, to reference a lifetime-extendible `let in` binding within a non-extending context, that non-extending context must syntactically be contained within the `let in`'s body expression. Because the `let in`'s initializer needs to be evaluated before its body, this means that anything being lifetime-extended must be evaluated before the body. This limits expressiveness for macro authors.
For example, consider a function application macro `extending_apply!` that treats the function argument as extending if the application is extending:
```rust
macro extending_apply_wrong($func:expr, $arg:expr) {
let arg = $arg in $func(arg)
}
```
This would evaluate the argument expression before the callee expression, opposite to how call expressions are normally evaluated. To fix this, we could accept an ergonomic hit and try to refactor, but we run into two more problems:
```rust
macro extending_apply_wrong($func:expr, $arg:expr) {
let func = $func in
let arg = $arg in
func(arg)
}
```
Most importantly, now `$func` is made extending as well when the macro invocation is extending. We can use a trick to mitigate this: applying `core::convert::identity` to the initializer forces it not to be extending.
```rust
macro extending_apply_trick($func:expr, $arg:expr) {
let func = core::convert::identity($func) in
let arg = $arg in
func(arg)
}
```
This isn't perfect, since the temporary binding `func` may still be lifetime-extended. If we care about its lifetime, we'll need a way to mark it as non-extendible:
```rust
macro extending_apply_nonextendible($func:expr, $arg:expr) {
nonextendible let func = $func in
extendible let arg = $arg in
func(arg)
}
```
Or alternatively, to drop `func` at the end of the macro rather than the enclosing temporary scope, we could use `match`. This requires inventing a way to ignore the temporary scope it introduces, since `let in` lacks the ability to express that without changing evaluation order. Otherwise, `$arg`'s temporaries would be scoped to the match arm:
```rust
macro extending_apply_transparent($func:expr, $arg:expr) {
transparent match $func {
func => let arg = $arg in func(arg)
}
}
```
The second issue is that `$func` is being converted to a value. As such, closures passed to `extending_apply!` may need to be borrowed explicitly to avoid moving, similar to calling a function with a closure argument. This only affects macros that need to be generic over how a place is accessed, e.g. through method resolution based duck typing. Additional language features such as [place aliases] could also provide a generic way to avoid conversion to a value.
#### Conditional evaluation
In particular, this limits conditional evaluation. `let in` has no way to make non-extended temporaries outlive the temporary scopes introduced by `if`, `match`, or other conditional evaluation constructs. To conditionally evaluate an expression `$expr` with an `match` arm, for example, `$expr` must appear within the `match` arm temporary scope. Using a `let in` expression to evaluate `$expr` outside of the arm would evaluate it unconditionally, but putting the `let in` expression inside the arm would mean it's still within the `match` arm temporary scope.
[place aliases]: https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/let.20place/with/563709558
## `super` expressions
`super` expressions, here written `super $label $expr`, are an inline way of lifetime-extending an expression without needing to create bindings. Conceptually, temporary scopes are determined for `$expr` as if it's lifetime-extendible and written at `$label`[^super-expression-place-context-issue]: if the label is in an extending context or lifetime-extended, the `super` expression itself has an extended temporary scope and `$expr` is treated as extending. Furthermore, the enclosing temporary scope of `$expr` is the temporary scope enclosing the labeled expression. If a `super` expression's result becomes a temporary, it can be thought of as a lifetime-extendible temporary of the expression at its label. This allows it to implement super macros: by labeling the expansion of a macro, a `super` expression with that label can be used to express super operands and super temporaries. Additionally, writing the lifetime-extended expression inline avoids the evaluation order issue with `let in` above.
There are multiple ways to design `super` expressions. For instance, they could always be value expressions; the re-scoped expression would be considered to be in a value expression context so it undergoes place-to-value conversion. In this case, `pin!` could potentially be written as:
```rust
pub macro pin($value:expr $(,)?) {
'top: {
// SAFETY: The value is pinned: it is a temporary value, to which no references exist outside this macro.
let ref_pinned = &mut super 'top $value;
unsafe { $crate::pin::Pin::new_unchecked(ref_pinned) }
}
}
```
Here, since `super 'top $value` is used as a place, it becomes a temporary. Its temporary lifetime is determined based on the temporary scoping properties of the top-level expression of the macro expansion, as that's where the label points. If a `pin!` invocation is extending or its result is lifetime-extended, then this temporary will be extended and `$value` will be extending. This allows the borrowed temporary to act as an extendible temporary of the `pin!` invocation.
A `super` expression could also effectively be transparent, acting the same as its sub-expression with modified scoping. In that case, one more step is needed to implement `pin!`: a place-to-value conversion operator that permits temporary lifetime extension, like pre-Rust-2024 block tail expressions. This can be implemented as a super macro:
```rust
macro value($value:expr) {
'value: { super 'value $value }
}
```
`super 'value` sets the enclosing temporary scope of `$value` to the enclosing temporary scope of the block expression, letting it ignore the block tail temporary scope. Then, `pin!` might look like:
```rust
pub macro pin($value:expr $(,)?) {
'top: {
// SAFETY: The value is pinned: it is a temporary value, to which no references exist outside this macro.
let ref_pinned = &mut super 'top value!($value);
unsafe { $crate::pin::Pin::new_unchecked(ref_pinned) }
}
}
```
Furthermore, if we had unsafe hygiene or unsafe call syntax, as in the [design for `super` expressions described by Niko Matsakis][niko-super], we could get rid of the `let` binding. Supposing we adjust label syntax to accommodate `super` expressions by allowing any expression to be labeled, this becomes (using value-conversion `super` expressions):
```rust
pub macro pin($value:expr $(,)?) {
// SAFETY: The value is pinned: it is a temporary value, to which no references exist outside this macro.
'top: $crate::pin::Pin::new_unchecked unsafe (&mut super 'top $value)
}
```
`format_args!` can similarly be translated. Since it's built into the compiler, let's look at an example expansion. `format_args!("{t}{t:?}{t}", t = get_string())` evaluates `get_string()` once and formats the result two different ways. This could expand to:
```rust
'top: {
let args = super 'top (&get_string(),);
unsafe {
format_arguments::new(
b"\xc0\xc0\xc8\x00\x00\x00",
&super 'top
[format_argument::new_display(args.0),
format_argument::new_debug(args.0)],
)
}
}
```
`format_argument` is the compiler-internal name for the private `core::fmt::rt::Argument` and `format_arguments` is the compiler-internal name for [`core::fmt::Arguments`](https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html).
There are multiple ways to write this. Here, the result of `super 'top (&get_string(),)` is moved into the `args` binding, rather than a temporary. Re-scoping the tuple of borrowed arguments lifetime-extends their temporary scopes without needing a separate `super` for each. Additionally, to limit the extent of the `unsafe` block, the array reference could be assigned to a variable, or an `unsafe` call could be used.
### Properties of `super` expressions
For all value expressions `$vexpr`, `'l: { &super 'l $vexpr }` and `'l: identity(&super 'l $vexpr)` are equivalent to `&$vexpr` in expression contexts other than lifetime-extended place operand contexts, for both value-conversion `super` and re-scoping-only `super`. Additionally, for re-scoping-only `super`, these hold for all expressions, rather than just value expressions. The former equivalence expresses that `super` allows "seeing through" temporary scopes (exemplified by the block tail expression) and the latter equivalence expresses that when its label is in an extending context, `super` is lifetime-extended and its re-scoped expression is extending.
Additionally, `super $label &$vexpr` is equivalent to `&super $label $vexpr` in value expression contexts. Likewise, `super $label (&$vexpr_0, &$vexpr_1)` is equivalent to `(&super $label $vexpr_0, &super $label $vexpr_1)` in value expression contexts. As before, for re-scoping-only `super`, these hold for all expressions, rather than just value expressions. This distributivity-like property can be used, for example, to limit the number of `super`s that appear in the expansion of `format_args!` to two, rather than needing one on each borrow.
### Syntax considerations for `super` expressions
Using labels feels noisy and like an abuse of notation, but in order for the properties of a `super let` to hold, there needs to be some way for `super` expressions to tell what is being extended past. If it's possible to infer the correct scopes in the most general case, it's unclear how. Using a context-relative notation rather than labels is much less clear, at least for the case of super macros where a single label at the outermost scope of the macro suffices.
Also see the [section on ergonomics] at the end; there are ways to adjust the syntax of `super` expressions to be a better fit for certain use cases like method chains, but a `super let` designed for end-user consumption would have substantially different priorities, leading to meaningfully different semantics. No change in surface-level syntax would make this ideal for that purpose.
[niko-super]: https://github.com/rust-lang/rust/issues/139076#issuecomment-2794348786
[^super-expression-place-context-issue]: This isn't completely the same as being written in the label's context. Similar to the current `super let` statement, it doesn't preserve whether the label is in a lifetime-extended place operand context[^place-context-issue], though it errs towards being more permissive instead of more restrictive. This is by design, prioritizing conceptual clarity over algebraic equivalences. The super macro abstraction motivates a mental model where the result of `super` can be used as a lifetime-extendible temporary; thus, if the result of its labeled expression is lifetime-extended, the `super` should be too. There's not a good mental model for why a super temporary or super operand should care about its macro invocation being in a lifetime-extended place operand context; that would be a different, more specific abstraction.
[section on ergonomics]: #Ergonomics
## Implementing super macros with functions
As Mara [points out on Zulip](https://rust-lang.zulipchat.com/#narrow/channel/403629-t-lang.2Ftemporary-lifetimes-2024/topic/Revival.20of.20super/near/536240054), there is another way to design APIs that permit temporary lifetime extension: make it an opt-in property of functions. Rather than basing lifetime-extending APIs on macros built from scope-manipulating expressions, it could be based on lifetime-extending functions.
### Super operands via function parameters
A lifetime-extending function argument, annotated `extending` at the function definition site, becomes an extending context in extending call expressions. With just lifetime-extending function parameters and either unsafe hygiene or unsafe calls, we can implement `pin!` using a normal borrowed temporary to represent its super temporary:
```rust
// A place-to-value conversion operator that doesn't drop temporaries.
// This is just the identity function.
fn value<T>(extending x: T) -> T {
x
}
// In this hypothetical, for simplicity, `Pin::new_unchecked`'s operand is marked `extending`.
pub macro pin($value:expr $(,)?) {
// SAFETY: The value is pinned: it is a temporary value, to which no references exist outside this macro.
$crate::pin::Pin::new_unchecked unsafe (&mut value($value))
}
```
This works similarly to the [old implementation of `pin!`](https://github.com/rust-lang/rust/blob/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/pin.rs#L1947-L2019): converting `$value` to a value before borrowing it ensures it becomes a temporary, and `Pin::new_unchecked`'s argument being lifetime-extending means the result can be assigned to a variable.
Implementing `format_args!` this way is possible as well by making the argument array parameter of `fmt::Arguments::new` lifetime-extending, but it's more difficult to express, especially without new features or runtime performance compromises. In `format_args!("{t}{t:?}{t}", t = get_string())`, `get_string()` is evaluated once and formatted twice; both formatting calls are provided the same reference. In the macro-based model, this can be accomplished straightforwardly by binding it to a variable. In the function-based model, the equivalent would be taking it as a function argument. As there can be an arbitrary number of reference arguments to format, of different referent types, the most direct options options available would be to use variadic generics, emulate variadic generics via trait implementations on tuples, or use an array instead of a tuple and dispatch dynamically, sacrificing performance. Extrapolating from this, lifetime-extending functions may have difficulty backing the implementation of variadic super macros.
### Super temporaries via placing functions
`super let` bears a resemblance to the concept of [placing functions][yosh-blog]; this is noted, for example, in [Mara's blog post on `super let`][mara-blog], [credited to @lorepozo@tech.lgbt on Mastodon](https://tech.lgbt/@lorepozo/111499621585993237). Similar to how `super let` elevates temporary values to higher scopes so they can be borrowed in the result of an expression, placing functions desugar to [in-place initialization] to effectively allocate in the caller's stack frame--in a sense, the parent scope of the function--thereby allowing a temporary to be borrowed in the return value. This correspondence can be taken further: the appropriate scope of the temporary in the caller's stack frame should be that of a super temporary originating from the call expression, so that it's lifetime-extended when appropriate to match how the return value is used.
As noted in the above sources, this can be used to implement `pin!` as a function. Using the `'super` lifetime from [Yoshua Wuyts's blog post on placing functions][yosh-blog] to represent the lifetime of a super temporary, this could look like:
```rust
pub placing fn pin<T>(extending value: T) -> Pin<&'super mut T> {
super let mut pinned = value;
// SAFETY: The value is pinned: it is the local above which cannot be named outside this function.
unsafe { Pin::new_unchecked(&mut pinned) }
}
```
In this model, a `super let` statement is sugar for writing to implicit uninitialized arguments representing the super temporaries corresponding to its bindings. Calling `pin(temp())` would thus implicitly provide `pin` with a pointer to uninitialized memory representing `pinned`, into which the result of `temp()` would be written. The only way to safely access the `pinned` place outside of `pin` is through its return value.
This does not appear to help with implementing `format_args!`.
[yosh-blog]: https://blog.yoshuawuyts.com/placing-functions/
[in-place initialization]: https://github.com/rust-lang/rust-project-goals/issues/395
#### Conditional initialization
In theory, it's possible to conditionally or fallibly initialize using placing `super let`. For instance:
```rust
placing fn opt_ref() -> Option<&'super Struct> {
if cond() {
super let x = Struct::new();
Some(&x)
} else {
None
}
}
```
Although `x` isn't always initialized when returning from `opt_ref`, all references to it are known to be initialized after returning, because it can't be named outside of `opt_ref`. If the return value of `opt_ref` is `Some(rx)`, then `*rx` must be initialized for `opt_ref` to borrow-check. If `opt_ref` returns `None`, there's no safe way to access the uninitialized place.
One additional bit of state would be needed to make this work: in addition to initializing `x`, `super let` would need to set a drop flag for it if it's not known to be initialized at the end of the function. This lets the caller know whether to run its destructor when it goes out of scope.
This is analogous to code like:
```rust
let x;
let y = if cond() {
x = Struct::new();
Some(&x)
} else {
None
};
```
`y` is always usable at the end because it only contains a reference to `x` when `x` is initialized. And when `x` is initialized, a drop flag is set so that its destructor is run when it leaves scope.
#### Integration with in-place initialization
The above analysis assumes that placing `super let` exists in a vacuum. In practice, it should share whatever mechanisms end up being used for [in-place initialization], and care should be taken to ensure they work well together. For instance, the role of inter-procedural drop flags in the above section are filled in the [November 12 2025 meeting document](https://hackmd.io/@rust-for-linux-/H11r2RXpgl) by `&own` references, which enable static inter-procedural reasoning about initialization state. A full design would require collaboration and is out of scope for this document.
#### Do placing `super let` statements compose nicely with other features?
Given that the original `super let` statements ended up unlike `let` statements in order to satisfy other properties expected of them, we should ask: what do we expect of placing `super let`, and is it possible to meet all of those expectations?
Importantly, unlike the original `super let`, placing `super let` should have a temporary scope around it; having loose temporaries escaping into the caller would be surprising, unhelpful, and would complicate the function's ABI. Thankfully, this is achievable because these `super let` statements only aim to represent super temporaries, not super operands; they don't have to be transparent to temporary scoping like the original `super let` did.
Ideally to be like `let`, placing `super let` should have a notion of temporary lifetime extension for its initializer as well. In theory, it does: lifetime-extended temporaries become super temporaries, gaining the scope of the `super let`'s bindings, just like how lifetime extension works for `let`. The complication here is that this implicitly adds more arguments to the function, thus inferring its desugared type would require determining what its lifetime-extended temporaries are. This has two further complications:
- We wouldn't want extra implicit arguments for promoted constants. However, this is determined comparatively late in compilation, after type-checking, utilizing type information.
- We would want to be able to extend super temporaries from other functions with placing `super let`, but those functions could be extending super temporaries from other functions, so we'd need to know what their super temporaries are too, and so on; this creates a potentially-cyclic dependency graph. In the general case, inferring super temporaries would involve computing a least fixed point.
It may be possible to solve this by requiring annotations listing the super temporaries emplaced, either on the `super let` statement or the function signature. Alternatively, it could be solved by not supporting temporary lifetime extension for `super let` initializers, instead requiring borrowed temporaries to be factored out into separate `super let` statements. This would also significantly limit expressiveness, though, as there wouldn't be a way to lifetime-extend super temporaries from other functions or macros.
Affecting the function's ABI also makes an awkward detail of `super let` and `super` expressions more noticeable: there's two ways two use `super let`, which both want slightly different semantics. `super let` can be used to create a binding that's meant to be referenced, or it can create a reference-containing binding that's meant to be used as a value. In the former case, it's the scoping for the bindings that matters, and in the latter case, it's the scoping for the initializer. With `super` expressions, this is mostly inconsequential: when using the result of `super` in a value expression context, you don't leave a temporary around waiting to be dropped. However, with a naïve placing `super let`, a binding that's only copied or moved would still be allocated on a caller's stack frame and passed by pointer to the desugared function. If it's possible to delay the desugaring of placing functions to after borrow-checking, an analysis could potentially determine whether references to a `super let` binding escape from the function. Otherwise, explicit annotations may be needed.
### Syntax considerations for lifetime-extending functions
A statement seems to be a good fit for placing `super let`, since, e.g. it should ideally have a temporary scope around it. However, instead of using `super let` statements, we could extend a different design with a notion of lifetime extension across function boundaries. Without further adjustments, this would hurt consistency for the other designs here, but if that can be ironed out, it could potentially let us have both in the language. For example, a special label could be used with `super` expressions to indicate that they lifetime-extend outside of the function.
It's likely worth requiring special syntax to call a lifetime-extending function. Unlike macros, which have a `!` sigil signifying they could do something special, a function or method call expression would not stand out at a glance as being unique; if not annotated, it would require familiarity with the function. Although a properly-annotated lifetime-extending function would only extend arguments' temporaries when necessary for borrow-checking to succeed, it happening silently could be surprising. For instance, suppose `rc: RefCell<Struct>`, where `Struct` has a method `fn name(extending &self) -> &str`. Then, the assignment `let name = rc.borrow().name();` would implicitly lifetime-extend the borrow on the `RefCell`; this is required in order to use `name` later, but could easily lead to subtle bugs if that was unexpected or forgotten about. One can imagine a reality where lifetime-extending functions are commonplace, effectively implementing the use-site ergonomic gains of [RFC 66] through explicit definition-site annotations rather than type-based inference, but investigating that design (and its consequences for code clarity) is not the aim of this document.
[RFC 66]: https://github.com/rust-lang/rfcs/blob/45bfa54b27a0d1e678ce2fcdd782a471f75463a5/text/0066-better-temporary-lifetimes.md
### Use-site extending sub-expression annotations
The above concern about implicit lifetime extension raises a question: instead of definition-site annotations, would it be reasonable to have call-site annotations on the arguments that should be super operands? Conceptually, `pin!` could look like:
```rust
pub macro pin($value:expr $(,)?) {
// SAFETY: The value is pinned: it is a temporary value, to which no references exist outside this macro.
$crate::pin::Pin::new_unchecked unsafe (
extending &mut core::convert::identity(extending $value)
)
}
```
Here, `identity` serves as a place-to-value conversion operator; since it's a function call, it doesn't introduce a temporary scope, and its argument being `extending` means it doesn't prevent lifetime extension either. The `extending` on `Pin::new_unchecked`'s argument likewise propagates an extending context; if the `pin!` is extending, this means that the mutable borrow is lifetime-extended and `$value` is extending.
In a way, the mental model for an annotation in a single call like `f(extending &temp())` is clear: it signals that the reference could appear in the return value of `f`. Typically for functions though, this is a property of the function itself, not of the specific call; the concept feels misplaced.
Extending annotations also aren't as clear as they could be for implementing super macros: since the annotations are at the use site, they could directly specify how temporaries should be scoped, but they don't. Instead, as in `pin!` above, a chain of `extending`s threads lifetime-extension from the top level to where it's needed. This chain must be unbroken for lifetime-extension to work; consider:
``` rust
let x = f(extending g(extending &temp()));
```
If either `extending` was removed, `temp()` would no longer be lifetime-extended, as though neither were present. This means `extending` in composite expressions can do nothing, making its semantics less clear there; it's context-dependent whether it does anything.
## Comparison
This section qualitatively evaluates each design covered in this document against our [design goals](#Design-goals). This serves as a summary and to make the evaluation more explicit.
### Expressiveness
- `super let` statements can currently implement super macros where the super operands are all evaluated in a top-level block. Conditional evaluation only works with lifetime-extended temporaries, since non-extended temporaries are scoped to a short temporary scope (usually an `if` block or `match` arm).
- `let in` expressions can implement super macros where the super operands are all evaluated first. Further changes could allow other bindings to be assigned at the top level as well. Like with the current `super let`, conditional evaluation only works with lifetime-extended temporaries.
- `super` expressions can implement super macros with flexible evaluation order.
- Lifetime-extending functions trade some flexibility in writing macros for better type-checking. In the general case, variadic super macros may require tuple `impl` hacks or variadic generics. These and extending sub-expression annotations can only affect which contexts are extending, so the only way to get a non-extended temporary across a temporary scope boundary without additional features is to lifetime-extend it.
- Extending sub-expression annotations are at least as expressive as lifetime-extending functions, but potentially the syntax could be applied to other sub-expressions as well, such as `match` scrutinees.
- Placing `super let` cannot implement super operands, but can add significant expressiveness to another design by allowing function results to borrow from temporaries.
### Conceptual clarity and intuitiveness
- `super let` statements currently do not behave like `let` statements, or like statements in general.
- `let in` expressions don't work like other binding constructs. A syntax emphasizing that may be needed.
- `super` expressions, like the current `super let` and `let in`, serve a dual purpose of acting as an extendible temporary and of re-scoping another expression (which may in turn lifetime-extend temporaries). This is conceptually muddier than having distinct ways to do each. Focusing on one potential use at a time, though, there's an okay mental model: `super` acts like an extendible temporary of the labeled expression, and/or it scopes its re-scoped expression as a potentially-extending operand of the labeled expression.
- Lifetime-extending function parameters serve a single purpose and can be conceptualized: a lifetime-extending argument may appear or be referenced in the result value of the function. However, adding it onto the language at this stage raises questions about when it should be used. On one extreme, is it only for helper functions in order to implement macros? On the other extreme, is code in the future meant to look like it would under [RFC 66]?
- Use-site extending sub-expression annotations are mostly like lifetime-extending function parameters in terms of mental model, except reasoning about individual calls rather than functions themselves. The mental model is less clear when viewing a composite expression as a whole: they thread an extending context through the expression to temporaries that need to be lifetime-extended, but do nothing outside of an extending context.
- Like the other designs that directly create temporaries, placing `super let` serves at least two individually-clear purposes: it creates a temporary for the caller, and it can lifetime-extend temporaries to be temporaries for the caller. Pulling these apart may be more important than in other cases since it affects desugaring and function ABI.
### Composition with other language features
- The current `super let`, `let in`, and `super` expressions all obey simple algebraic identities in most, but not all contexts; this is a limitation of Rust's current temporary scoping rules.
- Lifetime-extending function parameters and extending sub-expression annotations fit into Rust's model of temporary scoping perfectly, by leveraging its notion of extending contexts directly.
- For placing `super let`, this is complicated. It shouldn't satisfy the same identities as other proposals, since that would require functions' behavior to depend on the context they're executed in. Instead, see the [dedicated section](#Do-placing-super-let-statements-compose-nicely-with-other-features) for ways it could fit into the language and their tradeoffs.
### Code clarity
- The current `super let` and particularly `let in` may require refactoring to pull them out to the top-level of macros so that their lifetime-extension works. For `super let` statements, needing to work around the block tail temporary scope can be awkward.
- `super` expressions feel natural as a way to implement super macros, since only one label at the top level is needed. As a more general scoping construct, it's less clear what they're doing. If only blocks can be labeled, needing to work around the block tail temporary scope can also be messy.
- Lifetime-extending function parameters are clear at the definition site, but may warrant use-site annotations to avoid unintended lifetime extension and to make it easier to see which temporaries are lifetime-extended. And unless the use-site annotations are on the arguments, familiarity with the functions would still be necessary to know which arguments can be extending. Additionally, In cases like `format_args!`, needing to use functions for lifetime-extension can add significant complexity.
- Extending sub-expression annotations are similar to lifetime-extending function parameters in their limitations and complexities.
- Too much is unresolved to say exactly how clear placing `super let` would look. It could be fairly complex, but allowing some macros to be written as functions instead is potentially a big improvement.
## Design quirks: revisiting `extending_apply!`
As introduced in [the section on `let in`](#Evaluation-order-restrictions), `extending_apply!` is a function application macro that makes the function's argument expression extending if the call is extending but does not lifetime-extend the function. Trying to implement it is an informative exercise for the other designs as well; it brings out quirks in them too.
### `extending_apply!` with `super` expressions
`extending_apply!` can be written directly as long as labels can be applied to any expression:
```rust
macro extending_apply($func:expr, $arg:expr) {
'top: $func(super 'top $arg)
}
```
If labels can only be applied to blocks, we run into a familiar problem: `$func`'s temporaries will be scoped to the block's tail expression temporary scope.
```rust
macro extending_apply_wrong($func:expr, $arg:expr) {
'top: { $func(super 'top $arg) }
}
```
In this case, we can work around it without needing specific syntax for blocks without tail expression temporary scopes, though it is less clear:
```rust
// Special syntax makes this easier to understand:
macro extending_apply_transparent($func:expr, $arg:expr) {
'top: transparent { $func(super 'top $arg) }
}
// But we can express it just with `super`:
macro extending_apply_plain($func:expr, $arg:expr) {
'top: { super 'top ($func(super 'top $arg)) }
}
```
In `extending_apply_plain!`, the outer `super` replaces the enclosing temporary scope for `$func` without making it extending, because callee expressions are never extending.
### `extending_apply!` with lifetime-extending functions
Since lifetime-extending functions are functions, we can't duck type the callee argument to be generic over how it should be borrowed. As a trade-off, we get type checking.
```rust
fn extending_apply<A, B>(func: impl FnOnce(A) -> B, extending arg: A) -> B {
func(arg)
}
```
### `extending_apply!` with extending sub-expression annotations
`extending_apply!` is effectively an extending annotation, so expressing it with extending annotations is trivial:
```rust
macro extending_apply($func:expr, $arg:expr) {
$func(extending $arg)
}
```
## Ergonomics
A sufficiently lightweight notation for specifying temporary lifetimes has potential in hand-written code for marking expressions that need larger temporary scopes. This could in turn allow for Rust to introduce more temporary scopes to help prevent bugs with drop-sensitive temporaries living too long, as was proposed in the [Rust 2024 temporary scoping design document][temps-2024]. However, the variants of `super let` proposed in this document are designed to allow authoring APIs that behave like Rust syntax; this comes at the expense of clarity. For an end-user-focused temporary lifetime specification feature, the design goals followed in this document should have their priorities reworked: clarity and simplicity become more important than generality and expressiveness. Designing such a feature is considered potential future work.
Additional reading and considerations for ergonomics:
- When writing code by hand, if it's necessary to explicitly extend the scopes of temporaries, it would be most clear to use an exact scope, bypassing the need to look further outward to find the enclosing temporary scope and determine whether any temporaries would be extended past it. This gives up the ability to compose with Rust's temporary scoping rules, but that's primarily a concern for API designers, not end-user code.
- To reduce the potential for surprise from unexpectedly-extended scopes, it may be desirable to change the temporary scope of just one expression (and temporaries its result is syntactically known to reference). This is a tradeoff: requiring more local annotations adds more noise to code.
- If focusing on making method chains more ergonomic, postfix syntax could greatly reduce the need for parenthesization. This could make other uses less clear, however.
- [Niko's proposal for `super` expressions][niko-super] infers extended temporary scopes rather than using a label. This trades some flexibility for less syntactic noise in the most common cases.
- In [a comment](https://github.com/m-ou-se/blog/issues/12#issuecomment-1868620909) on [Mara's blog post on `super let`][mara-blog], Curtis Millar proposes names for commonly-used temporary scopes. This would avoid the need to explicitly place labels in the most common cases, as well as the need to look for labels when reading code, while keeping the temporary scope used explicit. However, it also means needing to know the special labels in order to understand code.
# Questions for discussion
- Do the design axioms sound right?
- Any feelings about the specific design directions?
- Thoughts on balancing expressiveness vs. ergonomics vs. clarity?
- Was anything important missed here?