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

Closed
wants to merge 3 commits into from
Closed
Changes from 1 commit
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
103 changes: 100 additions & 3 deletions src/expressions/match-expr.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,25 @@ MatchExpression ->
MatchArms?
`}`

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

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

MatchArm -> OuterAttribute* Pattern MatchArmGuard?
MatchArm ->
OuterAttribute* Pattern MatchArmGuard?

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

MatchConditions ->
MatchCondition ( `&&` MatchCondition )*

MatchCondition ->
OuterAttribute* `let` Pattern `=` Scrutinee
| Expression
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems to be missing some things.

Similar to if expressions, this will need two separate rules. Something like Expression | MatchConditions because without a let it is allowed to have LazyBooleanExpression. Then, MatchConditions will need to exclude LazyBooleanExpression.

I don't think this should use Scrutinee, because that rejects StructExpression, but that is allowed here.

This will also need similar precedence restrictions as if let has with restricting LazyBooleanExpression | RangeExpr | RangeFromExpr | RangeInclusiveExpr | AssignmentExpression | CompoundAssignmentExpression.

Copy link
Member Author

@Kivooeo Kivooeo Jun 9, 2025

Choose a reason for hiding this comment

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

I guess, something simple like this should work right? I just not sure if I can reuse LetChain like this

MatchArmGuard ->
    `if` MatchConditions

MatchConditions ->
      Expression _except [StructExpression]_
    | LetChain

Copy link
Contributor

Choose a reason for hiding this comment

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

No, because LetChain excludes StructExpression, but we don't want to exclude that here.
We also don't want MatchConditions to exclude StructExpression.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, it's little bit hard to write a working syntax to cover all cases, let's try something like this as a start

MatchArmGuard ->
    `if` MatchConditions

MatchConditions ->
    MatchCondition ( `&&` MatchCondition )*

MatchCondition ->
      OuterAttribute* `let` Pattern `=` Expression _except [ExcludedConditions]_
    | Expression _except [ExcludedConditions]_

@root ExcludedConditions ->
      LazyBooleanExpression
    | RangeExpr
    | RangeFromExpr
    | RangeInclusiveExpr
    | AssignmentExpression
    | CompoundAssignmentExpression

Copy link
Contributor

Choose a reason for hiding this comment

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

I think there needs to be a separate case for a chain vs not-chain.

For example:

match () {
    _ if x || y => {} // OK
    _ if x || y && let _ = () => {} // ERROR
    _ => {}
}

so I think what it needs is something like:

MatchConditions ->
     Expression
   | MatchConditionChain

MatchConditionChain ->
    MatchChainCondition ( `&&` MatchChainCondition )*

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

MatchGuardScrutinee -> Expression _except [ExcludedMatchConditions]_

@root ExcludedMatchConditions ->
      LazyBooleanExpression
    | RangeExpr
    | RangeFromExpr
    | RangeInclusiveExpr
    | AssignmentExpression
    | CompoundAssignmentExpression

right?

Copy link
Member Author

Choose a reason for hiding this comment

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

well, yeah, separate MatchGuardScrutinee is a good idea

```
<!-- TODO: The exception above isn't accurate, see https://github.com/rust-lang/reference/issues/569 -->

Expand Down Expand Up @@ -150,6 +160,93 @@ This allows shared borrows to be used inside guards without moving out of the sc
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.if.let.guard]
## If Let Guards
Match arms can include `if let` guards to allow conditional pattern matching within the guard clause.

r[expr.match.if.let.guard.syntax]
```rust,ignore
match expression {
pattern if let subpattern = guard_expr => arm_body,
...
}
```
Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible to make a working example?

Copy link
Member Author

Choose a reason for hiding this comment

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

Im not really sure, because it's not stable feature and contains behind #![feature...] gate that's aren't allowed in documentation and CI will fail with this error

Running target/debug/style-check ../src
error in ../src/expressions/match-expr.md: #![feature] attributes are not allowed

Copy link
Contributor

Choose a reason for hiding this comment

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

You can just add a full example that will fail CI for now.

Here, `guard_expr` is evaluated and matched against `subpattern`. If the `if let` expression in the guard matches successfully, the arm's body is executed. Otherwise, pattern matching continues to the next arm.

r[expr.match.if.let.guard.behavior]
When the pattern matches successfully, the `if let` expression in the guard is evaluated:
* The guard proceeds if the inner pattern (`subpattern`) matches the result of `guard_expr`.
* Otherwise, the next arm is tested.

```rust,ignore
let value = Some(10);

let msg = match value {
Some(x) if let Some(y) = Some(x - 1) => format!("Matched inner value: {}", y),
_ => "No match".to_string(),
};
```

r[expr.match.if.let.guard.scope]
* The `if let` guard may refer to variables bound by the outer match pattern.
* New variables bound inside the `if let` guard (e.g., `y` in the example above) are available within the body of the match arm where the guard evaluates to `true`, but are not accessible in other arms or outside the match expression.
Copy link
Member

Choose a reason for hiding this comment

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

May be worth being a bit more precise w.r.t drop behavior (i.e. that anything bound in the if let guards are dropped before evaluating other match arms)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, we can add something like this

r[expr.match.if.let.guard.drop]

* Variables bound inside `if let` guards are dropped before evaluating subsequent match arms.

* Temporaries created during guard evaluation follow standard drop semantics and are cleaned up appropriately.

What's your opinion on this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Drop behavior should be documented in the destructors chapter. I would expect it to be somewhat similar to the changes that were made for if (such as in destructors.scope.temporary.enclosing).

The stabilization report doesn't explain what the drop order is or how temporaries are handled, so it is difficult for me to evaluate what this should say.

Copy link
Member Author

Choose a reason for hiding this comment

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

Something like this? I'm just not sure how full should be explanation about drop order

r[destructors.scope.temporary.if-let-guard]
### If Let Guards in Match Expressions

When an `if let` guard is evaluated in a match expression:

1. **Guard evaluation scope**: Variables bound by the `if let` pattern and any temporaries created during guard evaluation have a scope that extends through the match arm body if the guard succeeds.

2. **Failed guard cleanup**: If the `if let` guard fails to match:
   - Any variables bound by the guard pattern are immediately dropped
   - Temporaries created during guard evaluation are dropped
   - Pattern matching continues to the next arm

3. **Successful guard**: If the guard succeeds:
   - Guard-bound variables remain live for the duration of the match arm body
   - They are dropped at the end of the match arm execution

4. **Multiple guards**: In arms with multiple guards connected by `&&`:
   - Each failed guard drops its own bindings before evaluating the next guard
   - Only successful guard bindings are available in the arm body

#### Example
...


```rust,ignore
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
```rust,ignore
```rust

let opt = Some(42);

match opt {
Some(x) if let Some(y) = Some(x + 1) => {
// Both `x` and `y` are available in this arm,
// since the pattern matched and the guard evaluated to true.
println!("x = {}, y = {}", x, y);
}
_ => {
// `y` is not available here --- it was only bound inside the guard above.
// Uncommenting the line below will cause a compile-time error:
// println!("{}", y); // error: cannot find value `y` in this scope
}
}

// Outside the match expression, neither `x` nor `y` are in scope.
```

* The outer pattern variables (`x`) follow the same borrowing behavior as in standard match guards (see below).

r[expr.match.if.let.guard.drop]

* Variables bound inside `if let` guards are dropped before evaluating subsequent match arms.

* Temporaries created during guard evaluation follow standard drop semantics and are cleaned up appropriately.

r[expr.match.if.let.guard.borrowing]
Variables bound by the outer pattern follow the same borrowing rules as standard match guards:
* A shared reference is taken to pattern variables before guard evaluation
* Values are moved or copied only when the guard succeeds
* Moving from outer pattern variables within the guard is restricted

```rust,ignore
fn take<T>(value: T) -> Option<T> { Some(value) }

let val = Some(vec![1, 2, 3]);
match val {
Some(v) if let Some(_) = take(v) => "moved", // ERROR: cannot move `v`
Some(v) if let Some(_) = take(v.clone()) => "cloned", // OK
_ => "none",
}
```
> [!NOTE]
> Multiple matches using the `|` operator can cause the pattern guard and the side effects it has to execute multiple times. For example:
> ```rust,ignore
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
> ```rust,ignore
> ```rust

> use std::cell::Cell;
>
> let i: Cell<i32> = Cell::new(0);
> match 1 {
> 1 | _ if let Some(_) = { i.set(i.get() + 1); Some(1) } => {}
> _ => {}
> }
> assert_eq!(i.get(), 2); // Guard is executed twice
> ```

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

Expand Down