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 3 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 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

All blocking issues have been resolved:


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, 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

@tmandry
Copy link
Member

tmandry commented May 28, 2025

I'm happy to stabilize this feature, and it looks ready now. A big thanks to @Kivooeo for the diligent and thorough stabilization work, and to @est31 for their mentorship.

@rfcbot reviewed

@rfcbot rfcbot added final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. and removed proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. labels May 28, 2025
@rfcbot
Copy link
Collaborator

rfcbot commented May 28, 2025

🔔 This is now entering its final comment period, as per the review above. 🔔

@traviscross traviscross added I-lang-radar Items that are on lang's radar and will need eventual work or consideration. and removed 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 28, 2025
Copy link
Member

@est31 est31 left a comment

Choose a reason for hiding this comment

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

some initial comments, will do final review once the FCP finishes.

@rustbot rustbot added the A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. label May 29, 2025
@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 29, 2025
@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from 79ba69e to eb65ac8 Compare May 29, 2025 04:56
@rustbot rustbot removed 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 29, 2025
@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from eb65ac8 to 520fe75 Compare May 29, 2025 04:57
@rustbot rustbot added 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 T-infra Relevant to the infrastructure team, which will review and decide on the PR/issue. WG-trait-system-refactor The Rustc Trait System Refactor Initiative (-Znext-solver) labels May 29, 2025
@rustbot

This comment was marked as resolved.

@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from b0364e5 to 520fe75 Compare May 29, 2025 11:00
@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch 2 times, most recently from 35bb20a to 11320a1 Compare May 29, 2025 11:26
@Kivooeo Kivooeo force-pushed the if-let-guard-stable branch from f70ba19 to 546a82c Compare May 29, 2025 11:45
Copy link
Member

Choose a reason for hiding this comment

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

I have some issues with how the stabilization report and the tests (especially related to this file) are presented.

I think one of the main reasons I was confused about the choice to stabilize let_chains in the 2024 edition was that it wasn't justified as clear as I wanted to. Having taken another look I figured putting it with my own words might be helpful:

let_chains was stabilized on the 2024 edition only, partly because #124085 is only on the 2024 edition. That specifically fixes the drop order of the following code: (play)

#![feature(if_let_guard)]
#![feature(let_chains)]
struct LoudDrop(&'static str);

impl LoudDrop {
    fn meow(&self) -> bool {
        true
    }
} 

impl Drop for LoudDrop {
    fn drop(&mut self) {
        print!("{}", self.0);
    }
}

fn main() {
    if let true = LoudDrop("1").meow()
            && let true = LoudDrop("2").meow()
            && let true = LoudDrop("3").meow()
            && let true = LoudDrop("4").meow()
            && 5 == 2 + 2 {
                
    } else {
        let _x = LoudDrop("5");
    }
    
    println!();
}

Before edition 2024, this code prints 54321. On edition 2024, this code prints 43215. Edition 2024 makes it so that the temporaries created as part of the chain gets dropped before we go to the else block instead of after the else block finishes. #104843 is extremely hard to get fixed before edition 2024, so it was proposed that let chains would only be usable on edition 2024.

Now, coming back to this feature. I was originally confused as to why the "drop order issue" only affected normal if let chains, not while let or if let guards. That's because drop order is only inconsistent when an else block exists. while lets do not have else blocks. if let guards on matches are clearly scoped to the arms. There is no "drop order MIR ICE issue" on these.

But this stabilization report, and the collection of tests should definitely be improved. Some sections, such as

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.

and

Unlike let chains in while and regular if the drop order in if let guard is predictable and stable across all editions and even if we writes let chains in if let guard it also works stable across all editions, this was tested by #140981 and compare-drop-order.rs

read like fluff to me. The relationship between drop-order.rs and compare-drop-order.rs was also confusing to me, before taking a closer look to realize that drop-order.rs compares the drop order temporaries as part of a normal if boolean versus an if let in a guard, whereas compare-drop-order.rs compares drop orders in an if let guard over an if let nested in the match.

Except it actually doesn't test the real issue. Here's a different test I wrote (play):

#![feature(if_let_guard)]
#![feature(let_chains)]
struct LoudDrop(&'static str);

impl LoudDrop {
    fn louder(&self) -> LoudDrop {
        Self(Box::leak(format!("({})", self.0).into_boxed_str()))
    }
} 

impl Drop for LoudDrop {
    fn drop(&mut self) {
        print!("{}", self.0);
    }
}

fn main() {
    match () {
        () if let Some(_a) = Some(LoudDrop("1").louder()) &&
        let Some(_b) = Some(LoudDrop("2").louder()) &&
        let 5 = 2 + 2 => (),

        () if let Some(_a) = Some(LoudDrop("3").louder()) &&
        let Some(_b) = Some(LoudDrop("4").louder()) => (),

        () => ()
    }
    
    println!();
    
    if let Some(_a) = Some(LoudDrop("1").louder()) &&
        let Some(_b) = Some(LoudDrop("2").louder()) && let 5 = 2+2 {
    }
    
    if let Some(_a) = Some(LoudDrop("3").louder()) &&
        let Some(_b) = Some(LoudDrop("4").louder()) {
    }

    println!();
}

On edition 2024 it prints

(2)2(1)1(4)4(3)3
(2)2(1)1(4)4(3)3

But before edition 2024 it prints

(2)2(1)1(4)4(3)3
(2)(1)21(4)(3)43

This is part of the issue with the drop order changes. These tests are not targeted enough for the subtlety at play with how consistent if let guard appears with let_chains. In fact, these tests do not test chains at all, and I was primarily concerned with how these components interact.

There are also several issues with the way some tests are described:

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

This is weird. I would normally write, "validates that if let guards are correctly treated in exhaustiveness checking"

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

The test is saying that a conditional move that happens in an if let guard does not affect other arms that try to use the same variable being moved. I wouldn't describe it as "false move errors or unsound behavior".

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

reads like fluff. warns.rs does not "ensure soundness". It checks that irrefutable guards and unreachable patterns in guards are correctly warned. (was there rationale and justification for not warning let guards that are irrefutable but bind to some variable? maybe w.r.t moves and stuff, unexplored) guard-mutability-2.rs is not about syntax rules it is about ownership/mutability.

I'm not sure how to proceed and perhaps this is a reason why writing stabilization reports are discouraged for people who are less familiar with the feature. Perhaps it would read better to me if it was rewritten or have the descriptions reworked, but I'm not sure who should be responsible for doing that. And I'm not actually sure if that is necessary given that people on lang already ticked their checkboxes.

Copy link
Contributor Author

@Kivooeo Kivooeo May 29, 2025

Choose a reason for hiding this comment

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

i've asked here in this thread about mir tests, i got clear answer that current tests are fine and we dont need more, i did compare mir during pre stabilization to remove some concerns about this, and again, ive asked about adding mir tests here, got answer that we dont need this

compare-drop-order.rs was originally created to show that there is no difference between if let inside match arm and if let guard, to show that they are the same in terms of drop order, this concerns was from @Nadrieril. this test wasnt mean to test let chains inside if let guard, because there is already a test for it #140981

i agree about not best description

reads like fluff. warns.rs does not "ensure soundness". It checks that irrefutable guards and unreachable patterns in guards are correctly warned. (was there rationale and justification for not warning let guards that are irrefutable but bind to some variable? maybe w.r.t moves and stuff, unexplored) guard-mutability-2.rs is not about syntax rules it is about ownership/mutability.

except this part, this is overview of all this tests, it's not a describe of each test, it doesnt mean that warns.rs shows exatcly this and only need for "ensure soundness", i said about this word for word about this test little bit before this part

The compiler correctly diagnoses irrefutable patterns, unreachable patterns

also, i want to acknowledge that a big part of the problem here is simply my inexperience, this is my first time writing a stabilization report, i’ve tried to do my best here, i read many past stabilization PRs before drafting this one, but since there’s no standardized template for stabilization PRs, some parts (especially the descriptions) were challenging

id love to improve this and learn the best practices from those with more experience, im definitely open to questions or suggestions for clarifying parts of the report beyond the points you already mentioned, which i really appreciate

Copy link
Member

Choose a reason for hiding this comment

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

reads like fluff

Yeah @Kivooeo 's writings read a bit as if they were LLM enhanced, which sometimes make them less pleasant to read. However that shouldn't itself be a reason to reject the stabilization proposal itself. the intent, covered areas, content, etc of course can always see improvements, but I'd say it's enough for a stabilization report. One needs to give people room to grow and improve, and @Kivooeo has demonstrated willingness of that.

Except it actually doesn't test the real issue. Here's a different test I wrote [...]

Such a comparison is included via #140981 / dcd2736, namely in drop-order-comparisons.rs where:

  • t_guard_if_let_chains_then_else is a clone of the edition 2024 variant of t_if_let_chains_then_else, but it's edition independent so it demonstrates that the drop order is the same across all editions.
  • same for t_guard_if_let_chains_then and t_if_let_chains_then: we see that edition 2024 drop order prevails.

Note that @Kivooeo linked #140981 in the snippet you were quoting, so the link to a "complicated" drop order test was there.

I was originally confused as to why the "drop order issue" only affected normal if let chains, not while let or if let guards. That's because drop order is only inconsistent when an else block exists. while lets do not have else blocks. if let guards on matches are clearly scoped to the arms. There is no "drop order MIR ICE issue" on these.

As you can see with t_if_let_chains_then, and the example you gave, if let chains do have differing drop order on different editions, even if there is no else block. I have made statements in the past, esp in #140204, that indicate that the absence of an else is enough to get 2024 drop order also on edition 2021, but my understanding has evolved since then. I was wrong, I'm sorry. Please don't get confused by them.

What stopped #140204 from progressing is that the ICE and compilation error inconsistency persists with while let chains, which are the reasons for why we can't stabilize if let chains on 2021. If you look at #140981, it adds tests that show that the match guard variants of those specific examples work both on edition 2021 and edition 2024. I can't find a single test that shows any if let guards edition inconsistency wrt drop order or scoping. So that's why I said it's fine to stabilize them, esp with the experience behind me of all the weird drop order edge cases encountered with if let.

Also, if you take a look, while let is affected by the drop order rescoping. that example has while let give the same output as if let, i.e. edition inconsistent, while if let guard chains are edition consistent and match edition 2024 behaviour of if let and while let. Maybe a possible resolution of #140204 is to apply edition independent drop order rescoping for while. If that doesn't break anyone (as while doesn't have an else), we can roll it out to get while let chains stable across editions. IDK, it needs further experiments.

I hope I could make it a little bit more clear why I think that it's fine to stabilize if let guards on 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.

i updated description, please take a look, it should be a way better now, more straightforward and clear to readers

i also wondering, if i should mention such case, because i said earlier that regular if let and if let guard are the same in terms of drop order, which is true when we are not using let chains inside, for example it wont be true that drop order is the same here for all editions if i understand that correctly, but not sure if i should mention this in description or write a test for it?

match Some(2) {
    Some(x)
        if let Some(y) = Some(3)
            && y > 5
            && x > 5 => {}
    _ => (),
}


match Some(2) {
    Some(x) => {
        if let Some(y) = Some(3)
            && y > 5
            && x > 5
        {}
    }
    _ => (),
}

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 disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. final-comment-period In the final comment period and will be merged soon unless new substantive objections are raised. 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. 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-infra Relevant to the infrastructure 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. 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.