Skip to content

Commit e0b3e46

Browse files
committed
added review on if let guard
1 parent 1973872 commit e0b3e46

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed

if-let-guard-review.md

+268
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# `if let` guard in-depth review
2+
3+
Rust’s *`if let` guards* feature allows match arms to include an `if let` condition, enabling a pattern match and a conditional check
4+
in one guard. In other words, a match arm can be written as:
5+
6+
```rust
7+
match value {
8+
PATTERN if let GUARD_PAT = GUARD_EXPR => { /* body */ },
9+
_ => { /* fallback */ },
10+
}
11+
```
12+
13+
This arm is selected **only if** `value` matches `PATTERN` *and* additionally `GUARD_EXPR` matches `GUARD_PAT`. Crucially, the
14+
variables bound by `PATTERN` are in scope when evaluating `GUARD_EXPR`, and the variables bound by both patterns
15+
(`PATTERN` and `GUARD_PAT`) are in scope in the arm’s body. For example:
16+
17+
```rust
18+
match foo {
19+
Some(x) if let Ok(y) = compute(x) => {
20+
// `x` from `Some(x)` and `y` from `Ok(y)` are both available here
21+
println!("{}, {}", x, y);
22+
}
23+
_ => {}
24+
}
25+
```
26+
27+
As the original RFC explains, the semantics are that the guard is chosen *if* the main pattern matches and the let-pattern
28+
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.)
29+
30+
Currently the feature is **unstable** and gated by `#![feature(if_let_guard)]`. If used without the feature, the compiler
31+
emits error E0658: “`if let` guards are experimental” (with a suggestion to use `matches!(…)` instead).
32+
Tests in the Rust repository (e.g. `feature-gate.rs`) verify that using `if let` in a guard without the feature flag
33+
indeed produces this error.
34+
35+
## Syntax and Examples
36+
37+
The syntax for an `if let` guard follows the existing match-guard form, except using `if let` after the pattern:
38+
39+
```text
40+
match EXPR {
41+
PAT if let P = E => BODY,
42+
// ...
43+
}
44+
```
45+
46+
Here `PAT` is an arbitrary pattern for the match arm, and `if let P = E` is the guard. You can also combine multiple conditions
47+
with `&&`. In fact, because of the related “let chains” feature, you can write multiple `let`-bindings chained by `&&` in the
48+
same guard. For example:
49+
50+
```rust
51+
match value {
52+
// Two let-conditions chained with `&&`
53+
(Some(a), Some(b)) if let Ok(x) = f(a) && let Ok(y) = g(b) => {
54+
// use a, b, x, y here
55+
}
56+
_ => {}
57+
}
58+
```
59+
60+
Examples of valid `if let` guards (with the feature enabled) include:
61+
62+
```rust
63+
match x {
64+
(n, m) if let (0, Some(color)) = (n/10, color_for_code(m)) => { /* ... */ }
65+
y if let Some(z) = helper(y) => { /* ... */ }
66+
_ => { /* ... */ }
67+
}
68+
```
69+
70+
If the syntax is used incorrectly, the compiler gives an appropriate error. For instance, writing `(let PAT = EXPR)` parenthesized
71+
or using `if (let PAT = EXPR)` (i.e. wrapping a `let` in extra parentheses) is not accepted as a valid guard and instead produces
72+
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.
73+
74+
## Semantics and Variable Scope
75+
76+
When a match arm has an `if let` guard, the evaluation proceeds as follows:
77+
78+
1. The match scrutinee is matched against the arm’s main pattern `PAT`. Any variables bound by `PAT` become available.
79+
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`.
80+
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).
81+
82+
Therefore, variables bound in the main pattern `PAT` are “live” during the evaluation of the guard, but any variables bound
83+
by `GUARD_PAT` only come into existence in the arm body (not in earlier code). This corresponds directly to the RFC’s reference
84+
explanation: “the variables of `pat` are bound in `guard_expr`, and the variables of `pat` and `guard_pat` are bound in `body_expr`”.
85+
86+
As an example, consider:
87+
88+
```rust
89+
match (opt, val) {
90+
(Some(x), _) if let Ok(y) = convert(x) => {
91+
// Here `x` and `y` are in scope
92+
println!("Converted {} into {}", x, y);
93+
}
94+
_ => {}
95+
}
96+
```
97+
98+
Here the pattern `(Some(x), _)` binds `x`. Then `convert(x)` is called, and its result is matched to `Ok(y)`. If both steps
99+
succeed, the body can use both `x` and `y`. If either fails (pattern fails or guard fails), this arm is skipped.
100+
101+
One important restriction is that a single match arm cannot have two `if`-guards. That is, you cannot write something like
102+
`PAT if cond1 if let P = E => ...` with two separate `if`s. You may combine a normal boolean condition with a `let`
103+
by chaining with `&&`, but only one `if` keyword is allowed. The RFC explicitly states “An arm may not have both an
104+
`if` and an `if let` guard” (i.e. you can’t do `if cond && let ...` *and* then another `if`, etc.).
105+
(You *can* do something like `if let P = E && cond` by writing `if let P = E && cond =>`, treating the boolean as part
106+
of a let-chain, but that is a single `if` in syntax.)
107+
108+
## Feature Gate and Errors
109+
110+
As of now, `if let` guards are still unstable. The compiler requires the feature flag `#![feature(if_let_guard)]` to enable them.
111+
If one uses an `if let` guard without the feature, one gets an error similar to:
112+
113+
```
114+
error[E0658]: `if let` guards are experimental
115+
|
116+
LL | _ if let true = true => {}
117+
| ^^^^^^^^^^^^^^^^
118+
= help: you can write `if matches!(<expr>, <pattern>)` instead of `if let <pattern> = <expr>`
119+
```
120+
121+
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.
122+
The tests also ensure the old (`let`-in-`if` without the feature) error is preserved. For example:
123+
124+
```rust
125+
match () {
126+
() if true && let 0 = 1 => {} // error: `let` expressions are unstable (since no feature)
127+
() if let 0 = 1 && true => {} // error: `if let` guards are experimental
128+
_ => {}
129+
}
130+
```
131+
132+
The test suite checks that these errors mention both the unstable-let and the experimental guard exactly as above.
133+
Once the feature is stabilized, these errors will no longer appear.
134+
135+
## Temporaries and Drop Order
136+
137+
A subtle aspect of `if let` guards is the handling of temporaries (and destructor calls) within the guard expression.
138+
The Rust reference explains that a *match guard* creates its own temporary scope: any temporaries produced by `GUARD_EXPR`
139+
live only until the guard finishes evaluating. Concretely, this means:
140+
141+
* The `guard_expr` is evaluated *after* matching `PAT` but *before* executing the arm’s body (if taken).
142+
* Any temporary values created during `guard_expr` are dropped immediately after the guard’s scope ends (i.e. before entering the arm body).
143+
* If the guard fails, those temporaries are dropped right then, and the compiler proceeds to the next arm.
144+
145+
In effect, the drop semantics are the same as for an ordinary match guard or an `if let` in an `if` expression: no unexpected
146+
extension of lifetimes. (In Rust 2024 edition, there is a finer rule that even in `if let`
147+
expressions temporaries drop before the `else` block; but for match guards the effect is that temporaries from the
148+
guard are dropped before the arm body.)
149+
150+
This behavior is exercised by the existing tests. For example, the `drop-order.rs` UI test uses `Drop`-implementing
151+
values in nested `if let` guards to verify the precise drop order. Those tests confirm that the values from the inner
152+
guards are dropped *first*, before values from outer contexts and before finally moving on to other arms. In short, the
153+
feature does not introduce any new irregularity in drop order: guard expressions are evaluated left-to-right
154+
(following let-chains semantics) and their temporaries die as soon as the guard completes.
155+
156+
## Lifetimes and Variable Scope
157+
158+
Aside from drop timing, lifetimes of references in the guard work as expected. Because the pattern variables (`PAT` bindings)
159+
are in scope during `GUARD_EXPR`, one can take references to them or otherwise use them. Any reference or borrow introduced
160+
by the guard is scoped to the guard and arm body. For example:
161+
162+
```rust
163+
match &vec {
164+
v if let [first, ref rest @ ..] = v[..] => {
165+
// `first` and `rest` borrowed from `v` are valid here
166+
println!("{}", first);
167+
}
168+
_ => {}
169+
}
170+
```
171+
172+
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
173+
(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.
174+
175+
Moreover, an `if let` guard cannot break exhaustiveness: each arm is either taken or skipped in the usual way.
176+
A guard cannot cause a pattern to match something it wouldn’t normally match, it only *restricts* a match further.
177+
Tests like `exhaustive.rs` ensure that match exhaustiveness is checked as usual (you still need a wildcard arm if needed).
178+
No special exhaustiveness rules are introduced.
179+
180+
## Mutability and Moves
181+
182+
Patterns inside guards obey the normal mutability and move semantics. You can use `mut`, `ref`, or `ref mut`
183+
in the guard pattern just like in a `let` or match pattern. For example, `if let Some(ref mut x) = foo()` will mutably
184+
borrow from `foo()`. The borrow-checker treats moves in a guard pattern exactly as it would in a regular pattern: a move of a
185+
binding only occurs if that branch is actually taken, and subsequent code cannot use a moved value.
186+
187+
This is tested by the **move-guard-if-let** suite. For instance, consider:
188+
189+
```rust
190+
fn same_pattern(c: bool) {
191+
let mut x: Box<_> = Box::new(1);
192+
let v = (1, 2);
193+
match v {
194+
(1, _) if let y = *x && c => (),
195+
(_, 2) if let z = *x => (), // uses x after move
196+
_ => {}
197+
}
198+
}
199+
```
200+
201+
With `#![feature(if_let_guard)]`, the compiler correctly reports that `x` is moved by the first guard and then used again by
202+
the second pattern, which is an error. In the test output one sees messages like “`value moved here`
203+
and “`value used here after move`” exactly pointing to the `if let` bindings. (These errors match the compiler’s normal behavior,
204+
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`),
205+
no move would occur. The test suite also covers using `&&` with or-patterns and ensures borrowck handles those correctly.
206+
207+
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
208+
ensures no use-after-move, and moves only happen if the pattern actually matches. The existing UI tests for moves and mutability
209+
all pass under the current implementation, so there is no unsoundness here.
210+
211+
## Shadowing and Macros
212+
213+
The usual Rust rules for shadowing and macros apply. An `if let` guard can introduce a new variable that *shadows* an existing one:
214+
215+
```rust
216+
let x = 10;
217+
match v {
218+
(true, _) if let x = compute() => {
219+
// Here the `x` from the guard shadows the outer `x`.
220+
println!("{}", x);
221+
}
222+
_ => {}
223+
}
224+
```
225+
226+
This is allowed (just as in ordinary `if let` expressions) and works as expected; the tests (`shadowing.rs`) verify that the
227+
scoping is consistent.
228+
229+
Macro expansion also works naturally. You can write macros that produce part of the guard. For example:
230+
231+
```rust
232+
macro_rules! m { () => { Some(5) } }
233+
match opt {
234+
Some(v) if let Some(w) = m!() => { /*...*/ }
235+
_ => {}
236+
}
237+
```
238+
239+
Since the parser sees the expanded code, `if let` guards inside macros are supported. The Rust tests include cases where macros
240+
expand to an `if let` guard (fully or partially) to ensure the feature handles macro hygiene correctly. In short, `if let` guards
241+
are not disabled or altered in macro contexts; they simply follow the normal macro expansion rules of Rust.
242+
243+
## Soundness and Pitfalls
244+
245+
No inherent unsoundness has been found in `if let` guards. They are purely syntactic sugar for nested pattern matching and condition
246+
checks. All borrow and move checks are done conservatively and correctly. The feature interacts with other parts of the language in
247+
predictable ways. For example:
248+
249+
* **Refutability:** An `if let` guard’s pattern is allowed to be refutable (since a failed match simply means skipping the arm). The tests ensure that irrefutable-let warnings do not occur (or can be allowed).
250+
* **Matching order:** Guards are evaluated in sequence per arm; if the first part of a let-chain fails, later parts aren’t evaluated (preventing needless moves or panics).
251+
* **No new invariants:** Guard patterns do not introduce new lifetime or aliasing invariants beyond normal patterns. Temporaries and borrows expire normally.
252+
253+
All of the edge cases are covered by the existing UI tests. For example, the `exhaustive.rs` test confirms that match exhaustiveness
254+
remains correct when using `if let` guards (i.e. a wildcard arm is needed if not all cases are covered).
255+
The `typeck.rs` and `type-inference.rs` tests verify that type inference and generic code work through `if let` guards as expected.
256+
The compiler’s own test suite includes dozens of `if let` guard tests under `src/test/ui/rfcs/rfc-2294-if-let-guard/`,
257+
and all of them pass with the current implementation.
258+
259+
## Conclusion
260+
261+
The feature is fully implemented in the compiler and exercised by many tests. Its syntax and semantics are clear and consistent with
262+
existing Rust rules: pattern bindings from the arm are in scope in the guard, and guard bindings are in scope in the arm body.
263+
The compiler enforces the usual ownership rules (as seen in the move tests) and handles temporaries in a straightforward way.
264+
265+
**Status:** implemented and well-tested, awaiting only formal documentation [(I've also made one)](https://github.com/rust-lang/reference/pull/1823) to be fully ready for a stable release.
266+
267+
**References:** details from RFC 2294 and the current compiler behavior are used above. Each cited source shows the design or
268+
diagnostics of `if let` guards in action.

0 commit comments

Comments
 (0)