Skip to content

Stabilize if let guards (feature(if_let_guard)) #141295

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 2 commits into
base: master
Choose a base branch
from

Conversation

Kivooeo
Copy link
Contributor

@Kivooeo Kivooeo commented May 20, 2025

Summary

This proposes the stabilization of if let guards (tracking issue: #51114, RFC: rust-lang/rfcs#2294). This feature enhances Rust's pattern matching by allowing if let expressions to be used directly within match arm guards, enabling more expressive, concise, and readable conditional logic in match statements.

What is being stabilized

The ability to use if let expressions within match arm guards is being stabilized. This allows for conditional pattern matching directly within the guard clause of a match arm, combining pattern destructuring with boolean conditions.

Example:

enum Command {
    Run(String),
    Stop,
    Pause,
}

fn process_command(cmd: Command, state: &mut String) {
    match cmd {
        Command::Run(name) if let Some(first_char) = name.chars().next() && first_char.is_ascii_alphabetic() => {
            // Both `name` and `first_char` are available here
            println!("Running command: {} (starts with '{}')", name, first_char);
            state.push_str(&format!("Running {}", name));
        }
        Command::Run(name) => {
            println!("Cannot run command '{}'. Invalid name.", name);
        }
        Command::Stop if state.contains("running") => {
            println!("Stopping current process.");
            state.clear();
        }
        _ => {
            println!("Unhandled command or state.");
        }
    }
}

fn main() {
    let mut current_state = String::new();
    process_command(Command::Run("my_app".to_string()), &mut current_state);
    process_command(Command::Run("123_app".to_string()), &mut current_state);
    process_command(Command::Stop, &mut current_state); // Will not stop due to state
    process_command(Command::Run("another_app".to_string()), &mut current_state);
    process_command(Command::Stop, &mut current_state); // Will stop
}

Motivation

The primary motivation for if let guards is to reduce boilerplate and improve the clarity of conditional logic within match statements. Prior to this feature, complex conditional checks often required nested if let statements within the match arm's body, leading to increased indentation and reduced readability.

Consider the following scenario without if let guards:

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

With if let guards, this becomes significantly more streamlined:

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

Also I should make a remark here and say that drop order or other things are identical in this both contructions, so think about if let guards as just like about easirer way to express this

Implementation and Testing

The if let guard feature has undergone extensive implementation and testing to ensure its stability, correctness, and consistent behavior across all Rust editions. This process has involved collaborative efforts, notably with @est31, who has provided invaluable insights and thorough testing, confirming identical functionality across editions.

Tests

Error messages and diagnostics

warns.rs, parens.rs, macro-expanded.rs, guard-mutability-2.rs, ast-validate-guards.rs - shows that if let guards are parsed with strict syntax rules, disallowing parentheses around the let expression and rejecting macro expansions that produce statements instead of expressions. The compiler correctly diagnoses irrefutable patterns, unreachable patterns, mutability violations, and unsupported constructs inside guards, ensuring soundness and clear error messages. The parser fails on invalid syntax without complex recovery, preventing cascading errors and maintaining clarity

Scoping and shadowing

scope.rs - verifies that bindings created inside if let match guards are properly scoped and usable in the corresponding match arm. Covers both let on the left-hand side and right-hand side of &&

shadowing.rs - validates that name shadowing works correctly within if let guards and does not lead to resolution issues. Demonstrates deep shadowing via multiple lets and ensures type resolution matches expected shadowed bindings

scoping-consistency.rs - ensures that temporaries created within if let guards are correctly scoped to the guard expression and remain valid for the duration of the match arm they’re used in

Exhaustiveness

exhaustive.rs - validates that if let guards do not affect exhaustiveness checking in match expressions, test fails due missing match arm

Type System

type-inference.rs - confirms that type inference works correctly in match guards using if let

typeck.rs - verifies that type mismatches in if let guards are caught as expected — the same way they are in other contexts

Drop

drop-order.rs - ensures that temporaries created in match guards are dropped in the correct order

compare-drop-order.rs - comparing drop order between regular if let in match arm and if let guard between all editions to show that drop order across all editions the same

drop-score.rs - ensures that temporaries introduced in if let guards (including let chains) live for the duration of the arm

Move

move-guard-if-let.rs, move-guard-if-let-chain.rs - tests verify that the borrow checker correctly understands how moves happen inside if let guards, especially with let chains. Specifically, a move of a variable inside a guard pattern only actually occurs if that guard pattern matches. This means the move is conditional, and the borrow checker must track this precisely to avoid false move errors or unsound behavior


Key aspects verified during testing include:

  • Variable Scope: Variables bound in the main pattern are accessible within the guard expression, and variables bound in the guard are accessible within the match arm body.
  • Chaining with &&: Multiple let bindings can be chained within a single guard using the && operator (only in 2024 edition, since let chains were stabilized only in 2024 edition).
  • Refutable Patterns: The patterns within the if let guard can be refutable, allowing for concise conditional logic based on the success or failure of the pattern match.
  • MIR Generation: The Mid-level Intermediate Representation (MIR) generated for if let guards has been carefully reviewed to ensure it aligns with the expected runtime behavior, particularly concerning variable lifetimes and drop order.

Edition Considerations

This feature is stable in all Rust editions. Any initial concerns regarding temporary drop order or variable scoping in older editions were resolved by backported compiler improvements, ensuring consistent and correct behavior across all editions. Users can adopt this feature immediately without needing to migrate their projects to a newer edition

Unresolved Issues

Note

This is my first stabilization PR, so if I missed any steps or there’s something I should adjust, please feel free to point it out — I’d be happy to improve it. I’ve followed the standard process as closely as I could, but I’m still learning the full stabilization workflow

Related

@rustbot
Copy link
Collaborator

rustbot commented May 20, 2025

r? @SparrowLii

rustbot has assigned @SparrowLii.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 20, 2025
@rustbot
Copy link
Collaborator

rustbot commented May 20, 2025

Some changes occurred to the CTFE machinery

cc @RalfJung, @oli-obk, @lcnr

Some changes occurred to MIR optimizations

cc @rust-lang/wg-mir-opt

Some changes occurred in compiler/rustc_codegen_ssa

cc @WaffleLapkin

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 6fe74d9 to 5ee8970 Compare May 20, 2025 17:08
@rustbot
Copy link
Collaborator

rustbot commented May 20, 2025

rust-analyzer is developed in its own repository. If possible, consider making this change to rust-lang/rust-analyzer instead.

cc @rust-lang/rust-analyzer

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from eb0e4b4 to 0358002 Compare May 20, 2025 17:13
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 92a5204 to ab138ce Compare May 20, 2025 17:35
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 5ceca48 to a20c4f6 Compare May 20, 2025 17:57
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch 2 times, most recently from 1dd9974 to 5796073 Compare May 20, 2025 18:56
@traviscross traviscross added T-lang Relevant to the language team, which will review and decide on the PR/issue. needs-fcp This change is insta-stable, so needs a completed FCP to proceed. S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 20, 2025
@traviscross
Copy link
Contributor

cc @est31 @ehuss

@traviscross
Copy link
Contributor

cc @Nadrieril

@SparrowLii
Copy link
Member

SparrowLii commented May 21, 2025

This needs a fcp so I'd like to roll this to someone more familiar with this feature
r? compiler

@rustbot rustbot added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label May 21, 2025
@rustbot rustbot assigned oli-obk and unassigned SparrowLii May 21, 2025
@traviscross traviscross added I-lang-nominated Nominated for discussion during a lang team meeting. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang labels May 21, 2025
@oli-obk
Copy link
Contributor

oli-obk commented May 21, 2025

r? @est31

Copy link
Contributor

Choose a reason for hiding this comment

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

(similarly using a random file to get an inline comment)

Could you distill the contents of the "Edition Considerations" section? This section needs to say which editions this will be stable in and why, which should be concise. I'm having a very tough time following wording like "...designed to integrate seamlessly across all Rust editions" when there isn't a simple statement like "x will be stable in [all editions][edition xxxx and later]".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, i just later realise that we didnt tested this in 2015 and 2018, i will update this block sure

@tgross35 tgross35 changed the title Stabilize if let guard Stabilize if let guards (feature(if_let_guard)) May 21, 2025
Copy link
Contributor

Choose a reason for hiding this comment

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

(from the top post)

The primary blocker identified during pre-stabilization testing was a set of subtle interactions related to drop order and variable scoping within match guards with let chains, comprehensively addressed and validated by the tests introduced in #140981. With this crucial aspect resolved, no other significant issues remain, and the feature is considered ready for stabilization.

Testing the current design is necessary, but it's much more important that you convince us why the current design is correct and why we don't need to iterate further - which no test in the world can do :).

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 be honest, I’m still trying to get a better grasp of what exactly counts as the “current design” in this context — whether it refers mostly to syntax, semantics, implementation details (like MIR/lowering), or a combination of all of these. I’ve mainly been focusing on making sure things work and are well-tested, but I now realize that’s not the same as being able to clearly explain why the design itself is correct and stable

I’d really appreciate any pointers or examples of what kind of justification would be most helpful for the team here

Copy link
Contributor

Choose a reason for hiding this comment

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

The things that matter here are mostly syntax and semantics, since that is the part we won't be able to change in the future. Lowering can come a bit into play here since drop order interacts with MIR.

As some twocents, the things you may want to consider mentioning are things like consistency with existing language behavior and anything that might be a "gotcha" for users. For example, can if_let_guards (if let on LHS of the =>) always be thought of as if let within a match arm's expression (if let on the RHS of the =>)? What order do things get dropped in? Are there any behavior differences across editions that will be user-visible?

Copy link
Contributor Author

@Kivooeo Kivooeo May 21, 2025

Choose a reason for hiding this comment

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

Thanks for the clarification — that helps a lot

Regarding the syntax and semantics: the final form of the syntax was actually proposed back in the original RFC from 2018, so in terms of syntax, things have been stable and intentional from the start

As for semantics as far as I concerned, the semantics between using if let guards on the LHS and an if let inside the match arm body (RHS) are intended to be equivalent in terms of behavior visible to the user. Both forms bind variables and control flow similarly, and they should produce the same observable effects

I've tested drop order for LHS and RHS cases and they are identical

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I don't expect syntax to change, it's fine to say that it is a logical extension to if guards that hasn't had any bikeshed. The other two are a bit trickier since it's about identifying possible edge cases. E.g. if this is to be stable in all editions, it would be useful to see examples showing how the changes to if let temporary scope come into play.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, yes they are identical, if we change

match ... {
    <pat> if <guard> => { <arm> }
    _ => {}
}

in tests to

match ... {
    <pat> => if <guard> {
        <arm>
    },
    _ => {}
}

nothing will change in drop order

Copy link
Contributor

@tgross35 tgross35 May 21, 2025

Choose a reason for hiding this comment

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

I think Nandi may be asking for a more first principals approach, e.g. does the compiler source use the same paths for both so we have a better idea that the results will always be identical? Or at least pointing to an existing test (or a new one) that shows the two patterns generate the same MIR in all editions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I will add test that tests drop order between if let guard and if let in match arm across all editions, that shows that they are the same, I'm not sure how can I make test for MIR but make a test that just shows that they have the same drop order

Copy link
Contributor Author

@Kivooeo Kivooeo May 21, 2025

Choose a reason for hiding this comment

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

Please take a look at this test, if this is ok
compare-drop-order.rs

Copy link
Member

Choose a reason for hiding this comment

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

Well, yes they are identical

Nice! That's all I wanted to make sure, this test looks ok to me.

Actually that can't be fully true because 1 | 1 if guard() can run the guard twice whereas 1 | 1 => if guard() { ... } will only run the guard once, but the basic idea is there.

Copy link
Member

@fee1-dead fee1-dead May 21, 2025

Choose a reason for hiding this comment

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

Thank you for letting us know that this is your first stabilization PR. I'll quickly point out two things:

(1) You claim in your PR description that

Multiple let bindings can be chained within a single guard using the && operator (only in 2024 edition, since let chains were stabilized only in 2024 edition).

.. This is not the case as shown in this file. This file is being run on 2015 edition and if merged now, we would accidentally stabilize let chains with the wrong drop order (and potentially broken MIR in some cases)

This was a subtle thing in the previous PR that stabilized let_chains in 2024 that made this permissible on all editions. See my comment at that time. If stabilizing this, that CondChecker should have LetChainsPolicy::EditionDependent instead.

(2) When stabilizing you should remove the code that gates the feature. As far as I can see, that code lives under https://github.com/rust-lang/rust/blob/master/compiler/rustc_parse/src/parser/expr.rs#L3407-L3436. Remove the code that checks for if let guards and adds it to gated spans.

Copy link
Contributor Author

@Kivooeo Kivooeo May 21, 2025

Choose a reason for hiding this comment

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

Thanks for your reply, let me check if I got you correctly
I should change this
LetChainsPolicy::AlwaysAllowed
to
LetChainsPolicy::EditionDependent

And that's it, right? So it will fix ability of if-let guards chaining on all editions which was added by accident

Copy link
Member

Choose a reason for hiding this comment

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

That's one part of it to fix (1). You must also make tests over revisions so that they can be tested on multiple editions to properly make sure that the gating does happen <2024 but is not gated on 2024.

Copy link
Contributor Author

@Kivooeo Kivooeo May 21, 2025

Choose a reason for hiding this comment

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

So after removing gate and AlwaysAllowed method now looks like this

    fn parse_match_arm_guard(&mut self) -> PResult<'a, Option<P<Expr>>> {
        // Used to check the `if_let_guard` feature mostly by scanning
        // `&&` tokens.
        if !self.eat_keyword(exp!(If)) {
            // No match arm guard present.
            return Ok(None);
        }

        let mut cond = self.parse_match_guard_condition()?;

        CondChecker::new(
            self,
            LetChainsPolicy::EditionDependent { current_edition: Edition::Edition2024 },
        )
        .visit_expr(&mut cond);

        Ok(Some(cond))
    }

not sure about comment here and should I use Edition2024 or EditionFuture

Copy link
Member

Choose a reason for hiding this comment

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

.. This is not the case as shown in this file. This file is being run on 2015 edition and if merged now, we would accidentally stabilize let chains with the wrong drop order (and potentially broken MIR in some cases)

This was a subtle thing in the previous PR that stabilized let_chains in 2024 that made this permissible on all editions. See my comment at that time. If stabilizing this, that CondChecker should have LetChainsPolicy::EditionDependent instead.

@fee1-dead note that we actually do have post-2024 drop order on all editions with if let guard chains. All the examples where while let and/or if let have weird behaviour or even broken mir work just fine with if let guards. See the tests added by #140981 .

So it's absolutely fine to stabilize if let guard chains on all editions. The PR description needs to be amended, of course.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, actually, this is what i was thinking when was removing this gate, like "why if drop order is working stable across all editions in if let guard already, wouldnt it be also stable for all editions for let chains". seems like we can also stable let chains across all editions as well, right?

Copy link
Member

Choose a reason for hiding this comment

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

we can't stabilize let chains on all editions because the drop order differs between editions, while it doesn't differ for if let guard chains.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

so two questions then

  1. can we make it work the same in all editions, or it's too complex
  2. should i return LetChainsPolicy::AlwaysAllowed

Copy link
Contributor Author

@Kivooeo Kivooeo May 21, 2025

Choose a reason for hiding this comment

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

and yes i will add information about this to pr description later today

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch 2 times, most recently from ec70e11 to aef3f5f Compare May 21, 2025 16:42
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from bb5916a to 49046a0 Compare May 21, 2025 17:13
@scottmcm
Copy link
Member

Musing: it's not obvious to me that stabilizing this in all editions is the best answer, even if I agree it's technically legal.

How should I weigh the potential confusion of "wait, I moved it from a normal if to a match arm if and it broke?" here?

@rustbot

This comment has been minimized.

@rustbot rustbot added has-merge-commits PR has merge commits, merge with caution. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels May 21, 2025
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 831fca2 to 6270aba Compare May 21, 2025 17:47
@rustbot rustbot removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. has-merge-commits PR has merge commits, merge with caution. labels May 21, 2025
@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 266e9d9 to 58b02c9 Compare May 21, 2025 17:56
@Kivooeo
Copy link
Contributor Author

Kivooeo commented May 21, 2025

@scottmcm

Thanks for your comment! Just to clarify, the drop order appears to be consistent across all editions. Could you please clarify what you mean by the potential confusion or breaking change for users? Are you referring to specific behavioral differences or edge cases that might cause code to behave differently when moving from a normal if to a match arm if let guard?

It would be helpful to understand the exact scenarios you’re concerned about

@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from fdec32f to ba28c26 Compare May 21, 2025 18:29
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 0699298 to d6688ef Compare May 21, 2025 19:26
@traviscross
Copy link
Contributor

We reviewed this briefly in the lang triage call today. We'll all probably need to have a closer look at this, and we'll of course be particularly interested in confirming that the drop order here is what we expect.

@Nadrieril
Copy link
Member

Musing: it's not obvious to me that stabilizing this in all editions is the best answer, even if I agree it's technically legal.

How should I weigh the potential confusion of "wait, I moved it from a normal if to a match arm if and it broke?" here?

I'm missing context, what breakage are you talking about? @Kivooeo has confirmed that moving from a if let guard to a if let in the match arm (or vice-versa) does not change behavior regardless of edition.

@Kivooeo
Copy link
Contributor Author

Kivooeo commented May 21, 2025

@traviscross I need your advice on this, would writing MIR drop order test for if-let guard will help team review it better and more precisely along side two existed (non MIR tests) that created for drop order: compare-drop-order, drop-order

The same way it was made for let chains back in the day here mir-match-guard-let-chains-drop-order

@Nadrieril
Copy link
Member

Nadrieril commented May 21, 2025

@Kivooeo the test you link is not a mir test, it's a normal (ui) test in the "mir" directory. A real MIR test is not necessary for this feature, since drop order can be observed with normal tests such as the ones you point to. It also seems to me that the existing tests are sufficient to demonstrate the drop order, unless there's a corner case I'm not thinking of.

@Kivooeo
Copy link
Contributor Author

Kivooeo commented May 21, 2025

@Nadrieril, yes :) I know it and specify this in parenthesis

along side two existed (non MIR tests) that created for drop order: compare-drop-order, drop-order

Well, I thought that this is a MIR test because it was in mir folder, hm, interesting, is there a real MIR tests? I guess it should do something like comparing right MIR output to that one that created by programm or something

So this is what am I trying to understand, would MIR test helpful when we do have this ui tests or not really

@est31
Copy link
Member

est31 commented May 21, 2025

we'll of course be particularly interested in confirming that the drop order here is what we expect.

@traviscross as a good starting point, you can look at the tests added by #140981. The PR description gives an overview.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
I-lang-nominated Nominated for discussion during a lang team meeting. needs-fcp This change is insta-stable, so needs a completed FCP to proceed. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.