# Design Meeting: Match Ergonomics 5 ## Context At the beginnings of rust, patterns had to correspond to the matched type reference-for-reference, with patterns like `&(x, ref y)`. This form is what we call a "fully explicit"/"fully desugared" pattern. `x`/`ref x`/`ref mut x` is called the "binding mode" of a binding. [RFC 2005 "Match ergonomics"](https://rust-lang.github.io/rfcs/2005-match-ergonomics.html) allowed patterns to omit `&`/`&mut`, causing a "default" binding mode to be deduced from the type. That default can still be overriden with an explicit `ref x`/`ref mut x`. After many years of experience with RFC2005 match ergonomics, two issues have come up many times, prompting us to rethink match ergonomics: - The surprising ["`mut` resets binding mode"](https://github.com/rust-lang/rust/issues/105647) behavior; - If `SomePattern(x)` gives `x: &T`, it's not always possible to write `SomePattern(&x)` to get `x: T` ([relevant issue](https://github.com/rust-lang/rust/issues/64586)). ## Current status We have had a fair amount of previous discussions on the topic: - 2023-12-13 design meeting: https://hackmd.io/YLKslGwpQOeAyGBayO9mdw - 2024-05-15 design meeting: https://hackmd.io/9SstshpoTP60a8-LrxsWSA - 2024-06-07 design meeting: https://hackmd.io/_ey51TmXRqyHq1fQ_t9pmQ - 2024-09-18 accidental design meeting: https://hackmd.io/@rust-lang-team/ryw4jv_a0#Vibe-check-Stabilize-RFC-3627 - Accepted RFC3627: https://github.com/rust-lang/rfcs/pull/3627 - Proposal to amend RFC3627: https://github.com/rust-lang/rust/issues/130501 - TC's big book of match ergonomics proposals: https://hackmd.io/zUqs2ISNQ0Wrnxsa9nhD0Q - My personal document with some design axioms: https://hackmd.io/aL5FRz-QTc6K0qtUzPoU9A - We implemented a [maximally future-compatible](https://github.com/rust-lang/rust/pull/131381) version of match ergonomics in edition 2024. It restricts what's allowed so that we can backwards-compatibly implement the changes that follow. - So far I've not heard of it causing much churn (note that the ["migrate compiler to edition 2024"](https://github.com/rust-lang/rust/pull/129636) PR was made _before_ we implemented that change, so we can't use it as data). Our consensus so far (AFAICT): - We're ready to alter how match ergonomics work (this leaves the behavior of fully-desugared patterns unchanged); - We want to fix ["`mut` resets binding mode"](https://github.com/rust-lang/rust/issues/105647); - If `SomePattern(x)` gives `x: &T`, we want `SomePattern(&p)` to be allowed ([relevant issue](https://github.com/rust-lang/rust/issues/64586)), and same for `&mut T`/`&mut p`; - We settled on "eat-one-layer" for the following case: ```rust! if let Some(x) = &Some(&0) { // `x: &&i32` } if let Some(&x) = &Some(&0) { // Today aka eat-two-layers: `x: i32` // eat-one-layer: `x: &i32` } ``` - As a convenience bonus, we want `&p` to be allowed on `&mut T`. Question: do we agree on that? There remain undecided points around: - Further edge-cases within eat-one-layer; - Should we allow `let Some(mut x) = &Some(0)`? - Should we allow `let Some(ref x) = &Some(0)`? what about `let Some(ref x) = &mut Some(0)`? - "Rule 3" i.e. should we downgrade `&mut` inherited references when we know they'll cause a borrow error; I would like to discuss these and find consensus on them. ## Edge cases of eat-one-layer "eat-one-layer" is the idea that: - if `let SomePattern(x): U` gives `x: &T`, `let SomePattern(&p): U` should be allowed for appropriate `p`; - if `let SomePattern(x): U` gives `x: &&T`, `let SomePattern(&x): U` should give `x: &T` regardless of `U` (instead of sometimes `x: T` as is the case in stable rust). This by itself leaves some edge-cases unspecified. Some terminology first: in the presence of a non-`move` default binding mode (aka an inherited reference), there are two types at play: ```rust! let [x]: &[T] = ...; // ^ in-memory type // x: &T // ^ "inherited" reference // ^^ user-visible type ``` Here is the choice to make: should a `&p`/`&mut p` pattern be generally matched against the in-memory type, or the user-visible type? Some points to consider: - Representative cases where the two options differ ([see in my tool, in-memory on the left and user-visible on the right; click on a row to see an explanation of the results](https://nadrieril.github.io/typing-rust-patterns/?q=%5B%26%26mut+x%5D%3A+%26%5B%26mut+T%5D&compare=true&opts2=AQEBAAEBAQEBAgIAAQEBAAEBAAABAAA%3D&opts1=AQEBAgEBAQEBAgIAAAAAAAAAAAAAAAA%3D&mode=compare&do_cmp=true&ty_d=3)): ```rust! let [x]: &[&mut T] = ...; // both : `x: &&mut T` let [&mut x]: &[&mut T] = ...; // in-memory : `x: &T` // user-visible: type error let [&x]: &[&mut T] = ...; // in-memory : `x: &T` because `&p` is allowed to match `&mut T` // user-visible: `x: &mut T` (can't copy `&mut`) let [x]: &mut [&T] = ...; // both : `x: &mut &T` let [&mut x]: &mut [&T] = ...; // in-memory : type error, or fallback to `x: &T` // user-visible: `x: &T` let [&x]: &mut [&T] = ...; // in-memory : `x: &mut T` or `x: &T` depending on rule 3 // user-visible: `x: &T` because `&p` can match `&mut` let [&(mut x)]: &[&T] = ...; // in-memory : error on `mut` binding with inherited reference because we keep the outer inherited reference // user-visible: `x: &T` because we eat the outer inherited reference ``` - `eat-one-layer` is defined and justified in terms of the user-visible type; - Matching the in-memory type first is what has been called "primacy of structural matching"; it's what RFCs 2005 and 3627 do; - "in-memory first" implies that manual monomorphization does not preserve type-checking of patterns: ```rust! fn first<T>(slice: &[T]) -> Option<&T> { match slice { [] => None, [&ref x, ..] => Some(x), } } // => fn first_mono(slice: &[&u8]) -> Option<&&u8> { match slice { [] => None, // `ref` is under a non-`move` default binding mode. Depending on what we choose below for this case: // - option 1: this is an error by itself; // - option 2: type error because `x: &u8`; // - option 3: borrow error because `x: &&u8` was obtained from `&&*(*slice)[0]`, which borrows a temporary. [&ref x, ..] => Some(x), // ERROR } } ``` ## `mut x` on inherited references Question: should we allow `let [mut x]: &[T] = ...;`? (In 2024 so far, this is an error). Per previous considerations, we want `x: &T`; this requires either: 1. Stabilizing a syntax like `mut ref x` or `ref (mut x)` so that this pattern can be desugared; 2. Giving up on the ability to desugar match ergonomics to stable rust syntax. Points to consider: - Option 2 make match ergonomics able to represent more patterns than non-ergonomic patterns; - Desugaring is useful as a way to explain what match ergonomics "really does", but the explanation still mostly works even with that edge case; - The two options are identical wrt implementation complexity, in either case the compiler will have to represent the `mut ref x` case internally. ## `ref x` on inherited references Question: should we allow `let [ref x]: &[T] = ...;` etc? (In 2024 so far, this is an error). There are roughly three options: 1. Always throw an error. 1. Reset the binding mode i.e. override the mutability of the inherited reference; gives `x: &T` here. 1. Create a temporary and borrow it; gives `x: &&T` here. Points to consider: - Option 3 is at odds with how Rust usually does things; - Option 2 is useful for the case of downgrading an inherited `&mut` to `&`: ```rust! let [ref x]: &mut [T] = <expr>; // With option 2: `x: &T` // Without option 2, one would have to write instead: let [&(ref x)]: &mut [T] = <expr>; // or immutably borrow the scrutinee if applicable: let [x]: &&mut [T] = &<expr>; ``` - Options 1 makes pattern macros (particularly with [guard patterns](https://rust-lang.github.io/rfcs//3637-guard-patterns.html)) harder to write; with options 2 and 3, `ref x` is always allowed and gives a `&T`; - Option 3 is more uniform for the purposes of type inference: it treats inherited and normal references the same. so "`ref` always adds a layer of reference", and conversely "removing `ref` always removes a layer of reference". I propose option 2. ## Rule 3 Take the following: ```rust! let &[x]: &&mut [T] = ...; // Desugars to: let &&mut [ref mut x]: &&mut [T] = ...; // borrow error ``` Here `[x]` is matched against type `&mut [T]`, which causes an inherited `&mut T` reference. Because we're under a shared reference, we end up with a borrow error. In RFC3627, TC proposed "rule 3": if the pattern is `&p`, while processing `p` downgrade all inherited references from `&mut` to `&`. The example would desugar instead to: ```rust! let &&[ref x]: &&mut [T] = ...; ``` Some points to consider: - This avoids some borrow errors; - If we allow option 2 for `ref x`, adding a `ref` would suffice to fix the borrow errors in question instead; - This does not solve all similar-looking errors, e.g. `let &[x]: &[&mut T]` is still an error; - This is only relevant when using match ergonomics under explicitly handled references. It does not apply if a pattern is fully explicit or if a pattern is fully match-ergonomic or if a pattern starts ergonomic then adds some explicit dereferences. - [Cases where the two options differ](https://nadrieril.github.io/typing-rust-patterns/?q=%5B%26%26mut+x%5D%3A+%26%5B%26mut+T%5D&compare=true&opts2=AQEBAAEBAQEBAAEAAAAAAAAAAAAAAAA%3D&opts1=AQEBAAEBAQABAAEAAAAAAAAAAAAAAAA%3D&mode=compare&do_cmp=true&ty_d=3&pat_d=3), without rule3 the left and with rule3 on the right, based on my otherwise prefered choices for the options above. You may increase type depth to have more examples. - `let &[&mut x]: &&mut [T]` becomes a type error; - `let &&mut [x]: &&mut [T]` is still allowed, which may feel inconsistent; - This makes typechecking less "local"/"compositional": this requires an extra bit of state when formalizing and implementing match ergonomics; - We can leave this undecided for now by erroring in cases where we would downgrade to `&`.