Stream: project-deref-patterns

Topic: Deref Patterns Syntax


Connor Horman (Apr 19 2021 at 23:05, on Zulip):

I would like to start the discussion on the syntax for Deref patterns.
So there has been a few different syntaxes proposed, which can be divided into:

The last option, using a keyword, would likely require reserving a keyword in a future edition (though using k# syntax or r#$ syntax would be an option in earlier editions), unless we reused the existing box keyword from box_syntax.

Potential sigils that have been considered are a new * sigil, or reusing the & sigil. The no syntax option would be nice for ergnomics, but it cannot be applicable in all cases (like with match ergonomics), so there should still be some syntax.

nikomatsakis (Apr 20 2021 at 19:12, on Zulip):

this seems like a good topic to explore, yes

nikomatsakis (Apr 20 2021 at 19:13, on Zulip):

One consideration: we may eventually want to permit moves out from behind the smart pointer

nikomatsakis (Apr 20 2021 at 19:13, on Zulip):

the box keyword was something we intended to use at one time for this

nikomatsakis (Apr 20 2021 at 19:14, on Zulip):

but I think that it would be very confusing to folks when used on something other than Box

Nick Cameron (Jul 06 2021 at 02:45, on Zulip):

I have been thinking a bit about the syntax. From Esteban's internals thread, there was a desire to use a single keyword/sigil whatever, as opposed to using the smart pointer name (e.g, Rc for deref'ing Rc, etc). Following this line of thought, I think we want to treat all deref patterns as 'pointer-like' as opposed to 'type-like' where we do need to name the type (structs, enums). Therefore, I would like to propose that we extend the use of & in patterns to match a 'pointer' rather than just specifically a borrowed reference. A further analogy is the * operator in expressions, which we use to deref any pointer, not just raw pointers.

Nick Cameron (Jul 06 2021 at 02:48, on Zulip):

IMO, to be correct we must only consider deref patterns where the matched expression is borrowed, e.g., where x: Rc<Foo>, match x would only permit structural matches on Rc, but match &x would permit deref patterns (this makes sense to me because the signature of deref takes &self), this leaves space later to support DerefMove

Nick Cameron (Jul 06 2021 at 02:50, on Zulip):

Example:

fn proposed(a: Rc<Foo>) {
    match &a { // : &Rc<Foo>
        &Foo::Bar => println!("bar"),
        &x => {} // x: Rc<Foo>
    }
}
Nick Cameron (Jul 06 2021 at 02:51, on Zulip):

Note that the & in the first pattern matches & and causes a deref

Nick Cameron (Jul 06 2021 at 02:52, on Zulip):

In the second pattern, we don't apply the deref rules because we don't need to.

Nick Cameron (Jul 06 2021 at 02:55, on Zulip):

Now, consider the match ergonomics rules, I would apply those here as well. Therefore the & is optional in many contexts, so we get:

fn proposed(a: Rc<Foo>) {
    match &a { // : &Rc<Foo>
        Foo::Bar => println!("bar"),
        x => {} // x: &Rc<Foo>
    }
}

Note that the type of x depends on how it is used (binding mode), but it is not really important because in most contexts, &Rc<Foo> coerces to &Foo.

Nick Cameron (Jul 06 2021 at 02:56, on Zulip):

This syntax gives us the desirable syntax for strings, but is not quite the same as the 'no sigil' option

Nick Cameron (Jul 06 2021 at 03:05, on Zulip):

The downside of this approach is that we can't use it on nested pointers, e.g., Option<Rc<Foo>>, however, in this case we can use as_ref to convert to Option<&Rc<Foo>>, and when matching that we could use a pattern like Some(&Foo::Bar). I'm not sure if this is a good thing or a bad thing

Nick Cameron (Jul 06 2021 at 03:08, on Zulip):

Note that this syntax does not subsume box patterns, and I think that is a feature, not a bug since box patterns (and * for Box) are a special case more similar to DerefMove than Deref

Connor Horman (Jul 06 2021 at 10:25, on Zulip):

So, the problem with using & is that it affects existing code.

Connor Horman (Jul 06 2021 at 10:27, on Zulip):

Or.. Hm.

Connor Horman (Jul 06 2021 at 10:30, on Zulip):

I think it may not under this suggestion, but it matches deref patterns less than may be desired. There are a few cases that &impl Deref<Target=T> doesn't coerce to &T, the main one being generic contexts. Whether this is a problem since x can be explicitly be deref-borrowed, I do not know.

Connor Horman (Jul 06 2021 at 10:33, on Zulip):

Note that this syntax does not subsume box patterns, and I think that is a feature, not a bug since box patterns (and * for Box) are a special case more similar to DerefMove than Deref

I don't necessarily see why the syntax used couldn't cause DerefMove for Box or other types in the future. Perhaps not immediately, but my intention is that deref patterns completely replace the totally magic box patterns eventually.

Nick Cameron (Jul 06 2021 at 22:41, on Zulip):

&impl Deref<Target=T> doesn't coerce to &T

could you expand on the generic case please? AIUI the signature of deref is &Self -> &Self::Target, so that would always align with matching the reference version

Nick Cameron (Jul 06 2021 at 22:45, on Zulip):

I don't necessarily see why the syntax used couldn't cause DerefMove for Box or other types in the future. Perhaps not immediately, but my intention is that deref patterns completely replace the totally magic box patterns eventually.

My assumption is that deref patterns for the borrowed and moved cases should have different syntax, otherwise we would need some magic to guess which to apply and I think that would always be unsatisfactory. We do tend to make borrowing distinct from moving in our syntax (consider
the match ergonomics rules as an example, we don't do any additional implicit borrowing, we only apply the rules if the type of the expression being matched is already borrowed. Likewise with implicit deref coercions).

Nick Cameron (Jul 06 2021 at 22:46, on Zulip):

So, I agree that deref patterns should replace box patterns in the long term, but I don't believe they should until we have DerefMove, and this proposal leaves room for that to happen in the future because it only applies to borrowed values in the short term

Nick Cameron (Jul 06 2021 at 22:50, on Zulip):

It might look like:

fn not_proposed_yet(a: Box<bool>) {
    match a {
        &true => println!("t"),  // & cannot be elided because a is not borrowed.
        &false => println!("f"),
    }
}

In this case we use the (implicit for Box) DerefMove impl, and in the match &a case we use the Deref impl

simulacrum (Jul 06 2021 at 22:51, on Zulip):

My feeling is that we shouldn't worry too much about this up front, that is, we likely want "no syntax" similarly to match ergonomics, and there's a lot to be said for that being a big win already, even before we get into stabilization. I think lang discussions (a few months ago now) also moved towards this

simulacrum (Jul 06 2021 at 22:53, on Zulip):

But an explicit syntax is definitely also desired, I think, so figuring that out makes sense - it just doesn't seem like the top concern at this juncture from my understanding.

Nick Cameron (Jul 06 2021 at 22:54, on Zulip):

I think my proposal is pretty much the 'no syntax' proposal, but the question is precisely how that works (and how we support the borrowed vs moving versions of deref in the long term feels important to me)

Nick Cameron (Jul 06 2021 at 22:56, on Zulip):

it just doesn't seem like the top concern at this juncture from my understanding.

@simulacrum What do you think are the major concerns? From reading the comments in various places and the design doc, it seems to me that the syntax and exhaustiveness checking are the open questions.

simulacrum (Jul 06 2021 at 23:02, on Zulip):

My impression is that there is essentially broad agreement on a limited subset for initial stabilization where exhaustiveness isn't a concern, and potentially even no need for an explicit syntax - e.g., "foo" matching for String.

simulacrum (Jul 06 2021 at 23:03, on Zulip):

I think the open questions are around the exact behavior on capture (e.g., marching on String with a @ "foo", does that give you a: String, a: &str, or a: &String?)

Nick Cameron (Jul 06 2021 at 23:07, on Zulip):

initial stabilization

presumably we'd want an RFC and an unstable implementation first?

Nick Cameron (Jul 06 2021 at 23:08, on Zulip):

I think the open questions are around the exact behavior on capture (e.g., marching on String with a @ "foo", does that give you a: String, a: &str, or a: &String?)

I'll start a new thread ...

simulacrum (Jul 06 2021 at 23:24, on Zulip):

I think the current trajectory in lang discussions https://github.com/rust-lang/lang-team/blob/master/design-meeting-minutes/2021-06-30-lang-team-projects.md (though not formally accepted) is that stuff can land as unstable in rustc for experimentation with just a project group in existence. But yeah, before we get to stabilization we'd want an RFC - it just doesn't need to cover all the possible feature area

Nick Cameron (Jul 06 2021 at 23:38, on Zulip):

:+1: cool!

Nick Cameron (Jul 07 2021 at 21:55, on Zulip):

@Connor Horman is there a concrete proposal for any of the alternative syntaxes? I've seen a few examples in the design doc, but not a detailed proposal

Nick Cameron (Jul 07 2021 at 22:01, on Zulip):

ping @Esteban K├╝ber any thoughts on the &/no sigil proposal above? Or about the syntax in general (more recent thoughts than the internals thread)

Connor Horman (Jul 07 2021 at 22:44, on Zulip):

Nick Cameron said:

Connor Horman is there a concrete proposal for any of the alternative syntaxes? I've seen a few examples in the design doc, but not a detailed proposal

Wdym concrete proposals? The design doc lists all of the currently and previously discussed syntax for deref patterns. It has examples of each of the syntaxes being used.

Nick Cameron (Jul 08 2021 at 00:32, on Zulip):

I guess discussion of pros and cons, people's preferences, analysis of edge cases or open questions. I'm basically trying to figure out how to move forward on choosing a syntax to implement

Josh Triplett (Jul 08 2021 at 07:00, on Zulip):

One possibility would be to start out only supporting built-in types and only dereferencing to literals. That much doesn't need syntax.

Nick Cameron (Jul 08 2021 at 08:03, on Zulip):

True, but I think in that case we'd want to think a bit about the next step in parallel with implementation, because I don't think we'd learn much from the implementation about the general case (although of course these things are always full of surprises :-) )

Nick Cameron (Jul 08 2021 at 20:36, on Zulip):

So thinking about the proposal to start implementing an absolutely minimal version, I think there is still the question of whether we require the matched expression to be borrowed, e.g.,

fn question(a: String, b: &String) {
    // match 1
    match a {
        "hello" => {}
        _ => {}
    }
    // match 2
    match &a {
        "hello" => {}
        _ => {}
    }
    // match 3
    match b {
        "hello" => {}
        _ => {}
    }
}

Match 2 and 3 have the same types but different shapes (potentially important, but I don't think it should be). In all matches the pattern has type &'static str.

In my proposal above, match 1 would not be allowed, but matches 2 and 3 would. I think the 'pure' no sigil proposal would allow match 1 too. The reason I don't think it should is that we would have to implicitly borrow a to have the right type for the deref function, and because in the future where DerefMove exists, I would like to apply DerefMove in the case of match 1 and Deref in the cases of 2 and 3.

Mario Carneiro (Jul 08 2021 at 22:33, on Zulip):

The reason I don't think it should is that we would have to implicitly borrow a to have the right type for the deref function,

But match has always allowed the scrutinee to be a place expression, and won't move it out unless the match arm contains a by-move variable binding. For that reason I would expect match 1 to not use DerefMove because it doesn't have any by-move bindings, and indeed you can implement equivalent functionality without having to move a into the match.

rpjohnst (Jul 08 2021 at 22:49, on Zulip):

this case is also weird because string literals have reference type "built in"- it might be easier to work out the rules relative to something like Vec<u8> or Box<i32> first?

Mario Carneiro (Jul 08 2021 at 23:06, on Zulip):

I think that in the simplest, no match ergonomics mode, you should be able to use the * sigil to transform a pattern of type T to U where T: Deref<Target=U> (working from the outside in, i.e. if we need *pat: T then it suffices that pat: U). Whether this calls Deref or DerefMut or DerefMove is determined later by whether the binding at the end is by-move/ref/mut. So for example:

fn proposal(a: Vec<u8>, b: Box<u8>) {
    match a {
        *(ref p) => {} // p: &[u8], a is not moved
    }
    match a {
        *p => {} // p: [u8], unsized local error
    }
    match a {
        *[x, 2] => {} // x: u8, a is not moved
        _ => {}
    }
    match b {
        *(ref mut a) => {} // a: &mut u8, b is mutably borrowed
    }
    match b {
        *a => {} // a: u8, b would normally be moved but not in this case because u8 is copy
    }
}
Mario Carneiro (Jul 08 2021 at 23:12, on Zulip):

Regarding string weirdness, I think the easiest solution is to be able to use "xyz" as a pattern of type either &str or str.

fn proposal2(a: String) {
    match a {
        *(ref p) => {} // p: &str, a is not moved
    }
    match a {
        *"foo" => {} // matching "foo" against a pattern place of type str
        ref x @ *"bar" => {} // x: &String
        *(ref x @ "bar") => {} // x: &str
        x @ *"bar" => {} // x: String, moves a
         _ => {}
    }
    match &*a {
        "foo" => {} // matching "foo" against a pattern place of type &str
        *"bar" => {} // matching "bar" against a pattern place of type str
         _ => {}
    }
}
Nick Cameron (Jul 08 2021 at 23:42, on Zulip):

this case is also weird because string literals have reference type "built in"- it might be easier to work out the rules relative to something like Vec<u8> or Box<i32> first?

Box is also weird because it has implicit/built-in move deref semantics as well as the explicit deref with borrowing semantics. Maybe Rc<i32> is the least weird place to start? (or Vec, though maybe the details of slice patterns will complicate that)

Nick Cameron (Jul 08 2021 at 23:51, on Zulip):

@Mario Carneiro using * as the sigil seems wrong to me - sigils in patterns usually deconstruct what they would construct in expressions, whereas here, the sigil would be doing roughly the same a it does in expression context. I.e., it is going 'the wrong way round'.

Mario Carneiro (Jul 08 2021 at 23:52, on Zulip):

I think you can find/replace & for * in the example if you want to use another sigil

Mario Carneiro (Jul 08 2021 at 23:53, on Zulip):

I am familiar with this reasoning about expression mirroring construction, but you don't use & to build a String, so I have found the analogue somewhat confusing in this context

Mario Carneiro (Jul 08 2021 at 23:54, on Zulip):

I normally associate use of * with calls to deref, so it more naturally springs to mind

Nick Cameron (Jul 08 2021 at 23:54, on Zulip):

Yeah, I think that using & is a stretch, the intuition for me is that & represents a 'pointer' in pattern context, not just a reference

Mario Carneiro (Jul 08 2021 at 23:55, on Zulip):

and it's also conveniently unused in pattern grammar

Mario Carneiro (Jul 08 2021 at 23:56, on Zulip):

I do like that it tends to produce patterns that look like &****(ref a) which are familiar from expression context

Nick Cameron (Jul 08 2021 at 23:56, on Zulip):

The synbtax which most conforms to current syntax, IMO, is to use the pointer name, i.e., use String to deref a String, Box to deref a Box, etc. (possibly with some other sigil or keyword to avoid ambiguity with structural matching), but there was a lot of pushback against that on the old internals thread. If we're using one sigil/keyword for all pointer-like things, then I think & makes sense

Nick Cameron (Jul 08 2021 at 23:58, on Zulip):

Mario Carneiro: I do like that it tends to produce patterns that look like &****(ref a) which are familiar from expression context

IMO this violates the principal that things which look similar should be similar and things which look different should be different. Because in that pattern, & and * are doing basically the same thing

Mario Carneiro (Jul 08 2021 at 23:59, on Zulip):

Is it? Deref does not change from one pointer type to another, it changes one place type to another. & dereferences pointers

Mario Carneiro (Jul 09 2021 at 00:00, on Zulip):

That is, * is being used to go from String to str

Mario Carneiro (Jul 09 2021 at 00:01, on Zulip):

there aren't any references directly involved, so I don't see how & can be made to fit

Mario Carneiro (Jul 09 2021 at 00:03, on Zulip):

If you pattern match directly on a type a: T with a bunch of derefs on it, you get a pattern like ***(ref p) without the initial &. You would only have that if you matched on a: &T

Nick Cameron (Jul 09 2021 at 00:06, on Zulip):

Hmm, that makes sense if you read * as an operator in the pattern (like ref) rather than as constructor (using some hand wavey terminology here)

Mario Carneiro (Jul 09 2021 at 00:08, on Zulip):

Well, I don't have any strong feelings about it. Other sigils work too, and I'm mainly interested in pinning down how the nuts and bolts of the matching itself work, to assist with the implementation side

Nick Cameron (Jul 09 2021 at 00:09, on Zulip):

It feels a bit weird that the deref method gives a &U and we are dereferencing that implicitly, but would require the & if the original type is borrowed. I see the reasoning in that we're treatingit as a place, but it sill seems odd to me, maybe I should just sit with it for a while :-)

Mario Carneiro (Jul 09 2021 at 00:10, on Zulip):

It's because we aren't actually calling deref; we actually don't know whether we'll be calling deref or deref_mut when we do typechecking. It's the same with regular deref coercion

Mario Carneiro (Jul 09 2021 at 00:12, on Zulip):

I haven't treated how match ergonomics interacts with this, or whether there should be additional match ergonomics extensions specifically for deref patterns. It would be cool to be able to insert derefs to fix type errors but I don't know how feasible that is (and it might also be tough to specify)

Connor Horman (Jul 09 2021 at 00:22, on Zulip):

Note: Reguardless of the syntax, an attempt to produce a by-value binding should be ill-formed except for Box (until we have DerefMove)

Mario Carneiro (Jul 09 2021 at 00:36, on Zulip):

Still assuming * as sigil: Match ergonomics ("default binding modes") are triggered when you use a struct pattern like Some(x): &Option<T> (using pat: T to mean that pat is being typechecked with expected type T), which leads to x: T [ref], where [ref] means that x will be changed to a ref binding even if the user doesn't write ref x.

Let's say that we have a goal *pat: T. We check that T has a deref to U. If it supports Deref but not DerefMut, we do pat: U [ref], if it has DerefMut then we do pat: U [ref mut], and if it has DerefMove (aka it is Box<U>) then we just check pat: U. Examples:

fn match_ergonomics(a: String, b: Vec<String>, c: Box<Option<String>>, d: &String) {
    match a {
        *a => {} // a: &mut str
    }
    match b {
        *b => {} // b: &mut [String]
    }
    match b {
        *[b] => {} // b: &mut String
        _ => {}
    }
    match b {
        *[*b, *"foo"] => {} // b: &mut str
        _ => {}
    }
    match b {
        *(ref b) => {} // b: &[String]
    }
    match c {
        *Some(*"foo") => {} // star is required for deref coercion, but does not move c
        _ => {}
    }
    match d {
        *x => {} // x: &String because we deref'd &String and ref-bound the result
        _ => {}
    }
    match d {
        **x => {} // x: &str
        _ => {}
    }
}

I'm not sure I totally like the consequences here, and in particular it would be nice to have x: &str in the second to last example, but &T itself implements deref (to T, of course), so even though we are using a different sigil it doesn't help here to determine that we want to deref T and not &T

rpjohnst (Jul 09 2021 at 00:58, on Zulip):

Nick Cameron said:

It feels a bit weird that the deref method gives a &U and we are dereferencing that implicitly, but would require the & if the original type is borrowed. I see the reasoning in that we're treatingit as a place, but it sill seems odd to me, maybe I should just sit with it for a while :-)

another case of the "treating it as a place" idea is the reasoning that led to &foo[i..j] requiring that "extra" &. at the time there were arguments for bare foo[i..j] (and also foo[i] for consistency, iirc?) to have reference-to-slice type, but in the end what won out was consistency with foo[i] being a place.

IOW it doesn't matter what the deref method (or the index) method returns, because the user isn't actually calling that method- they're using the */[i]/deref-pattern syntax which operates on places, and the method only returns a reference because Rust doesn't have C++-style reference types for places.

rpjohnst (Jul 09 2021 at 01:03, on Zulip):

though OTOH, Rust is already in kind of a weird place with Deref- in one sense it's the overloaded * operator and types that implement it are just "other pointer types" alongside &T, but in another sense things like deref coercion and binding modes mean that types like String or Vec get treated as both "fancy &str/&[T]" and as "owned" things, so things are fuzzy depending on context (e.g. for some fn f(s: &str) and string: String you typically call it with f(&string) -- which looks like a double indirection even though IIUC it desugars to &*string! -- rather than f(string) -- a consistent number of levels of indirection -- like you would if string: &str)

Mario Carneiro (Jul 09 2021 at 01:26, on Zulip):

you typically call it with f(&string) -- which looks like a double indirection even though IIUC it desugars to &*string!

Well, strictly speaking, it (eventually) desugars some more to f(Deref::deref(&string)), which does actually contain a double indirection - the call to deref is taking a &String

Mario Carneiro (Jul 09 2021 at 01:27, on Zulip):

but I guess that probably optimizes away

Nick Cameron (Jul 09 2021 at 02:38, on Zulip):

though OTOH, Rust is already in kind of a weird place with Deref ...

I think this weirdness comes from conflating the deref coercion rules with applying Deref::deref. Looking somewhere without potential for coercion, * strictly converts &T to &<T as Deref>::Target. Taking into account the coercion rules, the intuition is that we can implicitly convert between different forms of borrowed data, but not from owned to borrowed (or vice versa). The rules for receivers are even more flexible though.

Nick Cameron (Jul 09 2021 at 02:46, on Zulip):

It's because we aren't actually calling deref; we actually don't know whether we'll be calling deref or deref_mut when we do typechecking. It's the same with regular deref coercion

I don't understand this. When doing deref coercion, we don't call deref, but we do follow the signature, e.g., we convert &String to &str. Coercion can also do other conversions, such as && to & which are not given by a Deref impl.

Nick Cameron (Jul 09 2021 at 02:59, on Zulip):

But match has always allowed the scrutinee to be a place expression, and won't move it out unless the match arm contains a by-move variable binding. For that reason I would expect match 1 to not use DerefMove because it doesn't have any by-move bindings, and indeed you can implement equivalent functionality without having to move a into the match.

Nick Cameron (Jul 09 2021 at 03:04, on Zulip):

So, extending this to deref patterns. I'll use * (though I'm still not a fan) and assume that DerefMove exists, though I'm not sure it makes sense for String

Nick Cameron (Jul 09 2021 at 03:06, on Zulip):

The question is should *"foo" be a valid pattern for a branch of match a (delaying variables for a minute)

Nick Cameron (Jul 09 2021 at 03:10, on Zulip):

I guess one could argue that since a.deref() would give &str, then it should. OTOH, Deref::deref(a) would be a type error. a is OK, but requires &a in most contexts because str is unsized.

Mario Carneiro (Jul 09 2021 at 03:12, on Zulip):

Nick Cameron said:

It's because we aren't actually calling deref; we actually don't know whether we'll be calling deref or deref_mut when we do typechecking. It's the same with regular deref coercion

I don't understand this. When doing deref coercion, we don't call deref, but we do follow the signature, e.g., we convert &String to &str. Coercion can also do other conversions, such as && to & which are not given by a Deref impl.

Oops, I realize I've been using the wrong word. I am talking about the use of *x when x is not a reference type, for example &*x where x: Rc<u8>. This doesn't call deref so much as construct a place with the target type described by the deref impl; the later use of & or &mut is what determines whether this is eventually desugared to a call to deref or deref_mut. I will call this using the star operator for want of a better name.

Deref coercions are actually talking about how &T "decays" to &U without any syntax when typechecking demands it. In this case we could write the equivalent using the star operator as &**x, and no fancy type mismatch repairs are needed to make this work. The examples I have given thus far haven't been using this at all, but one could potentially use the same method to remove some of the stars from the match_ergonomics() example.

Nick Cameron (Jul 09 2021 at 03:12, on Zulip):

So here we get to an interesting intersection with the syntax: if we use *, then I think it makes sense to accept match a, since the behaviour matches that of the * operator (mapping to *Deref::deref(&a))

Nick Cameron (Jul 09 2021 at 03:13, on Zulip):

But with & it maybe doesn't make sense because that suggests the more regular coercion site rules which don't allow the implicit borrowing

Nick Cameron (Jul 09 2021 at 03:15, on Zulip):

I am talking about the use of x when x is not a reference type, for example &x where x: Rc<u8>. This doesn't call deref so much as construct a place with the target type described by the deref impl; the later use of & or &mut is what determines whether this is eventually desugared to a call to deref or deref_mut. I will call this using the star operator for want of a better name.

I don't think that is quite accurate: see https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#the-dereference-operator, "On non-pointer types *x is equivalent to *std::ops::Deref::deref(&x) in an immutable place expression context and *std::ops::DerefMut::deref_mut(&mut x) in a mutable place expression context."

Nick Cameron (Jul 09 2021 at 03:18, on Zulip):

When doing deref coercion, we don't call deref, but we do follow the signature

To clarify this, it should read "When type checking a deref coercion, the compiler doesn't call deref, but it does follow the signature. deref is called at runtime"

Mario Carneiro (Jul 09 2021 at 03:24, on Zulip):

I'm still trying to understand this properly. AIUI, if a: String, then in match a, a is not a place expression, it's a value expression. We don't necessarily move out of a though, only if the pattern value moves it.

If it was a value expression, then we would always move out of it. For example if you have a; alone on a line then that would count as a use of a and we wouldn't be able to use it afterwards. The only reason match a { _ => {} } doesn't move a out is because it's an lvalue expression. Similarly, if x: &[String, 1000000] then match *x { _ => {} } doesn't copy a million strings on the stack.

The question is should *"foo" be a valid pattern for a branch of match a (delaying variables for a minute)

Today that would cause a type error since "foo" has type &'static str. This is a problem because we can't add more sigils to make that pattern have type &str; it is at best a pattern of type str with default ref binding mode. That's why my suggestion is to just let string literals be used at type str too, although you still have to ref-bind them if you do something like a @ "foo". The default binding mode trick should avoid having to say ref a explicitly in almost all cases though.

Nick Cameron (Jul 09 2021 at 03:27, on Zulip):

AIUI, if a: String, then in match a, a is not a place expression

Whoops, I was being silly and misread the reference, of course a is a place expression

Mario Carneiro (Jul 09 2021 at 03:32, on Zulip):

The issues with string literals not working at type str means that there isn't a pattern you can write in this match today to make it work:

    match *"foo" { // note, we are matching on something of type str
        "foo" => {} // type error
        *"foo" => {} // that's not a thing
        ref a if matches!(a, "foo") => {} // okay fine
        _ => {}
    }
Nick Cameron (Jul 09 2021 at 03:33, on Zulip):

That's why my suggestion is to just let string literals be used at type str too, although you still have to ref-bind them if you do something like a @ "foo". The default binding mode trick should avoid having to say ref a explicitly in almost all cases though.

I agree with this. My question is if we must make the borrow explicit in the scrutinee. And I think that comes down to the syntax, if we use * I think that it is OK, and the pattern should have type str, which we could allow literals to match. If using & then I think we should require a borrowed type

Mario Carneiro (Jul 09 2021 at 03:34, on Zulip):

oh right, do we want people to have to write &mut &mut &mut (ref mut a)? That is really icky

Nick Cameron (Jul 09 2021 at 03:35, on Zulip):

To match what?

Mario Carneiro (Jul 09 2021 at 03:36, on Zulip):

If we are using & as the sigil then it seems like it implies that it is a shared reference. You can't use &x to match on &mut Option<T> after all

Nick Cameron (Jul 09 2021 at 03:37, on Zulip):

You can't use &x to match on &mut Option<T> after all

IMO, this is a mistake and we could fix it as part of this work.

Mario Carneiro (Jul 09 2021 at 03:38, on Zulip):

and even if we say that you have to use &mut for DerefMut patterns, what about DerefMove? box makes a comeback

Nick Cameron (Jul 09 2021 at 03:38, on Zulip):

Using & as the sigil has the mental model that & matches any pointer-like thing in a pattern

Mario Carneiro (Jul 09 2021 at 03:39, on Zulip):

I'm pretty okay with that. My main worry is about potential for ambiguity with other things that & does in a pattern meaning that it doesn't do what you want and you don't have alternate syntax for the right thing

Nick Cameron (Jul 09 2021 at 03:41, on Zulip):

what about DerefMove?

DerefMove would be used if the scrutinee has non-reference type and the pattern variable binds by move

Mario Carneiro (Jul 09 2021 at 03:43, on Zulip):

I meant under the assumption that different sigils are used for the different borrowing modes because pattern constructors mirror expr constructors

Nick Cameron (Jul 09 2021 at 03:46, on Zulip):

My main worry is about potential for ambiguity with other things that & does in a pattern meaning that it doesn't do what you want and you don't have alternate syntax for the right thing

Yeah, so I've thought about this a bit, but I'm pretty sure I haven't exhaustively covered the cases. The simple case is say an enum Foo with a Bar variant which also implements Deref, say to str.

So assuming f: &Foo, the following should be valid:

match f {
    &"hello" => {}
    &Foo::Bar => {}
    _ => {}
}

That seems OK ish since it is not ambiguous if taking types into account. If Foo derefs to itself, and you match &Foo::Bar, then the compiler could either call deref or not, but it seems obvious that you wouldn't.

Nick Cameron (Jul 09 2021 at 03:48, on Zulip):

I meant under the assumption that different sigils are used for the different borrowing modes because pattern constructors mirror expr constructors

Right, I'm not a fan of that assumption because IMO the logical conclusion is that we should use something like *Rc and *Box and *String, etc, which is unergonomic and boilerplatey

Mario Carneiro (Jul 09 2021 at 04:11, on Zulip):

That seems OK ish since it is not ambiguous if taking types into account. If Foo derefs to itself, and you match &Foo::Bar, then the compiler could either call deref or not, but it seems obvious that you wouldn't.

To formalize this a bit, to make automatic deref coercions work (i.e. no syntax implicit deref), the rule would be something like: if the goal is pat: T and the pattern fails to typecheck at type T, then try pat: U instead (where, as ever, T: Deref<Target=U>). I'm not sure exactly how this works for deref coercions in expression position, failing to typecheck seems like a really problematic guard condition so there is probably something more constrained, but whatever they use there could also be used here

Mario Carneiro (Jul 09 2021 at 04:13, on Zulip):

With that rule, you could remove most of the stars in the previous examples. In particular, match (x: String) { "foo" => ... } works, which makes me happy

Mario Carneiro (Jul 09 2021 at 04:16, on Zulip):

If Foo derefs to itself

:thinking:

fn foo<T: Deref<Target=T>>(x: T) {
    let x: &u8 = &x;
    // reached the recursion limit while auto-dereferencing `T`
}
Mario Carneiro (Jul 09 2021 at 04:18, on Zulip):

Are there any types that deref to themselves? It seems like you would often stumble into recursion limits like this. I'm going to say that this is evidence that you don't have to worry about that situation

Nick Cameron (Jul 09 2021 at 04:19, on Zulip):

I'm not sure exactly how this works for deref coercions in expression position, failing to typecheck seems like a really problematic guard condition so there is probably something more constrained

I believe it is not quite failure to type check. Anywhere that coercion is allowed, there is always a known 'target' type, and the compiler attempts to unify the target type and the expression type. If that fails it will apply possible coercions and try to unify again. So it's failure to unify two types, rather than failing to type check an expression.

Nick Cameron (Jul 09 2021 at 04:20, on Zulip):

Are there any types that deref to themselves?

Not that I'm aware of

Nick Cameron (Jul 09 2021 at 04:33, on Zulip):

I wonder if we could consider no-sigil syntax deref patterns as an extension of the match ergonomics rules? I.e., MER lets you match &Foo as a Foo, then apply the binding mode rules. What if we simply extend that to letting you match T as a U if <T as Deref>::Target =U (plus treat string lits as str or &str)? I think that is flexible and succinct. I wonder if it would look too magic?

Nick Cameron (Jul 09 2021 at 04:35, on Zulip):

The binding mode would require the specific Deref trait, and would error if one wasn't present

Nick Cameron (Jul 09 2021 at 04:39, on Zulip):

It feels like a much bigger change, and people will complain that match is getting even more magic, but honestly it feels in the right spirit - marking pointers in patterns is just boilerplate and we try hard elsewhere to eliminate the &** kinds of type tetris programming

rpjohnst (Jul 09 2021 at 17:42, on Zulip):

Nick Cameron said:

though OTOH, Rust is already in kind of a weird place with Deref ...

I think this weirdness comes from conflating the deref coercion rules with applying Deref::deref. Looking somewhere without potential for coercion, * strictly converts &T to &<T as Deref>::Target.

I don't believe this is the case, at least not syntactically- * doesn't operate on &Ts but on Ts, and it converts to <T as Deref>::Target without the extra &. For example given s: String, *s: str.

rpjohnst (Jul 09 2021 at 17:44, on Zulip):

or am i misreading your comment? i guess it works if you put the * inside the &, so &s + * = &*s which is of type &str

rpjohnst (Jul 09 2021 at 17:46, on Zulip):

...skimming the rest of this, i do agree that no-sigil should match the default binding rules stuff! if you have an Rc<Foo> and you match it with a Foo pattern then it should behave the same as if you had a &Foo, and implicitly deref + switch to ref mode for inner bindings

Jacob Lifshay (Jul 09 2021 at 18:45, on Zulip):

rpjohnst said:

...skimming the rest of this, i do agree that no-sigil should match the default binding rules stuff! if you have an Rc<Foo> and you match it with a Foo pattern then it should behave the same as if you had a &Foo, and implicitly deref + switch to ref mode for inner bindings

Hmm, seems ambiguous to me: should matching Vec<String> against [a, b] match as a: &String, a: &mut String, or, when Vec<T>: DerefMove, match as a: String?

Jacob Lifshay (Jul 09 2021 at 18:48, on Zulip):

I think deciding the default ref/ref mut binding mode should only be done from actual references, having it change if MyType decides it now wants to also implement DerefMut seems like a bad idea.

rpjohnst (Jul 09 2021 at 18:53, on Zulip):

I'm not sure I would go that far, though this does seem to be hitting the same "owned"/Deref weirdness as before. I guess to be consistent with coercions in expressions (at the expense of no longer being consistent with references themselves) then it should match as a: String, and then use ref/ref mut mode if you are matching on a &Vec or &mut Vec.

However, unlike expressions, you can't just insert a & or &mut to guide the coercion...

Nick Cameron (Jul 09 2021 at 21:10, on Zulip):

I don't believe this is the case, at least not syntactically- * doesn't operate on &Ts but on Ts, and it converts to <T as Deref>::Target without the extra &. For example given s: String, *s: str.

Yeah I think my explanation was terrible and at least a little wrong. *x expands to *Deref::deref(&x) if x isn't a reference.

Nick Cameron (Jul 09 2021 at 21:12, on Zulip):

Hmm, seems ambiguous to me: should matching Vec<String> against [a, b] match as a: &String, a: &mut String, or, when Vec<T>: DerefMove, match as a: String?

I think this would follow the same rules as the match ergonomics binding modes, i.e., it depends on the structure of the type and how the matched variables are used

Mario Carneiro (Jul 09 2021 at 22:04, on Zulip):

One issue with the scheme I mentioned earlier for determining the binding mode through a deref is that it means that matching vectors uses ref mut binding mode, so you get this:

match (vec: Vec<String>) {
    [a] => {} // a: &mut String
    _ => {}
}

This is okay I guess, it makes sense from the signature of the deref impl, but importantly it is future incompatible with adding DerefMove to Vec<T>, since then a would get type a: String and any code that assumes it's a reference would break (and the match would also start consuming the vec which could also break subsequent uses). I would like this to only happen when there has been some positive indication that a DerefMut/DerefMove impl will not be added later.

Jacob Lifshay (Jul 09 2021 at 22:07, on Zulip):

Nick Cameron said:

Hmm, seems ambiguous to me: should matching Vec<String> against [a, b] match as a: &String, a: &mut String, or, when Vec<T>: DerefMove, match as a: String?

I think this would follow the same rules as the match ergonomics binding modes, i.e., it depends on the structure of the type and how the matched variables are used

sounds good as long as the matching doesn't decide which mode to use based on if types implement DerefMut or DerefMove. erroring after deciding which mode to use due to a missing impl is fine, just don't change type deduction based on a type deciding to also impl DerefMut or DerefMove

Mario Carneiro (Jul 09 2021 at 22:11, on Zulip):

I think now that use of the deref operator in a pattern, implicitly or explicitly, should not change the default binding mode. Instead you just get an error if you try to move through a Deref/DerefMut or ref-mut bind through a Deref. That will produce much saner errors, and give people the tools to do something about the binding mode change. You will still get default ref binding mode if you match on a reference, so the same trick as today will work in the presence of deref patterns.

Mario Carneiro (Jul 09 2021 at 22:13, on Zulip):
match (vec: Vec<String>) {
  *[a] => {} // a: String, move error
  *[ref a] => {} // a: &String, okay
  [a] => {} // a: String, move error
  [*a] => {} // a: str, unsized local
  [*(ref a)] => {} // a: &str, okay
}
Mario Carneiro (Jul 09 2021 at 22:18, on Zulip):

Note that using *pat on the &Vec<String> will check pat at Vec<String> without a binding mode change, which is exactly what &pat would do in the same context, so this does give some evidence that maybe we don't need two operators for this and can reuse & for deref

Last update: Jul 24 2021 at 19:45UTC