Skip to content

Implement super let#139112

Merged
bors merged 6 commits intorust-lang:masterfrom
m-ou-se:super-let
Apr 7, 2025
Merged

Implement super let#139112
bors merged 6 commits intorust-lang:masterfrom
m-ou-se:super-let

Conversation

@m-ou-se
Copy link
Copy Markdown
Member

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

Tracking issue: #139076

This implements super let as proposed in #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 }
  1. 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:

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

g(&f(&temp()));

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

The first equivalence rule tells us this must be equivalent:

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 #138718)

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?

    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?

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:

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':

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.

With the current implementation, the only difference between let and super let at function scope, is that super let drops all the temporaries in the init expression at the } (of the function) rather than at the ; (of the (super) let statement).

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.

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants