Merge RFC 3627: Match ergonomics 2024

The FCP for RFC 3627 completed 2024-07-14 with a disposition to
merge.  Let's merge it.
This commit is contained in:
Travis Cross 2024-07-14 06:29:07 +00:00
commit e17e19ac7a
1 changed files with 598 additions and 0 deletions

View File

@ -0,0 +1,598 @@
- Feature Name: `ref_pat_eat_one_layer_2024`
- Start Date: 2024-05-06
- RFC PR: [rust-lang/rfcs#3627](https://github.com/rust-lang/rfcs/pull/3627)
- Tracking Issue: [rust-lang/rust#123076](https://github.com/rust-lang/rust/issues/123076)
# Summary
[summary]: #summary
Various changes to the match ergonomics rules:
- On edition ≥ 2024, `&` and `&mut` patterns only remove a single layer of
references.
- On edition ≥ 2024, `mut` on an identifier pattern does not force its binding
mode to by-value.
- On all editions, `&` patterns can match against `&mut` references.
- On all editions, the binding mode can no longer ever be implicitly set to
`ref mut` behind an `&` pattern.
# Motivation
[motivation]: #motivation
Match ergonomics have been a great success overall, but there are some surprising
interactions that regularly confuse users.
## `mut` resets the binding mode
`mut` resets the binding mode to by-value, which users do not expect; the
mutability of the binding would seem to be separate concern from its type
(<https://github.com/rust-lang/rust/issues/105647>,
<https://github.com/rust-lang/rust/issues/112545>).
```rust
let (x, mut y) = &(true, false);
let _: (&bool, bool) = (x, y);
```
## Cant cancel out an inherited reference
`&` and `&mut` patterns must correspond with a reference in the same position in
the scrutinee, even if there is an inherited reference present. Therefore, users
have no general mechanism to “cancel out” an inherited reference
(<https://users.rust-lang.org/t/reference-of-tuple-and-tuple-of-reference/91713/6>,
<https://users.rust-lang.org/t/cannot-deconstruct-reference-inside-match-on-reference-why/92147>,
<https://github.com/rust-lang/rust/issues/50008>,
<https://github.com/rust-lang/rust/issues/64586>).
```rust
fn foo(arg: &(String, Vec<i32>, u8)) {
// We want to extract `&String`, `&Vec`, and `u8` from the tuple.
let (s, v, u) = arg; // u is &u8, not what we wanted
let &(ref s, ref v, u) = arg; // we have to abandon match ergonomics entirely
}
```
## A single `&` can strip two references
When an `&` or `&mut` pattern is used in a location where there is also an
inherited reference present, both are stripped; adding a single `&` to the
pattern can remove two `&`s from the type of the binding.
```rust
let [a] = &[&42]; // a = &&42
let [&a] = &[&42]; // a = 42
```
# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation
Match ergonomics works a little differently in edition 2024 and above.
## `mut` no longer strips the inherited reference
`mut` on a binding does not reset the binding mode on edition ≥ 2024. Instead,
`mut` on a binding with non-default binding mode is an error.
```rust
//! Edition ≥ 2024
//let (x, mut y) = &(true, false); // ERROR
```
## `&` matches against `&mut`
On all editions, `&` patterns can match against `&mut` references. On edition
2024 and above, this includes "inherited" references as described below.
```rust
//! All editions
let &foo = &mut 42;
let _: u8 = foo;
```
```rust
//! Edition ≥ 2024
let [&foo] = &mut [42];
let _: u8 = foo;
```
## Matching against inherited references
In all editions, when you match against an `&` or `&mut` reference with the type
of its referent, you get an "inherited reference": the binding mode of
"downstream" bindings is set to `ref` or `ref mut`.
```rust
//! All editions
// `x` "inherits" the `&` from the scrutinee type.
let [x] = &[42];
let _: &u8 = x;
```
In edition 2024 and above, an `&` or `&mut` pattern can match against this
inherited reference, consuming it. A pattern that does this has no other effect.
```rust
//! Edition ≥ 2024
// `&` pattern consumes inherited `&` reference.
let [&x] = &[42];
let _: u8 = x;
// Examples from motivation section
fn foo(arg: &(String, Vec<i32>, u8)) {
let (s, v, &u) = arg;
let _: (&String, &Vec<i32>, u8) = (s, v, u);
}
let [&x] = &[&42];
let _: &u8 = x;
```
# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation
This explanation assumes familiarity with the current match ergonomics rules,
including the "default binding mode" terminology. Refer to [RFC 2005](./2005-match-ergonomics.md#detailed-design).
## The rules in brief
Building on the rules of [RFC 2005](./2005-match-ergonomics.md), this RFC adopts
the following five rules for match ergonomics:
- **Rule 1**: When the DBM (default binding mode) is not `move` (whether or not
behind a reference), writing `mut` on a binding is an error.
- **Rule 2**: When a reference pattern matches against a reference, do not
update the DBM.
- **Rule 3**: If we've previously matched against a shared reference in the
scrutinee (or against a `ref` DBM under *Rule 4*, or against a mutable
reference treated as a shared one or a `ref mut` DBM treated as a `ref` one
under *Rule 5*), set the DBM to `ref` whenever we would otherwise set it to
`ref mut`.
- **Rule 4**: If an `&` pattern is being matched against a non-reference type or
an `&mut` pattern is being matched against a shared reference type or a
non-reference type, **and if** the DBM is `ref` or `ref mut`, match the
pattern against the DBM as though it were a type.
- **Rule 5**: If an `&` pattern is being matched against a mutable reference
type (or against a `ref mut` DBM under *Rule 4*), act as if the type were a
shared reference instead (or that the `ref mut` DBM is a `ref` DBM instead).
*Rule 1* and *Rule 2* are edition-dependent and will be stabilized with
Rust 2024. The other three rules will be stabilized in all editions.
In the sections below, we describe these rules and their effects in more detail.
## Edition 2024: `mut` does not reset binding mode to by-value
In the new edition, `mut` no longer resets the binding mode to by-value;
instead, `mut` on a binding with a by-reference binding mode is an error.
```rust
//! Edition ≥ 2024
// let [mut a] = &[42]; //ERROR
```
## All editions: `&` patterns can match against `&mut` references
`&` patterns can match against `&mut` references.
```rust
//! All editions
let &foo = &mut 42;
let _: u8 = foo;
```
However, the `ref mut` binding mode cannot be used behind such patterns.
```rust
//! All editions
let &ref mut foo = &mut 42;
// ^~ERROR: replace `&` with `&mut `
let _: &mut u8 = foo;
```
However, if the type of the scrutinee is unknown, an `&` pattern will still
constrain inference to force it to be a shared reference.
```rust
//! All editions
fn generic<R: Ref>() -> (R, bool) {
R::meow()
}
trait Ref: Sized {
fn meow() -> (Self, bool);
}
impl Ref for &'static [(); 0] {
fn meow() -> (Self, bool) {
(&[], false)
}
}
impl Ref for &'static mut [(); 0] {
fn meow() -> (Self, bool) {
(&mut [], true)
}
}
fn main() {
let (&_, b) = generic();
assert!(!b);
}
```
```rust
//! All editions
fn generic<R: Ref>() -> R {
R::meow()
}
trait Ref: Sized {
fn meow() -> Self;
}
impl Ref for &'static mut [(); 0] {
fn meow() -> Self {
&mut []
}
}
fn main() {
let &_ = generic(); //~ERROR[E0277]: the trait bound `&_: Ref` is not satisfied
}
```
## Edition 2024: `&` and `&mut` can match against inherited references
When the default binding mode is `ref` or `ref mut`, `&` and `&mut` patterns can
reset it. `&` patterns will reset either `ref` or `ref mut` binding modes to
by-value, while `&mut` can only reset `ref mut`. An `&` or `&mut` pattern that
resets the binding mode in this way has no other effect.
```rust
//! Edition ≥ 2024
let [&x] = &[3u8];
let _: u8 = x;
let [&mut x] = &mut [3u8];
let _: u8 = x;
let [&x] = &mut [3u8];
let _: u8 = x;
```
```rust
//! All editions
//let [&mut x] = &[3u8]; // ERROR
```
`&` patterns are otherwise unchanged from older editions.
```rust
//! All editions
let &a = &3;
let _: u8 = a;
//let &b = 17; // ERROR
```
If the default binding mode is `ref`, then `&mut` patterns will not be able to
match against it, so they will match structurally instead (preserving the
binding mode).
```rust
//! Edition ≥ 2024
let [&mut x] = &[&mut 42];
let _: &u8 = x;
```
`&mut` patterns are otherwise unchanged.
```rust
//! All editions
let &mut x = &mut 3;
let _: u8 = x;
let &mut x = &mut &mut 3;
let _: &mut u8 = x;
let &mut x = &mut &&mut 3;
let _: &&mut u8 = x;
//let &mut x = &&mut 3; // ERROR
```
## All editions: the default binding mode is never set to `ref mut` behind an `&` pattern or reference
The binding mode is set to `ref` instead in such cases. (On older editions, this
allows strictly more code to compile.)
```rust
//! All editions (new)
let &[[a]] = &[&mut [42]];
let _: &u8 = a; // previously `a` would be `&mut u8`, resulting in a move check error
let &[[a]] = &mut [&mut [42]];
let _: &u8 = a;
```
```rust
//! Edition ≥ 2024
let &[[&a]] = &[&mut [42]];
let _: u8 = a;
//let &[[&mut a]] = &[&mut [42]]; // ERROR
```
# Migration
[migration]: #migration
This proposal, if adopted, would allow the same pattern to have different
meanings on different editions:
```rust
let [&a] = &[&0u8]; // `a` is `u8` on edition ≤ 2021, but `&u8` on edition ≥ 2024
let [mut a] = &[0u8]; // `a` is `u8` on edition ≤ 2021, but `&u8` on edition ≥ 2024
```
Instances of such incompatibilities appear to be uncommon, but far from unknown
(20 cases in `rustc`, for example). The migration lint for the feature entirely
desugars the match ergonomics of the affected pattern. This is necessary to
produce code that works on all editions, but it means that adopting the new
rules could require editing the affected patterns twice: once to desugar the
match ergonomics before adopting the new edition, and a second time to restore
match ergonomics after adoption of the new edition.
## Macro subpatterns
Unfortunately, when a subpattern derives from a macro expansion, fully
desugaring the match ergonomics may not be possible. For example:
```rust
//! crate foo (edition 2021)
#[macro_export]
macro_rules! foo {
($foo:ident) => {
[$foo]
};
}
```
```rust
//! crate bar (edition 2021, want to migrate to 2024)
extern crate foo;
use foo::*;
fn main() {
let ([&x], foo!(y)) = &([&0], [0]);
//~^ WARN: the semantics of this pattern will change in edition 2024
let _: i32 = x;
let _: &i32 = y;
}
```
In such cases, there is no possible machine-applicable suggestion we could emit
to produce code compatible with all editions (short of expanding the macro).
However, such code should be extremely rare in practice.
# Drawbacks
[drawbacks]: #drawbacks
This is a silent change in behavior, which is considered undesirable even
over an edition.
# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives
## Desirable property
[desirable-property]: #desirable-property
The proposed rules for new editions uphold the following property:
> For any two nested patterns `$pat0` and `$pat1`, such that `$pat1` uses match
> ergonomics only (no explicit `ref`/`ref mut`), and pattern match
> `let $pat0($pat1(binding)) = scrut`, either:
>
> - `let $pat0(temp) = scrut; let $pat1(binding) = temp;` compiles, with the
> same meaning as the original composed pattern match; or
> - `let $pat0(temp) = scrut; let $pat1(binding) = temp;` does not compile, but
> `let $pat0(ref temp) = scrut; let &$pat1(binding) = temp;` compiles, with the
> same meaning as the original composed pattern match.
In other words, the new match ergonomics rules are compositional.
## `mut` not resetting the binding mode
Admittedly, there is not much use for mutable by-reference bindings. This is
true even outside of pattern matching; `let mut ident: &T = ...` is not commonly
seen (though not entirely unknown either). The motivation for making this change
anyway is that the current behavior is unintuitive and surprising for users.
## Never setting default binding mode to `ref mut` behind `&`
### We cant delay this choice
#### Patterns that work only with this rule
```rust
//! All editions: works only with this rule
let &(i, j, [s]) = &(63, 42, &mut [String::from("🦀")]); // i: i32, j: i32, s: &String
```
```rust
//! Edition ≥ 2024: works with or without this rule (alternative to above)
let (&i, &j, [s]) = &(42, &mut [String::from("🦀")]); // i: i32, j: i32, s: &String
```
```rust
//! All editions: works with or without this rule (alternatives to above)
let &(i, j, [ref s]) = &(42, &mut [String::from("🦀")]); // i: i32, j: i32, s: &String
let &(i, j, &mut [ref s]) = &(42, &mut [String::from("🦀")]); // i: i32, j: i32, s: &String
```
#### Patterns that work only without this rule
```rust
//! Edition ≥ 2024: works only without this rule
let &[[&mut a]] = &[&mut [42]]; // x: i32
// `&mut` in pattern needs to match against either:
// - `&mut` in value at same position (there is none, so not possible)
// - inherited `&mut` (which the rule downgrades to `&`)
```
```rust
//! Edition ≥ 2024: works with or without this rule (alternatives to above)
let &[[&a]] = &[&mut [42]]; // x: i32
let &[&mut [a]] = &[&mut [42]]; // x: i32
```
### Makes behavior more consistent
On all editions, when a structure pattern peels off a shared reference and the
default binding mode is already `ref mut`, the binding mode gets set to `ref`:
```rust
//! All editions
let [a] = &mut &[42]; // x: &i32
```
But when the binding mode is set to `ref`, and a mutable reference is peeled
off, the binding mode remains `ref`:
```rust
//! All editions
let [a] = &&mut [42]; // x: &i32
```
In other words, immutability usually takes precedence over mutability. This
change, in addition to being generally useful, makes the match ergonomics rules
more consistent by ensuring that immutability *always* takes precedence over
mutability.
### Ensures that a desirable property is preserved
The current match ergonomics rules uphold the following desirable property:
> An `&mut` pattern is accepted if and only if removing the pattern would allow
> obtaining an `&mut` value.
For example:
```rust
//! All editions
let &mut a = &mut 42; // `a: i32`
let a = &mut 42; // `a: &mut i32`
let &[&mut a] = &[&mut 42]; // `a: i32`
//let &[a] = &[&mut 42]; // ERROR, but…
let &[ref a] = &[&mut 42]; // `a = &&mut i32` (so we did manage to obtain an `&mut i32` in some form)
```
Adopting the "no `ref mut` behind `&`" rule ensures that this property continues
to hold for edition 2024:
```rust
//! Edition ≥ 2024
let &[[&mut x]] = &[&mut [42]]; // If we were allow this, with `x: i32`
//let &[[x]] = &[&mut [42]]; // remove the `&mut` → ERROR, if the default binding mode is to be `ref mut`
// nothing we do will get us `&mut i32` in any form
```
## `&` patterns matching against `&mut`
There are several motivations for allowing this:
- It makes refactoring less painful. Sometimes, one is not certain whether an
unfinished API will end up returning a shared or a mutable reference. But as
long as the reference returned by said API is not actually used to perform
mutation, it often doesnt matter either way, as `&mut` implicitly reborrows
as `&` in many situations. Pattern matching is currently one of the most
prominent exceptions to this, and match ergonomics magnifies the pain because
a reference in one part of the pattern can affect the binding mode in a
different, faraway location[^nrmba]. If patterns can be written to always use
`&` unless mutation is required, then the amount of editing necessary to
perform various refactors is lessened.
- Its intuitive. `&mut` is strictly more powerful than `&`. Its conceptually a
subtype, and even if not implemented that way[^sub], coercions mean it often
feels like one in practice.
```rust
let a: &u8 = &mut 42;
```
[^nrmba]: This is especially true in light of the [new rule](#all-editions-the-default-binding-mode-is-never-set-to-ref-mut-behind-an--pattern-or-reference)
that prevents the default binding mode from being set to `ref mut` behind `&`.
[^sub]: Making `&mut` a subtype of `&` in actual implementation would require
adding significant complexity to the variance rules, but I do believe it to be
possible.
## Versus "eat-two-layers"
An alternative proposal would be to allow `&` and `&mut` patterns to reset the
binding mode when not matching against a reference in the same position in the
scrutinee, but to not otherwise change their behavior. This would have the
advantage of not requiring an edition change. However, it would remain confusing
for users. Notably, the [property from earlier](#desirable-property) would
continue to not be satisfied.
In addition, this approach would lead to tricky questions around when
mutabilities should be considered compatible. And there would be compatibility
concerns with certain proposals for "deref patterns".
(This alternative is currently implemented under a separate feature gate.)
# Unresolved questions
[unresolved-questions]: #unresolved-questions
- How much churn will be necessary to adapt code for the new edition? There are
0 instances of affected patterns in the standard library, and 20 in the
compiler, but that is all the data we have at the moment.
# Future possibilities
[future-possibilities]: #future-possibilities
- An explicit syntax for mutable by-reference bindings should be chosen at some
point, along with removing the prohibition on implicitly by-reference mutable
bindings.
- Future changes to reference types (partial borrows, language sugar for `Pin`,
etc) may interact with match ergonomics.
## Deref patterns
Because it is compositional, the “eat-one-layer” model proposed by this RFC is
fully compatible with proposals for "deref patterns", including allowing
`&`/`&mut` patterns to match against types implementing `Deref`/`DerefMut`. One
question that would need to be resolved is whether and how deref patterns
(explicit or implicit) affect the default binding mode.
## Matching `&mut` directly behind `&`
There is one notable situation where match ergonomics cannot be used, and
explicit `ref` is required. This happens where `&mut` is nested behind `&`:
```rust
// No way to avoid the `ref`, even with this RFC
let &&mut ref x = &&mut 42; // x: &i32
```
There are two strategies we could take to support this:
- `&mut` patterns could “strip off” outer `&`. For example, in
`let &mut x = &&mut 42;`, the `&mut` pattern would match the `&mut` reference
in the scrutinee, leaving `&` to be inherited and resulting in `x: &i32`.
- This may not extend gracefully to future language features (partial borrows,
for example) as it potentially relies on reference types forming a total
order.
- The compiler could insert `&mut ref` in front of identifier patterns of type
`&mut` that are behind an `&` pattern. For example, `let &x = &&mut 42;` would
be transformed into `let &&mut ref x = &&mut 42;`.
- The full desugaring would be more complicated, as it would need to handle
`@` patterns.