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 1 commit 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 allows if let expressions to be used directly within match arm guards, enabling conditional pattern matching within guard clauses.

What is being stabilized

The ability to use if let expressions within match arm guards.

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.");
        }
    }
}

Motivation

The primary motivation for if let guards is to reduce nesting and improve readability when conditional logic depends on pattern matching. Without this feature, such logic requires nested if let statements within match arms:

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

Implementation and Testing

The feature has been implemented and tested comprehensively across different scenarios:

Core Functionality Tests

Scoping and variable binding:

  • scope.rs - Verifies that bindings created in if let guards are properly scoped and available in match arms
  • shadowing.rs - Tests that variable shadowing works correctly within guards
  • scoping-consistency.rs - Ensures temporaries in guards remain valid for the duration of their match arms

Type system integration:

  • type-inference.rs - Confirms type inference works correctly in if let guards
  • typeck.rs - Verifies type mismatches are caught appropriately

Pattern matching semantics:

Error Handling and Diagnostics

  • warns.rs - Tests warnings for irrefutable patterns and unreachable code in guards
  • parens.rs - Ensures parentheses around let expressions are properly rejected
  • macro-expanded.rs - Verifies macro expansions that produce invalid constructs are caught
  • guard-mutability-2.rs - Tests mutability and ownership violations in guards
  • ast-validate-guards.rs - Validates AST-level syntax restrictions

Drop Order and Temporaries

Key insight: Unlike let_chains in regular if expressions, if let guards do not have drop order inconsistencies because:

  1. Match guards are clearly scoped to their arms
  2. There is no "else block" equivalent that could cause temporal confusion

Edition Compatibility

This feature stabilizes on all editions, unlike let_chains which was limited to edition 2024. This is safe because:

  1. if let guards don't suffer from the drop order issues that affected let_chains in regular if expressions
  2. The scoping is unambiguous - guards are clearly tied to their match arms
  3. Extensive testing confirms identical behavior across all editions

Interactions with Future Features

The lang team has reviewed potential interactions with planned "guard patterns" and determined that stabilizing if let guards now does not create obstacles for future work. The scoping and evaluation semantics established here align with what guard patterns will need.

Unresolved Issues


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 needs-fcp This change is insta-stable, or significant enough to need a team 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

@Kivooeo
Copy link
Contributor Author

Kivooeo commented Jul 2, 2025

(little off-topic but i have to say it) Thank you for the kind words! I also hope I’ll be able to return to working on Rust project. I still have some ambitious plans for reorganizing the tests that I wanted to finish this summer, but unfortunately I’ll have to postpone them until next year.

Regarding my previous message: I think it can serve as a good new starting point and help anyone interested in stabilizing this feature understand where things stand right now. I tried to summarize everything that’s currently relevant and that might help going forward.

Thanks again to everyone — I’ll definitely be back to work on Rust!

rust-bors bot added a commit that referenced this pull request Jul 3, 2025
add a scope for `if let` guard temporaries and bindings

This fixes my concern with `if let` guard drop order, namely that the guard's bindings and temporaries were being dropped after their arm's pattern's bindings, instead of before (#141295 (comment)). The guard's bindings and temporaries now live in a new scope, which extends until (but not past) the end of the arm, guaranteeing they're dropped before the arm's pattern's bindings. So far, this is the only way I've thought of to achieve this without explicitly rescheduling guards' drops to move them after the arm's pattern's.

I'm not sure this should be merged as-is. It's a little hacky and it introduces a new scope for *all* match arms rather than just those with `if let` guards. However, since I'm looking for feedback on the approach, I figured this is a relatively simple way to present it. As I mention in a FIXME comment, something like this will be needed for guard patterns (#129967) too[^1], so I think the final version should maybe only add these scopes as needed. That'll be better for perf too.

Tracking issue for `if_let_guard`: #51114

Tests are adapted from examples by `@traviscross,` `@est31,` and myself on #141295. cc, as I'd like your input on this.

I'm not entirely sure who to request for scoping changes, but let's start with r? `@Nadrieril` since this relates to guard patterns, we talked about it recently, and rustbot's going to ping you anyway. Feel free to reassign!

[^1]: e.g., new scopes are needed to keep failed guards inside `let` chain patterns from dropping existing bindings/temporaries; something like this could give a way of doing that without needing to reschedule drops. Unfortunately it may not help keep failed guards in `let` statement patterns from dropping the `let` statement's initializer, so it isn't a complete solution. I'm still not sure how to do that without rescheduling drops, changing how `let` statements' scopes work, or restricting the functionality of guard patterns in `let` statements (including `let`-`else`).
@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 87bedfc to 59125c0 Compare July 3, 2025 20:06
@rustbot

This comment was marked as off-topic.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 59125c0 to e7dd467 Compare July 3, 2025 20:08
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from e7dd467 to 48dbd58 Compare July 3, 2025 20:31
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 48dbd58 to a144613 Compare July 3, 2025 21:17
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from a144613 to 63476e5 Compare July 3, 2025 21:44
@rust-log-analyzer

This comment has been minimized.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 63476e5 to e34fd88 Compare July 3, 2025 23:55
@bors
Copy link
Collaborator

bors commented Jul 12, 2025

☔ The latest upstream changes (presumably #143766) made this pull request unmergeable. Please resolve the merge conflicts.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from e34fd88 to bde3195 Compare July 12, 2025 19:20
@apiraino apiraino removed the to-announce Announce this issue on triage meeting label Jul 24, 2025
@bors
Copy link
Collaborator

bors commented Jul 27, 2025

☔ The latest upstream changes (presumably #144526) made this pull request unmergeable. Please resolve the merge conflicts.

bors added a commit that referenced this pull request Aug 8, 2025
add a scope for `if let` guard temporaries and bindings

This fixes my concern with `if let` guard drop order, namely that the guard's bindings and temporaries were being dropped after their arm's pattern's bindings, instead of before (#141295 (comment)). The guard's bindings and temporaries now live in a new scope, which extends until (but not past) the end of the arm, guaranteeing they're dropped before the arm's pattern's bindings.

This only introduces a new scope for match arms with guards. Perf results (#143376 (comment)) seemed to indicate there wasn't a significant hit to introduce a new scope on all match arms, but guard patterns (#129967) will likely benefit from only adding new scopes when necessary (with some patterns requiring multiple nested scopes).

Tracking issue for `if_let_guard`: #51114

Tests are adapted from examples by `@traviscross,` `@est31,` and myself on #141295.
bors added a commit that referenced this pull request Aug 9, 2025
add a scope for `if let` guard temporaries and bindings

This fixes my concern with `if let` guard drop order, namely that the guard's bindings and temporaries were being dropped after their arm's pattern's bindings, instead of before (#141295 (comment)). The guard's bindings and temporaries now live in a new scope, which extends until (but not past) the end of the arm, guaranteeing they're dropped before the arm's pattern's bindings.

This only introduces a new scope for match arms with guards. Perf results (#143376 (comment)) seemed to indicate there wasn't a significant hit to introduce a new scope on all match arms, but guard patterns (#129967) will likely benefit from only adding new scopes when necessary (with some patterns requiring multiple nested scopes).

Tracking issue for `if_let_guard`: #51114

Tests are adapted from examples by `@traviscross,` `@est31,` and myself on #141295.
@dianne
Copy link
Contributor

dianne commented Aug 10, 2025

#143376 is merged now, so I believe the drop-order-bug-found-by-dianne concern can be resolved. cc @traviscross
I'm not totally sure how to go about guaranteeing test coverage for all corner cases, but we're closer now at least. The other unique/subtle scope/drop-related interaction I know if let guards have is with |s in the match arm's pattern. There's already quite a few tests for that, and as far as I can tell from reading the implementation it should work as expected. But only tests with if let guards over or-patterns will hit the relevant MIR-building codepath, so it's also worth considering if looking for corner cases.

And I'd be happy to pick up work on documenting if let guards and/or maintaining a stabilization PR if you're unable, @Kivooeo. I think you'd either need to give me permissions or I'd need to make my own PRs, but I'm interested in getting if_let_guard stabilized.

@jieyouxu
Copy link
Member

Before reattempting stabilization, should a call for testing period be conducted to invite contributors to explicitly try to break this feature? Especially with a focus on weird (or even outright wrong) drop order interactions, given the near miss.

@Kivooeo
Copy link
Contributor Author

Kivooeo commented Aug 10, 2025

Yes, if this is what we can easily do and have a lot of wishing to test this out then why not

About the documentation part, @dianne, yes you absolutely can open your own PR for documentation (I will close mine accordigly after that), but you might want to check mine if there anything valuable you can reuse (for examples syntax is correct)

As for this PR I could maintain this until October, hopefully we will get somewhere until this moment

And, feel free to ask anything to help when you will work on guard patterns in future!

@theemathas
Copy link
Contributor

This code compiles. Is it supposed to?

#![feature(if_let_guard)]

struct One(Two);
struct Two(Box<i32>);

fn main() {
    let a = One(Two(Box::new(1)));
    println!("{:p}", &a);
    match a {
        One(x) if let Two(ref y) = x => {
            // Using y here causes a compile error
            println!("{:p}", &x);
        }
        _ => {}
    }
}

@dianne
Copy link
Contributor

dianne commented Aug 11, 2025

I'd argue yes: y can be used later within the guard:

#![feature(if_let_guard)]

struct One(Two);
struct Two(Box<i32>);

fn main() {
    let a = One(Two(Box::new(1)));
    println!("{:p}", &a);
    match a {
        One(x) if let Two(ref y) = x
            // `y` is `&a.0` before `a.0` is moved into `x`
            && { println!("{y:p}"); true } =>
        {
            // Using y here causes a compile error
            println!("{:p}", &x);
        }
        _ => {}
    }
}

This is in essence the same as a guard like One(x) if guard(&x): guard runs before the binding is moved out of the scrutinee, so it sees the pre-move address.

@theemathas
Copy link
Contributor

This isn't necessarily wrong, but I think users will find this behavior surprising:

#![feature(if_let_guard)]

fn main() {
    match 1 {
        mut x if let y = &x => {
            x = 2;
            // prints 1
            println!("{y}");
        }
        _ => {}
    }
}

@dianne
Copy link
Contributor

dianne commented Aug 11, 2025

That is a bit funny.. it's consistent with stable non-if let guard behavior, but it's definitely easier to hit with an if let guard. I wonder if it'd be worth linting (maybe in clippy?) on guards that do certain things with references to by-mut-copy bindings. We could suggest making the binding non-mut and adding a separate let mut in the arm body, to make it clearer that the mutable x is a copy and y is not a reference to it.

For comparison, a normal guard that also exhibits this, which I'd want to behave the same as your if let guard:

fn main() {
    let y;
    match 1 {
        mut x if { y = &x; true } => {
            x = 2;
            // prints 1
            println!("{y}");
        }
        _ => {}
    }
}

@theemathas
Copy link
Contributor

theemathas commented Aug 11, 2025

This compiles. Is it supposed to?

#![feature(if_let_guard)]

struct Thing;

fn foo() -> &'static Thing {
    match Thing {
        x if let y = &x => {
            {
                // This should drop `x`, invalidating `y`.
                let a = x;
            }
            y
        }
        _ => {
            panic!();
        }
    }
}

See also #144939, which has similar behavior.

Edit: This seems to have similar behavior on normal if guards. I'm filing a separate issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. A-meta Area: Issues & PRs about the rust-lang/rust repository itself A-rustc-dev-guide Area: rustc-dev-guide A-testsuite Area: The testsuite used to check the correctness of rustc A-tidy Area: The tidy tool disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. S-blocked Status: Blocked on something else such as an RFC or other implementation work. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging S-waiting-on-team Status: Awaiting decision from the relevant subteam (see the T-<team> label). T-bootstrap Relevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap) T-infra Relevant to the infrastructure team, which will review and decide on the PR/issue. T-lang Relevant to the language team WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver)
Projects
None yet
Development

Successfully merging this pull request may close these issues.