From 006076a3e43d5033e052bb934c5642eef520521d Mon Sep 17 00:00:00 2001 From: dianne Date: Thu, 4 Sep 2025 10:51:27 -0700 Subject: [PATCH 1/2] document temporary scoping for destructuring assignments --- src/expressions/operator-expr.md | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/expressions/operator-expr.md b/src/expressions/operator-expr.md index 35e715d400..4cf2df3dff 100644 --- a/src/expressions/operator-expr.md +++ b/src/expressions/operator-expr.md @@ -862,6 +862,49 @@ r[expr.assign.destructure.discard-value] r[expr.assign.destructure.default-binding] Note that default binding modes do not apply for the desugared expression. +> [!NOTE] +> Although basic assignment expressions are not [temporary scopes], the desugaring of destructuring assignments restricts the temporary scope of its assigned value operand. +> For example: +> +> ```rust +> # fn temp() {} +> use std::convert::identity; +> +> let x; +> // In a basic assignment, `temp()` is dropped at the end of the +> // enclosing temporary scope, so `x` can be assigned and used +> // within the same temporary scope. +> (x = identity(&temp()), x); +> ``` +> +> ```rust,compile_fail,E0716 +> # fn temp() {} +> # use std::convert::identity; +> let x; +> // In a destructuring assignment, `temp()` is dropped at the end of +> // the `let` statement in the desugaring, so `x` cannot be assigned. +> [x] = [identity(&temp())]; // ERROR +> ``` +> +> Additionally, [temporary lifetime extension] applies to the `let` statement in a desugared destructuring assignment. +> +> ```rust +> # fn temp() {} +> # use std::convert::identity; +> let x; +> // The temporary scope of `temp()` is extended to the end of the +> // block in the desugaring, so `x` may be assigned, but it may not +> // be used. +> [x] = [&temp()]; +> ``` +> +> ```rust,compile_fail,E0716 +> # fn temp() {} +> # use std::convert::identity; +> let x; +> ([x] = [&temp()], x); // ERROR +> ``` + r[expr.compound-assign] ## Compound assignment expressions @@ -1025,6 +1068,8 @@ As with normal assignment expressions, compound assignment expressions always pr [unit]: ../types/tuple.md [Unit-only enums]: ../items/enumerations.md#unit-only-enum [value expression]: ../expressions.md#place-expressions-and-value-expressions +[temporary lifetime extension]: ../destructors.md#temporary-lifetime-extension +[temporary scopes]: ../destructors.md#temporary-scopes [temporary value]: ../expressions.md#temporaries [float-float]: https://github.com/rust-lang/rust/issues/15536 [Function pointer]: ../types/function-pointer.md From 1e63288cd16d8f6b4c5a79f186cafa875ff04a0d Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Mon, 8 Sep 2025 02:10:22 +0000 Subject: [PATCH 2/2] Revise text on temporaries and destructuring assignment Among other things, let's break up the one admonition into two admonitions, as there are separate ideas here; within the admonitions, let's pull more of the explanation out of code comments and into the main narrative; let's add rule identifiers for these admonitions; and let's reference these admonitions from the relevant places in the destructors chapter. --- src/destructors.md | 7 +++ src/expressions/operator-expr.md | 85 +++++++++++++++++++++++--------- 2 files changed, 68 insertions(+), 24 deletions(-) diff --git a/src/destructors.md b/src/destructors.md index 1ad3898a1d..1e03a318d0 100644 --- a/src/destructors.md +++ b/src/destructors.md @@ -266,6 +266,9 @@ smallest scope that contains the expression and is one of the following: > [!NOTE] > The [scrutinee] of a `match` expression is not a temporary scope, so temporaries in the scrutinee can be dropped after the `match` expression. For example, the temporary for `1` in `match 1 { ref mut z => z };` lives until the end of the statement. +> [!NOTE] +> The desugaring of a [destructuring assignment] restricts the temporary scope of its assigned value operand (the RHS). For details, see [expr.assign.destructure.tmp-scopes]. + r[destructors.scope.temporary.edition2024] > [!EDITION-2024] > The 2024 edition added two new temporary scope narrowing rules: `if let` temporaries are dropped before the `else` block, and temporaries of tail expressions of blocks are dropped immediately after the tail expression is evaluated. @@ -485,6 +488,9 @@ expression which is one of the following: * The final expression of an extending [`if`] expression's consequent, `else if`, or `else` block. * An arm expression of an extending [`match`] expression. +> [!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)` are all extending expressions. The borrows in `&0 + &1` and `f(&mut 0)` are not. @@ -640,6 +646,7 @@ There is one additional case to be aware of: when a panic reaches a [non-unwindi [binding modes]: patterns.md#binding-modes [closure]: types/closure.md [destructors]: destructors.md +[destructuring assignment]: expr.assign.destructure [expression]: expressions.md [identifier pattern]: patterns.md#identifier-patterns [initialized]: glossary.md#initialized diff --git a/src/expressions/operator-expr.md b/src/expressions/operator-expr.md index 4cf2df3dff..a604c07cf7 100644 --- a/src/expressions/operator-expr.md +++ b/src/expressions/operator-expr.md @@ -862,48 +862,83 @@ r[expr.assign.destructure.discard-value] r[expr.assign.destructure.default-binding] Note that default binding modes do not apply for the desugared expression. +r[expr.assign.destructure.tmp-scopes] > [!NOTE] -> Although basic assignment expressions are not [temporary scopes], the desugaring of destructuring assignments restricts the temporary scope of its assigned value operand. -> For example: +> The desugaring restricts the [temporary scope] of the assigned value operand (the RHS) of a destructuring assignment. +> +> In a basic assignment, the [temporary] is dropped at the end of the enclosing temporary scope. Below, that's the statement. Therefore, the assignment and use is allowed. > > ```rust > # fn temp() {} -> use std::convert::identity; -> +> fn f(x: T) -> T { x } > let x; -> // In a basic assignment, `temp()` is dropped at the end of the -> // enclosing temporary scope, so `x` can be assigned and used -> // within the same temporary scope. -> (x = identity(&temp()), x); +> (x = f(&temp()), x); // OK > ``` > +> Conversely, in a destructuring assignment, the temporary is dropped at the end of the `let` statement in the desugaring. As that happens before we try to assign to `x`, below, it fails. +> > ```rust,compile_fail,E0716 > # fn temp() {} -> # use std::convert::identity; -> let x; -> // In a destructuring assignment, `temp()` is dropped at the end of -> // the `let` statement in the desugaring, so `x` cannot be assigned. -> [x] = [identity(&temp())]; // ERROR +> # fn f(x: T) -> T { x } +> # let x; +> [x] = [f(&temp())]; // ERROR > ``` > -> Additionally, [temporary lifetime extension] applies to the `let` statement in a desugared destructuring assignment. +> This desugars to: +> +> ```rust,compile_fail,E0716 +> # fn temp() {} +> # fn f(x: T) -> T { x } +> # let x; +> { +> let [_x] = [f(&temp())]; +> // ^ +> // The temporary is dropped here. +> x = _x; // ERROR +> } +> ``` + +r[expr.assign.destructure.tmp-ext] +> [!NOTE] +> Due to the desugaring, the assigned value operand (the RHS) of a destructuring assignment is an [extending expression] within a newly-introduced block. +> +> Below, because the [temporary scope] is extended to the end of this introduced block, the assignment is allowed. > > ```rust > # fn temp() {} -> # use std::convert::identity; -> let x; -> // The temporary scope of `temp()` is extended to the end of the -> // block in the desugaring, so `x` may be assigned, but it may not -> // be used. -> [x] = [&temp()]; +> # let x; +> [x] = [&temp()]; // OK > ``` > +> This desugars to: +> +> ```rust +> # fn temp() {} +> # let x; +> { let [_x] = [&temp()]; x = _x; } // OK +> ``` +> +> However, if we try to use `x`, even within the same statement, we'll get an error because the [temporary] is dropped at the end of this introduced block. +> > ```rust,compile_fail,E0716 > # fn temp() {} -> # use std::convert::identity; -> let x; +> # let x; > ([x] = [&temp()], x); // ERROR > ``` +> +> This desugars to: +> +> ```rust,compile_fail,E0716 +> # fn temp() {} +> # let x; +> ( +> { +> let [_x] = [&temp()]; +> x = _x; +> }, // <-- The temporary is dropped here. +> x, // ERROR +> ); +> ``` r[expr.compound-assign] ## Compound assignment expressions @@ -1054,6 +1089,7 @@ As with normal assignment expressions, compound assignment expressions always pr [dropping]: ../destructors.md [eval order test]: https://github.com/rust-lang/rust/blob/1.58.0/src/test/ui/expr/compound-assignment/eval-order.rs [explicit discriminants]: ../items/enumerations.md#explicit-discriminants +[extending expression]: destructors.scope.lifetime-extension.exprs [field-less enums]: ../items/enumerations.md#field-less-enum [grouped expression]: grouped-expr.md [literal expression]: literal-expr.md#integer-literal-expressions @@ -1068,13 +1104,14 @@ As with normal assignment expressions, compound assignment expressions always pr [unit]: ../types/tuple.md [Unit-only enums]: ../items/enumerations.md#unit-only-enum [value expression]: ../expressions.md#place-expressions-and-value-expressions -[temporary lifetime extension]: ../destructors.md#temporary-lifetime-extension -[temporary scopes]: ../destructors.md#temporary-scopes +[temporary lifetime extension]: destructors.scope.lifetime-extension +[temporary scope]: destructors.scope.temporary [temporary value]: ../expressions.md#temporaries [float-float]: https://github.com/rust-lang/rust/issues/15536 [Function pointer]: ../types/function-pointer.md [Function item]: ../types/function-item.md [receiver]: expr.method.intro +[temporary]: expr.temporary [undefined behavior]: ../behavior-considered-undefined.md [Underscore expressions]: ./underscore-expr.md [range expressions]: ./range-expr.md