Skip to content

specify if let guards with updated scoping rules #1957

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions src/destructors.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,8 @@ patterns_in_parameters(
r[destructors.scope.bindings]
### Scopes of local variables

r[destructors.scope.bindings.intro]
Local variables declared in a `let` statement are associated to the scope of
the block that contains the `let` statement. Local variables declared in a
`match` expression are associated to the arm scope of the `match` arm that they
are declared in.
r[destructors.scope.bindings.let]
Local variables declared in a `let` statement are associated to the scope of the block that contains the `let` statement.

```rust
# struct PrintOnDrop(&'static str);
Expand All @@ -185,6 +182,43 @@ let declared_first = PrintOnDrop("Dropped last in outer scope");
let declared_last = PrintOnDrop("Dropped first in outer scope");
```

r[destructors.scope.bindings.match-arm]
Local variables declared in a `match` expression or pattern-matching `match` guard are associated to the arm scope of the `match` arm that they are declared in.

```rust
# #![allow(irrefutable_let_patterns)]
match PrintOnDrop("Dropped last in the first arm's scope") {
// When guard evaluation succeeds, control-flow stays in the arm and
// values may be moved from the scrutinee into the arm's bindings,
// causing them to be dropped in the arm's scope.
x if let y = PrintOnDrop("Dropped second in the first arm's scope")
&& let z = PrintOnDrop("Dropped first in the first arm's scope") =>
{
let declared_in_block = PrintOnDrop("Dropped in inner scope");
// Pattern-matching guards' bindings and temporaries are dropped in
// reverse order, dropping each guard condition operand's bindings
// before its temporaries. Lastly, variables bound by the arm's
// pattern are dropped.
}
_ => unreachable!(),
}

match PrintOnDrop("Dropped in the enclosing temporary scope") {
// When guard evaluation fails, control-flow leaves the arm scope,
// causing bindings and temporaries from earlier pattern-matching
// guard condition operands to be dropped. This occurs before evaluating
// the next arm's guard or body.
_ if let y = PrintOnDrop("Dropped in the first arm's scope")
&& false => unreachable!(),
// When a guard is executed multiple times due to self-overlapping
// or-patterns, control-flow leaves the arm scope when the guard fails
// and re-enters the arm scope before executing the guard again.
_ | _ if let y = PrintOnDrop("Dropped in the second arm's scope twice")
&& false => unreachable!(),
_ => {},
}
```

r[destructors.scope.bindings.patterns]
Variables in patterns are dropped in reverse order of declaration within the pattern.

Expand Down Expand Up @@ -255,9 +289,8 @@ smallest scope that contains the expression and is one of the following:
* A statement.
* The body of an [`if`], [`while`] or [`loop`] expression.
* The `else` block of an `if` expression.
* The non-pattern matching condition expression of an `if` or `while` expression,
or a `match` guard.
* The body expression for a match arm.
* The non-pattern matching condition expression of an `if` or `while` expression or a non-pattern-matching `match` [guard condition operand].
* The pattern-matching guard, if present, and body expression for a `match` arm.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To address #1823 (comment) (cc @theemathas), the temporary lifetime of an if let guard scrutinee is always extended, like for if let expressions. There aren't lifetime extension rules like there are for let statements. This rule should hopefully cover that (similar to how the analogous rule for "The pattern-matching condition(s) and consequent body of if" does below). There's an example further down too showing that the scrutinee lives until the end of the arm. Do you think this is sufficient, or would this benefit from additional clarification?

This comment was marked as resolved.

Copy link
Contributor

@theemathas theemathas Aug 12, 2025

Choose a reason for hiding this comment

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

Oh, are you saying that both the guard and the body expression together count as a single temporary scope (and not two)? I find that unintuitive, but I suppose that works.

Copy link
Contributor Author

@dianne dianne Aug 12, 2025

Choose a reason for hiding this comment

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

There's a single temporary scope for the arm, which encompasses the guard and body expression, so that if let scrutinees' temporaries live through the body, yes; if let scrutinees aren't temporary destruction scopes by default. Maybe it would be clearer to phrase the temporary scope as being as for the whole arm, with a clarifying note that it includes the guard. Something like "* The arm of a match expression. This includes the arm's guard, if present."

Personally, I'd like stricter scoping rules for if let, but this is consistent with if let/while let expressions. Making their scrutinees be temporary drop scopes by default with lifetime extension rules (like let statements have) would be a larger change requiring an edition break.

* Each operand of a [lazy boolean expression].
* The pattern-matching condition(s) and consequent body of [`if`] ([destructors.scope.temporary.edition2024]).
* The pattern-matching condition and loop body of [`while`].
Expand Down Expand Up @@ -317,8 +350,16 @@ while let x = PrintOnDrop("while let scrutinee").0 {
// Scrutinee is dropped at the end of the function, before local variables
// (because this is the tail expression of the function body block).
match PrintOnDrop("Matched value in final expression") {
// Dropped once the condition has been evaluated
// Non-pattern-matching guards' temporaries are dropped once the
// condition has been evaluated
_ if PrintOnDrop("guard condition").0 == "" => (),
// Pattern-matching guards' temporaries are dropped when leaving the
// arm's scope
_ if let "guard scrutinee" = PrintOnDrop("guard scrutinee").0 => {
let _ = &PrintOnDrop("lifetime-extended temporary in inner scope");
// `lifetime-extended temporary in inner scope` is dropped here
}
// `guard scrutinee` is dropped here
_ => (),
}
```
Expand Down Expand Up @@ -502,6 +543,7 @@ There is one additional case to be aware of: when a panic reaches a [non-unwindi
[closure]: types/closure.md
[destructors]: destructors.md
[expression]: expressions.md
[guard condition operand]: expressions/match-expr.md#match-guard-chains
[identifier pattern]: patterns.md#identifier-patterns
[initialized]: glossary.md#initialized
[interior mutability]: interior-mutability.md
Expand Down
96 changes: 89 additions & 7 deletions src/expressions/match-expr.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,35 @@ MatchExpression ->
MatchArms?
`}`

Scrutinee -> Expression _except [StructExpression]_
Scrutinee -> Expression _except_ [StructExpression]

MatchArms ->
( MatchArm `=>` ( ExpressionWithoutBlock `,` | ExpressionWithBlock `,`? ) )*
MatchArm `=>` Expression `,`?

MatchArm -> OuterAttribute* Pattern MatchArmGuard?

MatchArmGuard -> `if` Expression
MatchArmGuard -> `if` MatchConditions

MatchConditions ->
Expression
| MatchGuardChain

MatchGuardChain -> MatchGuardCondition ( `&&` MatchGuardCondition )*

MatchGuardCondition ->
Expression _except [ExcludedMatchConditions]_
| OuterAttribute* `let` Pattern `=` MatchGuardScrutinee

MatchGuardScrutinee -> Expression _except [ExcludedMatchConditions]_

@root ExcludedMatchConditions ->
LazyBooleanExpression
| RangeExpr
| RangeFromExpr
| RangeInclusiveExpr
| AssignmentExpression
| CompoundAssignmentExpression
```
<!-- TODO: The exception above isn't accurate, see https://github.com/rust-lang/reference/issues/569 -->

Expand Down Expand Up @@ -102,12 +122,11 @@ r[expr.match.guard]
r[expr.match.guard.intro]
Match arms can accept _match guards_ to further refine the criteria for matching a case.

r[expr.match.guard.type]
Pattern guards appear after the pattern and consist of a `bool`-typed expression following the `if` keyword.
r[expr.match.guard.condition]
Pattern guards appear after the pattern following the `if` keyword and consist of an [Expression] with a [boolean type][type.bool] or a conditional `let` match.

r[expr.match.guard.behavior]
When the pattern matches successfully, the pattern guard expression is executed.
If the expression evaluates to true, the pattern is successfully matched against.
When the pattern matches successfully, the pattern guard is executed. If all of the guard condition operands evaluate to `true` and all of the `let` patterns successfully match their [scrutinee]s, the match arm is successfully matched against and the arm body is executed.

r[expr.match.guard.next]
Otherwise, the next pattern, including other matches with the `|` operator in the same arm, is tested.
Expand Down Expand Up @@ -144,12 +163,75 @@ Before evaluating the guard, a shared reference is taken to the part of the scru
While evaluating the guard, this shared reference is then used when accessing the variable.

r[expr.match.guard.value]
Only when the guard evaluates to true is the value moved, or copied, from the scrutinee into the variable.
Only when the guard evaluates successfully is the value moved, or copied, from the scrutinee into the variable.
This allows shared borrows to be used inside guards without moving out of the scrutinee in case guard fails to match.

r[expr.match.guard.no-mutation]
Moreover, by holding a shared reference while evaluating the guard, mutation inside guards is also prevented.

r[expr.match.guard.let]
Guards can use `let` patterns to conditionally match a scrutinee and to bind new variables into scope when the pattern matches successfully.

> [!EXAMPLE]
> In this example, the guard condition `let Some(first_char) = name.chars().next()` is evaluated. If the `let` pattern successfully matches (i.e. the string has at least one character), the arm's body is executed. Otherwise, pattern matching continues to the next arm.
>
> The `let` pattern creates a new binding (`first_char`), which can be used alongside the original pattern bindings (`name`) in the arm's body.
> ```rust
> # enum Command {
> # Run(String),
> # Stop,
> # }
> let cmd = Command::Run("example".to_string());
>
> match cmd {
> Command::Run(name) if let Some(first_char) = name.chars().next() => {
> // Both `name` and `first_char` are available here
> println!("Running: {name} (starts with '{first_char}')");
> }
> Command::Run(name) => {
> println!("{name} is empty");
> }
> _ => {}
> }
> ```

r[expr.match.guard.chains]
## Match guard chains

r[expr.match.guard.chains.intro]
Multiple guard condition operands can be separated with `&&`.

> [!EXAMPLE]
> ```rust
> # let foo = Some([123]);
> # let already_checked = false;
> match foo {
> Some(xs) if let [single] = xs && !already_checked => { dbg!(single); }
> _ => {}
> }
> ```

r[expr.match.guard.chains.order]
Similar to a `&&` [LazyBooleanExpression], each operand is evaluated from left-to-right until an operand evaluates as `false` or a `let` match fails, in which case the subsequent operands are not evaluated.

r[expr.match.guard.chains.bindings]
The bindings of each `let` pattern are put into scope to be available for the next condition operand and the match arm body.

r[expr.match.guard.chains.or]
If any guard condition operand is a `let` pattern, then none of the condition operands can be a `||` [lazy boolean operator expression][expr.bool-logic] due to ambiguity and precedence with the `let` scrutinee.

> [!EXAMPLE]
> If a `||` expression is needed, then parentheses can be used. For example:
>
> ```rust
> # let foo = Some(123);
> match foo {
> // Parentheses are required here.
> Some(x) if (x < -100 || x > 20) => {}
> _ => {}
> }
> ```

r[expr.match.attributes]
## Attributes on match arms

Expand Down
3 changes: 3 additions & 0 deletions src/names/scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ r[names.scopes.pattern-bindings.let-chains]
* [`if let`] and [`while let`] bindings are valid in the following conditions as well as the consequent block.
r[names.scopes.pattern-bindings.match-arm]
* [`match` arms] bindings are within the [match guard] and the match arm expression.
r[names.scopes.pattern-bindings.match-guard-let]
* [`match` guard `let`] bindings are valid in the following guard conditions and the match arm expression.

r[names.scopes.pattern-bindings.items]
Local variable scopes do not extend into item declarations.
Expand Down Expand Up @@ -347,6 +349,7 @@ impl ImplExample {
[`macro_use` prelude]: preludes.md#macro_use-prelude
[`macro_use`]: ../macros-by-example.md#the-macro_use-attribute
[`match` arms]: ../expressions/match-expr.md
[`match` guard `let`]: expr.match.guard.let
[`Self`]: ../paths.md#self-1
[Associated consts]: ../items/associated-items.md#associated-constants
[associated items]: ../items/associated-items.md
Expand Down
Loading