Repository navigation
tracking issue for default binding modes in match (RFC 2005, match_default_bindings) #42640
Description
Activity
I'm actually not 100% sure the best way to implement this. It seems like we will need to add adjustments to patterns, and then integrate those into the various bits of the compiler. @eddyb, have any thoughts on that? You usually have some clever ideas when it comes to this sort of thing. =)
We should be able to treat adjustments more uniformly now, although it'd still be a lot of plumbing.
How does this interact with slice patterns (#23121)?
My expectation would be that, when we encounter a pattern like [a, ..b], we check the default binding mode. If it is "ref", then a becomes &slice[0] and b becomes (effectively) &slice[1..].
@eddyb do we want to use general purpose adjustments here, or something more limited (the current RFC, after all, only requires autoderef of & and &mut types). It seems like though if we can use adjustments, that'll lay us a nice foundation for the future potentially. But it may introduce a lot of questions (e.g., what does it mean to "unsize" a pattern) that are better left unasked.
@nikomatsakis Autoderef or autoref? If it's one bit per pattern making it separate for now is fine.
Autoderef or autoref? If it's one bit per pattern making it separate for now is fine.
autoderef -- that is, where you have a pattern like Some, the type you are matching might now be &Some or &mut Some (or &&Some, etc). I think I agree, I'm inclined to introduce a PatternAdjustment struct that just includes auto-deref and go from there.
Here is a rough and incomplete implementation plan for the match ergonomics RFC.
There are a few high-level things we have to do:
- figure out which patterns need to an "auto-deref" -- e.g., if you have
match &foo { Some(x) ... }, then we want to record on theSome(x)pattern that there is an automatic "dereference", meaning that it is equivalent to&Some(x) - figure out the "binding mode" for patterns, which will no longer always be explicit.
Right now, the code is setup to scrape this information directly from the "HIR" (the compiler's AST). The HIR node for a pattern is hir::Pat, of which the most interesting part is the kind field of type PatKind. If you have a pattern like Some(x), then, that would be represented as a tree:
TupleStruct- containing a vec of length 1, which is a Binding
- the
Bindingwould have a BindingMode ofBindByValue.
We want to make this equivalent to &Some(ref x), which would be encoded as a:
- Ref pattern, encoding the
&- TupleStruct, encoding the
Some- Binding with
BindingModeofref, encoding theref x
- Binding with
- TupleStruct, encoding the
We don't however have enough information to do this when we construct the HIR, since we need type checking results to do it, so we can't change the HIR itself. The way we typically handle this sort of thing then is to have the typeck encode "side tables" with auxiliary information. These tables are stored in TypeckTables and they encode all kinds of information.
In this case, I think we want two tables:
pat_adjustments, sort of roughly analogous to the existing adjustments table. It would have a type likeNodeMap<usize>, I think.- The key of such a map is the "id" of the
Pat-- stored in theidfield of thePatstruct. - The value would just be a number indicating how many implicit
&patterns we insert before this pattern. So, in our example, theSomepattern would wind up with a value of 1. For the other patterns, we'd probably just have no entry, meaning "none". (You could also store 0, but why waste the memory.)
- The key of such a map is the "id" of the
pat_binding_modeswould have typeNodeMap<hir::BindingMode>. It would, for each binding pattern, indicating the actual binding mode -- this may vary from what is found in the HIR, since we may be encoding arefand so forth. We may want to change the HIR then to have some different type, or perhaps anOption<hir::BindingMode>-- whereNonemeans that nothing was explicitly written, which is the normal case -- so as to make it more obvious that when the user writesxthis does not mean a "by value" binding mode anymore.
Probably a decent first PR is just to introduce the second table (pat_binding_modes) and rewrite all the existing code to use it. Right now, code that wants to find the binding mode of a binding extracts the value straight from the HIR, as you can see in the following examples:
- the check_match code, which enforces various sanity checks.
determine_pat_move_mode, which scrapes info from the HIR and also checks the type of the values being matched by value to decide if this is a copy-or-move;local_binding_mode, a helper in borrowck -- this uses the "HIR Map" to lookup, given the id, the node for a given binding, and extracts its binding mode; this would be rewritten to use the new table- the HAIR conversion code; this is a precursor to MIR construction, which constructs a lowered form of the patterns called the HAIR. The HAIR is intended to be a copy of the HIR taking into account all of the information encoded in various side-tables, so you don't want to change the HAIR itself, just this code which creates it by scraping the HIR.
This is not a comprehensive list, but it does have the major use-sites. You can get a more comprehensive list by doing rg 'BindByValue|BindByRef', which is what I did.
OK, no time for more, but hopefully this helps somebody get started! Please leave a note if you are interested in taking this on, and feel free to ping me on IRC (nmatsakis) or gitter (nikomatsakis) with any questions (or ask in #rustc).
I'll look into the first step:
Probably a decent first PR is just to introduce the second table (pat_binding_modes) and rewrite all the existing code to use it.
@tschottdorf woohoo!
179 remaining items
Any update on this issue? :)
So I think the place we're at now is @Boscop's original proposal, summarized by @nikomatsakis in #42640 (comment) as "make &P patterns, when matching a non-reference type and in a "default binding mode" of ref, go back to a default binding mode of move."
While in a sense this gives & two meanings, as I expressed in #42640 (comment), AFAICT it's the only thing that's backwards-compatible, unlike @eddyb's and my later proposals. So it's probably the best we can do if we want to solve the problem in #50008.
I'd like to solve this---it came up again on Reddit and it's kind of a pain. Is the next step just to write an RFC for it?
@rpjohnst The blocker I think is that there are several possible solutions to this problem and considerable dissensus among the lang team (and certainly the community also) about what would be best:
- As you said, using the
&sigil is one option. - Another option would be to introduce a new pattern syntax, like
(a, move b), instead of(a, &b). - Finally, it's been argued that another, currently postponed proposal to insert dereferences to convert
&TtoTwhereT: Copywould resolve this problem in practice.
Personally, I lean toward one of the latter two options because I think the use of & with inverted meaning in patterns is, despite its conceptual elegance, very confusing for many users, who don't seem to think of the reference operator as a type constructor/destructurer per se.
@withoutboats Ah, I didn't realize this would be in conflict with the second two options, especially in light of #48448.
So would the next step be to get consensus on which approach to take? Do we need to stick with just one, or could we have both to round out the existing behavior?
@rpjohnst yea, we need consensus building to move forward here. as far as i know, we don't even have consensus that we shouldn't do more than one of them.
- Finally, it's been argued that another, currently postponed proposal to insert dereferences to convert &T to T where T: Copy would resolve this problem in practice.
Always, unconditionally? But &mut T couldn't be unconditionally dereferenced/moved, because the code in the body might want to mutate the original.
(Also, wouldn't this solution break existing code (on stable) that uses match_default_bindings, where code in the body is now manually dereferencing the reference?)
- Another option would be to introduce a new pattern syntax, like (a, move b), instead of (a, &b).
Please don't forget, it should be possible to mark the moved-out copy as mut locally, like &(mut x) to copy the &T as a T and make it locally mutable. (Using the existing &(mut x) and &mut (mut x) syntax.)
- As you said, using the & sigil is one option.
I really think the best option is to use the & sigil because that's the most consistent/familiar way and what people would expect.
And people would expect the &(mut x) syntax to work with the & sigil in this case, too!
(If we don't use the & sigil, we'd also have to special-case another syntax to make the move-copied value locally mut!)
Btw, what I would like (in addition to going with the & syntax option), is being able to use & also for move-copying a &mut T (instead of having to write &mut x) (this would also allow using &(mut x) to move-copy a &mut T and make it mut in the local scope).
Personally, I lean toward one of the latter two options because I think the use of & with inverted meaning in patterns is, despite its conceptual elegance, very confusing for many users, who don't seem to think of the reference operator as a type constructor/destructurer per se.
I've talked to several people who expected it to already work like this because it also works like this when the reference wasn't created implicitly by match_default_bindings but inherent in the type before.
The reasoning difficulty for this is not the & syntax but recognizing that match_default_bindings implicitly turned the constituents into references. After that, the reader's reasoning can follow the normal rules that apply when matching a &T with a &x pattern (including the &(mut x) syntax), so if we settle on the & syntax, it only requires the reader to recognize that match_default_bindings is active here, but uses familiar and consistent syntax (unlike the other 2 proposed solutions).
(Now you may say that seeing a move keyword there would make it clearer to the reader what's going on, BUT: In many instances where match_default_bindings is used, all constituents will be left as a reference, so Rust coders have to be able to spot instances where match_default_bindings is active anyway! We always have to keep a type checker running in our head while reading/writing Rust code anyway.)
Having thought through this some more, this is one place in the language where I can easily see supporting both & and move like I mentioned above. We already have two "modes" and two prominent corresponding mental models- old-style patterns with "&T is an address," and ergonomics-style patterns with "&T is a less-permissive T."
So people (and, more importantly IMO, particular pieces of programs given their context) can use the one that expresses their intent more clearly. When & (and later, Box/Rc/etc) matter as part of the structure of your type, use &; when they don't and you just want to talk about permissions, use ref/move.
Does this make sense to anyone else? Or would people rather stick to just one solution? Or do people like the "coerce &T to T where T: Copy" idea better? In the last case, would such a coercion be more than something like autoderef-on-operators (e.g. in argument position), and does that really seem like something we could get consensus on (I would be really surprised)?
This is implemented and stabilizing, but we never closed it! The only real remaining question is #44849 and we have a separate issue for that.
@withoutboats @rpjohnst Any update on this?
I think if we support move for this case, we'd also need to allow it in non-match_default_bindings situations for consistency, e.g. let x = Some(&1); match x { Some(move a) => a, None => 0 } and v.iter().map(|move item| ..) and I wouldn't like that, because then we have 2 competing equivalent ways to express the same thing (which wouldn't be orthogonal)!
I also don't like the auto-move of T:Copy because some Copy types are very large, and apart from this, I think the programmer should have a choice, to leave it as a ref or to explicitly move it out.
I think the &a syntax is our best option and I don't think it would be confusing, once people understand that match_default_bindings gives references. The &a syntax behaves here just like in let x = Some(&1); match x { Some(&a) => a, None => 0 } and v.iter().map(|&item| ..).
I really don't think it'll be a problem, especially because people wouldn't accidentally use the &a syntax unless they want to move it out.
One of the examples in the RFC cannot be built:
fn main(){
let x = &Some((3, 3));
match x {
// Here, each of the patterns are treated independently
Some((x, 3)) | &Some((ref x, 5)) => {}
_ => {}
}
}Why?
Related: #77358
This is a tracking issue for the "match ergonomics using default bindings mode" RFC (rust-lang/rfcs#2005).
Status: Awaiting stabilization PR and docs PR! Mentoring instructions here.
Steps:
Unresolved questions: