# Match ergonomics redesign My own personal notes. Documents from the successive design meetings: - 2023-12-13: https://hackmd.io/YLKslGwpQOeAyGBayO9mdw - 2024-05-15: https://hackmd.io/9SstshpoTP60a8-LrxsWSA - 2024-06-07: https://hackmd.io/4hJmztFnS_KWG47Zemx-lw - Latest source for accepted rules: https://github.com/rust-lang/rfcs/pull/3627 ## Design axioms The ones that feel most crucial: 1. Patterns do what I mean: writing the right pattern doesn't require sigil-golf to appease the type-checker; 1. Patterns can be fully precise: I can spell out exactly which places I want to access with a pattern; - Consequence: in an unsafe block I have confidence I'm not triggering anything unexpected. 1. References get out of the way: I can write a pattern without `&`/`&mut`/`ref`/`ref mut` and it will do the obvious thing; 1. Patterns are polymorphic: the behavior of a pattern doesn't change if I make a generic type more specific; The ones that feel important to figure out the proposal: 5. References get out of the way: If I don't need mutability, I can treat `&mut T` like it's `&T`; 1. Mutability is predictable: A `&mut` pattern gives mutable access to the place inside it; 1. Mutability is predictable: A `&` pattern prevents mutable access to any place inside it; 1. If `x: &T`, I can copy out the value by writing `&` before the binding; 1. Inherited and real references are indistinguishable; 1. I can make any binding mutable by adding `mut` before it; Vaguer ones: 12. Patterns accomodate semantically-irrelevant code changes; 1. Patterns compose: `if let <pat1>(<pat2>) = <expr>` behaves the same as `if let <pat1>(x) = <expr> && let <pat2> = x`; 1. Patterns mirror expressions; 1. Patterns borrow as little as needed; We mostly have mirroring and composition for patterns without `&`/`&mut`/`ref`/`ref mut` (which I would call "purely structural patterns"). It gets tricky with references. We could also extend these to cover my other pattern work with axioms like: 16. Smart pointers get out of the way: the ergonomic distance between using a reference and a smart pointer is as small as necessary; 1. Patterns accomodate semantically-irrelevant code changes: switching from `Rc` to `Arc` should require minimal code changes; 1. Impossible cases can be omitted: I shouldn't have to write code for cases that are statically impossible; 1. Pattern semantics are predictable: I can tell what data a pattern touches by looking at it. This is crucial when matching on partially-initialized data. ## Typing rules NOTE: this has been superceded by further discussions; it is kept for posterity only. I want to formulate match ergonomics in terms of types, ideally as a typing rule. The rule would look like: `<pat> @ <expr> : <type>` so we can track which place is tested or used for bindings. Given a pattern `p` and a type `T`, we start with `p @ q: T` (where `q` is a dummy scrutinee expression to) and apply the rules. This will either error or assign each binding to an expression and a type. I implemented these rules and variations in a little tool: https://github.com/Nadrieril/typing-rust-patterns. The expression is the only implicit state that is tracked. This is a feature: implicit state is confusing. This means we don't get match ergonomics 2024 rule 3. Interestingly, the expression implicitly tracks the DBM: it can be either a place (corresponding to `move` DBM), `&<place>` (corresponding to `ref` DBM) or `&mut <place>` (corresponding to `ref mut` DBM). Note that we only inspect the expression for bindings, never in the middle of a pattern. Note: a rule looks like: ``` <precondition1>, <precondition2>, ... ------------------------------------- "Rule name" <postcondition>, <side conditions> ``` It is applied bottom-to-top. Read the rule like "to get `bottom thing`, if side conditions apply, we need `top things` to be (recursively) true". At most one rule should apply to any given case; if no rule applies, then the pattern is not valid for that type. The example derivations below should help understand how this works. ```rust //! Syntax of patterns: //! - `(mut)? (ref)? (mut)? x`: a binding; //! - `[p]`: a tuple with one elements; //! - `[p0, p1]`: a tuple with two elements; //! - `&p`, `&mut p`: dereferencing patterns. //! //! We use tuples for illustration; this applies identially to any adt patterns. //// Constructor rules /// Matching a constructor against its type p0 @ q.0: T0, p1 @ q.1: T1, .. -------------------------------- "Constructor" [p0, p1, ..] @ q: [T0, T1, ..] /// Matching a constructor against a reference p0 @ &(*q).0: &T0, p1 @ &(*q).1: &T1, .. ------------------------------------------ "ConstructorRef" [p0, p1, ..] @ q: &[T0, T1, ..] p0 @ &mut (*q).0: &mut T0, p1 @ &mut (*q).1: &mut T1, .. ---------------------------------------------------------- "ConstructorRef" [p0, p1, ..] @ q: &mut [T0, T1, ..] /// Matching a constructor against nested references [p0, ..] @ *q: &T ----------------- "ConstructorMultiRef" [p0, ..] @ q: &&T [p0, ..] @ &**q: &T --------------------- "ConstructorMultiRef" [p0, ..] @ q: &&mut T [p0, ..] @ *q: &T --------------------- "ConstructorMultiRef" [p0, ..] @ q: &mut &T [p0, ..] @ &mut **q: &mut T --------------------------- "ConstructorMultiRef" [p0, ..] @ q: &mut &mut T //// Dereferencing rules p @ *q: T ---------- "Deref(EatOuter)" &p @ q: &T p @ *q: T ------------------ "Deref(EatOuter)" &mut p @ q: &mut T // If we allow using a `&` pattern on `&mut` (aka match ergo 2024 rule 5). // The reborrow prevents `&(ref mut x)`, see the "Mutability is predictable" axiom. &p @ &*q: &T -------------- "DerefMutWithShared(EatOuter)" &p @ q: &mut T //// Binding rules // Success case let x: T = q ------------ "Binding" x @ q: T // The extra constraint on `q` is because we inspect the DBM. // If we removed the constraint we'd get the more natural behavior of "just working". let mut x: T = q ---------------------------------- "Binding" mut x @ q: T, q is not a reference // Stable behavior: `mut x` resets the DBM. // Error instead for match ergo 2024 rule 1. mut x @ q: T -------------- "MutBindingResetBindingMode" mut x @ &q: &T // Stable behavior: `mut x` resets the DBM. // Error instead for match ergo 2024 rule 1. mut x @ q: T ---------------------- "MutBindingResetBindingMode" mut x @ &mut q: &mut T // The extra constraint on `q` is to avoid referencing a temporary. x @ &q: &T ---------------------------------- "BindingBorrow" ref x @ q: T, q is not a reference x @ &mut q: &mut T -------------------------------------- "BindingBorrow" ref mut x @ q: T, q is not a reference // Stable behavior: `ref x` resets the DBM. ref x @ q: T -------------- "RefBindingResetBindingMode" ref x @ &q: &T ref x @ q: T ---------------------- "RefBindingResetBindingMode" ref x @ &mut q: &mut T ref mut x @ q: T ------------------ "RefBindingResetBindingMode" ref mut x @ &q: &T ref mut x @ q: T -------------------------- "RefBindingResetBindingMode" ref mut x @ &mut q: &mut T ``` Compared to the rules in https://github.com/rust-lang/rust/issues/127559: - Rule 1: :heavy_check_mark:; - Rule 2: partially: we do get `[&x]: &[&T] => x: &T` but not `[&mut x]: &[&mut T]`; - Rule 3: partially: we accept `&[[&x]]: &[&mut [T]]` but not `&[[x]]: &[&mut [T]]`; - Rule 4: I _think_ we do "rule 4 early", see the below derivations; - Rule 5: :heavy_check_mark:. ### Example derivations - `Some((&x, ref y)): &Option<(T, U)>` => `x: T`, `y: &U` ```rust Some((&x, ref y)) @ p: &Option<(T, U)> // <= (constructor rule) (&x, ref y) @ &(*p as Some).0: &(T, U) // <= (constructor rule) &x: &(*&(*p as Some).0).0: &T ref y: &(*&(*p as Some).0).1: &U // <= (dereferencing rule on the first one) x: *&(*&(*p as Some).0).0: T ref y: &(*&(*p as Some).0).1: &U // <= (binding rules on both, assuming the version of today's rust) let x: T = *&(*&(*p as Some).0).0 let y: &U = &(*&(*p as Some).0).1 // Final bindings (simplified): let x: T = (*p as Some).0.0; let y: &U = &(*p as Some).0.1; ``` - `[&x]: &[&T]` => `x: &T` ```rust [&x] @ p: &[&T] // Applying rule `ConstructorRef` &x @ &(*p).0: &&T // Applying rule `Deref(EatOuter)` x @ *&(*p).0: &T // Applying rule `Binding` let x: &T = *&(*p).0 // Final bindings (simplified): let x: &T = (*p).0; ``` - `[&x]: &[&mut T]` => `x: &mut T`, move error ```rust [&x] @ p: &[&mut T] // Applying rule `ConstructorRef` &x @ &(*p).0: &&mut T // Applying rule `Deref(EatOuter)` x @ *&(*p).0: &mut T // Applying rule `Binding` let x: &mut T = *&(*p).0 // Final bindings (simplified): let x: &mut T = (*p).0; // Borrow-check error: CantCopyRefMut ``` - `[&&mut x]: &[&mut T]` => `x: T` ```rust [&&mut x] @ p: &[&mut T] // Applying rule `ConstructorRef` &&mut x @ &(*p).0: &&mut T // Applying rule `Deref(EatOuter)` &mut x @ *&(*p).0: &mut T // Applying rule `Deref(EatOuter)` x @ **&(*p).0: T // Applying rule `Binding` let x: T = **&(*p).0 // Final bindings (simplified): let x: T = *(*p).0; ``` - `[&mut x]: &mut [&T]` => `x: &T` ```rust [&mut x] @ p: &mut [&T] // Applying rule `ConstructorRef` &mut x @ &mut (*p).0: &mut &T // Applying rule `Deref(EatOuter)` x @ *&mut (*p).0: &T // Applying rule `Binding` let x: &T = *&mut (*p).0 // Final bindings (simplified): let x: &T = (*p).0; ``` - `[&mut x]: &[&mut T]` => type error ```rust [&mut x] @ p: &[&mut T] // Applying rule `ConstructorRef` &mut x @ &(*p).0: &&mut T // Type error for `&mut x @ &(*p).0: &&mut T`: MutabilityMismatch ``` - `&[[x]]: &[&mut [T]]` => `x: &mut T`, borrow error ```rust &[[x]] @ p: &[&mut [T]] // Applying rule `Deref(EatOuter)` [[x]] @ *p: [&mut [T]] // Applying rule `Constructor` [x] @ (*p).0: &mut [T] // Applying rule `ConstructorRef` x @ &mut (*(*p).0).0: &mut T // Applying rule `Binding` let x: &mut T = &mut (*(*p).0).0 // Final bindings (simplified): let x: &mut T = &mut (*(*p).0).0; // Borrow-check error: MutBorrowBehindSharedBorrow ``` - `&[[&x]]: &[&mut [T]]` => `x: T`, borrow error if we don't use simplification rules ```rust &[[&x]] @ p: &[&mut [T]] // Applying rule `Deref(EatOuter)` [[&x]] @ *p: [&mut [T]] // Applying rule `Constructor` [&x] @ (*p).0: &mut [T] // Applying rule `ConstructorRef` &x @ &mut (*(*p).0).0: &mut T // Applying rule `DerefMutWithShared(EatOuter)` &x @ &*&mut (*(*p).0).0: &T // Applying rule `Deref(EatOuter)` x @ *&*&mut (*(*p).0).0: T // Applying rule `Binding` let x: T = *&*&mut (*(*p).0).0 // Final bindings (simplified): let x: T = (*(*p).0).0; ``` - `&[[&mut x]]: &[&mut [T]]` => `x: T`, borrow error if we don't use simplification rules ```rust &[[&mut x]] @ p: &[&mut [T]] // Applying rule `Deref(EatOuter)` [[&mut x]] @ *p: [&mut [T]] // Applying rule `Constructor` [&mut x] @ (*p).0: &mut [T] // Applying rule `ConstructorRef` &mut x @ &mut (*(*p).0).0: &mut T // Applying rule `Deref(EatOuter)` x @ *&mut (*(*p).0).0: T // Applying rule `Binding` let x: T = *&mut (*(*p).0).0 // Final bindings (simplified): let x: T = (*(*p).0).0; ``` - `[&ref mut x]: &mut [T]` => `x: &mut T`, borrow error ```rust [&ref mut x] @ p: &mut [T] // Applying rule `ConstructorRef` &ref mut x @ &mut (*p).0: &mut T // Applying rule `DerefMutWithShared(EatOuter)` &ref mut x @ &*&mut (*p).0: &T // Applying rule `Deref(EatOuter)` ref mut x @ *&*&mut (*p).0: T // Applying rule `BindingBorrow` x @ &mut *&*&mut (*p).0: &mut T // Applying rule `Binding` let x: &mut T = &mut *&*&mut (*p).0 // Final bindings (simplified): let x: &mut T = &mut *&(*p).0; // Borrow-check error: MutBorrowBehindSharedBorrow ```