Description
openedon Sep 18, 2024
Context
RFC3627 set out to improve some unintuitive edges of match ergonomics. The subtlest part involves fixing this case:
let [x]: &[&T] = ...;
// x: &&T
let [&x]: &[&T] = ...;
// x: T
where a &
pattern appears to remove two layers of references. T-lang agreed on the desireability of "eat-one-layer" instead, namely that a &
pattern should only ever remove one layer of reference.
For that, the RFC proposes rule 2: "When a reference pattern matches against a reference, do not update the default binding mode". While this is arguably a straightforward change from an implementation perspective, let me show you that it does not appropriately solve the problem we set out to solve from a language perspective.
Issue
The reason is simple: there are two references at play in &&T
; with rule 2 we match the pattern against the inner one of these. Some consequences (you can try these out in my online tool which can run both TC's and my solvers; just note that rule4-early has bugs when combined with rule 5):
- The mutability that matters is the inner one:
let [x]: &[&mut T] = ...;
// x: &&mut T
let [&x]: &[&mut T] = ...;
// with rule 2: Type error
// with rule 4E: x: &mut T + borrow checking error
let [&mut x]: &[&mut T] = ...;
// with rule 2: x: &T
// with rule 4E: type error
let [x]: &mut [&T] = ...;
// x: &mut &T
let [&x]: &mut [&T] = ...;
// with rule 2: `x: &mut T`, which causes a borrow-checking error
// with rule 4E: type error
let [&mut x]: &mut [&T] = ...;
// with rule 2: Type error
// with rule 4E: x: &T
- References are considered inherited when they shouldn't, which is visible with
mut
orref
bindings:
let [&x]: &[&T] = ...;
// with rule 2: x: &T
// with rule 4E: x: &T
let [&ref x]: &[&T] = ...;
// with rule 2: x: &T because the reference was considered inherited and `ref x` overrides that
// with rule 4E: x: &&T
let &[x]: &[&T] = ...;
// with rule 2: x: &T
// with rule 4E: x: &T
let &[ref x]: &[&T] = ...;
// with rule 2: x: &&T because the reference was not considered inherited
// with rule 4E: x: &&T
- Combined with the rest of RFC3627, we get weirdly inconsistent behaviors such as:
let [&mut (ref x)]: &mut [&mut T] = ...;
// RFC3627: `x: &T` because we got an inherited `&mut` and `ref` overrode it
// with rule 4E: x: &&mut T
let [&mut (ref x)]: &mut [& T] = ...;
// RFC3627: `x: &&T` because the mutability mismatch triggered rule 4 instead
// with rule 4E: x: &&T
In short: rule 2 does "eat-one-layer" but eats the wrong layer. The fix is simply to eat the other one. In the language of RFC3627: "when the binding mode is ref or ref mut, match the pattern against the binding mode as if it was a reference"; this has been called "rule 4-early" in our discussions.
Edition
RFC3627 proposed to enable rules 1 and 2 over the edition. I propose to instead enable rules 1 and 4-early over the edition. Note that rule 4-early also replaces rule 4.
While I'm at it I would like to add a small additional rule, to enable fixing all the surprises:
- Rule 1.5: When the DBM (default binding mode) is not
move
, writingref
on a binding is an error.
We can always revert to the previous behavior (i.e. ref x
swallows a reference if it is inherited) if we wish to later.
cc @traviscross