-
Notifications
You must be signed in to change notification settings - Fork 563
Specify temporary lifetime extension through expressions #2051
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
base: master
Are you sure you want to change the base?
Changes from 5 commits
e856ddf
596d282
71178bb
a0ae157
e37d803
6536b3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -375,11 +375,8 @@ r[destructors.scope.lifetime-extension] | |
| > [!NOTE] | ||
| > The exact rules for temporary lifetime extension are subject to change. This is describing the current behavior only. | ||
|
|
||
| r[destructors.scope.lifetime-extension.let] | ||
| The temporary scopes for expressions in `let` statements are sometimes | ||
| *extended* to the scope of the block containing the `let` statement. This is | ||
| done when the usual temporary scope would be too small, based on certain | ||
| syntactic rules. For example: | ||
| r[destructors.scope.lifetime-extension.intro] | ||
| The temporary scopes for expressions are sometimes *extended*. This is done when the usual temporary scope would be too small, based on certain syntactic rules. For example: | ||
|
|
||
| ```rust | ||
| let x = &mut 0; | ||
|
|
@@ -388,21 +385,27 @@ let x = &mut 0; | |
| println!("{}", x); | ||
| ``` | ||
|
|
||
| r[destructors.scope.lifetime-extension.static] | ||
| Lifetime extension also applies to `static` and `const` items, where it | ||
| makes temporaries live until the end of the program. For example: | ||
| r[destructors.scope.lifetime-extension.sub-expressions] | ||
| If a [borrow], [dereference][dereference expression], [field][field expression], or [tuple indexing expression] has an extended temporary scope, then so does its operand. If an [indexing expression] has an extended temporary scope, then the indexed expression also has an extended temporary scope. | ||
|
|
||
| ```rust | ||
| const C: &Vec<i32> = &Vec::new(); | ||
| // Usually this would be a dangling reference as the `Vec` would only | ||
| // exist inside the initializer expression of `C`, but instead the | ||
| // borrow gets lifetime-extended so it effectively has `'static` lifetime. | ||
| println!("{:?}", C); | ||
| # use core::sync::atomic::{AtomicU64, Ordering::Relaxed}; | ||
| # static X: AtomicU64 = AtomicU64::new(0); | ||
| # struct PrintOnDrop(&'static str); | ||
| # impl Drop for PrintOnDrop { | ||
| # fn drop(&mut self) { | ||
| # X.fetch_add(1, Relaxed); | ||
| # println!("{}", self.0); | ||
| # } | ||
| # } | ||
| let x = &(0, PrintOnDrop("tuple 1 dropped")).0; | ||
| let ref y = (0, PrintOnDrop("tuple 2 dropped")).0; | ||
| // Though only its first field is borrowed, the temporary for the entire tuple | ||
| // lives to the end of the block in both cases. | ||
| println!("{x}, {y}"); | ||
| # assert_eq!(0, X.load(Relaxed)); | ||
| ``` | ||
|
|
||
| r[destructors.scope.lifetime-extension.sub-expressions] | ||
| If a [borrow], [dereference][dereference expression], [field][field expression], or [tuple indexing expression] has an extended temporary scope, then so does its operand. If an [indexing expression] has an extended temporary scope, then the indexed expression also has an extended temporary scope. | ||
|
|
||
| r[destructors.scope.lifetime-extension.patterns] | ||
| #### Extending based on patterns | ||
|
|
||
|
|
@@ -445,7 +448,7 @@ So `ref x`, `V(ref x)` and `[ref x, y]` are all extending patterns, but `x`, `&r | |
|
|
||
| r[destructors.scope.lifetime-extension.patterns.let] | ||
| If the pattern in a `let` statement is an extending pattern then the temporary | ||
| scope of the initializer expression is extended. | ||
| scope of the initializer expression is extended to the scope of the block containing the `let` statement. | ||
|
|
||
| ```rust | ||
| # fn temp() {} | ||
|
|
@@ -473,37 +476,102 @@ let &ref x = &*&temp(); // OK | |
| r[destructors.scope.lifetime-extension.exprs] | ||
| #### Extending based on expressions | ||
|
|
||
| r[destructors.scope.lifetime-extension.exprs.borrows] | ||
| The [temporary scope] of the operand of a [borrow] expression is the *extended scope* of the operand expression, defined below. | ||
|
|
||
| r[destructors.scope.lifetime-extension.exprs.super-macros] | ||
| The [scope][temporary scope] of each [super temporary] of a [super macro call] expression is the extended scope of the super macro call expression. | ||
|
Comment on lines
476
to
+483
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two things here:
|
||
|
|
||
| r[destructors.scope.lifetime-extension.exprs.extending] | ||
| For a let statement with an initializer, an *extending expression* is an | ||
| expression which is one of the following: | ||
| The extended scope of an expression is defined in terms of *extending expressions* and their *extending parents*. An extending expression is an expression which is one of the following: | ||
|
Comment on lines
485
to
+486
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Three things here:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following up on making "extending parents" make more sense and making it clear that "extending expression" isn't a classification of expressions but a classification of subexpressions, maybe it would be worth renaming "extending expression" to "extending subexpression"? I think it makes more sense from a spec point of view, but it's more awkward to read and write (and it means having to adapt language elsewhere), so I haven't gone through with it.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It has some appeal, but I similarly see the problems you mention. If one says "extending subexpression", then one kind of wants the parent to be an "extending expression" (in the same way as "header", "subheader", etc.), but that's probably confusing. |
||
|
|
||
| * The initializer expression. | ||
| * The operand of an extending [borrow] expression. | ||
| * The [super operands] of an extending [super macro call] expression. | ||
| * The operand(s) of an extending [array][array expression], [cast][cast | ||
| * The operand of a [borrow] expression, the extending parent of which is the borrow expression. | ||
| * The [super operands] of a [super macro call] expression, the extending parent of which is the macro call expression. | ||
| * The operand(s) of an [array][array expression], [cast][cast | ||
| expression], [braced struct][struct expression], or [tuple][tuple expression] | ||
| expression. | ||
| * The arguments to an extending [tuple struct] or [tuple enum variant] constructor expression. | ||
| * The final expression of an extending [block expression] except for an [async block expression]. | ||
| * The final expression of an extending [`if`] expression's consequent, `else if`, or `else` block. | ||
| * An arm expression of an extending [`match`] expression. | ||
| expression, the extending parent of which is the array, cast, braced struct, or tuple expression. | ||
| * The arguments to a [tuple struct] or [tuple enum variant] constructor expression, the extending parent of which is the constructor expression. | ||
| * The final expression of a plain [block expression] or [`unsafe` block expression], the extending parent of which is the block expression. | ||
| * The final expression of an [`if`] expression's consequent, `else if`, or `else` block, the extending parent of which is the `if` expression. | ||
| * An arm expression of a [`match`] expression, the extending parent of which is the `match` expression. | ||
|
Comment on lines
+488
to
+496
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's maybe not what we want to do right now, but it occurred to me that these and the other rules could probably be usefully presented in the manner of inference rules, e.g. (and making no assertions about correctness of this sketch): enum Scope {
Block,
Stmt,
// ...
}
struct S(scope: Scope, ext_scope: Scope);
/// Borrow expressions
S(x, x) ⊢ $expr
----------------
S(s, x) ⊢ &$expr
/// Array expressions
S(s, x) ⊢ $expr
----------------------
S(s, x) ⊢ [$expr, ...]
/// Function calls
S(s, s) ⊢ $expr
---------------------------
S(s, x) ⊢ $path($expr, ...)
/// Let initializers
S(Stmt, Block) ⊢ &$expr
---------------------------------
S(Stmt, Block) ⊢ let ... = &$expr
// Etc.When @Nadrieril and I were working on match ergonomics, we ended up finding it rather helpful for analysis to present the rules in this manner. See, e.g. this document for a discussion of notation. @Nadrieril ended up writing a cool web tool for doing analysis and comparisons based on this kind of notation. It's similarly been occurring to me here that it would be helpful to have a tool that encodes the formal rules in software and walks through the analysis of an expression. (Of course, one option is always to do something like that in the compiler with an approach similar to
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty familiar with inference rules; if it would help to discuss them in that way, I could write them out for different variants of lifetime extension (e.g. the stable version, the compiler PR version where
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would definitely be helpful. I could see including that directly in the Reference as well. |
||
|
|
||
| > [!NOTE] | ||
| > The desugaring of a [destructuring assignment] makes its assigned value operand (the RHS) an extending expression within a newly-introduced block. For details, see [expr.assign.destructure.tmp-ext]. | ||
|
|
||
| So the borrow expressions in `&mut 0`, `(&1, &mut 2)`, and `Some(&mut 3)` | ||
| > [!NOTE] | ||
| > `rustc` does not treat [array repeat operands] of [array] expressions as extending expressions. Whether it should is an open question. | ||
| > | ||
| > For details, see [Rust issue #146092](https://github.com/rust-lang/rust/issues/146092). | ||
|
|
||
| So the borrow expressions in `{ &mut 0 }`, `(&1, &mut 2)`, and `Some(&mut 3)` | ||
| are all extending expressions. The borrows in `&0 + &1` and `f(&mut 0)` are not. | ||
|
|
||
| r[destructors.scope.lifetime-extension.exprs.borrows] | ||
| The operand of an extending [borrow] expression has its [temporary scope] [extended]. | ||
| r[destructors.scope.lifetime-extension.exprs.parent] | ||
| The extended scope of an extending expression is the extended scope of its extending parent. | ||
|
|
||
| r[destructors.scope.lifetime-extension.exprs.super-macros] | ||
| The [super temporaries] of an extending [super macro call] expression have their [scopes][temporary scopes] [extended]. | ||
| r[destructors.scope.lifetime-extension.exprs.let] | ||
| The extended scope of the initializer expression of a `let` statement is the scope of the block containing the `let` statement. | ||
|
|
||
| > [!EXAMPLE] | ||
| > In this example, the temporary value holding the result of `temp()` is extended to the end of the block in which `x` is declared: | ||
| > | ||
| > ```rust,edition2024 | ||
| > # fn temp() {} | ||
| > let x = { &temp() }; | ||
| > println!("{x:?}"); | ||
| > ``` | ||
| > | ||
| > `temp()` is the operand of a borrow expression, so its temporary scope is its extended scope. | ||
| > To determine its extended scope, look outward: | ||
| > | ||
| > * Since borrow expressions' operands are extending, the extended scope of `temp()` is the extended scope of its extending parent, the borrow expression. | ||
| > * `&temp()` is the final expression of a plain block. Since the final expressions of plain blocks are extending, the extended temporary scope of `&temp()` is the extended scope of its extending parent, the block expression. | ||
| > * `{ &temp() }` is the initializer expression of a `let` statement, so its extended scope is the scope of the block containg that `let` statement. | ||
| > | ||
| > If not for temporary lifetime extension, the result of `temp()` would be dropped after evaluating the tail expression of the block `{ &temp() }` ([destructors.scope.temporary.enclosing]). | ||
|
|
||
| r[destructors.scope.lifetime-extension.exprs.static] | ||
| The extended scope of the body expression of a [static][static item] or [constant item], and of the final expression of a [const block expression], is the entire program. This prevents destructors from being run. | ||
|
|
||
| ```rust | ||
| # #[derive(Debug)] struct PanicOnDrop; | ||
| # impl Drop for PanicOnDrop { fn drop(&mut self) { panic!() } } | ||
| # impl PanicOnDrop { const fn new() -> PanicOnDrop { PanicOnDrop } } | ||
| const C: &PanicOnDrop = &PanicOnDrop::new(); | ||
| // Usually this would be a dangling reference as the result of | ||
| // `PanicOnDrop::new()` would only exist inside the initializer expression of | ||
| // `C`, but instead the borrow gets lifetime-extended so it effectively has | ||
| // a `'static` lifetime and its destructor is never run. | ||
| println!("{:?}", C); | ||
| // `const` blocks may likewise extend temporaries to the end of the program: | ||
| // the result of `PanicOnDrop::new()` is not dropped. | ||
| println!("{:?}", const { &PanicOnDrop::new() }); | ||
| ``` | ||
|
|
||
| r[destructors.scope.lifetime-extension.exprs.other] | ||
| The extended scope of any other expression is its [temporary scope]. | ||
|
|
||
| > [!NOTE] | ||
| > `rustc` does not treat [array repeat operands] of extending [array] expressions as extending expressions. Whether it should is an open question. | ||
| > In this case, the expression is not extending, meaning it cannot be a borrow expression or a [super operand][super operands] to a [super macro call] expression, so its temporary scope is given by [destructors.scope.temporary.enclosing]. | ||
|
|
||
| > [!EXAMPLE] | ||
| > In this example, the temporary value holding the result of `temp()` is extended to the end of the statement: | ||
| > | ||
| > For details, see [Rust issue #146092](https://github.com/rust-lang/rust/issues/146092). | ||
| > ```rust,edition2024 | ||
| > # fn temp() {} | ||
| > # fn use_temp(_: &()) {} | ||
| > use_temp({ &temp() }); | ||
| > ``` | ||
| > | ||
| > `temp()` is the operand of a borrow expression, so its temporary scope is its extended scope. | ||
| > To determine its extended scope, look outward: | ||
| > | ||
| > * Since borrow expressions' operands are extending, the extended scope of `temp()` is the extended scope of its extending parent, the borrow expression. | ||
| > * `&temp()` is the final expression of a plain block. Since the final expressions of plain blocks are extending, the extended scope of `&temp()` is the extended scope of its extending parent, the block expression. | ||
| > * `{ &temp() }` is the argument of a call expression, which is not extending. Since no other cases apply, its extended scope is its temporary scope. | ||
| > * Per [destructors.scope.temporary.enclosing], the temporary scope of `{ &temp() }`, and thus the extended scope of `temp()`, is the scope of the statement. | ||
| > | ||
| > If not for temporary lifetime extension, the result of `temp()` would be dropped after evaluating the tail expression of the block `{ &temp() }` ([destructors.scope.temporary.enclosing]). | ||
|
|
||
| #### Examples | ||
|
|
||
|
|
@@ -606,22 +674,6 @@ let x = 'a: { break 'a &temp() }; // ERROR | |
| # x; | ||
| ``` | ||
|
|
||
| ```rust,edition2024,compile_fail,E0716 | ||
| # use core::pin::pin; | ||
| # fn temp() {} | ||
| // The argument to `pin!` is only an extending expression if the call | ||
| // is an extending expression. Since it's not, the inner block is not | ||
| // an extending expression, so the temporaries in its trailing | ||
| // expression are dropped immediately. | ||
| pin!({ &temp() }); // ERROR | ||
| ``` | ||
|
|
||
| ```rust,edition2024,compile_fail,E0716 | ||
| # fn temp() {} | ||
| // As above. | ||
| format_args!("{:?}", { &temp() }); // ERROR | ||
| ``` | ||
|
|
||
| r[destructors.forget] | ||
| ## Not running destructors | ||
|
|
||
|
|
@@ -647,6 +699,7 @@ There is one additional case to be aware of: when a panic reaches a [non-unwindi | |
| [Assignment]: expressions/operator-expr.md#assignment-expressions | ||
| [binding modes]: patterns.md#binding-modes | ||
| [closure]: types/closure.md | ||
| [constant item]: items/constant-items.md | ||
| [destructors]: destructors.md | ||
| [destructuring assignment]: expr.assign.destructure | ||
| [expression]: expressions.md | ||
|
|
@@ -660,6 +713,7 @@ There is one additional case to be aware of: when a panic reaches a [non-unwindi | |
| [promoted]: destructors.md#constant-promotion | ||
| [scrutinee]: glossary.md#scrutinee | ||
| [statement]: statements.md | ||
| [static item]: items/static-items.md | ||
| [temporary]: expressions.md#temporaries | ||
| [unwinding]: panic.md#unwinding | ||
| [variable]: variables.md | ||
|
|
@@ -681,22 +735,22 @@ There is one additional case to be aware of: when a panic reaches a [non-unwindi | |
|
|
||
| [array expression]: expressions/array-expr.md#array-expressions | ||
| [array repeat operands]: expr.array.repeat-operand | ||
| [async block expression]: expr.block.async | ||
| [block expression]: expressions/block-expr.md | ||
| [borrow]: expr.operator.borrow | ||
| [cast expression]: expressions/operator-expr.md#type-cast-expressions | ||
| [const block expression]: expr.block.const | ||
| [dereference expression]: expressions/operator-expr.md#the-dereference-operator | ||
| [extended]: destructors.scope.lifetime-extension | ||
| [field expression]: expressions/field-expr.md | ||
| [indexing expression]: expressions/array-expr.md#array-and-slice-indexing-expressions | ||
| [struct expression]: expressions/struct-expr.md | ||
| [super macro call]: expr.super-macros | ||
| [super operands]: expr.super-macros | ||
| [super temporaries]: expr.super-macros | ||
| [super temporary]: expr.super-macros | ||
| [temporary scope]: destructors.scope.temporary | ||
| [temporary scopes]: destructors.scope.temporary | ||
| [tuple expression]: expressions/tuple-expr.md#tuple-expressions | ||
| [tuple indexing expression]: expressions/tuple-expr.md#tuple-indexing-expressions | ||
| [`unsafe` block expression]: expr.block.unsafe | ||
|
|
||
| [`for`]: expressions/loop-expr.md#iterator-loops | ||
| [`if let`]: expressions/if-expr.md#if-let-patterns | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this may need a terminology update, a carve-out in
destructors.scope.lifetime-extension.exprs, or at least clarification on what "extended temporary scope" means. In particular, if you have an expression likeit's important that this rule takes precedence, so that
temp()lives past the block.destructors.scope.lifetime-extension.exprs.borrowscurrently is worded to be the definitive lifetime of borrow operators' operands, but if only that was considered,temp()would be dropped at the end of the tail expression.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is partially addressed by the new example in 6536b3b. I think it'll still also want some reformulation or clarification of
lifetime-extension.sub-expressionsto make really make sense, though.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't it almost feel as though the things in this rule should be rolled in to "extending based on expressions"? E.g.:
Or is there some important distinction we're trying to encode here in these being handled separately?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a couple distinctions.
First, except for borrow expression operands, none of those subexpressions are extending. For instance,
prints "temporary dropped", then "next statement". Intuitively, since the result of indexing is evaluated and then copied into
x, the temporary doesn't need to be extended past its use, so we don't extend it. The purpose oflifetime-extension.sub-expressionsis to handle reborrows, borrows of projections, etc.: if we'd instead writtenwe'd be reborrowing the temporary's field rather than evaluating it, so we would extend the temporary, so it'd print "next statement", then "temporary dropped". Extending subexpressions generally should only be those where if you substituted a borrow expression in, it's syntactically obvious that the borrow will need to be live to use the result of evaluating the extending parent expression. Maybe there's a way to express that intuition productively in an admonition somehow?
Second,
lifetime-extension.sub-expressionsalso applies when extending based on patterns:prints "next statement", then "temporary dropped". Because
xis borrowing from a place based on the temporary, the entire temporary is extended. The example inlifetime-extension.sub-expressionsillustrates this in a bit more detail.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't fully understand how to apply this rule in the same way that
rustcdoes. I put together a series of examples and mechanically analyzed them according to the rules in this PR. Can you explain the behavior of the last two examples in light of the behavior of the ones that precede them?Playground link
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting that aside, and setting aside the matter of the interaction with "extending based on patterns" (which as we discussed on Zulip, might end up needing to be restructured eventually given how it would interact with a binding version of
super letorsuper let in), it feels to me as though these are properly a kind of extending expression but that, as we work outward, we're carrying a flag indicating whether we last stepped through a "full" extending expression or through a more "limited" extending expression, and the value of that flag affects our behavior, e.g. when we get out to the let initializer.(To the degree that's not true, maybe it's worth considering whether it should be.)
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My notes on each example, per the mental model I was aiming for with that change (I'll address the limited extending model after):
let _ = (&e.log(1)).0; //~ Not extended.Our analyses agree here.
let _ = &(&e.log(2)).0; //~ Extended.Our analyses agree here.
let _ = &(&(e.log(2),).0).0; //~ Extended.e.log(2)here isn't extended; instead, it's immediately moved into the tuple temporary, which is extended. I believe its scope is that of the tuple expression, per destructors.scope.operands. I'd also add another step: the reason(&(e.log(2),).0).0uses its extended scope as its temporary scope is because it's the operand of a borrow expression. Otherwise, our analyses agree.let _ = &[(e.log(2),).0][0].0; //~ Extended.In this case, the temporary that gets extended is the array given by evaluating
[(e.log(2),).0]. A place based on it,array[0].0, is borrowed, and since we want that borrow to be live for the block, we extend the array's lifetime.let _ = &[(e.log(2),).0]; //~ Extended.As with 4, the temporary that gets extended is the array given by evaluating
[(e.log(2),).0].let _ = &[(&e.log(1)).0]; //~ Not extended.As with 4 and 5, the temporary being extended is the array. The key here is that we're not borrowing from the
e.log(1)temporary in the final value; we evaluate(&e.log(1)).0to something likeLogDrop(_e1, 1)where_e1is a reborrow ofeand then move that value into the array. As such,e.log(1)does not need to be extended; this is why tuple indexing expressions are not extending parents.let _ = &(&e.log(1),).0; //~ Not extended.This one we should extend according to the new model we're discussing.
e.log(1)should have the tuple expression's temporary scope, which should be extended. Indeed, it's currently an error to use the result because it borrows from the droppede.log(1)temporary. I don't think my compiler PR handles this yet either, but I'd like for it to work.The way rustc does this is when it extends
(&e.log(1),).0it applies lifetime-extension.sub-expressions to extend the place(s) it's based on as well (so(&e.log(1),)) and then stops.For the limited extending model, I think there's a conceptual reason to keep them distinct. Extending parent expressions represent things like constructors and conditionals that build (or select) a value and are guaranteed to contain (or evaluate to) their extending subexpressions. Projections like
.0are sort of a dual to that: if we want to extend the lifetime of a projected place, we have to extend the base place too, thus we extend the indexed operand. Actually, I think under this PR's model, we can clean it up and get&out of the "sub-expressions" list so it's just the base places of place expressions. I'll experiment with that on the compiler side of things and update this PR if it works out.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder what the right thing to do is in cases like
I see a few options:
letstatement extends itstemp(). This is safest in a way: this means we're not extending unused temporaries to the end of the block. Of course, it's still possible today to create unused extended temporaries, e.g. withlet _ = &temp();.ywould extend itstemp(), since the array would have an extended temporary scope andtemp()would have the array's temporary scope as its temporary scope. In the case ofx, the array wouldn't have an extended temporary scope because the result of indexing is used as a value.temp()s, because in either case we can see that thetemp()could end up referenced in the final value. I'd have to think through what the exact rules would be to end up with that result.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. Regarding 3..=6... oops, yes, of course -- I must have been staring at too many variations for too long. Interesting about 7.
Revised analysis of the interesting cases:
Playground link
Regarding the
sub-expressionsrule:In "extending based on expressions", we're defining expressions to have an extended scope even if that extended scope is never used. E.g.:
For
sub-expressions, then, it seems we have to account for that somehow.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a terminology problem. I couldn't think of a better term than "extended scope" at the time, but there's meant to be a distinction. Every expression has an extended scope. Borrow expressions' operands use their extended scope as their temporary scope; as such, they're said to have an extended temporary scope (though it looks like I forgot to add that definition). The initializer expression of a
letstatement with an extending pattern also has an extended temporary scope.I can think of a few things that might help:
sub-expressionsis e.g. saying that the tuple operand expression of a tuple indexing expression has the same temporary scope as the tuple indexing expression. We don't need a notion of "extended temporary scope" to express that.extended_parentscope" to match with the notions of "parentscope" and "var_parentscope" used there. That feels too close to "extending parent" though, maybe something like "extended ancestor scope" would be fine? I'm not sure.With this in mind, there isn't intended to be any ambiguity or a need to determine precedence between
exprsandsub-expressions. The rules should be written in a way that makes everything clear. They just haven't quite gotten there yet.