Skip to content
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

eRFC: if- and while-let-chains, take 2 #2497

Merged
merged 15 commits into from
Aug 24, 2018

Conversation

Centril
Copy link
Contributor

@Centril Centril commented Jul 13, 2018

πŸ–ΌοΈ Rendered

⏭ Tracking issue - Main

⏭ Tracking issue - Edition transitioning

πŸ“ Summary

Extends if let and while let-expressions with chaining, allowing you to combine multiple lets and bool-typed conditions together naturally. After implementing this RFC, you'll be able to write, among other things:

fn param_env<'a, 'tcx>(tcx: TyCtxt<'a, 'tcx, 'tcx>, def_id: DefId) -> ParamEnv<'tcx> {
    if let Some(Def::Existential(_)) = tcx.describe_def(def_id)
        && let Some(node_id) = tcx.hir.as_local_node_id(def_id)
        && let hir::map::NodeItem(item) = tcx.hir.get(node_id)
        && let hir::ItemExistential(ref exist_ty) = item.node
        && let Some(parent) = exist_ty.impl_trait_fn
    {
        return param_env(tcx, parent);
    }

    ...
}

The main aim of this RFC is to decide that this is a problem worth solving as well as discussing a few available options. Most importantly, we want to make let PAT = EXPR && .. a possible option for Rust 2018.

πŸ’– Thanks

To everyone who participated in RFC #2260 and to the survey participants.
To @scottmcm for collaborating with me on the original RFC.
To @aturon and @nikomatsakis for taking the time to discuss this with me.
To @SergioBenitez for checking the feasibility of this in Rust 2018.
To @oli-obk for providing me with the useful example in the summary ;)
To @joshtriplett for the consultation on the style.
To @kennytm for improving clarity on the Rust 2018 migration technical changes.

@Centril Centril added T-lang Relevant to the language team, which will review and decide on the RFC. I-nominated labels Jul 13, 2018
text/0000-if-let-chains.md Outdated Show resolved Hide resolved
@SergioBenitez
Copy link
Contributor

I am very for this change. This syntax feels natural and intuitive, and it is something I've pined for.

My remarks are:

  1. While I have often wanted to reach for if let chains, I've never instinctively wanted while let chains. As such, personally, I do not see while let chaining as a necessary addition.
  2. With somewhat less frequency than && chaining, I have also reached for || chaining in if let clauses. In particular, in combination with &&:
    if let A(x) = foo() || (let B(x) == bar() && x.is_this()) {
        // use x
    }
    Again, this construction and syntax feel natural to me, and I would consider it if let chaining without || to be inconsistent with the rest of the language. That being said, I'd rather have && chaining alone than nothing at all.

@Centril
Copy link
Contributor Author

Centril commented Jul 13, 2018

@SergioBenitez

I've never instinctively wanted while let chains. As such, personally, I do not see while let chaining as a necessary addition.

Thank you for the data point and the remarks.

I think the primary motivation for also changing while let is consistency with if let so as to reduce surprises for users. Uniformity is our friend in making teaching and learning easier :)

That said, if we consider iterating over multiple sources that produce elements in a zip-like fashion (but not strictly .zip(..) for iterators), then I think chaining while let can be useful. It can also be useful to chain while let with a side-condition as in while let Foo(bar) && bar.is_special() { .. }.

Again, this construction and syntax feel natural to me, and I would consider it if let chaining without || to be inconsistent with the rest of the language. That being said, I'd rather have && chaining alone than nothing at all.

I'm personally undecided on adding || here. What this RFC does do is to make || a possible addition in Rust 2018, if we so choose. For now, due to the time constraints of shipping the edition, I'd like to punt on adding || to the language to a future RFC and discussions.

text/0000-if-let-chains.md Outdated Show resolved Hide resolved
text/0000-if-let-chains.md Outdated Show resolved Hide resolved
@Centril
Copy link
Contributor Author

Centril commented Jul 13, 2018

@aturon has asked me to clarify what exactly is being proposed to be accepted with this RFC. So here goes...

The precedent set by closing RFCs #2443, #2441, and #2429 is that we don't accept RFCs reserving syntax without accepting a feature along with it. Therefore, to be consistent with this policy, and for the purposes of making if let PAT = EXPR && ... a possible syntax in Rust 2018, we need also to accept a feature along with the reservation of this syntax in Rust 2018.

So the idea with this RFC is that we accept the full feature now (the idea of chaining...), but leave finalizing the syntax itself unresolved-ish as we did with #2071 (existential type Foo: Bar; - unlike 2071, this RFC does not suggest a temporary syntax however). This will leave us room both to make the changes necessary in terms of the lint in Rust 2015 as well as the hard error in Rust 2018.

After the Rust 2018 has shipped, and when we have time, we can then implement the proposed syntax and experiment with it on nightly. The syntax will still have to be finalized by some other decision, such as with another RFC or on the tracking issue.

text/0000-if-let-chains.md Outdated Show resolved Hide resolved
text/0000-if-let-chains.md Outdated Show resolved Hide resolved
text/0000-if-let-chains.md Outdated Show resolved Hide resolved
text/0000-if-let-chains.md Outdated Show resolved Hide resolved
text/0000-if-let-chains.md Outdated Show resolved Hide resolved
text/0000-if-let-chains.md Outdated Show resolved Hide resolved
@petrochenkov
Copy link
Contributor

petrochenkov commented Jul 13, 2018

My opinion hasn't changed since #2260 (comment).
I still think this is a half-assed solution that will likely block a proper solution in the future (for social rather than technical reasons).

We should aim for supporting a convenient non-exhaustive pattern-matching expressions in general (EXPR op PAT: bool) rather than special case if and while, because it solves much more issues than chaining alone.

Furthermore, as evidenced in RFC 2260, making EXPR is PAT, which has other problems we've previously noted, an expression is also tricky due to the non-obvious scoping rules for bindings it entails. Mainly because of this, support for EXPR is PAT has been slow to develop.

That's just not true, for expressions in if and while the rules can be exactly the same as in this RFC, and outside of if/while these expressions can desugar into if expr { true } else { false } (at least this is how it's done in my implementation).

If "slow to develop" refers to my promise to write an RFC for pattern-matching expressions
based on my implementation (#2411 (comment)), then the reason is that I was busy with more urgent things and deferred this work until 2018 edition completion.
Sigh, logistically this RFC is perfectly timed, since I still can't write a counter-RFC right now or spend too much time arguing.

@aturon
Copy link
Member

aturon commented Jul 13, 2018

@petrochenkov Totally understood -- did you see @Centril's clarifying comment? My understanding is that this RFC is meant to commit us to (1) some solution to this problem and (2) carving out a bit of space in the grammar so that the solution could be as discussed in the RFC.

FWIW, I also have significant misgivings about solving this problem by essentially extending the special-cased treatment of if let, and would prefer something more compositional. But I am in favor of the commitments being proposed.

@Centril I wonder if this should be made formally an "eRFC" to signify more clearly the issues above?

@petrochenkov
Copy link
Contributor

@aturon
Oh, if this is actually about reserving if let PAT = EXPR1 && EXPR2 as if (let PAT = EXPR1) && EXPR2 in the grammar, then I'm on board πŸ˜„
I'm just worrying that the accepted version will become set in stone eventually.

@kennytm
Copy link
Member

kennytm commented Jul 13, 2018

These operators all have lower precedence than &&:

  • ||
  • .., ..=
  • =, +=, etc
  • return, break

Which means we also have the following ambiguity to consider:

    if let Range { start: _, end: false } = true..true && false {
        println!("(current behavior)");
    } else {
        panic!();
    }
    let result = loop {
        if let Some(0u32) = break true && false {
            panic!();
        }
        panic!();
    };
    assert_eq!(result, false);

And then || and && can be interpreted prefix operators:

    const F: fn() -> bool = || true;
    if let Range { start: F, end } = F..|| false {
        println!("(current behavior)");
        assert!(!end());
    } else {
        panic!();
    }
    let t = &&true;
    if let Range { start: true, end } = t..&&false {
        println!("(current behavior)");
        assert_eq!(**end, false);
    } else {
        panic!();
    }

Please also ensure the parser accepts the following:

if a || b {}   // <-- no parenthesis, it is ok.
if a && b || c && d {}
// interpret as: `(a && b) || (c && d)`

and rejects the following before if-let-or is supported:

if let Some(a) = b && c || d {}
// interpret as: `((let Some(a) = b) && c) || d`

@Centril Centril changed the title RFC: if- and while-let-chains, take 2 eRFC: if- and while-let-chains, take 2 Jul 13, 2018
@Centril
Copy link
Contributor Author

Centril commented Jul 13, 2018

@aturon eRFC it is ;)

@petrochenkov Yep, it's about making the proposed solution possible and recognizing that some solution (which could be EXPR match PAT) is necessary.

I would personally be fine with experimenting with if EXPR match PAT && .. and if let PAT = EXPR && .. concurrently in the nightly compiler and see which one comes out on top.

That's just not true, for expressions in if and while the rules can be exactly the same as in this RFC, and outside of if/while these expressions can desugar into if expr { true } else { false } (at least this is how it's done in my implementation).

I see; my impression was that it was more complicated from the discussions at #2260. This is the "bindings only live until the end of a full expression" rule?

If "slow to develop" refers to my promise to write an RFC for pattern-matching expressions
based on my implementation (#2411 (comment)),

Not at all :) It is referring to my perception of the support for EXPR op PAT within the community and the lang team (which could be wrong, but the survey is at least quite suggestive as to the community support).

then the reason is that I was busy with more urgent things and deferred this work until 2018 edition completion.

I thank you for it ❀️

@joshtriplett
Copy link
Member

I can't say I'm a huge fan of the specific proposed syntax, but I'm inclined to take this step regardless, so that even if we don't end up going with this particular syntax we can detect potentially ambiguous uses of it. I'm honestly tempted to argue that either grouping should require parentheses.

Minor nit: for consistency, can you please always put && at the start of a line, rather than at the end? In addition to matching the standard style/rustfmt, I also think that more clearly distinguishes the && from the expression on the right-hand side of the let.

Also, in the example of how while let would translate to a loop, you have unbalanced {s at the ends of several lines.

@Centril Centril self-assigned this Jul 13, 2018
@Centril Centril added A-syntax Syntax related proposals & ideas A-control-flow Proposals relating to control flow. labels Nov 23, 2018
@H2CO3
Copy link

H2CO3 commented Dec 24, 2018

@Centril I somehow couldn't find in this RFC if the sudden precedence change of the && and || operators always happens in the control expression of if and while, or only when there's at least one let binding? And if there are any more changes to operator precedence which have unintended consequences? The text of the rendered proposal says:

To be precise, the changes in this RFC entail that || has the lowest precedence at the top level of if STUFF { .. }. The operator && has then the 2nd lowest precedence and binds more tightly than ||. If the user wants to disambiguate, they can write (EXPR && EXPR) or { EXPR && EXPR } explicitly.

The phrasing here is not particularly clear (e.g. what is STUFF? what exactly does the user want to disambiguate?). To me it kind of suggests that the precedence change always happens and that from now on, parentheses will be basically obligatory around the top-level (non-let) expression. I'm also confused because I don't understand why the latter change is necessary (and if it is, I would find it very annoying).

@Centril
Copy link
Contributor Author

Centril commented Dec 24, 2018

@H2CO3

@Centril I somehow couldn't find in this RFC if the sudden precedence change of the && and || operators always happens in the control expression of if and while, or only when there's at least one let binding?

The precedence change is to bind && more loosely than let pat = expr in if $stuff { ... } so by definition it only applies if there's at least one let binding. In other words, let pat = expr becomes a sort of pseudo-expression and it has a higher precedence than &&.

The phrasing here is not particularly clear (e.g. what is STUFF? what exactly does the user want to disambiguate?). To me it kind of suggests that the precedence change always happens and that from now on, parentheses will be basically obligatory around the top-level (non-let) expression. I'm also confused because I don't understand why the latter change is necessary (and if it is, I would find it very annoying).

I agree; it wasn't my finest phrasing. These days I think it's better to think of this in terms of what happens to let pat = expr rather than what happens to &&. To be clear, if let p = q && r { ... } will be legal and associate as if (let p = q) && r { ... } and if (let p = q) && r { ... } will also be legal. Parenthesis won't be required unless you want to disambiguate if let p = q && r { ... } to if let p = (q && r) { ... } (which is a weird thing to write...).

@H2CO3
Copy link

H2CO3 commented Dec 24, 2018

@Centril Thanks for the explanation!

@Centril
Copy link
Contributor Author

Centril commented Dec 24, 2018

@H2CO3 Welcome :)

@hadronized
Copy link
Contributor

Argh, I didn’t see that RFC and I wish I did… I’m not a huge fan of the proposal, it seems too niche and introduces too much sugar (mixes two very different constructs β€” if _, and Β« if_let _ Β», blending _: bool with _: ??? ). But the RFC is merged, so ignore this comment IΒ guess. 😞

@Centril
Copy link
Contributor Author

Centril commented Jan 30, 2019

This is definitely not niche; in particular, this pattern arises frequently in the compiler (similar code like proc macros, clippy, anything that deals with AST-looking trees) and will pay for itself many times over. if and if let are not very different constructs; in particular, when let pat = expr is considered as an expression itself, then the distinction between these constructs evaporate completely making the language conceptually simpler in the process.

@hadronized
Copy link
Contributor

Yes, but let pat = expr can be considered an expression ONLY when it implies destructuring, otherwise it doesn’t make any sense (as it’s a statetement). With what you say, I would expect this to be valid:

let x = Some("foo");
let y = true || let Some(foo) = x;

Which is definitely not part of the language (and shouldn’t, as introducing bindings in expressions seems completely out of question).

So, yeah, maybe you’re introducing β€œconceptually simpler” sugar for very specific cases (again, IΒ truly think it’s niche, and you put on a name on that niche: compilers and ASTs), but you’re also introducing confusion pretty much everywhere else where that β€œconceptually simpler” concept would feel applicable (while it’s not).

@hadronized
Copy link
Contributor

hadronized commented Jan 30, 2019

I would also like to mention that if let pat = expr { _1 } else { _2 } can be seen as a shortcut to match pat { expr => { _1 }, _ => { _1 } }. So to me, it’s not very the same construct as a single if, which is a simple instruction that is pretty simple to understand.

@Nemo157
Copy link
Member

Nemo157 commented Jan 31, 2019

Just yesterday I wanted this feature while changing the MPSC channels in futures, that's very much not the compilers/ASTs niche. If you really want I'm sure I can find some more examples from other non-compiler/AST projects I have worked on.

@hadronized
Copy link
Contributor

In the code you showed, you could have used a guarded match. That’s already part of the language and it will do the job for most cases. Having lazy pattern-matching mixed with booleans makes me incomfortable. I’m not saying I’m against lazy pattern-matching (it’s actually handy). I’m just saying that using the if keyword for that is not a good move. But as I said yesterday, the RFC is already merged and we’re just bikeshedding. Tant pis pour moi

@kennytm kennytm mentioned this pull request Aug 23, 2021
JohnTitor added a commit to JohnTitor/rust that referenced this pull request Jul 17, 2022
…shtriplett

Stabilize `let_chains` in Rust 1.64

# Stabilization proposal

This PR proposes the stabilization of `#![feature(let_chains)]` in a future-compatibility way that will allow the **possible** addition of the `EXPR is PAT` syntax.

Tracking issue: rust-lang#53667
Version: 1.64 (beta => 2022-08-11, stable => 2022-10-22).

## What is stabilized

The ability to chain let expressions along side local variable declarations or ordinary conditional expressions. For example:

```rust
pub enum Color {
    Blue,
    Red,
    Violet,
}

pub enum Flower {
    Rose,
    Tulip,
    Violet,
}

pub fn roses_are_red_violets_are_blue_printer(
    (first_flower, first_flower_color): (Flower, Color),
    (second_flower, second_flower_color): (Flower, Color),
    pick_up_lines: &[&str],
) {
    if let Flower::Rose = first_flower
        && let Color::Red = first_flower_color
        && let Flower::Violet = second_flower
        && let Color::Blue = second_flower_color
        && let &[first_pick_up_line, ..] = pick_up_lines
    {
        println!("Roses are red, violets are blue, {}", first_pick_up_line);
    }
}

fn main() {
    roses_are_red_violets_are_blue_printer(
        (Flower::Rose, Color::Red),
        (Flower::Violet, Color::Blue),
        &["sugar is sweet and so are you"],
    );
}
```

## Motivation

The main motivation for this feature is improving readability, ergonomics and reducing paper cuts.

For more examples, see the [RFC](https://github.com/rust-lang/rfcs/blob/master/text/2497-if-let-chains.md).

## What isn't stabilized

* Let chains in match guards (`if_let_guard`)

* Resolution of divergent non-terminal matchers

* The `EXPR is PAT` syntax

## History

* On 2017-12-24, [RFC: if- and while-let-chains](rust-lang/rfcs#2260)
* On 2018-07-12, [eRFC: if- and while-let-chains, take 2](rust-lang/rfcs#2497)
* On 2018-08-24, [Tracking issue for eRFC 2497, "if- and while-let-chains, take 2](rust-lang#53667)
* On 2019-03-19, [Run branch cleanup after copy prop](rust-lang#59290)
* On 2019-03-26, [Generalize diagnostic for x = y where bool is the expected type](rust-lang#59439)
* On 2019-04-24, [Introduce hir::ExprKind::Use and employ in for loop desugaring](rust-lang#60225)
* On 2019-03-19, [[let_chains, 1/6] Remove hir::ExprKind::If](rust-lang#59288)
* On 2019-05-15, [[let_chains, 2/6] Introduce Let(..) in AST, remove IfLet + WhileLet and parse let chains](rust-lang#60861)
* On 2019-06-20, [[let_chains, 3/6] And then there was only Loop](rust-lang#61988)
* On 2020-11-22, [Reintroduce hir::ExprKind::If](rust-lang#79328)
* On 2020-12-24, [Introduce hir::ExprKind::Let - Take 2](rust-lang#80357)
* On 2021-02-19, [Lower condition of if expression before it's "then" block](rust-lang#82308)
* On 2021-09-01, [Fix drop handling for `if let` expressions](rust-lang#88572)
* On 2021-09-04, [Formally implement let chains](rust-lang#88642)
* On 2022-01-19, [Add tests to ensure that let_chains works with if_let_guard](rust-lang#93086)
* On 2022-01-18, [Introduce `enhanced_binary_op` feature](rust-lang#93049)
* On 2022-01-22, [Fix `let_chains` and `if_let_guard` feature flags](rust-lang#93213)
* On 2022-02-25, [Initiate the inner usage of `let_chains`](rust-lang#94376)
* On 2022-01-28, [[WIP] Introduce ast::StmtKind::LetElse to allow the usage of `let_else` with `let_chains`](rust-lang#93437)
* On 2022-02-26, [1 - Make more use of `let_chains`](rust-lang#94396)
* On 2022-02-26, [2 - Make more use of `let_chains`](rust-lang#94400)
* On 2022-02-27, [3 - Make more use of `let_chains`](rust-lang#94420)
* On 2022-02-28, [4 - Make more use of `let_chains`](rust-lang#94445)
* On 2022-02-28, [5 - Make more use of `let_chains`](rust-lang#94448)
* On 2022-02-28, [6 - Make more use of `let_chains`](rust-lang#94465)
* On 2022-03-01, [7 - Make more use of `let_chains`](rust-lang#94476)
* On 2022-03-01, [8 - Make more use of `let_chains`](rust-lang#94484)
* On 2022-03-01, [9 - Make more use of `let_chains`](rust-lang#94498)
* On 2022-03-08, [Warn users about `||` in let chain expressions](rust-lang#94754)

From the first RFC (2017-12-24) to the theoretical future stabilization day (2022-10-22), it can be said that this feature took 4 years, 9 months and 28 days of research, development, discussions, agreements and headaches to be settled.

## Divergent non-terminal matchers

More specifically, rust-lang#86730.

```rust
macro_rules! mac {
    ($e:expr) => {
        if $e {
            true
        } else {
            false
        }
    };
}

fn main() {
    // OK!
    assert_eq!(mac!(true && let 1 = 1), true);

    // ERROR! Anything starting with `let` is not considered an expression
    assert_eq!(mac!(let 1 = 1 && true), true);
}
```

To the best of my knowledge, such error or divergence is orthogonal, does not prevent stabilization and can be tackled independently in the near future or effectively in the next Rust 2024 edition. If not, then https://github.com/c410-f3r/rust/tree/let-macro-blah contains a set of changes that will consider `let` an expression.

It is possible that none of the solutions above satisfies all applicable constraints but I personally don't know of any other plausible answers.

## Alternative syntax

Taking into account the usefulness of this feature and the overwhelming desire to use both now and in the past, `let PAT = EXPR` will be utilized for stabilization but it doesn't or shall create any obstacle for a **possible** future addition of `EXPR is PAT`.

The introductory snippet would then be written as the following.

```rust
if first_flower is Flower::Rose
    && first_flower_color is Color::Red
    && second_flower is Flower::Violet
    && second_flower_color is Color::Blue
    && pick_up_lines is &[first_pick_up_line, ..]
{
    println!("Roses are red, violets are blue, {}", first_pick_up_line);
}
```

Just to reinforce, this PR only unblocks a **possible** future road for `EXPR is PAT` and does emphasize what is better or what is worse.

## Tests

* [Verifies the drop order of let chains and ensures it won't change in the future in an unpredictable way](https://github.com/rust-lang/rust/blob/master/src/test/ui/mir/mir_let_chains_drop_order.rs)

* [AST lowering does not wrap let chains in an `DropTemps` expression](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/ast-lowering-does-not-wrap-let-chains.rs)

* [Checks pretty printing output](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/ast-pretty-check.rs)

* [Verifies uninitialized variables due to MIR modifications](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/chains-without-let.rs)

* [A collection of statements where `let` expressions are forbidden](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/disallowed-positions.rs)

* [All or at least most of the places where let chains are allowed](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/feature-gate.rs)

* [Ensures that irrefutable lets are allowed in let chains](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/irrefutable-lets.rs)

* [issue-88498.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/issue-88498.rs), [issue-90722.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/issue-90722.rs), [issue-92145.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/issue-92145.rs) and [issue-93150.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/issue-93150.rs) were bugs found by third parties and fixed overtime.

* [Indexing was triggering a ICE due to a wrongly constructed MIR graph](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/no-double-assigments.rs)

* [Protects the precedence of `&&` in relation to other things](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/protect-precedences.rs)

* [`let_chains`, as well as `if_let_guard`, has a valid MIR graph that evaluates conditional expressions correctly](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/then-else-blocks.rs)

Most of the infra-structure used by let chains is also used by `if` expressions in stable compiler versions since rust-lang#80357 and rust-lang#88572. As a result, no bugs were found since the integration of rust-lang#88642.

## Possible future work

* Let chains in match guards is implemented and working but stabilization is blocked by `if_let_guard`.

* The usage of `let_chains` with `let_else` is possible but not implemented. Regardless, one attempt was introduced and closed in rust-lang#93437.

Thanks `@Centril` for creating the RFC and huge thanks (again) to `@matthewjasper` for all the reviews, mentoring and MIR implementations.

Fixes rust-lang#53667
workingjubilee pushed a commit to tcdi/postgrestd that referenced this pull request Sep 15, 2022
Stabilize `let_chains` in Rust 1.64

# Stabilization proposal

This PR proposes the stabilization of `#![feature(let_chains)]` in a future-compatibility way that will allow the **possible** addition of the `EXPR is PAT` syntax.

Tracking issue: #53667
Version: 1.64 (beta => 2022-08-11, stable => 2022-10-22).

## What is stabilized

The ability to chain let expressions along side local variable declarations or ordinary conditional expressions. For example:

```rust
pub enum Color {
    Blue,
    Red,
    Violet,
}

pub enum Flower {
    Rose,
    Tulip,
    Violet,
}

pub fn roses_are_red_violets_are_blue_printer(
    (first_flower, first_flower_color): (Flower, Color),
    (second_flower, second_flower_color): (Flower, Color),
    pick_up_lines: &[&str],
) {
    if let Flower::Rose = first_flower
        && let Color::Red = first_flower_color
        && let Flower::Violet = second_flower
        && let Color::Blue = second_flower_color
        && let &[first_pick_up_line, ..] = pick_up_lines
    {
        println!("Roses are red, violets are blue, {}", first_pick_up_line);
    }
}

fn main() {
    roses_are_red_violets_are_blue_printer(
        (Flower::Rose, Color::Red),
        (Flower::Violet, Color::Blue),
        &["sugar is sweet and so are you"],
    );
}
```

## Motivation

The main motivation for this feature is improving readability, ergonomics and reducing paper cuts.

For more examples, see the [RFC](https://github.com/rust-lang/rfcs/blob/master/text/2497-if-let-chains.md).

## What isn't stabilized

* Let chains in match guards (`if_let_guard`)

* Resolution of divergent non-terminal matchers

* The `EXPR is PAT` syntax

## History

* On 2017-12-24, [RFC: if- and while-let-chains](rust-lang/rfcs#2260)
* On 2018-07-12, [eRFC: if- and while-let-chains, take 2](rust-lang/rfcs#2497)
* On 2018-08-24, [Tracking issue for eRFC 2497, "if- and while-let-chains, take 2](rust-lang/rust#53667)
* On 2019-03-19, [Run branch cleanup after copy prop](rust-lang/rust#59290)
* On 2019-03-26, [Generalize diagnostic for x = y where bool is the expected type](rust-lang/rust#59439)
* On 2019-04-24, [Introduce hir::ExprKind::Use and employ in for loop desugaring](rust-lang/rust#60225)
* On 2019-03-19, [[let_chains, 1/6] Remove hir::ExprKind::If](rust-lang/rust#59288)
* On 2019-05-15, [[let_chains, 2/6] Introduce Let(..) in AST, remove IfLet + WhileLet and parse let chains](rust-lang/rust#60861)
* On 2019-06-20, [[let_chains, 3/6] And then there was only Loop](rust-lang/rust#61988)
* On 2020-11-22, [Reintroduce hir::ExprKind::If](rust-lang/rust#79328)
* On 2020-12-24, [Introduce hir::ExprKind::Let - Take 2](rust-lang/rust#80357)
* On 2021-02-19, [Lower condition of if expression before it's "then" block](rust-lang/rust#82308)
* On 2021-09-01, [Fix drop handling for `if let` expressions](rust-lang/rust#88572)
* On 2021-09-04, [Formally implement let chains](rust-lang/rust#88642)
* On 2022-01-19, [Add tests to ensure that let_chains works with if_let_guard](rust-lang/rust#93086)
* On 2022-01-18, [Introduce `enhanced_binary_op` feature](rust-lang/rust#93049)
* On 2022-01-22, [Fix `let_chains` and `if_let_guard` feature flags](rust-lang/rust#93213)
* On 2022-02-25, [Initiate the inner usage of `let_chains`](rust-lang/rust#94376)
* On 2022-01-28, [[WIP] Introduce ast::StmtKind::LetElse to allow the usage of `let_else` with `let_chains`](rust-lang/rust#93437)
* On 2022-02-26, [1 - Make more use of `let_chains`](rust-lang/rust#94396)
* On 2022-02-26, [2 - Make more use of `let_chains`](rust-lang/rust#94400)
* On 2022-02-27, [3 - Make more use of `let_chains`](rust-lang/rust#94420)
* On 2022-02-28, [4 - Make more use of `let_chains`](rust-lang/rust#94445)
* On 2022-02-28, [5 - Make more use of `let_chains`](rust-lang/rust#94448)
* On 2022-02-28, [6 - Make more use of `let_chains`](rust-lang/rust#94465)
* On 2022-03-01, [7 - Make more use of `let_chains`](rust-lang/rust#94476)
* On 2022-03-01, [8 - Make more use of `let_chains`](rust-lang/rust#94484)
* On 2022-03-01, [9 - Make more use of `let_chains`](rust-lang/rust#94498)
* On 2022-03-08, [Warn users about `||` in let chain expressions](rust-lang/rust#94754)

From the first RFC (2017-12-24) to the theoretical future stabilization day (2022-10-22), it can be said that this feature took 4 years, 9 months and 28 days of research, development, discussions, agreements and headaches to be settled.

## Divergent non-terminal matchers

More specifically, rust-lang/rust#86730.

```rust
macro_rules! mac {
    ($e:expr) => {
        if $e {
            true
        } else {
            false
        }
    };
}

fn main() {
    // OK!
    assert_eq!(mac!(true && let 1 = 1), true);

    // ERROR! Anything starting with `let` is not considered an expression
    assert_eq!(mac!(let 1 = 1 && true), true);
}
```

To the best of my knowledge, such error or divergence is orthogonal, does not prevent stabilization and can be tackled independently in the near future or effectively in the next Rust 2024 edition. If not, then https://github.com/c410-f3r/rust/tree/let-macro-blah contains a set of changes that will consider `let` an expression.

It is possible that none of the solutions above satisfies all applicable constraints but I personally don't know of any other plausible answers.

## Alternative syntax

Taking into account the usefulness of this feature and the overwhelming desire to use both now and in the past, `let PAT = EXPR` will be utilized for stabilization but it doesn't or shall create any obstacle for a **possible** future addition of `EXPR is PAT`.

The introductory snippet would then be written as the following.

```rust
if first_flower is Flower::Rose
    && first_flower_color is Color::Red
    && second_flower is Flower::Violet
    && second_flower_color is Color::Blue
    && pick_up_lines is &[first_pick_up_line, ..]
{
    println!("Roses are red, violets are blue, {}", first_pick_up_line);
}
```

Just to reinforce, this PR only unblocks a **possible** future road for `EXPR is PAT` and does emphasize what is better or what is worse.

## Tests

* [Verifies the drop order of let chains and ensures it won't change in the future in an unpredictable way](https://github.com/rust-lang/rust/blob/master/src/test/ui/mir/mir_let_chains_drop_order.rs)

* [AST lowering does not wrap let chains in an `DropTemps` expression](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/ast-lowering-does-not-wrap-let-chains.rs)

* [Checks pretty printing output](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/ast-pretty-check.rs)

* [Verifies uninitialized variables due to MIR modifications](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/chains-without-let.rs)

* [A collection of statements where `let` expressions are forbidden](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/disallowed-positions.rs)

* [All or at least most of the places where let chains are allowed](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/feature-gate.rs)

* [Ensures that irrefutable lets are allowed in let chains](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/irrefutable-lets.rs)

* [issue-88498.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/issue-88498.rs), [issue-90722.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/issue-90722.rs), [issue-92145.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/issue-92145.rs) and [issue-93150.rs](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/issue-93150.rs) were bugs found by third parties and fixed overtime.

* [Indexing was triggering a ICE due to a wrongly constructed MIR graph](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/no-double-assigments.rs)

* [Protects the precedence of `&&` in relation to other things](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/protect-precedences.rs)

* [`let_chains`, as well as `if_let_guard`, has a valid MIR graph that evaluates conditional expressions correctly](https://github.com/rust-lang/rust/blob/master/src/test/ui/rfc-2497-if-let-chains/then-else-blocks.rs)

Most of the infra-structure used by let chains is also used by `if` expressions in stable compiler versions since rust-lang/rust#80357 and rust-lang/rust#88572. As a result, no bugs were found since the integration of rust-lang/rust#88642.

## Possible future work

* Let chains in match guards is implemented and working but stabilization is blocked by `if_let_guard`.

* The usage of `let_chains` with `let_else` is possible but not implemented. Regardless, one attempt was introduced and closed in rust-lang/rust#93437.

Thanks `@Centril` for creating the RFC and huge thanks (again) to `@matthewjasper` for all the reviews, mentoring and MIR implementations.

Fixes #53667
@alper
Copy link

alper commented Dec 6, 2024

Can somebody explain to me why the Swift if let with commas was not considered?

@kennytm
Copy link
Member

kennytm commented Dec 6, 2024

@alper It was considered and the community highly disliked it. There is literally a survey report inside the RFC document.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-control-flow Proposals relating to control flow. A-syntax Syntax related proposals & ideas disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this RFC. T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

Successfully merging this pull request may close these issues.