---
title: "Design meeting 2024-11-27: Match ergonomics part 4"
tags: ["T-lang", "design-meeting", "minutes"]
date: 2024-11-27
discussion: https://rust-lang.zulipchat.com/#narrow/channel/410673-t-lang.2Fmeetings/topic/Design.20meeting.202024-11-27
url: https://hackmd.io/4vY9b9UKTVGzajcORceh-A
---
# Design Meeting: Match ergonomics part 4
## 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 desirable 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` (aka RFC3637's "rule 5").
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?
- in-memory: a reference pattern should be checked against the in-memory type. This is what RFC3637 proposes;
- user-visible: a reference pattern should be checked against the in-memory type. This is what's been called "rule 4-early".
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`) <-- ndm: this means you get an error? tm: yes, that's what the tool shows on this row
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 `&`.
## TC's thoughts
TC: Having written the "Part 3" design document and the rules for RFC 3627, I'll say that at this point I've warmed to a point of ambivalence on Rule 4E (that is, the "inherited lifetimes" and whatnot that Nadri proposes) in place of Rule 2 and Rule 4X. I still think we should adopt Rule 3 and Rule 5.
If we feel warmly about Rule 4E, here's the specific proposal I would adopt.
(See [here](https://hackmd.io/zUqs2ISNQ0Wrnxsa9nhD0Q) for notes on this notation, see [here](https://github.com/rust-lang/rfcs/blob/master/text/3627-match-ergonomics-2024.md#the-rules-in-brief) for the rules we adopted in RFC 3627, and see [here](https://github.com/rust-lang/rust/pull/131381#issue-2571622044) for the rules we adopted for Rust 2024.)
### RFC 3627 modulo Rule 4E
He we adapt RFC 3627 with *Rule 4E* (early) in place of *Rule 4* and *Rule 2*.
- **Rule 4E**: If the DBM is `ref` or `ref mut`, match a reference pattern against the DBM as though it were a type before considering the scrutinee.
Nadri: in terms of the options above, this is:
- "user-visible" for `&p` on `&&T`
- option 2 for `ref x` on inherited reference
- don't allow `mut x` on inherited reference
- yes to rule 3
```rust
// - `$ident` is an identifier.
// - `$pat` is a pattern.
// - `T`, `U` are types.
//
// Without loss of generality, we assume that `[..]` is the only
// constructor and that it has only arity of one.
enum BindingMode {
Move,
RefMut,
Ref,
}
struct DBM(mode: BindingMode, max_mode: BindingMode);
/// Unwrap
DBM(mode, max) ⊢ $pat : T
----------------------------
DBM(mode, max) ⊢ [$pat]: [T]
/// Pass `&mut` w/Rule 3
DBM(RefMut, RefMut) ⊢ [$pat]: T
-----------------------------------------
DBM(Move, Move | RefMut) ⊢ [$pat]: &mut T
DBM(RefMut, RefMut) ⊢ [$pat]: T
------------------------------------
DBM(RefMut, RefMut) ⊢ [$pat]: &mut T
DBM(Ref, Ref) ⊢ [$pat]: T
-------------------------------------
DBM(Move | Ref, Ref) ⊢ [$pat]: &mut T
/// Pass `&`
DBM(Ref, Ref) ⊢ [$pat]: T
--------------------------
DBM(_, _) ⊢ [$pat]: &T
/// Mutable deref w/Rule 4E
DBM(Move, max) ⊢ $pat: T
----------------------------------
DBM(Move, max) ⊢ &mut $pat: &mut T
DBM(Move, RefMut) ⊢ $pat: T
----------------------------------
DBM(RefMut, RefMut) ⊢ &mut $pat: T
/// Shared deref w/Rule 4E
DBM(Move, Ref) ⊢ $pat: T
--------------------------
DBM(Move, _) ⊢ &$pat: &T
DBM(Move, Ref) ⊢ $pat: T
-------------------------
DBM(Ref, Ref) ⊢ &$pat: T
/// Rule 5 w/Rule 4E
DBM(Move, max) ⊢ &$pat: &T
------------------------------
DBM(Move, max) ⊢ &$pat: &mut T
DBM(Ref, Ref) ⊢ &$pat: T
------------------------------
DBM(RefMut, RefMut) ⊢ &$pat: T
/// RefMut binding w/Rule 1
DBM(Move, max) ⊢ $ident: &mut T
---------------------------------------
DBM(_, max) ⊢ ref mut $ident: T
/// Ref binding w/Rule 1
DBM(Move, max) ⊢ $ident: &T
-------------------------------
DBM(_, max) ⊢ ref $ident: T
/// Mutable binding w/Rule 1
DBM(Move, max) ⊢ $ident: T
------------------------------
DBM(Move, max) ⊢ mut $ident: T
/// Apply DBM
DBM(Move, max) ⊢ ref mut $ident: T
------------------------------------
DBM(RefMut, max) ⊢ $ident: T
DBM(Move, Ref) ⊢ ref $ident: T
------------------------------
DBM(Ref, Ref) ⊢ $ident: T
/// Binding
---------------------------
DBM(Move, Move) ⊢ $ident: T
----------------------------------- where T: Copy
DBM(Move, RefMut | Ref) ⊢ $ident: T
```
---
## Nadri's thoughts
If we're listing our preferences, my own is:
- "user-visible" for `&p` on `&&T`
- option 2 for `ref x` on inherited reference
- allow `mut x` on inherited reference, don't care about desugaring
- no to rule 3
# Discussion
## Attendance
- People: TC, nikomatsakis, tmandry, Josh, scottmcm, pnkfelix, Nadri, cramertj, Xiang
## Meeting roles
- Minutes, driver: TC
## Loosening Rule 1C (clarification)
TC: Above, Nadri proposes to loosen Rule 2 such that we allow overriding the default binding mode when given `ref` when the DBM is `ref mut`. E.g.:
```rust
let [ref x] = &mut [T]; // x: &T
```
My feeling is that once we do this, we might as well just go back to Rule 1 and allow overriding the binding mode by explicitly specifying `ref` and `ref mut` in general. I don't see a strong reason to not do this (for consistency) once we've reclosed the door to creating "double references" (as with the Option 3 that Nadri mentions).
Nadri: I don't see how that differs from what I'm proposing as "Option 2".
TC: OK. So you'd repropose a clean Rule 1 then.
Nadri: I forget the exact phrasing of your rules, but I suspect so.
TC: From RFC 3627 (as a diff to the Rust 2021 rules):
> 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.
Nadri: uhh so this is about `mut`, but you included also `ref` in your modified version of this rule (1C), so I guess we _are_ coming back to that
Nadri: maybe that's clearer: in terms of diff with rust 2021, "Option 2" is saying "keep the 2021 behavior wrt `ref x` patterns".
TC: And `ref mut` ones.
Nadri: indeed
## Loosening Rule 1C (what to do)
TC: Rust 2024 disallows this (which Rust 2021 allows):
```rust
let [ref x] = &mut [T]; // x: &T
```
```rust
let [ref mut x] = &mut [T]; // x: &mut T
```
## Clarification on eat-one-layer / Whether to adopt Rule 4E
Nadri: most improtant to me is line 62:
> 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?
>
> - in-memory: a reference pattern should be checked against the in-memory type. This is what RFC3637 proposes;
> - user-visible: a reference pattern should be checked against the user-visible type. This is what's been called "rule 4-early".
```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`) <-- ndm: this means you get an error? tm: yes, that's what the tool shows on this row
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
```
nikomatsakis: This part I feel fairly sure about, and it argues for "user-visible". I want to teach people by saying:
> * Patterns are (mostly) constructors and bindings, e.g., `Some(x)` or `[x]`.
> * Your IDE will show you the types on the bindings that result: in short `&` and `&mut` are propagated inward. If you are not seeing the types you want, then think in terms of balancing:
> * put an `&` (or `&mut`) in the pattern to "take an `&` (or `&mut`) away" (dereference)
> * put an `&` (or `&mut`) on the RHS to "add an `&` (or `&mut`)" (reference)
> * Add `mut` to make the binding assignable without altering its type (or gets an error)
```
Pat = Constructor ( Pat* )
| Constructor { (field: Pat)* }
| [mut?] lv
| & Pat
| &mut Pat
```
Nadri: some design axioms around this:
1. If `x: &T`, I can copy out the value by writing `&` before the binding;
2. Patterns are polymorphic
Niko: OK so this example:
```rust!
let [x]: &[&mut T] = ...;
// both : `x: &&mut T`
let [&mut x]: &[&mut T] = ...;
// in-memory : `x: &T`
// user-visible: type error // <-- aligns with my intuition, which is that you start with the "outer types" and unwind them
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` tho) <-- ndm: this means you get an error? tm: yes, that's what the tool shows on this row
```
NM: I don't expect this to work and so I don't expect `[&mut x]: &[&mut T]` to work:
```rust
let &mut x : &u32
```
cramertj: this is what I meant by not distinguishing `*` on the rhs being confusing
NM: ah I see, yes, if you translate `&` and `&mut` to `*Deref::deref(x)` and `*DerefMut::deref_mut(x)` then an error would be expected
```rust
DerefMut::deref_mut(&42u32)
```
cramertj: it just so happens that `*` chooses to require either `Deref` or `DerefMut` based on what requirements the user has, but there isn't an equivalent pattern-position thing (both `&` and `&mut` patterns specify the mutability of referent)
tmandry: how should they get a reference to `T`?
nikomatsakis: don't :P i.e., there is no convenient way to go from `&&mut T` to `&T` inside of this subset, you would need to add `ref` bindings. I am ok with that but it's a downside.
cramertj: An alternative is to allow `let &&x = &&mut val;`, and this copies.
nikomatsakis: that copies `T` out, no? that's not what tmandry asked for, which is `let &&mut (ref x) = val`
cramertj: Yes, or `let &&(ref x) = val`
Nadri: or just use `x: &&mut T` as-is and let coercions give you `&T`
nikomatsakis: Revised minimal starting point that I think should have top priority:
> * Patterns are (mostly) constructors and bindings, e.g., `Some(x)` or `[x]`.
> * Your IDE will show you the types on the bindings that result: in short `&` and `&mut` are propagated inward. If you are not seeing the types you want, then think in terms of balancing:
> * put an `&` (or `&mut`) in the pattern to "take an `&` (or `&mut`) away" (dereference)
> * put an `&` (or `&mut`) on the RHS to "add an `&` (or `&mut`)" (reference)
> * You match types from "outside-in", so given an `&T` and `&mut T`, you write `&Pat` and `&mut Pat` to discharge them
> * `let [x] : &[&mut u32]` -- `x: &&mut u32`, so writing `&x` gets `&mut u32` (but it errors)
**THEREFORE** I think we should match against user-visible.
```
Pat = Constructor ( Pat* )
| Constructor { (field: Pat)* }
| lv
| & Pat
| &mut Pat
```
Nadri: a different way to see it is composability:
```rust!
let [x] : &[&mut u32] = ...;
let &y = x;
// should work like:
let [&y] : &[&mut u32] = ...;
```
TC: In my view, the key thing, in terms of the mental model, is whether you think of the default binding mode as a bit of state you carry forward or whether you view it as "sliding" a reference across the type.
Nadri: I agree that RFC2005 gives us something consistent. The premise of this thing is that we wanted to add more `&` in places we couldn't before. What we aimed to solve is that, the premise users are confused about, is putting the `&` where they think they can. RFC XXX doesn't properly solve that. They want the user-visible type but RFC XXX37 doesn't do that.
tmandry: I think we did intend to solve it, but nadri I think you are saying that the *way* we solved it is correct, and doesn't align with user's expectations. I see this distinction between "matching from the outside in always" versus "primary of structural matching". The way I'm framing it in my head is do we want a simpler mental model or do we want "do what the user means", giving them fewer errors and annoying edge cases to work around. I see the argument for both.
Josh (from comments): In terms of trying to reach a (partial) consensus:
- Full support for eat-one-layer; no concerns there.
- General support for the rules Niko and others are stating here, that you should be able to do "balancing" to get the type you want.
- Objection to having `mut x` ever produce a mutable `&T` binding without being explicit.
- Objection to having any patterns written using match ergonomics that can't be written using explicit binding modes.
Josh: Also, I'd have a preference for `mut (ref x)` over `mut ref x`, because the latter seems ambiguous with `ref mut x`.
NM: I agree with your framing TC -- I generally think that weighing "type" against "pattern" is aligned with how user's think about it, and that fewer rules will come to a 3-tuple including default-binding-mode intuitively.
FK: I think we are all generally aligned with user-visible. I think best argument for primary of structural matching is sort of a principled CS approach of like "you have to match the types". I don't think it's what users will want.
JT: Over time. Prior discusson was confusing in terms of what the various models are. Tyler made a distinction between "simple mental model" and "do-what-the-user-wants" but I think users want a simple mental model, and we should hesitate to assume that a more complex model is more what users want. Also, we should get back to trying to establish partial consensus.
TC: If I were to defend the primacy of structural matching, I'd frame it in terms of... well, pattern matching, in a very literal sense. One matches up "the same things in the same places on the LHS and the RHS", where "the same places" is defined according to the structural boundaries. There is, I think, a kind of visceral intuition here. It's what I leaned into in the "Part 3" design document.
TC: But I'm cursed by knowledge on this one by having dug so deeply into this. I think we can make a great case for either one. At this point I'm pretty happy with either, and I don't think we can make a bad call here.
## Examples of "makes pattern macros harder to write"
Josh:
> - 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`;
Examples of this?
Nadri:
```rust!
macro_rules! hashmap {
($e:expr : $p:pat) => {
(ref x if let Some($p) = x.get($e)) // guard pattern
}
}
fn foo() {
let mut x = HashMap::new();
x.insert("a", 42);
match x {
// works
hashmap!{"a": 42} => ...,
...
}
match &x {
// works
hashmap!{"a": 42} => ...,
...
}
let opt_map = Some(x);
match &opt_map {
// works with options 2 and 3, would break with option 1
Some(hashmap!{"a": 42}) => ...,
...
}
}
```
Nadri: not like you need guard patterns to trigger this, I just don't know a compelling case for pattern macros without them off the top of my head.
## Evaluating the null option
Josh: With the exception of the question/discussion above about pattern macros, it doesn't feel like the options being presented here are supported by explanations of why the option makes code wildly easier to write, or more understandable, or otherwise worth the complexity and surface area of more complex rules here. The current Rust 2024 plan of "you can't mix match ergonomics and explicit `ref`/`mut`/`ref mut` *at all*" feels really simple and understandable by comparison.
I'm not ruling out the possibility that we should consider additional complexity here, but it feels like we've dived directly to "what" and "how" in great detail, and skipped right pas "why".
TC: There is still the motivation in RFC 3627. What Nadri proposes here can be seen as another ever-so-slightly different path to addressing the same motivations in RFC 3627.
Josh: Clarifying something: *some* of these rules seem like potential simplifications rather than additional complexity. For instance, eat-one-layer *seems* at first glance like a simplification that reduces surprises.
TC: Right. Rules 1-2 in that RFC were the restrictive ones. We adopted modified versions of those, Rule 1C and Rule 2C in Rust 2024 (these were even more restrictive). Then Rules 3-5 in the RFC went the other way in allowing more code. I'd still frame them as simplifications, though, as was discussed in our "Part 3" design meeting.
TC: Similarly, Nadri proposes Rule 4E as a simplification as compared with Rule 4 (adopted in RFC 3627) and Rule 2C (adopted in Rust 2024).
Nadri: what do you mean by "the options being presented here"? all of them? would you rather keep the minimal version implemented today for 2024?
Josh: I'd like to seriously consider keeping the current 2024 behavior, yes, modulo any additions that seem like simplifications rather than additional complexity.
Nadri: what's your criterion for "more complexity" here? in terms of user understandability, I think eat-one-layer is good. in terms of formalizability also.
Josh: I agree with that. But some of the rules (defining interactions between `ref`/`mut`/`ref mut` and match ergonomics) seem like they add complexity rather than removing it. And adding complexity is potentially OK when it's clear what it buys us, but it's not obvious that we *need* it.
Nadri: Josh, same question: are you talking about complexity for user understandability or something else?
Josh: User understandability. The explicit binding modes are easy to understand. Match ergonomics *inherently* adds complexity but tries to DWIM for the user. In simple cases like writing `Some(x)` to match `&Some(Value)` and getting an implicit `ref x`, that seems like relatively little complexity for plenty of user value. But the rules we're getting into here seem like a *lot* of complexity for an unclear amount of user value. I can appreciate the concept of "just add `&`" or "just add `ref`" when you get a type you don't expect, but that's moving in a direction of a system that's hard for users to understand *unless* it happens to reliably do what they want.
Nadri: I somewhat disagree with "explicit binding modes are easy to understand"
Josh: I'm not saying they're always easier to *use*. I'm saying they're easy to *understand*, in the sense of knowing exactly what semantics your written code has and what the compiler will do with it. And trading off between "easy to understand" and "magically easy to use" is something to be done with care, because if the magic fails, the user needs to fall back on understanding.
Nadri: explicit binding modes are semantically predictable yes. match ergonomics feel like autoref/autoderef to me: less explicit, still easy to reason about, and a huge help. tho this does not need any of the additions discussed here tbh
Josh: I agree that match ergonomics are easier to use, at least in simpler cases. The question is then how much we need to allow *mixing* the two.
Nadri: part of my intent with this is that we can make pretty clear typing rules for mixed patterns that can just treat `&T` like `&T` whether or not the reference is inherited. I think that's a win and the underlying rules are elegant.
Nadri: right now, in 2024, we have two almost separate systems for handling references in patterns. eat-one-layer + user-visible makes the two work together seamlessly.
Josh: I'm prepared to believe that may be true. I feel like the document is not justifying that in terms of the behavior the user observes and needs to understand, and I'm not following why it would be the case. I can *hypothesize* things a user might want to write and why the user might want to write them. But it feels like I'm looking at a list of proposed rules and people's recommendations for which rules they like, and trying to infer why they're a good or bad idea, without a clear picture from a *user's* perspective on where and why this comes up, and how it would feel to use the system *without* these rules.
I very much appreciate the argument I think I'm seeing here for "this cleans the system up from a formal logic" perspective; I don't fully *follow* that argument but I'm happy to accept expert consensus from those who do. I'm just not seeing a clear *Rust user* perspective. Does that distinction make sense?
Relatedly, I ran the current 2024 edition migration on some large codebases, and I was really satisfied with the places where it made me change my use of mixed match ergonomics and explicit binding modes.
```diff
fn entry_link_target_bytes(&self, entry: &Self::Entry<'_>) -> ... {
match entry.entry {
- Entry::Link { ref target } => { ... },
+ &Entry::Link { ref target } => { ... },
```
Nadri: that's a very helpful data point, ty
## Rule 3 and non-match ergonomics cases
nikomatsakis: WRT rule 3, the doc says...
- This does not solve all similar-looking errors, e.g. `let &[x]: &[&mut T]` is still an error;
...but is that a requirement? I guess we'd be "special casing" `&mut` but it -- ah, well, ok. I guess I can see why. The idea is that there is no "inherited" scope at all. We could plausibly track the fact that we are under an `&` pattern though.
Nadri: I really don't want to special case `&mut` here, that would break "patterns are polymorphic" and would still not fix more complex cases like `let &[x]: &[MyRef<T>]` with `struct MyRef<T>(&mut T)`
TC: The other point I'd make about...
```rust!
let &[x]: &[&mut T]
```
...is that it's out of scope. Certainly we could do something about it, but the objective here was improving match ergonomics, which that example does not use at all.
cramertj: Conceptually, I think this is similar to:
```rust!
let a = &mut 5;
let x = *&a; // we create a reference, then passing through the `&` conceptually dereferences
```
which is an error today:
```
error[E0507]: cannot move out of a shared reference
--> src/main.rs:3:13
|
3 | let x = *&a;
| ^^^ move occurs because value has type `&mut i32`, which does not implement the `Copy` trait
|
help: consider removing the dereference here
|
3 - let x = *&a;
3 + let x = &a;
|
```
Nadri: yep, pretty much. and the error that rule 3 aims to solve is similarly something like
```rust!
let a = &&mut 5;
let x = &mut **a;
```
## nikomatsakis noodles
nikomatsakis: For this topic I keep coming back to -- what are the set of "axioms" from which the rest of the setup "unspools"?
I like the framing of "user-visible" (or: effective) type vs the "in memory" type. Overall, I think that most things should match against the "user-visible" type and not the "in memory" type. By overriding sense is that users for the most part will not be able to, or even think to try to, understand the rules that deeply, but that when they see `x: &T` in their IDE that they don't want, they will want to put an `&` somewhere to "get rid of it".
I'm a bit unsure how to fit `ref`-behavior in this. Naively, it points to `ref` introducing a temporary ("ref adds a `&`") but that seems to be very rarely what you want. I also think we should be encouraging users to have simpler patterns and more complex scrutinees expressions. In other words, I think that `let Some(x) = &foo()` is preferable to `let &Some(ref x) = foo()`. Users are familiar with borrows and operations in expressions; complex patterns are less familiar and harder to work with.
There seems to be another axis, which is how much to "DWIM". I think the idea of having `&` match against `&mut` comes from this, as does e.g. the `ref` overriding the user-visible type.
In any case I would appreciate a framing that doesn't focus on the individual rules so much as the axioms and which then works out the implications in terms of those changes.
I guess my sense is something like this:
* I want to define a "minimized pattern/expression set" that we teach people to start and which is focused on "user-visible type". Something like
* Patterns are (mostly) constructors and bindings, e.g., `Some(x)` or `[x]`.
* To decide on whether you get a ref or not, think in terms of balancing:
* put an `&` (or `&mut`) in the pattern to "take an `&` (or `&mut`) away" (dereference)
* put an `&` (or `&mut`) on the RHS to "add an `&` (or `&mut`)" (reference)
* Add `mut` to make the binding assignable without altering its type
* I would drive more advanced usage based on "DWIM" as well as things like how much one can write macros and other code that doesn't know the context as well
* `ref` and `ref-mut` bindings override everything else and just refer to memory, that seems overall more likely to be what you want
cramertj: I agree with these principles. I think the tricky bit with `&` vs. `&mut` is that we don't have a mirror for exactly how "dereference mutably" vs. "dereference non-mutably" behave.
```rust!
let &x = y; // this is dereferencing `y` "immutably"
let &mut x = y; // this is dereferencing `y` "mutably"
```
Both are roughly equivalent to `x = *y`, but `x = *y` surface syntax doesn't specify mutability vs. immutability. `Deref` and `DerefMut` do this under the covers, but the behavior isn't user-visible.
nikomatsakis: I'm not clear cramertj on why this matters.
cramertj:
```rust!
let x = &5u8; // some Copy type behind a shared reference
let y = *x; // copies
let &y = x; // copies
let &mut y = x; // error-- doesn't match
```
nikomatsakis: OK. I just don't see that as a particular problem. I think the user is going to write `let y = x` to start. They are going to see an `&` and go "I don't want that" and put an `&` in front of `y`, and it's good. If you had `let z = &mut 22` and then you did `let z1 = z`, you would see `&mut u32` and think "I don't want that" and put an `&mut` in front. And that's good too. Or at least, that's how *I* work right now and would expect others to work. The model is basically "write the pattern ignoring borrows entirely, then add `&` and `&mut` to get what you need".
cramertj: I agree with all of this. I think the intuition I'm trying to suggest is that the binding resulting from an `&lhs`/`&mut lhs` pattern should work equivalently to if the user had dereferenced the "conceptual type" being bound. So `let &&mut x = &&mut 5;` would behave the same as `let x = **(&&mut 5);`. The reason this feels slightly different to me is that `*` does not specify whether it is using `Deref` or `DerefMut`, so it doesn't "feel" as weird as an `&&mut x` pattern.
TC: Note also that an extensive set of goals and design axioms were laid out in our "Part 3" meeting, and these largely still apply here.
https://hackmd.io/_ey51TmXRqyHq1fQ_t9pmQ#Design-axioms
NM: TC, I'll look but I guess what I'm saying is that I'd rather start with those and derive the proposed changes then look at the proposed changes individually. I want to consider different "sets of rules", I guess is what I mean, and see how well I think they hang together overall, I find it hard to evaluate a particular rule in isolation.
TC: Sure, it's just easy to forget our earlier motivations for the general design, which were present both in that doc and in RFC 3627, so I want to be sure we recall those.
Nadri: I in part derived the set of changes from these design axioms: https://hackmd.io/aL5FRz-QTc6K0qtUzPoU9A
Nadri: but I see that they may not be at the level you want
## ref x on inherited references and "Option 3 is at odds with how Rust usually does things"
pnkfelix: Regarding:
> Question: should we allow let [ref x]: &[T] = ...; etc? (In 2024 so far, this is an error). There are roughly three options:
>
> 1,2, [...]
> 3. Create a temporary and borrow it; gives x: &&T here.
>
> [...] Option 3 is at odds with how Rust usually does things
pnkfelix: I wanted to better understand this claim about option 3. I'm not sure if its saying that creating a temporary as a side-effect of pattern-matching is at odds with status quo in Rust (which I would not argue with but am not sure is a terribly strong statement), or if there's some other aspect of this that is at odds with Rust design principles.
pnkfelix: (to my mind, there is a big tension here between 1. "adding `ref` always adds an extra `&` to the type of the introduced binding", which sounds good, and 2. "sometimes adding `ref` into a pattern will cause a new anonymous temporary to be introduced, potentially to the surprise of the unwary programmer" which sounds bad. And I want to understand how best to resolve that tension. I *think* I'd be happy e.g. if it were an compile-time error to have that anonymous temporary be silently introduced, though I haven't played with it yet)
Nadri: my feeling is that "patterns access existing data" is an important implicit principle, and option 3 would break that. I'm quite open to contradition on this
pnkfelix: Do we expect the "patterns access existing data" principle to be upheld going forward, (e.g. with things like deref patterns interacting with a user-provided deref operator?) (Honestly I've forgotten whether a user-provided PartialEq can similarly get called during pattern-match; I know we've taken great pains to forbid that in the past)
Nadri: deref patterns preserve this
## `mut x` on inherited references: requiring `mut (ref x)`
> 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.
Josh: I think we should rule out possibility 2: it should *always* be possible to write any pattern using fully explicit binding modes. For the rest: I don't think we should allow `mut x` in this context and have it give you a mutable `&T` place. I think we should error and tell you that you need to write `let [mut (ref x)]: &[T] = ...;` if you want that.
## Overtime musings
Nadri: ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f552b482a4da5cf9f5ac53070761ebf3))
```rust!
fn main() {
let mut pair = (42, 100);
match &mut pair {
&mut (ref x, _) => {
pair.1 += pair.0 + x; // error
}
}
println!("{pair:?}");
}
```