Skip to content

if let guards documentation #1823

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 3 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
65 changes: 63 additions & 2 deletions src/destructors.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,15 @@ r[destructors.scope.nesting.match-arm]
* The parent of the expression after the `=>` in a `match` expression is the
scope of the arm that it's in.

r[destructors.scope.nesting.match-guard-pattern]
* The parent of an `if let` guard pattern is the scope of the guard expression.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm struggling with this one. Isn't the parent of both the scrutinee and the bindings of an if let guard the entire arm? I'm not able to find a way that it is different from destructors.scope.nesting.match-guard.

For example, in:

match x {
    Some(a) if let [one] = some_expression => {}
    _ => {}
}

I would expect the parent of both one and some_expression to be the match arm (and drop after the arm body).

Can you clarify this for me?

Copy link
Author

@Kivooeo Kivooeo Jun 11, 2025

Choose a reason for hiding this comment

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

Hm, yes, we can delete this because it duplucate of destructors.scope.nesting.match-guard, or at least alligns in a way it shouldnt


r[destructors.scope.nesting.match-guard-bindings]
* Variables bound by `if let` guard patterns have their drop scope determined by
guard success:
- On guard failure: dropped immediately after guard evaluation
- On guard success: scope extends to the end of the match arm body
Comment on lines +132 to +136
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I understand the need for this rule. Since the bindings are scoped to the match arm, I would expect destructors.scope.nesting.match-guard to cover this. That is, as we leave the arm (success or failure), the bindings will get dropped. Can you clarify how this would be different from the other rule?

Copy link
Author

Choose a reason for hiding this comment

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

This one also can be removed by the same reason as above

Copy link
Author

@Kivooeo Kivooeo Jun 11, 2025

Choose a reason for hiding this comment

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

But I'm unsure where should I write about drop on failure case, because they are different on success and failure, I think we can keep it, but I'm not sure where exactly

Copy link

@dianne dianne Jun 13, 2025

Choose a reason for hiding this comment

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

I can't speak to the spec, but implementation-wise, there's no difference between success and failure. if let guards declare their bindings in the scope of the match arm. Likewise, the scrutinee of an if let guard has the match arm scope as its temporary scope[^1]. Maybe that's the missing detail? Here, wildcards are used to demonstrate the temporary scope of the scrutinee (playground link).

fn main() {
    assert_drop_order(1..=3, |o| match () {
        () if let (_, _) = (o.log(2), o.log(3))
            && true =>
        {
            o.log(1);
        }
        _ => unreachable!(),
    });
}

On both success and failure, the reason if let guards' bindings and temporaries are dropped is because of breaking out of the arm's scope. This is even the case when the guard has to be evaluated multiple times for the same arm because of or-pattern expansion.

The difference between the guard's bindings and the match arm's pattern's bindings is all in sequencing. By-value bindings for the match arm's pattern are only created if the guard succeeds (expr.match.guard.value). When a guard fails, the guard's scrutinee and bindings/temporaries from earlier in the let chain will be dropped, since we leave the arm's scope. But the values bound by the arm's pattern won't, because they haven't been moved yet; the match's scrutinee is still in the match's scope, which we don't leave when a guard fails.

EDIT: Updated the example to actually demonstrate the scrutinee's temporary scope. I guess the drop order for the tuple is because tuples drop the left field first, then the right? Whereas if you create bindings for the fields, the right one is dropped first, then left.


r[destructors.scope.nesting.match]
* The parent of the arm scope is the scope of the `match` expression that it
belongs to.
Expand Down Expand Up @@ -204,8 +213,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 condition expression of an `if` or `while` expression.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this changing if and while to drop the "non-pattern matching" qualifier?

Copy link
Author

@Kivooeo Kivooeo Jun 11, 2025

Choose a reason for hiding this comment

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

i forgot to return it actually, but after this change i realise that we should keep them separatly actually, something like this would be better?

* The non-pattern matching condition expression of an `if` or `while` expression.
* A `match` guard expression.
* The scrutinee expression of an `if let` guard pattern.

* A `match` guard expression, including `if let` guard patterns.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm a little confused by this. Don't match guards have different temporary behavior of non-pattern matching conditions versus let conditions? Looking at drop-order.rs I see that if_guard and if_let_guard have different drop orders.

Copy link

Choose a reason for hiding this comment

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

I believe the temporary scope of an if let match guard is the whole match arm, yes (see my other comment for an example). Temporaries for normal boolean guards are dropped by the end of the guard, but the scrutinee of an if let guard lives for the arm.

* The body expression for a match arm.
* Each operand of a [lazy boolean expression].
* The pattern-matching condition(s) and consequent body of [`if`] ([destructors.scope.temporary.edition2024]).
Expand Down Expand Up @@ -415,6 +424,58 @@ let x = (&temp()).use_temp(); // ERROR
# x;
```

r[destructors.scope.match-guards]
### Match Guards and Pattern Binding
Comment on lines +427 to +428
Copy link
Contributor

Choose a reason for hiding this comment

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

Overall I'm uncertain what this section is saying that is different from the temporary scope rules in destructors.scope.temporary.enclosing. I'm also unclear about why this is differentiating if the pattern matches or not. Either case is handled when execution leaves the arm (success or failure), right?

Copy link
Author

Choose a reason for hiding this comment

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

i guess im over-engineering documentation, i will try to look on it from different angle


r[destructors.scope.match-guards.basic]
Match guard expressions create their own temporary scope. Variables bound within
guard patterns have conditional drop scopes based on guard evaluation results.

r[destructors.scope.match-guards.if-let]
For `if let` guards specifically:

1. **Guard pattern evaluation**: The `if let` pattern is evaluated within the
guard's temporary scope.
2. **Conditional binding scope**:
- If the pattern matches, bound variables extend their scope to the match arm body
- If the pattern fails, bound variables are dropped immediately
3. **Temporary cleanup**: Temporaries created during guard evaluation are dropped
when the guard scope ends, regardless of pattern match success.

r[destructors.scope.match-guards.multiple]
For multiple guards connected by `&&`:
- Each guard maintains its own temporary scope
- Failed guards drop their bindings before subsequent guard evaluation
- Only successful guard bindings are available in the arm body
- Guards are evaluated left-to-right with short-circuit semantics

```rust
# struct PrintOnDrop(&'static str);
# impl Drop for PrintOnDrop {
# fn drop(&mut self) {
# println!("drop({})", self.0);
# }
# }
# fn expensive_operation(x: i32) -> Option<PrintOnDrop> {
# Some(PrintOnDrop("expensive result"))
# }

match Some(42) {
// Guard creates temporary scope for pattern evaluation
Some(x) if let Some(y) = expensive_operation(x) => {
// Both x (from main pattern) and y (from guard) are live
// y will be dropped at end of this arm
println!("Success case");
} // y dropped here
Some(x) => {
// If guard failed, y was already dropped during guard evaluation
// expensive_operation result was also dropped
println!("Guard failed case");
}
None => {}
}
```

r[destructors.forget]
## Not running destructors

Expand Down
97 changes: 90 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 `if let` expression successfully matches (i.e., the string has at least one character), the arm's body is executed with both `name` and `first_char` available. Otherwise, pattern matching continues to the next arm.
>
> The key point is that the `if let` guard creates a new binding (`first_char`) that's only available if the guard succeeds, and this binding 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 All @@ -171,3 +253,4 @@ r[expr.match.attributes.inner]
[Range Pattern]: ../patterns.md#range-patterns
[scrutinee]: ../glossary.md#scrutinee
[value expression]: ../expressions.md#place-expressions-and-value-expressions
[scope and drop section]: ../destructors.md#match-guards-and-pattern-binding
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 if the guard succeeds.

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