../if-let-guard-feature

Feature `if let` guard overview

Rust’s if let guards feature allows match arms to include an if let condition, enabling a pattern match and a conditional check in one guard. In other words, a match arm can be written as:

match value {
    PATTERN if let GUARD_PAT = GUARD_EXPR => { /* body */ },
    _ => { /* fallback */ },
}

This arm is selected only if value matches PATTERN and additionally GUARD_EXPR matches GUARD_PAT. Crucially, the variables bound by PATTERN are in scope when evaluating GUARD_EXPR, and the variables bound by both patterns (PATTERN and GUARD_PAT) are in scope in the arm’s body. For example:

match foo {
    Some(x) if let Ok(y) = compute(x) => {
        // `x` from `Some(x)` and `y` from `Ok(y)` are both available here
        println!("{}, {}", x, y);
    }
    _ => {}
}

As the original RFC explains, the semantics are that the guard is chosen if the main pattern matches and the let-pattern in the guard also matches. (Per design, a match arm may have either a normal if guard or an if let guard, but not both simultaneously.)

Currently the feature is unstable and gated by #![feature(if_let_guard)]. If used without the feature, the compiler emits error E0658: “if let guards are experimental” (with a suggestion to use matches!(…) instead).
Tests in the Rust repository (e.g. feature-gate.rs) verify that using if let in a guard without the feature flag indeed produces this error.


Syntax and Examples

The syntax for an if let guard follows the existing match-guard form, except using if let after the pattern:

match EXPR {
    PAT if let P = E => BODY,
    // ...
}

Here PAT is an arbitrary pattern for the match arm, and if let P = E is the guard. You can also combine multiple conditions with &&. In fact, because of the related “let chains” feature, you can write multiple let-bindings chained by && in the same guard. For example:

match value {
    // Two let-conditions chained with `&&`
    (Some(a), Some(b)) if let Ok(x) = f(a) && let Ok(y) = g(b) => {
        // use a, b, x, y here
    }
    _ => {}
}

Examples of valid if let guards (with the feature enabled) include:

match x {
    (n, m) if let (0, Some(color)) = (n/10, color_for_code(m)) => { /* ... */ }
    y if let Some(z) = helper(y) => { /* ... */ }
    _ => { /* ... */ }
}

If the syntax is used incorrectly, the compiler gives an appropriate error. For instance, writing (let PAT = EXPR) parenthesized or using if (let PAT = EXPR) (i.e. wrapping a let in extra parentheses) is not accepted as a valid guard and instead produces a parse error “expected expression, found let statement”. This is tested in the Rust UI tests (e.g. parens.rs and feature-gate.rs). In short, if let must appear exactly as a guard after an if, not inside extra parentheses.


Semantics and Variable Scope

When a match arm has an if let guard, the evaluation proceeds as follows:

  1. The match scrutinee is matched against the arm’s main pattern PAT. Any variables bound by PAT become available.
  2. If the main pattern matches, then the guard expression is evaluated. In that expression, the bindings from PAT can be used. The guard expression is of the form let GUARD_PAT = GUARD_EXPR.
  3. The result of GUARD_EXPR is matched against GUARD_PAT. If this succeeds, then execution enters the arm’s body. Otherwise the arm is skipped (and later arms are tried).

Therefore, variables bound in the main pattern PAT are “live” during the evaluation of the guard, but any variables bound by GUARD_PAT only come into existence in the arm body (not in earlier code). This corresponds directly to the RFC’s reference explanation: “the variables of pat are bound in guard_expr, and the variables of pat and guard_pat are bound in body_expr”.

As an example, consider:

match (opt, val) {
    (Some(x), _) if let Ok(y) = convert(x) => {
        // Here `x` and `y` are in scope
        println!("Converted {} into {}", x, y);
    }
    _ => {}
}

Here the pattern (Some(x), _) binds x. Then convert(x) is called, and its result is matched to Ok(y). If both steps succeed, the body can use both x and y. If either fails (pattern fails or guard fails), this arm is skipped.

One important restriction is that a single match arm cannot have two if-guards. That is, you cannot write something like PAT if cond1 if let P = E => ... with two separate ifs. You may combine a normal boolean condition with a let by chaining with &&, but only one if keyword is allowed. The RFC explicitly states “An arm may not have both an if and an if let guard” (i.e. you can’t do if cond && let ... and then another if, etc.).
(You can do something like if let P = E && cond by writing if let P = E && cond =>, treating the boolean as part of a let-chain, but that is a single if in syntax.)


Feature Gate and Errors

As of now, if let guards are still unstable. The compiler requires the feature flag #![feature(if_let_guard)] to enable them. If one uses an if let guard without the feature, one gets an error similar to:

error[E0658]: `if let` guards are experimental
   |
LL |     _ if let true = true => {}
   |        ^^^^^^^^^^^^^^^^
   = help: you can write `if matches!(<expr>, <pattern>)` instead of `if let <pattern> = <expr>`

This message is verified by the compiler’s test suite (e.g. feature-gate.rs) and comes from the feature-gate code in the parser.
The tests also ensure the old (let-in-if without the feature) error is preserved. For example:

match () {
    () if true && let 0 = 1 => {}      // error: `let` expressions are unstable (since no feature)
    () if let 0 = 1 && true => {}      // error: `if let` guards are experimental
    _ => {}
}

The test suite checks that these errors mention both the unstable-let and the experimental guard exactly as above.
Once the feature is stabilized, these errors will no longer appear.


Temporaries and Drop Order

A subtle aspect of if let guards is the handling of temporaries (and destructor calls) within the guard expression. The Rust reference explains that a match guard creates its own temporary scope: any temporaries produced by GUARD_EXPR live only until the guard finishes evaluating. Concretely, this means:

In effect, the drop semantics are the same as for an ordinary match guard or an if let in an if expression: no unexpected extension of lifetimes. (In Rust 2024 edition, there is a finer rule that even in if let expressions temporaries drop before the else block; but for match guards the effect is that temporaries from the guard are dropped before the arm body.)

This behavior is exercised by the existing tests. For example, the drop-order.rs UI test uses Drop-implementing values in nested if let guards to verify the precise drop order. Those tests confirm that the values from the inner guards are dropped first, before values from outer contexts and before finally moving on to other arms. In short, the feature does not introduce any new irregularity in drop order: guard expressions are evaluated left-to-right (following let-chains semantics) and their temporaries die as soon as the guard completes.


Lifetimes and Variable Scope

Aside from drop timing, lifetimes of references in the guard work as expected. Because the pattern variables (PAT bindings) are in scope during GUARD_EXPR, one can take references to them or otherwise use them. Any reference or borrow introduced by the guard is scoped to the guard and arm body. For example:

match &vec {
    v if let [first, ref rest @ ..] = v[..] => {
        // `first` and `rest` borrowed from `v` are valid here
        println!("{}", first);
    }
    _ => {}
}

Here v is &Vec, and the guard borrows parts of it; those references are valid in the arm body. If a guard binds by value (e.g. if let x = some_moveable), the usual move/borrow rules apply (see below), but in all cases the scopes follow the match-arm rules.

Moreover, an if let guard cannot break exhaustiveness: each arm is either taken or skipped in the usual way. A guard cannot cause a pattern to match something it wouldn’t normally match, it only restricts a match further. Tests like exhaustive.rs ensure that match exhaustiveness is checked as usual (you still need a wildcard arm if needed). No special exhaustiveness rules are introduced.


Mutability and Moves

Patterns inside guards obey the normal mutability and move semantics. You can use mut, ref, or ref mut in the guard pattern just like in a let or match pattern. For example, if let Some(ref mut x) = foo() will mutably borrow from foo(). The borrow-checker treats moves in a guard pattern exactly as it would in a regular pattern: a move of a binding only occurs if that branch is actually taken, and subsequent code cannot use a moved value.

This is tested by the move-guard-if-let suite. For instance, consider:

fn same_pattern(c: bool) {
    let mut x: Box<_> = Box::new(1);
    let v = (1, 2);
    match v {
        (1, _) if let y = *x && c => (),
        (_, 2) if let z = *x => (),     // uses x after move
        _ => {}
    }
}

With #![feature(if_let_guard)], the compiler correctly reports that x is moved by the first guard and then used again by the second pattern, which is an error. In the test output one sees messages like “value moved here” and “value used here after move” exactly pointing to the if let bindings. (These errors match the compiler’s normal behavior, confirming that if let guards do not bypass the borrow rules.) In contrast, if the pattern had used ref (e.g. if let ref y = x), no move would occur. The test suite also covers using && with or-patterns and ensures borrowck handles those correctly.

In summary, moving or borrowing in an if let guard is just like doing so in a regular if let or match: the borrow checker ensures no use-after-move, and moves only happen if the pattern actually matches. The existing UI tests for moves and mutability all pass under the current implementation, so there is no unsoundness here.


Shadowing and Macros

The usual Rust rules for shadowing and macros apply. An if let guard can introduce a new variable that shadows an existing one:

let x = 10;
match v {
    (true, _) if let x = compute() => {
        // Here the `x` from the guard shadows the outer `x`.
        println!("{}", x);
    }
    _ => {}
}

This is allowed (just as in ordinary if let expressions) and works as expected; the tests (shadowing.rs) verify that the scoping is consistent.

Macro expansion also works naturally. You can write macros that produce part of the guard. For example:

macro_rules! m { () => { Some(5) } }
match opt {
    Some(v) if let Some(w) = m!() => { /*...*/ }
    _ => {}
}

Since the parser sees the expanded code, if let guards inside macros are supported. The Rust tests include cases where macros expand to an if let guard (fully or partially) to ensure the feature handles macro hygiene correctly. In short, if let guards are not disabled or altered in macro contexts; they simply follow the normal macro expansion rules of Rust.


Soundness and Pitfalls

No inherent unsoundness has been found in if let guards. They are purely syntactic sugar for nested pattern matching and condition checks. All borrow and move checks are done conservatively and correctly. The feature interacts with other parts of the language in predictable ways. For example:

All of the edge cases are covered by the existing UI tests. For example, the exhaustive.rs test confirms that match exhaustiveness remains correct when using if let guards (i.e. a wildcard arm is needed if not all cases are covered). The typeck.rs and type-inference.rs tests verify that type inference and generic code work through if let guards as expected. The compiler’s own test suite includes dozens of if let guard tests under src/test/ui/rfcs/rfc-2294-if-let-guard/, and all of them pass with the current implementation.


Conclusion

The feature is fully implemented in the compiler and exercised by many tests. Its syntax and semantics are clear and consistent with existing Rust rules: pattern bindings from the arm are in scope in the guard, and guard bindings are in scope in the arm body. The compiler enforces the usual ownership rules (as seen in the move tests) and handles temporaries in a straightforward way.

Status: implemented and well-tested, awaiting only formal documentation (I've also made one) to be fully ready for a stable release.

References: details from RFC 2294 and the current compiler behavior are used above. Each cited source shows the design or diagnostics of if let guards in action.