--- title: "Design meeting 2024-02-14: Temporary Lifetimes" tags: ["T-lang", "design-meeting", "minutes"] date: 2024-02-14 discussion: https://rust-lang.zulipchat.com/#narrow/stream/410673-t-lang.2Fmeetings/topic/Design.20meeting.202024-02-14 url: https://hackmd.io/LBCK4dQlT8ipGCNA6yM_Nw --- # Temporary lifetimes draft RFC (v2) - Feature Name: `new_temp_lifetime` - Start Date: (fill me in with today's date, YYYY-MM-DD) - RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000) - Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000) - [Hackmd with meeting notes](https://hackmd.io/-l7-r7GiSWu2HJrPOI74BA?edit) # Summary [summary]: #summary Motivation: * Two particular aspects of today's temporary rules are a common source of bugs and confusion (e.g., see this [example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e3d3747f371b62750939c47591705d74) for an illustration of both issues): * Temporaries in match expressions last until the end of the match, which commonly leads to deadlocks. * Temporaries in the tail expression in a block live longer than the block itself, so that e.g. `{expr;}` and `{expr}` can behave very differently. * Rust's current temporary rules give no way to create a named temporary value whose lifetime escapes the current block. This in turn creates ergonomic issues: * There is no way to write macros that introduce temporaries that work in all positions, so e.g. the result of the `format_args!` macro cannot always be stored in a let ([example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=85efb70b0d999626cc030d4ff0912550)). * When writing blocks in a nested expression, you can't have references to `let` values that escape ([example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b0708ed68979532534c226036e84951e)). Solution: * Introduce `super let`, which lets you introduce temporary bindings that have names. * The expression `& $expr` is equivalent to `{ super let tmp = $expr; &tmp }`. * In Rust 2024, adjust temporary rules to remove known footguns: * Temporaries in match expressions are freed before testing patterns, rather than at the end of the match. * Temporaries in the tail expression of a block are freed before the end of the block. # Motivation [motivation]: #motivation > Why are we doing this? What use cases does it support? What is the expected outcome? Rust programs sometimes need to introduce temporary locations so that they can create references. As a simple example, the expression `&foo()` needs to invoke `foo()`, store the result into a temporary location `t`, and then take a reference `&t`; temporaries can also be introduced when invoking `&self` or `&mut self` methods, e.g. with a call like `return_mutex().lock()`, which must store the result of `return_mutex()` into a temporary to invoke `lock`, as `lock` is an `&self` method. Whenever a temporary value is created, it raises the question of when that temporary location will be freed. Given that freeing a value also causes its destructor to run, this choice can affect important questions like when a lock will be released. The current rules pre-date the RFC process, but they were [motivated in this blog post from 2023](https://smallcultfollowing.com/babysteps/blog/2023/03/15/temporary-lifetimes/). There are two core principles: * By default, temporaries are dropped at the end of the innermost enclosing statement. This is generally "long enough" to allow method chains and other interesting expressions to work, but not so long that users are surprised by e.g. a lock that is not freed. * Example: given `let value = foo(&String::new())`, the string returned by `String::new()` will be freed at the end of the `let` statement. * But sometimes we can see syntactically that a reference to a temporary will be stored in a `let` binding; in that case, we give the temporary an *extended* lifetime, so that it lasts until the end of the block. * Example: given `let value = &String::new()`, the string returned by `String::new()` will be freed at the end of the enclosing block. Over time, two problems have been identified with the rules as formulated. ## There is no explicit way to declare a temporary, which is hostile to macros In blocks today the `let` keyword introduces named storage that is freed at the end of the block: ```rust { let value1 = String::new(); let value2 = String::new(); ... let pair = (&value1, &value2); ... // `value1` and `value2` are freed here } ``` References to `let`-bound variables must only be used within a block and you will get a compilation error if those references escape: ```rust let pair = { let value1 = String::new(); let value2 = String::new(); ... (&value1, &value2) //~^ ERROR because `value1` and `value2` are freed before the `let` ends }; ``` One way to resolve this is to declare local variables in an outer scope: ```rust let (value1, value); let pair = { value1 = String::new(); value2 = String::new(); (&value1, &value2) }; // OK ``` This pattern is very flexible, but it is non-obvious. The other problem with this pattern it is *non-local*, meaning that it requires changes outside of the block itself. As a result, it cannot be used as part of a macro definition. Today, it is not always possible to write a macro `foo!($expr)` that can be used wherever `$expr` is valid. The `pin!` macro is one example where we were able to do so, but the solution was non-obvious, was first thought to be impossible, and relied on internal details only available to the standard library. The `format_args!` macro is one example where we cannot do this. As a result, attempting to store the result of `format_args!` into a let binding gets surprising errors ([example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=b0708ed68979532534c226036e84951e)): ```rust! fn main() { // This is OK. A temporary is created to store the result of // `String::new()` and that temporary is freed at the end of the statement: println!("{}", format_args!("{}", String::new())); // This fails to compile. As before, a temporary is created to // store the result of `String::new` and that temporary is freed at the end // of the statement (i.e., after the `let`), but that means that `rc` // contains a reference to that freed temporary... let rc = format_args!("{}", String::new()); // ...and so we get an error here. println!("{}", rc); } ``` ## Temporaries live too long in match expressions and block tail expressions, leading to bugs Two particular aspects of today's temporary rules are a common source of bugs and confusion (e.g., this [example](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e3d3747f371b62750939c47591705d74) illustrates both issues): * Temporaries in match expressions last until the end of the match, which commonly leads to deadlocks. * Temporaries in the tail expression in a block live longer than the block itself, so that e.g. `{expr;}` and `{expr}` can behave very differently. This research found these issues to be the dominant cause of real-world bugs in the code they examined: - [Paper]() TODO: Fill in title of and link to paper. # Guide-level explanation [Guide-level-explanation]: #guide-level-explanation This section gives an "intuitive" definition of the RFC changes. It is written to target someone who knows Rust's existing rules fairly well. In the [reference-level explanation][reference-level-explanation] section, we explain the rules in detail. ## `super let` for introducing explicit temporaries This RFC introduces a new `let` form, written `super let`, which declares a variable that outlives the current block. Intuitively, the lifetime of a `super let`-bound variable is "as long as the result of the block". The following code compiles now: ```rust! let pair = { super let value1 = String::new(); super let value2 = String::new(); ... (&value1, &value2) }; ``` ### The "super let equivalence" The core property of `super let` is that... ``` & $expr ``` ...is exactly equivalent to... ```rust { super let v = &$expr; v } ``` The difference is that, in the second form, `v` has a name, and you can use that to build up more complex structures or do other interesting things. For example, ``` SomeStruct { field: & $expr, } ``` ...is exactly equivalent to... ```rust { super let v = &$expr; SomeStruct { field: v, } } ``` More importantly, it can be used to express expressions with potentially lifetime-extended temporaries that are impossible to express without `super let`: ```rust= { super let v = &$expr; SomeStruct::new(v) } ``` ### Using `super let` to define a transparent `pin!` macro Using `super let`, we can define the `pin!` macro without any tricks and without relying on any internal details only available to the standard library. E.g.: ```rust let x = pin!(bar()); ``` ...expands to... ```rust let x = { super let pinned = bar(); unsafe { Pin::new_unchecked(&mut pinned) } }; ``` Note that, when defined this way, the `unsafe` block does not encompass too much, i.e., the call to `bar()` is outside of the `unsafe` block. The `pinned` value above will live exactly as long as it would if the pin macro had expanded to a simple struct expression: ```rust let x = Pin { pinned: &mut { bar() }, }; ``` ### Using `super let` to define a transparent `format_args!` macro Using `super let`, the `format_args!` macro can be defined in a way that allows it to work in any position: ```rust let x = format_args!("Hello {name}!"); ``` ...expands to... ```rust let x = { super let args = [fmt::Argument::new_display(&name)]; fmt::Arguments::new(&["Hello ", "!"], &args) }; ``` Such that the temporary `args` array will live exactly as long as it would if `format_args!()` had expanded to a simple struct expression instead of a function call: ```rust let x = fmt::Arguments { strings: &["Hello ", "!"], args: &[fmt::Argument::new_display(&name)], }; ``` ## Shortening temporary lifetimes for match scrutinees, and block tail expressions If you want to declare a variable that lives longer than the block, you have to declare the `let` in an outer scope. ```rust= // This does not compile ... let final_result: Option<&Struct> = if condition() { let intermediate: Vec<_> = compute(); // ^~~~~~~~~~~~ does not live long enough intermediate.as_ref().last() } else { None }; // So it can be written as so ... let intermediate; let final_result: Option<&Struct> = if condition() { intermediate = compute(); intermediate.as_ref().last() } else { None } ``` Because `let`-bound variables are freed at the end of the block, references to those variables you will get a compilation error if they escape from the block. If you wish to have references to those values escape from the block, you will get an error. Today blocks allow you to introduce named storage with `let`. This creates variable bindings that last until the end of the block. With `super let`, however, one is allowed to write the same program in the same logical structure. ```rust= let final_result: Option<&Struct> = if condition() { super let intermediate: Vec<_> = compute(); // ^~~~~~~~~~~~ it now lives as long as `final_result`, // so sub-borrows can be taken intermediate.as_ref().last() } else { None }; ``` The RFC also shortens the temporary lifetimes in Rust 2024 for two particular cases, `match` scrutinees and `if` conditions. Under this RFC, match scrutinees will get shorter temporary lifetimes so that the temporary values in a scrutinee expression will expire before a match arm is committed. ```rust= { // This will not compile: // match obj.retrieve_position().last() { // Some(last) => { .. } // None => { .. } // } // Instead: let vector: Vec<_> = obj.retrieve_position(); match vector.last() { Some(last) => { .. } None => { .. } } } { // This will not compile: // if let Some(last) = obj.retrieve_position().last() { // ^~~~~~~~~~~~~~~~~~~~~~~ ^ // | | // | | // E0716: temporary value is .. | // .. freed here ----------------- // ... // } // Instead: let vector: Vec<_> = obj.retrieve_position(); if let Some(last) = vector.last() { ... } } ``` With regards to block tail expression, this RFC will also shorten the temporary lifetime to the tail expression alone, as opposed to the whole block which is incidentally how the Rust 2021 rules applies. ```rust= // This will compile again fn foo() -> i32 { let cell = std::cell::RefCell::new(Some(1)); cell.borrow().unwrap() // ^~~~~~~~ This is kept alive after `cell` is dropped in Rust 2021. } ``` ### Syntactically extending temporary lifetimes In a limited sense, lifetime can be extended in match scrutinees. We shall see it in this example. ```rust { let rc: RefCell<Vec<u32>> = RefCell::new(vec![1, 2, 3]); match &rc.borrow()[1..] { // ^~~~~~~~~~~ E0716: temporary value is freed at the end of this *match scrutinee expression* // the `Ref<'_>` guard is dropped // before entering any match arm, // because the borrow is on the subslice [1..] and // not on `rc.borrow()`. slice => { ... } } } ``` Instead, this can be rewritten as follows, but not generally. ```rust { let rc: RefCell<Vec<u32>> = RefCell::new(vec![1, 2, 3]); match &**rc.borrow() { // ^~~ these borrows and dereferences are signals // that lifetime of the temporary values behind will be extended. [_, slice @ ..] => { // Now the `Ref<'_>` is actually alive inside this arm println!("{slice:?}"); // And this works again, too. ... } } } ``` ### How to rewrite your match expression Whenever Rust 2024 raises an issue about a part of your scrutinee expression of a `match` not living long enough, like the following snippet, you can declare a local variable binding as suggested. ```rust { let rc: RefCell<Vec<u32>> = RefCell::new(vec![1, 2, 3]); match rc.borrow()[1..].first() { // ^~~~~~~~~~~ E0716: temporary value is freed at the end of this *match scrutinee expression* Some(element) => println!("{element}"), None => println!("None"), } } // ...becomes: { let rc: RefCell<Vec<u32>> = RefCell::new(vec![1, 2, 3]); let b = rc.borrow(); match b.first() { Some(element) => println!("{element}"), None => println!("None"), } // Optional: drop(b); } // ...or, less readably but more locally: { let rc: RefCell<Vec<u32>> = RefCell::new(vec![1, 2, 3]); match { super let b = rc.borrow(); b.first() } { Some(element) => println!("{element}"), None => println!("None"), } } ``` ## Edition migration # Reference-level explanation [Reference-level-explanation]: #reference-level-explanation :::info **We are working on this text but it has been omitted from this version of the doc for reasons of brevity.** ::: In this RFC, we will create a model for lifetime of temporary values, which will be referred to as *temporary lifetimes* henceforth, that arise from evaluating Rust expressions and statements. We would like to build a clear mental framework to reason about temporary lifetimes. ## Design axioms We believe that... - **When in doubt, shorter scopes are more reliable.** - We have witnessed a few examples that could have benefited from shorter scopes. In those deadlocking examples, a longer lifetime assigned leads to hard to detect issues at run-time. By assigning a shorter lifetime, however, such traps will surface as borrow check errors. - **Larger scopes are more convenient.** - Larger scopes are in general more convenient and give fewer borrow checker errors. Therefore, we should try to assign longer lifetimes when it is clear that's what the user intended. Examples like `let x = &compute()` are a pattern in which temporaries will need to outlive the binding in order for the binding to make sense. In other cases, however, we avoid extension if we would need to rely on information other than syntactical features or if any fine-grained lifetime analysis would be required. - **Scopes should be predictable and stable.** - Assigning scopes should not depend on type-checking or fine-grained analysis of expressions. The proposed rules could be complex, but they avoid fine-grained analysis and, instead, work largely by analysing the syntatical structure of expressions. For instance, we have a set of rules for `$expr` in `let $pat = $expr`, but the rules do not depend on fine-grained details of the `$pat` pattern. - **It should always be possible to rewrite any Rust 2021 expression `E` with an enclosing block `{ ... }` such that under our proposed rules the behavior would be the same.** - To achieve this, we need to introduce some way inside a block to create values with the same scope as temporaries would in `E` under Rust Edition 2021. Our proposed `super let` achieves this. As an example, `{super let value = temp(); &value}` works exactly in the same way as `&temp()` does under Rust 2021. These design axioms are ordered by precedence. If they come into conflict, axioms earlier in the list are more imporatnt. # Drawbacks [drawbacks]: #drawbacks The proposed rule is a departure from the adopted rules in Rust 2021. It would mandate linting upgrades and there might be a necessity to develop a migration assistance to help rewriting code to adapt to the new expectation. # Rationale and alternatives [rationale-and-alternatives]: #rationale-and-alternatives ## Why was the `super` keyword chosen, and what alternatives were considered? The `super` keyword is currently used to refer to enclosing modules (e.g., `use super::foo` or `pub(super)`). The use in `let` is "similar but different" in that it refers to the enclosing expression in which a block appears. None of the [existing keywords](https://doc.rust-lang.org/reference/keywords.html) other than `super` seems even close to suitable for this use case. So, the alternative would be to introduce a new keyword. However, using a new keyword would limit this feature to new editions. Moreover, we expect this to be a niche feature mostly used by macro authors, and a dedicated keyword seems not worth the price. ## What about a pure expression, like `super { $expr }`? There is some similarity between promoting the storage of a value such that the lifetime of a reference to that value can be used in a wider scope and promoting (due to const evaluation) a value to static storage such that a reference to that value can be used anywhere. Given that similarity, we considered the following model: - `let { $expr }` promotes the value such that it can be referenced by the enclosing *statement*. - `super { $expr }` promotes the value such that it can be referenced by the *block* enclosing the *statement*. - `const { $expr }` promotes the value such that it can be referenced *anywhere*. While there may be value in later and separately considering `let` and `super` blocks, we've become convinced that they make less sense for the problems this RFC has set out to solve. Most compellingly, `super let` lends itself to a straightforward local equivalence that works at the level of a block: ```rust { super let v = &$expr; v } --------------------------- &$expr ``` Because `super` and `let` blocks would need to take into account the enclosing *statement*, they don't lend themselves to such straightforward block-level equivalences. The fact that `super let` *is* the statement is what allows this simple equivalence to work. The appealingness of this simple local equivalence and the fact that it straightforwardly solves the problems this RFC sets out to solve were the decisive factors for us. # Prior art [prior-art]: #prior-art There has been an earlier attempt with RFC 66. However, during the experiment with a trial implementation, we found a great burden has fallen on the experimental compiler to resolve types prematurely while it introduces problems of inconsistency when generics are concerned. This experiment has, therefore, enlightened the principle to rely solely on syntactical structure of a Rust program to assign temporary lifetimes, as opposed to relying on typing suggested by RFC 66. # Unresolved questions [unresolved-questions]: #unresolved-questions - We would like to further examine the use case of this proposal in macro development in Rust. - We would also like to investigate the impact of the new rules on existing Rust ecosystem and discover the necessary migration guide and tooling in order for a smooth transition, when this proposal is implemented in Rust Edition 2024. --- # Discussion ## Attendance - People: TC, Xiangfei Ding, Mara, Niko, tmandry, pnkfelix, Josh, scottmcm ## Meeting roles - Minutes, driver: TC ## "The Correct Equivalence" nikomatsakis: There may be some confusion on the right equivalence -- I noticed some inconsistency in the doc. I believe the right form is that `&$expr` is equivalent to `{super let x = &$expr; x}` (but not `{let x = &$expr; x}`). This is different in a subtle way from `{super let x = $expr; &x}`. That is only equivalent for "value-expressions" but not lvalues. Compare: ```rust // &a.b.c { super let x = a.b.c; // moves &x } { super let x = &a.b.c; // borrows &x } // &foo() { super let x = foo(); // lives as long as result of block &x // ok } { super let x = &foo(); // *also* lives as long as result of block (lifetime extension) &x // ok } ``` I believe this means the `pin!` expansion isn't quite right. (niko is wrong here, because you actually WANT a move, see below) Instead of ```rust let x = foo({ super let pinned = bar(); unsafe { Pin::new_unchecked(&mut pinned) } }); ``` it should be ```rust let x = foo({ super let pinned = &mut bar(); unsafe { Pin::new_unchecked(pinned) } }); ``` (Same with `format_args`) Mara: Agree on the equivalence. But don't agree on `pin!()`. pin!(a.field) shouldn't borrow that field. it should move it. (pin!() today uses `&mut { $expr }`, with extra braces). NM: E.g.: ```rust pin!(a.b.c) // this needs to "move" `a.b.c`, so you want `{super let x = a.b.c; ...}` ``` ## Will this make `format_args!` work in an `if`-`else` as well? Josh: The examples show `format_args!` in a let binding. Will `super let` also make the following constructs possible? ```rust let x = if condition { format_args!("...") } else { format_args!("...") }; println!("... {x} ..."); println!("... {} ...", if condition { format_args!("...") } else { format_args!("...") }); ``` Mara: Yes, it should. It will work exactly as written. JT: That would be *great*. This is a common source of issues when trying to use `format_args!` to avoid a temporary `String`. Mara: Other than super let, this needs temporary lifetime extention to apply not just to blocks but also if/else bodies. But that's the plan, and we can do that with no problems. JT: Would it also apply to `match` and `if let`, or are there any forms to which this does not apply? Will this also be extended to match arms? Mara: Yes, this will also be extended to match arms. ## Nit: lifetime extension in match example nikomatsakis: The example we use for lifetime extension in match... ```rust match &**rc.borrow() { ``` ...feels correct but maybe kinda confusing, because we are transforming from `&foo()[]` to a different form etc. ```rust match &foo() {} // ----- extended to result scope of match (I think? I forget exactly what rules we landed on here. --niko) // (yes, here we have extension --xiang) // vs match foo.borrow().do_something() { } // fn do_something(&self) // ------------ dropped before match arms ``` ## Will reducing lifetime extension in match/if subtly break existing code? Josh: The last time the topic of temporary lifetimes came up, there were concerns about whether changes would lead to locks being dropped too soon. Do we have reasonable confidence that the proposed changes won't break existing code in ways that will compile but be *semantically* incorrect? Including for code that is misusing locks (e.g. `Mutex<()>` to lock separate data not owned by the mutex)? Mara: Example that will no longer compile: ```rust match temp_string().as_str() { "this will" => break, _ => ":(", } ``` Mara: Above is about something that will stop compiling, like nearly all breakage. Worse is changes in semantics, which is what Josh is asking about. Those situations are extremely unlikely though. NM: The intent is that we would preserve the behavior for existing code to start, using the edition. Things are getting dropped earlier, but it's unlikely to impact people because -- first, the match over unit lock pattern is relatively uncommon -- and secondly because it's unlikely you can acquire a lock in this way. Josh: Example with `if let Some(_guard) = mutex_unit.lock() { .. }`. NM: ```rust if let Some(guard) = something.try_lock() { /* unchanged */ } ``` Mara+NM: That still works fine! JT: This fully answers my question and concern. JT: Aside: there might be value in a lint against `Mutex<()>` or `RwLock<()>`; that'd have false positives insofar as you sometimes need that, but it might be nice to steer people away from it by default. Given the above, though, this is off-topic for this proposal. Mara: sometimes you need it.. NM: I'd like to have something like `StandaloneMutex` (need a better name) that explicitly says "doesn't guard data" and encourage people to migrate to that. JT: +1 ## The usual suggested desugaring scottmcm: We have a bunch of things where we desugar to match *because* of the temporary lifetimes. This will change how such things work. Do we thus instead of `match $expr { x => frobl(x) }` start using `super let x = $expr; frobl(x)` in those places? Do we need to change any of our current desugarings? (Maybe `for`'s'?). pnkfelix: I'm assuming that's the right answer. What edition machinery do we want for that? Xiang: Yes, `for .. in ..` and `while let ..` needs `super let` to maintain the same semantics. Mara: Anyone using the match trick should instead be using `super let`. It will make the macro better: you can use `let f = m!();`. TC: So we should put this in the edition guide. TC/pnkfelix: Are there any automated things we could do also? Mara: This isn't actually edition-dependent. If we use a keyword like `super`, this could be used in all editions. NM: If you had an example such as: ```rust match foo.lock().do_test() { x => { // in Rust 2021, lock is held here // in Rust 2024, it is not } } ``` ...then a macro that did `match $expr { x => .. }` would change behavior if the definition moved from Rust 2021 to Rust 2024. ```rust macro_rules! foo { ($e:expr) => { match $e { x => ... } } } // foo!(foo.lock().do_test()) // changes depending on edition of macro definition ``` Xiang: what happens when 2021 and 2024 macros are used in conjunction? *Consensus*: Something about this should go in the edition migration guide. ## Modifying `let` vs the binding scottmcm: For example, today we have `let (mut x, mut y) = tuple;` rather than `mut let (x, y) = tuple;`. What are the trade-offs that make `super let (x, y) = tuple;` better than `let (super x, super y) = tuple;`, say? (This would be a good "Rationale and Alternatives" section, IMHO.) Nadri: we discussed this here: https://rust-lang.zulipchat.com/#narrow/stream/403629-t-lang.2Ftemporary-lifetimes-2024/topic/.60super.60.20in.20patterns Nadri: my recollection is that `let (super x, y) = &<expr>` requires the whole expression to be lifetime-extended so it wasn't super useful. In by-value cases like `let (super x, y) = foo();` then that makes sense to only extend `x` but unclear if that pulls its own weight. Mara: It's a property of the init expression, not of the binding. Today we also apply temporary lifetime extension if you use `ref` in any of the bindings. TC: There was a lot of discussion about this point, and everyone who thought at first that we might want to put it in the pattern came around to the idea that it should not go there. NM: I'll add a section about that to the document. ## What's `super` in something that's not a basic BlockExpr? scottmcm: What should I expect from things like `loop { super let x = ...; ... }`? Is that fresh every time through the loop? Does it always need to be `mut`? Nadri: `mut` is orthogonal, the same way we reuse the space for `x` in `loop { let x = ... }` without needing `mut`. scottmcm: If I manually did this with `let x; loop { x = 4 }`, though, [I'd need to add `mut`](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=db695f6aa1e1fa9bcba00789ba5ed09d). Nadri: yeah, so `super let` can't directly desugar to a `let` in the parent scope, but that's true for other reasons I think. Nadri: here's something I expect to work in a loop: ```rust let val = loop { if thing_is_ready() { super let x = foo(); break &x; } compute_some_more(); }; ``` scottmcm: given the above conversation about super let in if, that feels like it might be "super super let", since it need to move outside multiple `{`s? Nadri: I don't think so; if I recall, `super let` here isn't "with the lifetime of the parent block" but "with the lifetime of the value this block flows into". You need something like that for lifetime extension to work recursively. I was confused by this for a while. Don't trust me on this tho. EDIT: turns out I'm wrong NM: super let wouldn't work at loop {}, in our current version, and I don't expect the example above to compile. `super let` means "you can reference this in the tail expression of the block", but in that case, the value is propagated out via `break` instead. The tail expression of the `if` would be local to the loop body, so a ref to `x` couldn't escape. ## Alternative syntax Josh: I continue to find the discrepancy unfortunate between `super let` meaning "parent block" and `super::xyz` meaning "parent module". This document suggests that this is primarily for use in macros, so do we *need* a keyword here, or could we write `#[xyz] let ...`, applying an attribute to the `let` statement? (Not going to bikeshed what `xyz` should be here.) Josh: Counterpoint: we could decide that we want to set the precedent that `super` just means "some parent scope", non-specifically, and that we're willing to generalize the usage. But we should decide if that's worth it here for the brevity. (As much as `super let` is a very clever and brief construction.) Nadri: that would tie in with the discussion around using `super` in modules that are in functions Josh: That's exactly my concern, yeah. People have said they want to be able to reference the function scope from within something defined inside the function. I've seen `fn::` proposed for that purpose. scottmcm: Could we use a contextual keyword here? JT: Maybe, but probably I'm making a broader point. Mara: My original idea was that "super" refers to the 'parent let statement': ```rust { let v2 = { super let v1 = temp(); .. }; .. } ``` Mara: Without the other changes in the RFC, inside another let statement is the only place where super let is relevant. (With the RFC, it is also relevant in e.g. `match`.) NM: I don't love the keyword `super`, but it vaguely matches the intuition, and it's a niche thing. JT: `super let` is the cleanest and most clever syntax that vaguely matches our intuitions, but it then broadens the meaning of `super`. TC: Sounds like this should be addressed as an alternative in the RFC. scottmcm: To niko's point about "not warning to burn a keyword on this", that's why I ask about contextual. If we can do `parent let x = …;` or something without needing it to be a full keyword, that might change the trade-offs for whether something other than specifically `super` is worth doing. ## Weighing potential for bugs vs ergonomic hit tmandry: I'm concerned about the ergonomic hit of narrowing lifetimes of match scrutinees and I think that would create frequent inconveniences. I also can't recall a time I was bitten by the current rules in a way that introduced a bug. How can we be sure the tradeoff is worth it? nikomatsakis: I recommend you read the paper. I'll go back and summarize but my recollection is that this was the root cause for most of the bugs they cited. I also think the current rules lead to very surprising errors that are fixed by adding `;` after a match which is confusing to me... ```rust fn foo() { match foo { } // `;` sometimes needed here } ``` tmandry: I at least would like to quantify the impact of how much code needs to be rewritten. https://github.com/rust-lang/lang-team/blob/master/design-meeting-minutes/2020-11-18-understanding-memoryand-thread-safety-practices-and-issues-in-real-world-rust-programs.md NM: I've seen many bugs due to this. The bugs were caused by things being held too long. Josh: Quoting one of the (**draft**) "design axioms": > Systems programmers need to know what is happening and where, and so system details and especially performance costs in Rust are transparent and tunable. Josh: I love that this shifts lifetime extension from a magic rule in the compiler to an *explicit* extension inside of macros, which makes it more transparent. Mara: I went through the book that I wrote on atomics and locks. There's a half page of warnings about this that I could remove after this change. There are also code examples that break, e.g.: ```rust match temp_string().as_str() { "this will" => break, _ => ":(", } ``` There are positives and negatives. JT: What is the fix for that? Mara: Idiomatic: ```rust let temp = temp_string(); match temp.as_str() { "this will" => break, _ => ":(", } ``` Xiang: we are emitting suggestions for this case in the error messages, so we can just apply it right away. pnkfelix: or this: ```rust let temp; ... match { temp = temp_string(); temp.as_str() } { "this will not" => break, _ => ":)" } ``` Mara: Or inline with super-let (ugly): ```rust match { super let temp = temp_string(); temp.as_str() } { "this will" => break, _ => ":(", } ``` Tyler: Or `super { ... }` blocks if we add those. ![image](https://hackmd.io/_uploads/H17tptqjT.png) (From paper: 30/38 bugs caused by this.) NM: My feeling was, let's fix the problem first, then maybe there's some sugary thing that we can do later. scottmcm: Yes, if we are going to tell people to rewrite things, then that's where it hits. NM: We could file a concern on this. I'd like to move forward with the RFC and gather some quantitative data on impact. I would at least like to do the tail expression change though. Mara: The RFC is basically four different proposals that make sense together, but also make sense individually, so we could still continue with the remainder if one has too big of an impact. JT: I think we should make the change to reduce temporary lifetimes in `match` in Rust 2024. (The meeting ended here.) ----- (We didn't get to the questions below in the meeting.) ## Macro use vs "normal" use scottmcm: The text makes a good point that macros cannot manually add an outer-scoped `let foo;` binding that's initialized later. But for code that's *not* a macro, would we prefer people do that? Nadri: I can see idiomatic uses of `super let`, e.g. within an if/match; I've wanted that before: ```rust let x = if condition() { some_reference } else { super let local_thing = ...; &local_thing } ``` Josh: It gives people a new tool, which may also help them write spaghetti, but that's OK. scottmcm: Not worried about the potential for spaghetti, more exploring the frequency that people would be encountering it, and how that might influence how it's restricted or how smooth the syntax needs to be. ## leverage similar for Block tail expression? pnkfelix: is there a world where we would consider making expressions in block tail position *stop* implicitly adopting the current "super scope", and instead we would make people write `{ ...; super EXPR }` to achieve what you get via `{ ...; EXPR }` today, in order to make their intent completely clear (and also have parity with use of `super let` introduced by this RFC?) pnkfelix: (oh, i guess this is sort-of covered in the RFC in the hypothesized `super { EXPR }` form..., though I don't think it was motivated as clearly as it could be by pointing it out as a *migration path* for current users of the semantics for block-tail expressions) ## `super let else`? Josh: Does `super let Some(x) = expr else { ... }` work as expected? Xiang: Not yet in the trial implementation, but personally I can see that it fits. Mara: Could work, but also fine if we don't add it. (`super let a = ..; let .. = a else ..;`) Josh: Please do include it in the RFC; given the existence of `super let` and `let ... else`, it seems important for orthogonality that they work together in the obvious way. Nadri: does `if super let Some(x) = ...` make any sense? I imagine it's only for `let` statements, not expressions.