- Feature Name:
ref_pat_eat_one_layer_2024 - Start Date: 2024-05-06
- RFC PR: rust-lang/rfcs#3627
- Tracking Issue: rust-lang/rust#123076
Summary
Various changes to the match ergonomics rules:
- On edition ≥ 2024,
&and&mutpatterns only remove a single layer of references. - On edition ≥ 2024,
muton an identifier pattern does not force its binding mode to by-value. - On all editions,
&patterns can match against&mutreferences. - On all editions, the binding mode can no longer ever be implicitly set to
ref mutbehind an&pattern.
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).
let (x, mut y) = &(true, false);
let _: (&bool, bool) = (x, y);
Can’t 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).
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.
let [a] = &[&42]; // a = &&42
let [&a] = &[&42]; // a = 42
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.
//! 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.
//! All editions
let &foo = &mut 42;
let _: u8 = foo;
//! 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.
//! 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.
//! 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
This explanation assumes familiarity with the current match ergonomics rules, including the “default binding mode” terminology. Refer to RFC 2005.
The rules in brief
Building on the rules of RFC 2005, 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), writingmuton 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
refDBM under Rule 4, or against a mutable reference treated as a shared one or aref mutDBM treated as arefone under Rule 5), set the DBM torefwhenever we would otherwise set it toref mut. - Rule 4: If an
&pattern is being matched against a non-reference type or an&mutpattern is being matched against a shared reference type or a non-reference type, and if the DBM isreforref 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 aref mutDBM under Rule 4), act as if the type were a shared reference instead (or that theref mutDBM is arefDBM 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.
//! Edition ≥ 2024
// let [mut a] = &[42]; //ERROR
All editions: & patterns can match against &mut references
& patterns can match against &mut references.
//! All editions
let &foo = &mut 42;
let _: u8 = foo;
However, the ref mut binding mode cannot be used behind such patterns.
//! 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.
//! 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);
}
//! 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.
//! Edition ≥ 2024
let [&x] = &[3u8];
let _: u8 = x;
let [&mut x] = &mut [3u8];
let _: u8 = x;
let [&x] = &mut [3u8];
let _: u8 = x;
//! All editions
//let [&mut x] = &[3u8]; // ERROR
& patterns are otherwise unchanged from older editions.
//! 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).
//! Edition ≥ 2024
let [&mut x] = &[&mut 42];
let _: &u8 = x;
&mut patterns are otherwise unchanged.
//! 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.)
//! 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;
//! Edition ≥ 2024
let &[[&a]] = &[&mut [42]];
let _: u8 = a;
//let &[[&mut a]] = &[&mut [42]]; // ERROR
Migration
This proposal, if adopted, would allow the same pattern to have different meanings on different editions:
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:
//! crate foo (edition 2021)
#[macro_export]
macro_rules! foo {
($foo:ident) => {
[$foo]
};
}
//! 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
This is a silent change in behavior, which is considered undesirable even over an edition.
Rationale and alternatives
Desirable property
The proposed rules for new editions uphold the following property:
For any two nested patterns
$pat0and$pat1, such that$pat1uses match ergonomics only (no explicitref/ref mut), and pattern matchlet $pat0($pat1(binding)) = scrut, either:
let $pat0(temp) = scrut; let $pat1(binding) = temp;compiles, with the same meaning as the original composed pattern match; orlet $pat0(temp) = scrut; let $pat1(binding) = temp;does not compile, butlet $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 can’t delay this choice
Patterns that work only with this rule
//! All editions: works only with this rule
let &(i, j, [s]) = &(63, 42, &mut [String::from("🦀")]); // i: i32, j: i32, s: &String
//! 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
//! 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
//! 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 `&`)
//! 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:
//! 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:
//! 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
&mutpattern is accepted if and only if removing the pattern would allow obtaining an&mutvalue.
For example:
//! 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:
//! 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 doesn’t matter either way, as
&mutimplicitly 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 location1. If patterns can be written to always use&unless mutation is required, then the amount of editing necessary to perform various refactors is lessened. - It’s intuitive.
&mutis strictly more powerful than&. It’s conceptually a subtype, and even if not implemented that way2, coercions mean it often feels like one in practice.
let a: &u8 = &mut 42;
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 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
- 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
- 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 &:
// 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:
&mutpatterns could “strip off” outer&. For example, inlet &mut x = &&mut 42;, the&mutpattern would match the&mutreference in the scrutinee, leaving&to be inherited and resulting inx: &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 refin front of identifier patterns of type&mutthat are behind an&pattern. For example,let &x = &&mut 42;would be transformed intolet &&mut ref x = &&mut 42;.- The full desugaring would be more complicated, as it would need to handle
@patterns.
- The full desugaring would be more complicated, as it would need to handle