Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental feature gate for super let #139080

Merged
merged 2 commits into from
Apr 3, 2025
Merged

Conversation

m-ou-se
Copy link
Member

@m-ou-se m-ou-se commented Mar 28, 2025

This adds an experimental feature gate, #![feature(super_let)], for the super let experiment.

Tracking issue: #139076

Liaison: @nikomatsakis

Description

There's a rough (inaccurate) description here: https://blog.m-ou.se/super-let/

In short, super let allows you to define something that lives long enough to be borrowed by the tail expression of the block. For example:

let a = {
    super let b = temp();
    &b
};

Here, b is extended to live as long as a, similar to how in let a = &temp();, the temporary will be extended to live as long as a.

Properties

During the temporary lifetimes work we did last year, we explored the properties of "super let" and concluded that the fundamental property should be that these two are always equivalent in any context:

  1. & $expr
  2. { super let a = & $expr; a }

And, additionally, that these are equivalent in any context when $expr is a temporary (aka rvalue):

  1. & $expr
  2. { super let a = $expr; & a }

This makes it possible to give a name to a temporary without affecting how temporary lifetimes work, such that a macro can transparently use a block in its expansion, without that having any effect on the outside.

Implementing pin!() correctly

With super let, we can properly implement the pin!() macro without hacks: ✨

pub macro pin($value:expr $(,)?) {
    {
        super let mut pinned = $value;
        unsafe { $crate::pin::Pin::new_unchecked(&mut pinned) }
    }
}

This is important, as there is currently no way to express it without hacks in Rust 2021 and before (see hacky definition), and no way to express it at all in Rust 2024 (see issue).

Fixing format_args!()

This will also allow us to express format_args!() in a way where one can assign the result to a variable, fixing a long standing issue:

let f = format_args!("Hello {name}!"); // error today, but accepted in the future! (after separate FCP)

Experiment

The precise definition of super let, what happens for super let x; (without initializer), and whether to accept super let _ = _ else { .. } are still open questions, to be answered by the experiment.

Furthermore, once we have a more complete understanding of the feature, we might be able to come up with a better syntax. (Which could be just a different keywords, or an entirely different way of naming temporaries that doesn't involve a block and a (super) let statement.)

@m-ou-se m-ou-se added T-lang Relevant to the language team, which will review and decide on the PR/issue. I-lang-nominated Nominated for discussion during a lang team meeting. labels Mar 28, 2025
@rustbot

This comment was marked as outdated.

@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Mar 28, 2025
@m-ou-se m-ou-se assigned nikomatsakis and unassigned jieyouxu Mar 28, 2025
@nikomatsakis
Copy link
Contributor

@rustbot labels +I-lang-nominated

I'm happy to serve as champion/liaison for this, it's something @m-ou-se and I have extensively discussed in the past; but if others on @rust-lang/lang are interested, that could work too.

@traviscross
Copy link
Contributor

traviscross commented Mar 30, 2025

Exciting. First of all , thanks to @m-ou-se for putting this up.

Given that the primary equivalence includes a reference on the RHS of the super let,

                &$expr
---------------------------
{ super let a = &$expr; a }

and that consequently this behavior must involve extending the lifetime of temporaries created in this RHS, rather than just extending the "lifetime" (note scare quotes) of the named place itself, I find myself wondering whether it wouldn't be better to express this as a new type of reference constructor, &super $expr / &super mut $expr, rather than as a new type of let statement. Then we'd write, e.g.,

let a = {
    let b = &super temp();
    b
};

and,

pub macro pin($value:expr $(,)?) {
    {
        let pinned = &super mut { $value }; // <-- Force a move.
        unsafe { $crate::pin::Pin::new_unchecked(pinned) }
    }
}

and we've have the equivalence:

                &$expr
---------------------------
{ let a = &super $expr; a }

This feels to me like less new syntax, and for that reason, appealingly, would resolve two of the three open questions:

what happens for super let x; (without initializer), and whether to accept super let _ = _ else { .. }

What this gives up is being able to directly name the temporary rather than naming a reference to it, but -- I don't know -- this point felt more compelling to me in earlier drafts before we realized the primary equivalence is the one where super let is used with a reference on the RHS anyway.

Maybe this is actually a benefit regardless. We're talking about extending lifetimes, and references are what have lifetimes. By ensuring there's always a reference, we can talk about what's happening, e.g. with extending temporary lifetimes, in terms of the lifetime of that reference. That model feels a bit better to me at the moment, and seems maybe simpler to explain.

(It's been awhile since we talked about this. I recall there were reasons we thought that doing an expression position syntax might be challenging to reason about. I'm not sure offhand whether framing this as a reference constructor, rather than as the other things I recall us exploring, might help at all with those. It's something to explore.)

I'm curious of course to hear what people think.

@m-ou-se
Copy link
Member Author

m-ou-se commented Mar 30, 2025

I have considered that, but that would actually not solve #138718

This is the pin definition you suggest, which we'd use with a &super expression:

pub macro pin($value:expr $(,)?) {
    {
        let pinned = &super mut { $value }; // <-- Force a move.
        unsafe { $crate::pin::Pin::new_unchecked(pinned) }
    }
}

But how would that behave in

select(std::pin::pin!(foo(&mut temp())));

?

We must force a move, but we must also make sure that temp() lives long enough. In your expansion, you use an extra {} for that move, but that { $value } causes (non-extended) temporaries in $value to be dropped, as of Rust 2024.

This is exactly what caused #138596

We must make sure that the temporaries in $value are not dropped at the ; of the let. (Even when not lifetime-extended!) In my opinion, that makes that statement special, not the expression. That statement effectively does not have its own scope. That's why I've been talking about 'super' (let) statements, rather than 'super' expressions.

I agree it's a bit weird, but that's why we need to experiment.

@m-ou-se
Copy link
Member Author

m-ou-se commented Mar 30, 2025

Basically, the second equivalence,

                  & $rvalue
    ------------------------------
    { super let a = $rvalue; & a }

can be used to force a move (lvalue-to-rvalue conversion).

In Rust 2021 and before, you'd be able to trigger that with this eqiuvalence:

      & $rvalue
    -------------  ?
    & { $rvalue }

But that equivalence is gone in Rust 2024, as temporaries are dropped at the }. (Causing #138596.)

One could attempt to use the identity function:

              & $rvalue
    ---------------------  ?
    & identity( $rvalue )

But that's also not equivalent, because of temporary lifetime extension that might apply to temporaries in $rvalue.

I think it makes sense to use a let statement to force a move.

@m-ou-se
Copy link
Member Author

m-ou-se commented Mar 30, 2025

Anyway, I'm not necessarily opposed to &super or something like that, if we can find a good solution.

The point of this experiment is to have a syntax, so we can play around with lots of test cases. Then we can come up with the actual syntax afterwards, once we form a better understanding of all the cases where we'd use this.

@traviscross
Copy link
Contributor

traviscross commented Mar 31, 2025

Yes, agree it's weird (kind of a cursed problem) and that working all this out is what we gain from the experiment.

But how would that behave in

select(std::pin::pin!(foo(&mut temp())));

?

Probably I was expecting let x = &super mut $expr, and similarly super let x = &mut $expr, to extend the temporary and all temporaries in $expr, including non-extended ones, such that the reference could then appear in the tail expression of the block. Under that rule, the above would work, but indeed probably that rule would do "too much", particularly when $expr contains blocks itself.

Anyway, I'm entirely in favor of experimentation here under any syntax. Probably I just hope we can find some elegant way to avoid if at all possible having to add another kind of let. But maybe in the end it will work out best. Hopefully this will all come into a bit sharper focus as we work out how to write down all the rules exactly for what gets extended when.

@m-ou-se
Copy link
Member Author

m-ou-se commented Mar 31, 2025

It should affect non-extended temporaries, but it should not affect non-extended temporaries in blocks:

select(pin!({temp().f()})); // should drop temp() at the `}`
select(pin!(temp().f())); // should drop temp() at the `;`

Comment on lines +1 to +4
fn main() {
super let a = 1;
//~^ ERROR `super let` is experimental
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to run this back through CI, but in follow-up work, let's please remember to add a test that confirms we get this error when the code is cfged out. That's correctly done here (it's apparent in the code and I tested locally), but we should always want a test for this.

@traviscross
Copy link
Contributor

@bors r+ rollup

@bors
Copy link
Collaborator

bors commented Apr 2, 2025

📌 Commit 14e6a96 has been approved by traviscross

It is now in the queue for this repository.

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Apr 2, 2025
@traviscross
Copy link
Contributor

Thanks again to @m-ou-se for putting this forward. We can I think call this lang experiment accepted.

(We didn't quite get to this one in the lang meeting today, but we circulated it async without hearing objections, and this is rather clearly necessary work. The feature gate is of course a two-day door. And both Niko and I are willing to champion it (we'll flip a coin or something).)

m-ou-se added a commit to m-ou-se/rust that referenced this pull request Apr 3, 2025
Experimental feature gate for `super let`

This adds an experimental feature gate, `#![feature(super_let)]`, for the `super let` experiment.

Tracking issue: rust-lang#139076

Liaison: `@nikomatsakis`

## Description

There's a rough (inaccurate) description here: https://blog.m-ou.se/super-let/

In short, `super let` allows you to define something that lives long enough to be borrowed by the tail expression of the block. For example:

```rust
let a = {
    super let b = temp();
    &b
};
```

Here, `b` is extended to live as long as `a`, similar to how in `let a = &temp();`, the temporary will be extended to live as long as `a`.

## Properties

During the temporary lifetimes work we did last year, we explored the properties of "super let" and concluded that the fundamental property should be that these two are always equivalent in any context:

1. `& $expr`
2. `{ super let a = & $expr; a }`

And, additionally, that these are equivalent in any context when `$expr` is a temporary (aka rvalue):

1. `& $expr`
2. `{ super let a = $expr; & a }`

This makes it possible to give a name to a temporary without affecting how temporary lifetimes work, such that a macro can transparently use a block in its expansion, without that having any effect on the outside.

## Implementing pin!() correctly

With `super let`, we can properly implement the `pin!()` macro without hacks: ✨

```rust
pub macro pin($value:expr $(,)?) {
    {
        super let mut pinned = $value;
        unsafe { $crate::pin::Pin::new_unchecked(&mut pinned) }
    }
}
```

This is important, as there is currently no way to express it without hacks in Rust 2021 and before (see [hacky definition](https://github.com/rust-lang/rust/blob/2a06022951893fe5b5384f8dbd75b4e6e3b5cee0/library/core/src/pin.rs#L1947)), and no way to express it at all in Rust 2024 (see [issue](rust-lang#138718)).

## Fixing format_args!()

This will also allow us to express `format_args!()` in a way where one can assign the result to a variable, fixing a [long standing issue](rust-lang#92698):

```rust
let f = format_args!("Hello {name}!"); // error today, but accepted in the future! (after separate FCP)
```

## Experiment

The precise definition of `super let`, what happens for `super let x;` (without initializer), and whether to accept `super let _ = _ else { .. }` are still open questions, to be answered by the experiment.

Furthermore, once we have a more complete understanding of the feature, we might be able to come up with a better syntax. (Which could be just a different keywords, or an entirely different way of naming temporaries that doesn't involve a block and a (super) let statement.)
bors added a commit to rust-lang-ci/rust that referenced this pull request Apr 3, 2025
…iaskrgr

Rollup of 7 pull requests

Successful merges:

 - rust-lang#139080 (Experimental feature gate for `super let`)
 - rust-lang#139145 (slice: Remove some uses of unsafe in first/last chunk methods)
 - rust-lang#139149 (unstable book: document import_trait_associated_functions)
 - rust-lang#139273 (Apply requested API changes to `cell_update`)
 - rust-lang#139282 (rustdoc: make settings checkboxes always square)
 - rust-lang#139283 (Rustc dev guide subtree update)
 - rust-lang#139294 (Fix the `f16`/`f128` feature gates on integer literals)

r? `@ghost`
`@rustbot` modify labels: rollup
bors added a commit to rust-lang-ci/rust that referenced this pull request Apr 3, 2025
…iaskrgr

Rollup of 7 pull requests

Successful merges:

 - rust-lang#139080 (Experimental feature gate for `super let`)
 - rust-lang#139145 (slice: Remove some uses of unsafe in first/last chunk methods)
 - rust-lang#139149 (unstable book: document import_trait_associated_functions)
 - rust-lang#139273 (Apply requested API changes to `cell_update`)
 - rust-lang#139282 (rustdoc: make settings checkboxes always square)
 - rust-lang#139283 (Rustc dev guide subtree update)
 - rust-lang#139294 (Fix the `f16`/`f128` feature gates on integer literals)

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors merged commit dbd7f52 into rust-lang:master Apr 3, 2025
6 checks passed
@rustbot rustbot added this to the 1.88.0 milestone Apr 3, 2025
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Apr 3, 2025
Rollup merge of rust-lang#139080 - m-ou-se:super-let-gate, r=traviscross

Experimental feature gate for `super let`

This adds an experimental feature gate, `#![feature(super_let)]`, for the `super let` experiment.

Tracking issue: rust-lang#139076

Liaison: ``@nikomatsakis``

## Description

There's a rough (inaccurate) description here: https://blog.m-ou.se/super-let/

In short, `super let` allows you to define something that lives long enough to be borrowed by the tail expression of the block. For example:

```rust
let a = {
    super let b = temp();
    &b
};
```

Here, `b` is extended to live as long as `a`, similar to how in `let a = &temp();`, the temporary will be extended to live as long as `a`.

## Properties

During the temporary lifetimes work we did last year, we explored the properties of "super let" and concluded that the fundamental property should be that these two are always equivalent in any context:

1. `& $expr`
2. `{ super let a = & $expr; a }`

And, additionally, that these are equivalent in any context when `$expr` is a temporary (aka rvalue):

1. `& $expr`
2. `{ super let a = $expr; & a }`

This makes it possible to give a name to a temporary without affecting how temporary lifetimes work, such that a macro can transparently use a block in its expansion, without that having any effect on the outside.

## Implementing pin!() correctly

With `super let`, we can properly implement the `pin!()` macro without hacks: ✨

```rust
pub macro pin($value:expr $(,)?) {
    {
        super let mut pinned = $value;
        unsafe { $crate::pin::Pin::new_unchecked(&mut pinned) }
    }
}
```

This is important, as there is currently no way to express it without hacks in Rust 2021 and before (see [hacky definition](https://github.com/rust-lang/rust/blob/2a06022951893fe5b5384f8dbd75b4e6e3b5cee0/library/core/src/pin.rs#L1947)), and no way to express it at all in Rust 2024 (see [issue](rust-lang#138718)).

## Fixing format_args!()

This will also allow us to express `format_args!()` in a way where one can assign the result to a variable, fixing a [long standing issue](rust-lang#92698):

```rust
let f = format_args!("Hello {name}!"); // error today, but accepted in the future! (after separate FCP)
```

## Experiment

The precise definition of `super let`, what happens for `super let x;` (without initializer), and whether to accept `super let _ = _ else { .. }` are still open questions, to be answered by the experiment.

Furthermore, once we have a more complete understanding of the feature, we might be able to come up with a better syntax. (Which could be just a different keywords, or an entirely different way of naming temporaries that doesn't involve a block and a (super) let statement.)
@m-ou-se m-ou-se deleted the super-let-gate branch April 3, 2025 19:21
bors added a commit to rust-lang-ci/rust that referenced this pull request Apr 4, 2025
Implement `super let`

Tracking issue: rust-lang#139076

This implements `super let` as proposed in rust-lang#139080, based on the following two equivalence rules.

1. For all expressions `$expr` in any context, these are equivalent:
  - `& $expr`
  - `{ super let a = & $expr; a }`

2. And, additionally, these are equivalent in any context when `$expr` is a temporary (aka rvalue):
  - `& $expr`
  - `{ super let a = $expr; & a }`

So far, this experiment has a few interesting results:

## Interesting result 1

In this snippet:

```rust
super let a = f(&temp());
```

I originally expected temporary `temp()` would be dropped at the end of the statement (`;`), just like in a regular `let`, because `temp()` is not subject to temporary lifetime extension.

However, it turns out that that would break the fundamental equivalence rules.

For example, in

```rust
g(&f(&temp()));
```

the temporary `temp()` will be dropped at the `;`.

The first equivalence rule tells us this must be equivalent:

```rust
g({ super let a = &f(&temp()); a });
```

But that means that `temp()` must live until the last `;` (after `g()`), not just the first `;` (after `f()`).

While this was somewhat surprising to me at first, it does match the exact behavior we need for `pin!()`: The following _should work_. (See also rust-lang#138718)

```rust
g(pin!(f(&mut temp())));
```

Here, `temp()` lives until the end of the statement. This makes sense from the perspective of the user, as no other `;` or `{}` are visible. Whether `pin!()` uses a `{}` block internally or not should be irrelevant.

This means that _nothing_ in a `super let` statement will be dropped at the end of that super let statement. It does not even need its own scope.

This raises questions that are useful for later on:

- Will this make temporaries live _too long_ in cases where `super let` is used not in a hidden block in a macro, but as a visible statement in code like the following?

    ```rust
    let writer = {
        super let file = File::create(&format!("/home/{user}/test"));
        Writer::new(&file)
    };
    ```

- Is a `let` statement in a block still the right syntax for this? Considering it has _no_ scope of its own, maybe neither a block nor a statement should be involved

This leads me to think that instead of `{ super let $pat = $init; $expr }`, we might want to consider something like `let $pat = $init in $expr` or `$expr where $pat = $init`. Although there are also issues with these, as it isn't obvious anymore if `$init` should be subject to temporary lifetime extension. (Do we want both `let _ = _ in ..` and `super let _ = _ in ..`?)

## Interesting result 2

What about `super let x;` without initializer?

```rust
let a = {
    super let x;
    x = temp();
    &x
};
```

This works fine with the implementation in this PR: `x` is extended to live as long as `a`.

While it matches my expectations, a somewhat interesting thing to realize is that these are _not_ equivalent:

- `super let x = $expr;`
- `super let x; x = $expr;`

In the first case, all temporaries in $expr will live at least as long as (the result of) the surrounding block.
In the second case, temporaries will be dropped at the end of the assignment statement. (Because the assignment statement itself "is not `super`".)

This difference in behavior might be confusing, but it _might_ be useful.
One might want to extend the lifetime of a variable without extending all the temporaries in the initializer expression.

On the other hand, that can also be expressed as:

- `let x = $expr; super let x = x;` (w/o temporary lifetime extension), or
- `super let x = { $expr };` (w/ temporary lifetime extension)

So, this raises these questions:

- Do we want to accept `super let x;` without initializer at all?

- Does it make sense for statements other than let statements to be "super"? An expression statement also drops temporaries at its `;`, so now that we discovered that `super let` basically disables that `;` (see interesting result 1), is there a use to having other statements without their own scope? (I don't think that's ever useful?)

## Interesting result 3

This works now:

```rust
super let Some(x) = a.get(i) else { return };
```

I didn't put in any special cases for `super let else`. This is just the behavior that 'naturally' falls out when implementing `super let` without thinking of the `let else` case.

- Should `super let else` work?

## Interesting result 4

This 'works':

```rust
fn main() {
    super let a = 123;
}
```

I didn't put in any special cases for `super let` at function scope. I had expected the code to cause an ICE or other weird failure when used at function body scope, because there's no way to let the variable live as long as the result of the function.

This raises the question:

- Does this mean that this behavior is the natural/expected behavior when `super let` is used at function scope? Or is this just a quirk and should we explicitly disallow `super let` in a function body? (Probably the latter.)

---

The questions above do not need an answer to land this PR. These questions should be considered when redesigning/rfc'ing/stabilizing the feature.
Zalathar added a commit to Zalathar/rust that referenced this pull request Apr 7, 2025
Implement `super let`

Tracking issue: rust-lang#139076

This implements `super let` as proposed in rust-lang#139080, based on the following two equivalence rules.

1. For all expressions `$expr` in any context, these are equivalent:
  - `& $expr`
  - `{ super let a = & $expr; a }`

2. And, additionally, these are equivalent in any context when `$expr` is a temporary (aka rvalue):
  - `& $expr`
  - `{ super let a = $expr; & a }`

So far, this experiment has a few interesting results:

## Interesting result 1

In this snippet:

```rust
super let a = f(&temp());
```

I originally expected temporary `temp()` would be dropped at the end of the statement (`;`), just like in a regular `let`, because `temp()` is not subject to temporary lifetime extension.

However, it turns out that that would break the fundamental equivalence rules.

For example, in

```rust
g(&f(&temp()));
```

the temporary `temp()` will be dropped at the `;`.

The first equivalence rule tells us this must be equivalent:

```rust
g({ super let a = &f(&temp()); a });
```

But that means that `temp()` must live until the last `;` (after `g()`), not just the first `;` (after `f()`).

While this was somewhat surprising to me at first, it does match the exact behavior we need for `pin!()`: The following _should work_. (See also rust-lang#138718)

```rust
g(pin!(f(&mut temp())));
```

Here, `temp()` lives until the end of the statement. This makes sense from the perspective of the user, as no other `;` or `{}` are visible. Whether `pin!()` uses a `{}` block internally or not should be irrelevant.

This means that _nothing_ in a `super let` statement will be dropped at the end of that super let statement. It does not even need its own scope.

This raises questions that are useful for later on:

- Will this make temporaries live _too long_ in cases where `super let` is used not in a hidden block in a macro, but as a visible statement in code like the following?

    ```rust
    let writer = {
        super let file = File::create(&format!("/home/{user}/test"));
        Writer::new(&file)
    };
    ```

- Is a `let` statement in a block still the right syntax for this? Considering it has _no_ scope of its own, maybe neither a block nor a statement should be involved

This leads me to think that instead of `{ super let $pat = $init; $expr }`, we might want to consider something like `let $pat = $init in $expr` or `$expr where $pat = $init`. Although there are also issues with these, as it isn't obvious anymore if `$init` should be subject to temporary lifetime extension. (Do we want both `let _ = _ in ..` and `super let _ = _ in ..`?)

## Interesting result 2

What about `super let x;` without initializer?

```rust
let a = {
    super let x;
    x = temp();
    &x
};
```

This works fine with the implementation in this PR: `x` is extended to live as long as `a`.

While it matches my expectations, a somewhat interesting thing to realize is that these are _not_ equivalent:

- `super let x = $expr;`
- `super let x; x = $expr;`

In the first case, all temporaries in $expr will live at least as long as (the result of) the surrounding block.
In the second case, temporaries will be dropped at the end of the assignment statement. (Because the assignment statement itself "is not `super`".)

This difference in behavior might be confusing, but it _might_ be useful.
One might want to extend the lifetime of a variable without extending all the temporaries in the initializer expression.

On the other hand, that can also be expressed as:

- `let x = $expr; super let x = x;` (w/o temporary lifetime extension), or
- `super let x = { $expr };` (w/ temporary lifetime extension)

So, this raises these questions:

- Do we want to accept `super let x;` without initializer at all?

- Does it make sense for statements other than let statements to be "super"? An expression statement also drops temporaries at its `;`, so now that we discovered that `super let` basically disables that `;` (see interesting result 1), is there a use to having other statements without their own scope? (I don't think that's ever useful?)

## Interesting result 3

This works now:

```rust
super let Some(x) = a.get(i) else { return };
```

I didn't put in any special cases for `super let else`. This is just the behavior that 'naturally' falls out when implementing `super let` without thinking of the `let else` case.

- Should `super let else` work?

## Interesting result 4

This 'works':

```rust
fn main() {
    super let a = 123;
}
```

I didn't put in any special cases for `super let` at function scope. I had expected the code to cause an ICE or other weird failure when used at function body scope, because there's no way to let the variable live as long as the result of the function.

This raises the question:

- Does this mean that this behavior is the natural/expected behavior when `super let` is used at function scope? Or is this just a quirk and should we explicitly disallow `super let` in a function body? (Probably the latter.)

---

The questions above do not need an answer to land this PR. These questions should be considered when redesigning/rfc'ing/stabilizing the feature.
Zalathar added a commit to Zalathar/rust that referenced this pull request Apr 7, 2025
Implement `super let`

Tracking issue: rust-lang#139076

This implements `super let` as proposed in rust-lang#139080, based on the following two equivalence rules.

1. For all expressions `$expr` in any context, these are equivalent:
  - `& $expr`
  - `{ super let a = & $expr; a }`

2. And, additionally, these are equivalent in any context when `$expr` is a temporary (aka rvalue):
  - `& $expr`
  - `{ super let a = $expr; & a }`

So far, this experiment has a few interesting results:

## Interesting result 1

In this snippet:

```rust
super let a = f(&temp());
```

I originally expected temporary `temp()` would be dropped at the end of the statement (`;`), just like in a regular `let`, because `temp()` is not subject to temporary lifetime extension.

However, it turns out that that would break the fundamental equivalence rules.

For example, in

```rust
g(&f(&temp()));
```

the temporary `temp()` will be dropped at the `;`.

The first equivalence rule tells us this must be equivalent:

```rust
g({ super let a = &f(&temp()); a });
```

But that means that `temp()` must live until the last `;` (after `g()`), not just the first `;` (after `f()`).

While this was somewhat surprising to me at first, it does match the exact behavior we need for `pin!()`: The following _should work_. (See also rust-lang#138718)

```rust
g(pin!(f(&mut temp())));
```

Here, `temp()` lives until the end of the statement. This makes sense from the perspective of the user, as no other `;` or `{}` are visible. Whether `pin!()` uses a `{}` block internally or not should be irrelevant.

This means that _nothing_ in a `super let` statement will be dropped at the end of that super let statement. It does not even need its own scope.

This raises questions that are useful for later on:

- Will this make temporaries live _too long_ in cases where `super let` is used not in a hidden block in a macro, but as a visible statement in code like the following?

    ```rust
    let writer = {
        super let file = File::create(&format!("/home/{user}/test"));
        Writer::new(&file)
    };
    ```

- Is a `let` statement in a block still the right syntax for this? Considering it has _no_ scope of its own, maybe neither a block nor a statement should be involved

This leads me to think that instead of `{ super let $pat = $init; $expr }`, we might want to consider something like `let $pat = $init in $expr` or `$expr where $pat = $init`. Although there are also issues with these, as it isn't obvious anymore if `$init` should be subject to temporary lifetime extension. (Do we want both `let _ = _ in ..` and `super let _ = _ in ..`?)

## Interesting result 2

What about `super let x;` without initializer?

```rust
let a = {
    super let x;
    x = temp();
    &x
};
```

This works fine with the implementation in this PR: `x` is extended to live as long as `a`.

While it matches my expectations, a somewhat interesting thing to realize is that these are _not_ equivalent:

- `super let x = $expr;`
- `super let x; x = $expr;`

In the first case, all temporaries in $expr will live at least as long as (the result of) the surrounding block.
In the second case, temporaries will be dropped at the end of the assignment statement. (Because the assignment statement itself "is not `super`".)

This difference in behavior might be confusing, but it _might_ be useful.
One might want to extend the lifetime of a variable without extending all the temporaries in the initializer expression.

On the other hand, that can also be expressed as:

- `let x = $expr; super let x = x;` (w/o temporary lifetime extension), or
- `super let x = { $expr };` (w/ temporary lifetime extension)

So, this raises these questions:

- Do we want to accept `super let x;` without initializer at all?

- Does it make sense for statements other than let statements to be "super"? An expression statement also drops temporaries at its `;`, so now that we discovered that `super let` basically disables that `;` (see interesting result 1), is there a use to having other statements without their own scope? (I don't think that's ever useful?)

## Interesting result 3

This works now:

```rust
super let Some(x) = a.get(i) else { return };
```

I didn't put in any special cases for `super let else`. This is just the behavior that 'naturally' falls out when implementing `super let` without thinking of the `let else` case.

- Should `super let else` work?

## Interesting result 4

This 'works':

```rust
fn main() {
    super let a = 123;
}
```

I didn't put in any special cases for `super let` at function scope. I had expected the code to cause an ICE or other weird failure when used at function body scope, because there's no way to let the variable live as long as the result of the function.

This raises the question:

- Does this mean that this behavior is the natural/expected behavior when `super let` is used at function scope? Or is this just a quirk and should we explicitly disallow `super let` in a function body? (Probably the latter.)

---

The questions above do not need an answer to land this PR. These questions should be considered when redesigning/rfc'ing/stabilizing the feature.
rust-timer added a commit to rust-lang-ci/rust that referenced this pull request Apr 7, 2025
Rollup merge of rust-lang#139112 - m-ou-se:super-let, r=lcnr

Implement `super let`

Tracking issue: rust-lang#139076

This implements `super let` as proposed in rust-lang#139080, based on the following two equivalence rules.

1. For all expressions `$expr` in any context, these are equivalent:
  - `& $expr`
  - `{ super let a = & $expr; a }`

2. And, additionally, these are equivalent in any context when `$expr` is a temporary (aka rvalue):
  - `& $expr`
  - `{ super let a = $expr; & a }`

So far, this experiment has a few interesting results:

## Interesting result 1

In this snippet:

```rust
super let a = f(&temp());
```

I originally expected temporary `temp()` would be dropped at the end of the statement (`;`), just like in a regular `let`, because `temp()` is not subject to temporary lifetime extension.

However, it turns out that that would break the fundamental equivalence rules.

For example, in

```rust
g(&f(&temp()));
```

the temporary `temp()` will be dropped at the `;`.

The first equivalence rule tells us this must be equivalent:

```rust
g({ super let a = &f(&temp()); a });
```

But that means that `temp()` must live until the last `;` (after `g()`), not just the first `;` (after `f()`).

While this was somewhat surprising to me at first, it does match the exact behavior we need for `pin!()`: The following _should work_. (See also rust-lang#138718)

```rust
g(pin!(f(&mut temp())));
```

Here, `temp()` lives until the end of the statement. This makes sense from the perspective of the user, as no other `;` or `{}` are visible. Whether `pin!()` uses a `{}` block internally or not should be irrelevant.

This means that _nothing_ in a `super let` statement will be dropped at the end of that super let statement. It does not even need its own scope.

This raises questions that are useful for later on:

- Will this make temporaries live _too long_ in cases where `super let` is used not in a hidden block in a macro, but as a visible statement in code like the following?

    ```rust
    let writer = {
        super let file = File::create(&format!("/home/{user}/test"));
        Writer::new(&file)
    };
    ```

- Is a `let` statement in a block still the right syntax for this? Considering it has _no_ scope of its own, maybe neither a block nor a statement should be involved

This leads me to think that instead of `{ super let $pat = $init; $expr }`, we might want to consider something like `let $pat = $init in $expr` or `$expr where $pat = $init`. Although there are also issues with these, as it isn't obvious anymore if `$init` should be subject to temporary lifetime extension. (Do we want both `let _ = _ in ..` and `super let _ = _ in ..`?)

## Interesting result 2

What about `super let x;` without initializer?

```rust
let a = {
    super let x;
    x = temp();
    &x
};
```

This works fine with the implementation in this PR: `x` is extended to live as long as `a`.

While it matches my expectations, a somewhat interesting thing to realize is that these are _not_ equivalent:

- `super let x = $expr;`
- `super let x; x = $expr;`

In the first case, all temporaries in $expr will live at least as long as (the result of) the surrounding block.
In the second case, temporaries will be dropped at the end of the assignment statement. (Because the assignment statement itself "is not `super`".)

This difference in behavior might be confusing, but it _might_ be useful.
One might want to extend the lifetime of a variable without extending all the temporaries in the initializer expression.

On the other hand, that can also be expressed as:

- `let x = $expr; super let x = x;` (w/o temporary lifetime extension), or
- `super let x = { $expr };` (w/ temporary lifetime extension)

So, this raises these questions:

- Do we want to accept `super let x;` without initializer at all?

- Does it make sense for statements other than let statements to be "super"? An expression statement also drops temporaries at its `;`, so now that we discovered that `super let` basically disables that `;` (see interesting result 1), is there a use to having other statements without their own scope? (I don't think that's ever useful?)

## Interesting result 3

This works now:

```rust
super let Some(x) = a.get(i) else { return };
```

I didn't put in any special cases for `super let else`. This is just the behavior that 'naturally' falls out when implementing `super let` without thinking of the `let else` case.

- Should `super let else` work?

## Interesting result 4

This 'works':

```rust
fn main() {
    super let a = 123;
}
```

I didn't put in any special cases for `super let` at function scope. I had expected the code to cause an ICE or other weird failure when used at function body scope, because there's no way to let the variable live as long as the result of the function.

This raises the question:

- Does this mean that this behavior is the natural/expected behavior when `super let` is used at function scope? Or is this just a quirk and should we explicitly disallow `super let` in a function body? (Probably the latter.)

---

The questions above do not need an answer to land this PR. These questions should be considered when redesigning/rfc'ing/stabilizing the feature.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-lang-nominated Nominated for discussion during a lang team meeting. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants