Skip to content

Document let_chains again #1740

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
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
@@ -63,7 +63,7 @@
- [Closure expressions](expressions/closure-expr.md)
- [Loop expressions](expressions/loop-expr.md)
- [Range expressions](expressions/range-expr.md)
- [If and if let expressions](expressions/if-expr.md)
- [If expressions](expressions/if-expr.md)
- [Match expressions](expressions/match-expr.md)
- [Return expressions](expressions/return-expr.md)
- [Await expressions](expressions/await-expr.md)
1 change: 0 additions & 1 deletion src/attributes/type_system.md
Original file line number Diff line number Diff line change
@@ -206,7 +206,6 @@ let _ = EnumWithNonExhaustiveVariants::First as u8;

Non-exhaustive types are always considered inhabited in downstream crates.

[`if let`]: ../expressions/if-expr.md#if-let-expressions
[`match`]: ../expressions/match-expr.md
[attributes]: ../attributes.md
[enum]: ../items/enumerations.md
6 changes: 2 additions & 4 deletions src/const_eval.md
Original file line number Diff line number Diff line change
@@ -102,10 +102,10 @@ r[const-eval.const-expr.const-fn]
* Calls of [const functions] and const methods.

r[const-eval.const-expr.loop]
* [loop], [while] and [`while let`] expressions.
* [loop] and [while] expressions.

r[const-eval.const-expr.if-match]
* [if], [`if let`] and [match] expressions.
* [if] and [match] expressions.

r[const-eval.const-context]
## Const context
@@ -185,7 +185,6 @@ of whether you are building on a `64` bit or a `32` bit system.
[grouped]: expressions/grouped-expr.md
[interior mutability]: interior-mutability.md
[if]: expressions/if-expr.md#if-expressions
[`if let`]: expressions/if-expr.md#if-let-expressions
[lazy boolean]: expressions/operator-expr.md#lazy-boolean-operators
[let statements]: statements.md#let-statements
[literals]: expressions/literal-expr.md
@@ -202,4 +201,3 @@ of whether you are building on a `64` bit or a `32` bit system.
[struct]: expressions/struct-expr.md
[tuple expressions]: expressions/tuple-expr.md
[while]: expressions/loop-expr.md#predicate-loops
[`while let`]: expressions/loop-expr.md#predicate-pattern-loops
15 changes: 8 additions & 7 deletions src/destructors.md
Original file line number Diff line number Diff line change
@@ -67,8 +67,9 @@ leaves a drop scope all variables associated to that scope are dropped in
reverse order of declaration (for variables) or creation (for temporaries).

r[destructors.scope.desugaring]
Drop scopes are determined after replacing [`for`], [`if let`], and
[`while let`] expressions with the equivalent expressions using [`match`].
Drop scopes can be determined by replacing [`for`], [`if`], and [`while`]
expressions with equivalent expressions using [`match`], [`loop`] and
`break`.

r[destructors.scope.operators]
Overloaded operators are not distinguished from built-in operators and [binding
@@ -203,11 +204,11 @@ smallest scope that contains the expression and is one of the following:
* A statement.
* The body of an [`if`], [`while`] or [`loop`] expression.
* The `else` block of an `if` expression.
* The condition expression of an `if` or `while` expression, or a `match`
guard.
* The non-pattern matching condition expression of an `if` or `while` expression,
or a `match` guard.
* The body expression for a match arm.
* Each operand of a [lazy boolean expression].
* The pattern-matching condition and consequent body of [`if let`] ([destructors.scope.temporary.edition2024]).
* The pattern-matching condition(s) and consequent body of [`if`] ([destructors.scope.temporary.edition2024]).
* The entirety of the tail expression of a block ([destructors.scope.temporary.edition2024]).

> [!NOTE]
@@ -479,10 +480,10 @@ There is one additional case to be aware of: when a panic reaches a [non-unwindi
[tuple indexing expression]: expressions/tuple-expr.md#tuple-indexing-expressions

[`for`]: expressions/loop-expr.md#iterator-loops
[`if let`]: expressions/if-expr.md#if-let-expressions
[`if let`]: expressions/if-expr.md#if-let-patterns
[`if`]: expressions/if-expr.md#if-expressions
[`let` statement]: statements.md#let-statements
[`loop`]: expressions/loop-expr.md#infinite-loops
[`match`]: expressions/match-expr.md
[`while let`]: expressions/loop-expr.md#predicate-pattern-loops
[`while let`]: expressions/loop-expr.md#while-let-patterns
[`while`]: expressions/loop-expr.md#predicate-loops
5 changes: 2 additions & 3 deletions src/expressions.md
Original file line number Diff line number Diff line change
@@ -41,7 +41,6 @@ ExpressionWithBlock ->
| UnsafeBlockExpression
| LoopExpression
| IfExpression
| IfLetExpression
| MatchExpression
)
```
@@ -311,9 +310,9 @@ They are never allowed before:

[`Copy`]: special-types-and-traits.md#copy
[`Drop`]: special-types-and-traits.md#drop
[`if let`]: expressions/if-expr.md#if-let-expressions
[`if let`]: expressions/if-expr.md#if-let-patterns
[`Sized`]: special-types-and-traits.md#sized
[`while let`]: expressions/loop-expr.md#predicate-pattern-loops
[`while let`]: expressions/loop-expr.md#while-let-patterns
[array expressions]: expressions/array-expr.md
[array indexing]: expressions/array-expr.md#array-and-slice-indexing-expressions
[assign]: expressions/operator-expr.md#assignment-expressions
3 changes: 1 addition & 2 deletions src/expressions/block-expr.md
Original file line number Diff line number Diff line change
@@ -258,7 +258,7 @@ r[expr.block.attributes.inner-attributes]
[Inner attributes] are allowed directly after the opening brace of a block expression in the following situations:

* [Function] and [method] bodies.
* Loop bodies ([`loop`], [`while`], [`while let`], and [`for`]).
* Loop bodies ([`loop`], [`while`], and [`for`]).
* Block expressions used as a [statement].
* Block expressions as elements of [array expressions], [tuple expressions],
[call expressions], and tuple-style [struct] expressions.
@@ -282,7 +282,6 @@ fn is_unix_platform() -> bool {
[`for`]: loop-expr.md#iterator-loops
[`loop`]: loop-expr.md#infinite-loops
[`unsafe` blocks]: ../unsafe-keyword.md#unsafe-blocks-unsafe-
[`while let`]: loop-expr.md#predicate-pattern-loops
[`while`]: loop-expr.md#predicate-loops
[array expressions]: array-expr.md
[call expressions]: call-expr.md
173 changes: 86 additions & 87 deletions src/expressions/if-expr.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
r[expr.if]
# `if` and `if let` expressions

## `if` expressions
# `if` expressions

r[expr.if.syntax]
```grammar,expressions
IfExpression ->
`if` Expression _except [StructExprStruct]_ BlockExpression
(`else` ( BlockExpression | IfExpression | IfLetExpression ) )?
`if` IfConditions BlockExpression
(`else` ( BlockExpression | IfExpression ) )?

IfConditions -> IfCondition ( `&&` IfCondition )*

IfCondition ->
Expression _except [StructExprStruct]_
| `let` Pattern `=` Scrutinee _except [LazyBooleanExpression]_
Comment on lines 6 to +14
Copy link
Contributor

Choose a reason for hiding this comment

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

This can't be right, e.g. (considering only parsing):

_ = if let () = 0..0 {}; //~ ERROR

That is, the exception isn't just LazyBooleanExpression; it's any boolean expression with a lower precedence than &&.

We're also asking, I think, for something kind of magic to happen here. When we have,

_ = if true == false && let () = () {}; //~ OK

we're hoping for Expression to be true == false. But when we have,

_ = if true || false && let () = () {}; //~ ERROR

we're hoping for Expression to try to be true || false && let () = (), notice that's impossible, and fail (i.e., rather than allowing Expression to be true || false, which would be wrong). But this isn't really encoded anywhere.

Ideas?

Copy link
Member Author

Choose a reason for hiding this comment

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

That is, the exception isn't just LazyBooleanExpression; it's any boolean expression with a lower precedence than &&.

You are right that the exception isn't just that, and maybe it's misleading to list only LazyBooleanExpression because people might think that's an exhaustive list of exceptions.

From what I can see, the reference doesn't require the grammar to be "precedence fitting", i.e. only allow what's possible to express in text form following precedence rules. Instead, it puts the precedence into a table and assumes the grammar is parsed according to the precedence. I would say that table also applies here, as it's part of the larger "expression" family of things.

Because people might think it's exhaustive, we could maybe remove the exception altogether and replace it with a note instead?

we're hoping for Expression to try to be true || false && let () = (), notice that's impossible, and fail (i.e., rather than allowing Expression to be true || false, which would be wrong).

|| has weaker precedence, so there isn't a top level && chain any more. On the other hand, == has stronger precedence.

The example with || is parsed to sth like:

If(
    BinOp(
        ||,
        true,
        BinOp(
            &&,
            false,
            Let((), ()),
        )
    ),
    Block([])
)

The example with == is parsed like:


If(
    BinOp(
        &&,
        true,
        BinOp(
            ==,
            false,
            Let((), ()),
        )
    ),
    Block([])
)

So in the || example, the BinOp with && is actually not top level, its arms can't be IfConditions. So its arms don't have a "permit to let". While in the == example, it has such a permission.

So I think this property is emergent from the grammar and the fact that it is parsed according to precedence.

```
<!-- TODO: The exception above isn't accurate, see https://github.com/rust-lang/reference/issues/569 -->

r[expr.if.intro]
An `if` expression is a conditional branch in program control.
The syntax of an `if` expression is a condition operand, followed by a consequent block, any number of `else if` conditions and blocks, and an optional trailing `else` block.
The syntax of an `if` expression is a sequence of one or more condition operands separated by `&&`,
followed by a consequent block, any number of `else if` conditions and blocks, and an optional trailing `else` block.

r[expr.if.condition-bool]
The condition operands must have the [boolean type].
r[expr.if.condition]
Condition operands must be either an [_Expression_] with a [boolean type] or a conditional `let` match.

r[expr.if.condition-true]
If a condition operand evaluates to `true`, the consequent block is executed and any subsequent `else if` or `else` block is skipped.
If all of the condition operands evaluate to `true` and all of the `let` patterns successfully match their [scrutinee]s,
the consequent block is executed and any subsequent `else if` or `else` block is skipped.

r[expr.if.else-if]
If a condition operand evaluates to `false`, the consequent block is skipped and any subsequent `else if` condition is evaluated.
If any condition operand evaluates to `false` or any `let` pattern does not match its scrutinee,
the consequent block is skipped and any subsequent `else if` condition is evaluated.

r[expr.if.else]
If all `if` and `else if` conditions evaluate to `false` then any `else` block is executed.

r[expr.if.result]
An if expression evaluates to the same value as the executed block, or `()` if no block is evaluated.
An `if` expression evaluates to the same value as the executed block, or `()` if no block is evaluated.

r[expr.if.type]
An `if` expression must have the same type in all situations.
@@ -43,6 +49,7 @@ if x == 4 {
println!("x is something else");
}

// `if` can be used as an expression.
let y = if 12 * 15 > 150 {
"Bigger"
} else {
@@ -52,39 +59,25 @@ assert_eq!(y, "Bigger");
```

r[expr.if.let]
## `if let` expressions

r[expr.if.let.syntax]
```grammar,expressions
IfLetExpression ->
`if` `let` Pattern `=` Scrutinee _except [LazyBooleanExpression]_ BlockExpression
(`else` ( BlockExpression | IfExpression | IfLetExpression ) )?
```
## `if let` patterns

r[expr.if.let.intro]
An `if let` expression is semantically similar to an `if` expression but in place of a condition operand it expects the keyword `let` followed by a pattern, an `=` and a [scrutinee] operand.

r[expr.if.let.pattern]
If the value of the scrutinee matches the pattern, the corresponding block will execute.
`let` patterns in an `if` condition allow binding new variables into scope when the pattern matches successfully.

r[expr.if.let.else]
Otherwise, flow proceeds to the following `else` block if it exists.

r[expr.if.let.result]
Like `if` expressions, `if let` expressions have a value determined by the block that is evaluated.
The following examples illustrate bindings using `let` patterns:

```rust
let dish = ("Ham", "Eggs");

// this body will be skipped because the pattern is refuted
// This body will be skipped because the pattern is refuted.
if let ("Bacon", b) = dish {
println!("Bacon is served with {}", b);
} else {
// This block is evaluated instead.
println!("No bacon will be served");
}

// this body will execute
// This body will execute.
if let ("Ham", b) = dish {
println!("Ham is served with {}", b);
}
@@ -94,47 +87,9 @@ if let _ = 5 {
}
```

r[expr.if.let.else-if]
`if` and `if let` expressions can be intermixed:

```rust
let x = Some(3);
let a = if let Some(1) = x {
1
} else if x == Some(2) {
2
} else if let Some(y) = x {
y
} else {
-1
};
assert_eq!(a, 3);
```

r[expr.if.let.desugaring]
An `if let` expression is equivalent to a [`match` expression] as follows:

<!-- ignore: expansion example -->
```rust,ignore
if let PATS = EXPR {
/* body */
} else {
/*else */
}
```

is equivalent to

<!-- ignore: expansion example -->
```rust,ignore
match EXPR {
PATS => { /* body */ },
_ => { /* else */ }, // () if there is no else
}
```

r[expr.if.let.or-pattern]
Multiple patterns may be specified with the `|` operator. This has the same semantics as with `|` in `match` expressions:
Multiple patterns may be specified with the `|` operator.
This has the same semantics as with `|` in [`match` expressions]:

```rust
enum E {
@@ -148,27 +103,71 @@ if let E::X(n) | E::Y(n) = v {
}
```

r[expr.if.let.lazy-bool]
The expression cannot be a [lazy boolean operator expression][expr.bool-logic].
Use of a lazy boolean operator is ambiguous with a planned feature change of the language (the implementation of if-let chains - see [eRFC 2947][_eRFCIfLetChain_]).
When lazy boolean operator expression is desired, this can be achieved by using parenthesis as below:
r[expr.if.chains]
## Chains of conditions

r[expr.if.chains.intro]
Multiple condition operands can be separated with `&&`.

r[expr.if.chains.order]
Similar to a `&&` [_LazyBooleanOperatorExpression_], each operand is evaluated from left-to-right until an operand evaluates as `false` or a `let` match fails,
in which case the subsequent operands are not evaluated.

r[expr.if.chains.bindings]
The bindings of each pattern are put into scope to be available for the next condition operand and the consequent block.

The following is an example of chaining multiple expressions, mixing `let` bindings and boolean expressions, and with expressions able to reference pattern bindings from previous expressions:

<!-- ignore: pseudo code -->
```rust,ignore
// Before...
if let PAT = EXPR && EXPR { .. }
```rust
fn single() {
let outer_opt = Some(Some(1i32));

if let Some(inner_opt) = outer_opt
&& let Some(number) = inner_opt
&& number == 1
{
println!("Peek a boo");
}
}
```

// After...
if let PAT = ( EXPR && EXPR ) { .. }
The above is equivalent to the following without using chains of conditions:

// Before...
if let PAT = EXPR || EXPR { .. }
```rust
fn nested() {
let outer_opt = Some(Some(1i32));

if let Some(inner_opt) = outer_opt {
if let Some(number) = inner_opt {
if number == 1 {
println!("Peek a boo");
}
}
}
}
```

r[expr.if.chains.or]
If any condition operand is a `let` pattern, then none of the condition operands can be a `||` [lazy boolean operator expression][_LazyBooleanOperatorExpression_] due to ambiguity and precedence with the `let` scrutinee.
If a `||` expression is needed, then parentheses can be used. For example:

// After...
if let PAT = ( EXPR || EXPR ) { .. }
```rust
# let foo = Some(123);
# let condition1 = true;
# let condition2 = false;
// Parentheses are required here.
if let Some(x) = foo && (condition1 || condition2) { /*...*/ }
```

[_eRFCIfLetChain_]: https://github.com/rust-lang/rfcs/blob/master/text/2497-if-let-chains.md#rollout-plan-and-transitioning-to-rust-2018
[`match` expression]: match-expr.md
r[expr.if.edition2024]
> [!EDITION-2024]
> Before the 2024 edition, let chains are not supported and only a single _IfCondition_ is allowed in an `if` expression.

[_BlockExpression_]: block-expr.md
[_Expression_]: ../expressions.md
[_LazyBooleanOperatorExpression_]: operator-expr.md#lazy-boolean-operators
[_Pattern_]: ../patterns.md
[_Scrutinee_]: match-expr.md
[`match` expressions]: match-expr.md
[boolean type]: ../types/boolean.md
[scrutinee]: ../glossary.md#scrutinee
Loading