Deref Patterns, requirements and Syntax Tracking Document

Discussion on irlo: https://internals.rust-lang.org/t/somewhat-random-idea-deref-patterns/13813
Discussion on Zulip: https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Deref.20patterns
Older irlo discussion: https://internals.rust-lang.org/t/pre-pre-rfc-match-ergonomics-for-container-types-restricted-method-calls-in-patterns/13371
Also discussed on the box patterns issue: https://github.com/rust-lang/rust/issues/29641

Goal

Be able to match through a Deref or DerefMut smart pointer ergonomically.

let x: Option<Rc<bool>> = ...;
match x {
    Some(deref true) => ...,
    Some(x) => ...,
    None => ...,
}

Syntax options

  • No keyword or sigil at all
  • No keyword or sigil for literals (e.g. matching String with "hello"), keyword or sigil for non-literals (matching String with x and having x work as type &str)
  • Only allow literals, disallow non-literals
  • Sigil *: *<pattern>
  • Sigil &: &<pattern>
    • Overloads the existing & pattern
  • Keyword box: box <pattern>
    • Reuses and generalizes box_pattern syntax
    • This would likely be confusing for non-Box types
  • Keyword deref: deref <pattern>
    • This would require reserving a new keyword
    • Keyword would confict with the exact method this would work with.

Constraints

  • this affects match exhaustiveness. If we don't restrict the Deref impls, then it is unsound to consider the following as exhaustive:
    ​​​​let x: Option<SomePointer<bool>> = ...;
    ​​​​match x {
    ​​​​    Some(deref true) => ...,
    ​​​​    Some(deref false) => ...,
    ​​​​    None => ...,
    ​​​​}
    
  • we can't generally commit to only derefing once per subpattern, because e.g.:
    ​​​​match x {
    ​​​​    Some(deref 0) => ...,
    ​​​​    Some(deref mut 1) => ...,
    ​​​​    Some(deref 2) => ...,
    ​​​​}
    
    • Note: this case could be implemented as a single deref_mut, then reborrowing. However, this would not work in a case like this:
      ​​​​​​​​match &mut x {
      ​​​​​​​​    Some(deref v@0..3) => ..., // (1)
      ​​​​​​​​    Some(deref mut v@4..7) => ..., // (2)
      ​​​​​​​​    v@&None => ..., // (3)
      ​​​​​​​​    Some(deref v@8..11) => ..., // (4)
      ​​​​​​​​}
      
      • The above requires at least one deref_mut and one deref. Depending on the Deref impl, the shared borrow in pattern 3 may invalidate the mutable reference used in 1 and 2 (at the very least, for wrapper types like ManuallyDrop)
      • However, when x is immutable, it should be valid to only deref once.
  • if we allow side-effecting Deref impls, then people may start depending on the number and order of the deref calls, like they depended on drop order even if it was supposed to be unspecified.
    • Lang Team only wants "pure" Deref impls. But what about impure Deref that are idempotent (like Lazy/SyncLazy)
  • if we want the "no syntax" option, the type of x in Some(x) and Some(x @ true) could be different
    • that already happens with & and match ergonomics I think
  • some types can both be destructured and have a Deref impl
    • e.g. Cow and AssertUnwindSafe
    • outright ambiguous in the "no syntax" case:
      ​​​​​​​​let cowcow: Cow<Cow<bool>> = ...
      ​​​​​​​​match &cowcow {
      ​​​​​​​​    Cow::Owned(x) => {} // what's the type of `x`?
      ​​​​​​​​}
      
    • How would exhaustive matching handle mixing deref patterns with destructuring
      ​​​​​​​​let unwind_safe: AssertUnwindSafe<bool> = ...;
      ​​​​​​​​match &unwind_safe{
      ​​​​​​​​    AssertUnwindSafe(true) => {},
      ​​​​​​​​    deref false => {},
      ​​​​​​​​    /* Would the above be considered exhasutive */
      ​​​​​​​​}
      
      • Probably would be impossible for user-defined types; may be difficult for little advantage for standard library types (aside from adding #[lang] items to Cow and AssertUnwindSafe).
    • What about Cow when T::Owned is not T (str, [T], custom ToOwned impl)
  • DerefMove still isn't a thing, yet we'd like to accomodate moving out of smart pointers in patterns in the future
    • so we need to be careful when things are owned to not default to taking refs where in the future we would want to move out
    ​​​​let x: Box<Option<bool>> = ...
    ​​​​match x {
    ​​​​    Some(ref x) => {} // ok
    ​​​​    Some(ref mut x) => {} // ok
    ​​​​    Some(x) => {} // should error for now
    ​​​​}
    
  • mutability propagates backwards: the choice of deref vs deref_mut depends on subpatterns
    ​​​​let x: Box<Option<bool>> = ...
    ​​​​match x {
    ​​​​    Some(ref x) => {}
    ​​​​    Some(ref mut x) => {}
    ​​​​}
    
    • what happens if we mix ref and ref mut in a same pattern? Probably using deref_mut is ok.

Possible solutions

  • Restrict to only a chosen set of Standard Library types

    • Current List:

      • Language-level References: &T and &mut T
        • Do we need to allow language-level references in deref patterns (absent generic deref patterns)?
      • Smart Pointers: Box, Arc, Rc
        • Precedent in Special Casing these in the standard library, with reciever types
      • Growable slice collections: Vec, String, OsString, CString, PathBuf
        • Vec and String also would be nice, since the deref targets ([T] and str resp.)
      • Mutex/RefCell guards: Ref, RefMut, MutexGuard, RwLockReadGuard, RwLockWriteGuard
      • Wrapper types: IOSlice, IOSliceMut, ManuallyDrop, AssertUnwindSafe (note: AssertUnwindSafe can be destructured)
        • ManuallyDrop in particular would be nice.
      • Pin<P> where P is a qualifying type
        • Likewise to Box, Arc and Rc, this has precedent in being special-cased in the language.
      • Could Lazy and SyncLazy qualify?
        • The implementations are not pure (can run arbitrary user-provided code), but are idempotent wrt. structure and address (and side effects)
        • Probably
      • Could Cow qualify?
        • Deref is idempotent wrt. structure and address
        • DerefMut is not. The address stability question cannot be resolved. Additionally, it can invoke a user-provided Clone, which is a safe trait and may not produce a structurally equivalent value. Cow does not implement DefefMut so this is not an issue
        • Only an issue when mixing Deref and DerefMut accesses in patterns
        • Cow can qualify for deref patterns, provided a DerefMut impl is not added.
    • Initial Project will persue this solution, providing the list of requirements for a standard library type to qualify, as well as propose an initial list

    • The standard library implementation would provide an unstable trait or attribute or otherwise (at it's option), applied to all qualifying types

  • Require a _ pattern for the match to count as exhaustive

    ​​​​let x: Option<SomePointer<bool>> = ...;
    ​​​​match x {
    ​​​​    Some(true) => ...,
    ​​​​    Some(false) => ...,
    ​​​​    Some(_) => ...,
    ​​​​    None => ...,
    ​​​​}
    
    • people might rely on the order and number of deref calls, which would limit future changes
  • An unsafe DerefPure trait that restricts deref.

    • to make exhaustiveness sound, we need idempotency: successive calls must return "the same thing"
      • need at least structural stability between deref{,_mut} calls without an intervening access through &mut to the pointer or pointee, other than a deref_mut call.
      • this disallows DerefMut for Cow since it must clone which may not preserve structural stability Moot point, Cow is not DerefMut
        • unless we can restrict to structural clones
          • either with a new trait
          • or allowing only auto-derived Clone impls, akin to how only auto-derived PartialEq impls are allowed for constants in patterns
        • unless we disallow mixing deref and deref_mut on a same pointer
      • might also want address stability without an intervening move or access to the pointer through &mut.
        • would be useful for other optimizations too
      • could have stronger requirements like outright purity
        • would prevent Lazy
        • unsure what the benefits are
    • Some requirements are novel for unsafe traits.
  • restrict to const deref impls

    • Probably not enough to make exhaustiveness sound
      • could modify a &mut ref and thus not be idempotent
      • maybe enough if we don't allow DerefMut?
    • May be overly restrictive (e.g. a Lazy that reads a file)
    • Currently disqualifies all standard library smart pointers, due to not permitting raw pointer dereference.
Select a repo