Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Match ergonomics 2024 #3627

Merged
merged 26 commits into from
Jul 14, 2024

Conversation

Jules-Bertholet
Copy link
Contributor

@Jules-Bertholet Jules-Bertholet commented May 6, 2024

Rendered

Changes to match ergonomics for the 2024 edition.

@rustbot label T-lang A-patterns A-edition-2024

Tracking:

@Jules-Bertholet Jules-Bertholet changed the title Add match ergonomics 2024 RFC Match ergonomics 2024 May 6, 2024
@rustbot rustbot added A-edition-2024 Area: The 2024 edition A-patterns Pattern matching related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC. labels May 6, 2024
Co-authored-by: kennytm <[email protected]>
text/3627-match-ergonomics-2024.md Outdated Show resolved Hide resolved
text/3627-match-ergonomics-2024.md Outdated Show resolved Hide resolved
@rpjohnst
Copy link

rpjohnst commented May 6, 2024

The "& patterns matching against &mut" part seems reasonable but also fairly independent of the other changes. It might make more sense as part of a deref patterns RFC, which might (or might not!) also choose to use & more generally.

@Jules-Bertholet
Copy link
Contributor Author

The "& patterns matching against &mut" part seems reasonable but also fairly independent of the other changes.

It could be split off, yes. I mention this in the rationale section, but one of the motivating factors for including it is the "no inherited ref mut behind &" rule. With that rule, whether an inherited reference is ref or ref mut (and therefore, whether it can be matched with &mut) depends on non-local factors, so allowing users to always use & unless they need mutation becomes especially important.

@rpjohnst
Copy link

rpjohnst commented May 7, 2024

The "no inherited ref mut behind &" rules seems equally independent and forwards-compatible. (And also equally reasonable! Just probably worth considering holistically alongside deref patterns.)

@Jules-Bertholet
Copy link
Contributor Author

The "no inherited ref mut behind &" rules seems equally independent and forwards-compatible.

It is not. On edition 2024, this example would work without the rule, but errors with it: let &[[&mut a]] = &[&mut [42]];

(I'll edit the RFC to make that more clear)

@rpjohnst
Copy link

rpjohnst commented May 7, 2024

I'm not sure why (a version of) "no inherited ref mut behind &" couldn't accept that example. The binding mode could be something like "ref mut behind ref" rather than flattening all the way to ref. Indeed, places behave similarly: given x: &&mut i32, *x still has type &mut i32. You may not be able to move or write through it, but that doesn't make it a &i32.

Really I'm not sure why we would even want to reject it in the first place. There is still a &mut there, after all, and the problem only arises if we try to move it out from behind a &. It would be unfortunate if, for example, let [[&mut a]] = [&mut [42]] worked but let &[[&mut a]] = &[&mut [42]] did not.

This is exactly the sort of thing that would be good to work out as part of deref patterns. Going back to the comparison with places, the reason you can get a &i32 from *x is that auto(de)ref performs a reborrow- the exact expression-side equivalent to deref patterns.

Accepting this example is a different sort of consistency than the "immutability takes precedence" one. Instead, it's "inherited reference type matches skipped reference type." This also brings back the locality that "& patterns matching against &mut" is trying to compensate for.

If we want to defer any decision at all here, I believe we could instead forbid matching &mut against "ref mut behind ref" entirely. That would make it forward compatible to implement either "no inherited ref mut behind &" or "ref mut behind ref is a distinct mode."

@Jules-Bertholet
Copy link
Contributor Author

Instead, it's "inherited reference type matches skipped reference type."

If there are several skipped reference types with different mutabilities, you have to choose: eg in let [[/*HERE*/ x]] = &[&mut[42]];, which mutability do you use to determine that of the inherited reference at HERE? So it's impossible to be fully consistent with the principle you give.

text/3627-match-ergonomics-2024.md Outdated Show resolved Hide resolved
let _: &mut u8 = foo;
```

## Edition 2024: `&` and `&mut` can match against inherited references
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it! With this, is there even a reason to keep supporting &mut in patterns?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, without it we can't get desugar match ergonomics that get mutable references into places: let &mut Struct { ref mut field } = ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, okay. Sounds worthy of a clippy lint for if it's not combined with ref mut then :)

@rpjohnst
Copy link

rpjohnst commented May 7, 2024

If there are several skipped reference types with different mutabilities, you have to choose: ..., which mutability do you use to determine that of the inherited reference?

The innermost one, of course! Why would it be anything else? That's how default binding modes already work, and we're merely exposing that to the user so they can exit the current mode, not trying to reinvent how the mode is entered.

@Jules-Bertholet
Copy link
Contributor Author

The innermost one, of course! Why would it be anything else? That's how default binding modes already work

No it's not! Outer shared + inner mutable results in ref, not ref mut. https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c0c3f5fbe3499b54813bbe3af95e31eb

@rpjohnst
Copy link

rpjohnst commented May 7, 2024

What I'm saying is that it is how initially entering a default binding mode already works. That's why the example let &[[x]] = &[&mut [42]] produces x: &mut i32 even though it's underneath a &.

The inconsistency we're both talking about is the difference in behavior between "behind an explicit &" and "in default ref mode," demonstrated by the example above and your latest example let [a] = &&mut [42] => a: &i32.

You're proposing we resolve it in favor of the binding mode (essentially letting explicit & patterns change a sort of "not-yet-default binding mode); I'm proposing we resolve it in favor of the actual skipped reference type (and converting the existing ref->&mut->ref->& mode transition to a more auto(de)ref-like ref->&mut->ref mut->& mechanism), for the reasons I gave above- greater locality, and alignment with explicit patterns, places, and auto(de)ref.

@Jules-Bertholet
Copy link
Contributor Author

Jules-Bertholet commented May 7, 2024

Here's another way of looking at it: if &mut patterns "unwrap" a mutable reference, it should be possible to get a mutable reference by removing the pattern.

Simple case:

let &mut x = &mut 42; // x: i32
let x = &mut 42; // x: &mut i32

Roundabout case:

let &[&mut x] = &[&mut 42]; // x: i32
//let &[x] = &[&mut 42]; // ERROR, but…
let &[ref x] = &[&mut 42]; // x: &&mut i32 -> we can get an &mut in the end

However:

let &[[&mut x]] = &[&mut [42]]; // If we allow this, with x: i32
//let &[[x]] = &[&mut [42]]; // and then remove the &mut… -> ERROR move check, if the default binding mode is to be `ref mut`

// nothing we do will get us &mut i32 in any form

@rpjohnst
Copy link

rpjohnst commented May 7, 2024

Indeed, that is a demonstration of the fact that we are limited to a single layer of inherited reference/a single binding mode. If we relaxed that (not that I think this would be useful in practice), you could get a &mut in that last example:

let &[[&mut x]] = &[&mut [42]]; // x: i32
let &[[x]] = &[&mut [42]]; // ERROR move check, unless the pattern equivalent of auto(de)ref coerces &mut i32 -> x: &i32
let &[[ref x]] = &[&mut [42]]; // HYPOTHETICAL: A `ref` binding + an inherited `&mut` -> x: &&mut i32

My proposal is not so much breaking the user's ability to remove a &mut pattern to get a mutable reference, but (somewhat uselessly, on its own) extending that ability to all inherited &muts, and then restricting their use in the same way they are restricted in places. (With the additional possibility of making them nicer to work with via an auto(de)ref-like mechanism, as part of deref patterns.)

If I understand what you're getting at, your proposal would avoid this edge case by preemptively replacing &mut patterns with & in these places where you would be forbidden from using the &mut anyway. I think this is a reasonable design goal but it doesn't need to be accomplished by the binding modes themselves, which as you've noted, makes pattern matching less local.

@Nadrieril
Copy link
Member

that is a demonstration of the fact that we are limited to a single layer of inherited reference/a single binding mode. If we relaxed that...

It is semantically impossible to relax that. E.g.

let var = Some(0);
if let Some(x) = &&var {
   //...
}

Here x cannot be &&u32, because where would we store the &u32 that it points to? There is no &u32 in memory we could make it point to. There is however a u32, so we can have x: &u32 pointing inside var.

@rpjohnst
Copy link

rpjohnst commented May 7, 2024

Well, that was only a hypothetical to demonstrate a different way of thinking about the limitation Jules pointed out. But I don't think it's entirely impossible to materialize those extra reference layers- we could introduce temporaries for them around the match, it would just be a bit silly, and they would have shortened lifetimes. (Not to mention confusing to be able to layer binding modes like my example.)

@tmandry
Copy link
Member

tmandry commented May 29, 2024

Many thanks for the clarifications, @rpjohnst and @Jules-Bertholet. I think I have a good grasp of your positions now.

My primary reason for jumping into this thread has always been to make the argument that we not make the "& patterns match against &mut values" change (yet), because that overlaps with ongoing deref patterns work, and would be a backwards-compatible change if it turns out to be the right choice there.

Leaving more design space open for deref patterns is desirable. At the same time, I struggle somewhat to come up with a scenario in which adopting "& matches &mut" now restricts us meaningfully in the future.

While I'd like to avoid going too deep into hypotheticals, is there any way you could sketch out the kind of question this might restrict our choices on in the future, @rpjohnst?

cc @WaffleLapkin, who was also worried about interactions here.

This last point should behave the same way as the RFC (e.g. let &[a] = &&mut [42]; // a: &i32), except that it now rejects let &[[&a]] = &[&mut [42]]; and accepts let &[[&mut a]] = &[&mut [42]];

For my own understanding, is there a "nuclear option" that rejects both of these and allows us to choose one later? Does that mean delaying "& and &mut can match against inherited references" entirely, and is that something we can even do?

@Jules-Bertholet
Copy link
Contributor Author

For my own understanding, is there a "nuclear option" that rejects both of these and allows us to choose one later? Does that mean delaying "& and &mut can match against inherited references" entirely,

It's possible to do, we would have to forbid matching against inherited references in the specific situation that the rule affects. It would be confusing and irritating for users, though. Unless there a compelling reason to think we might gain new information in the future to better inform this choice (and I don't see us), delay will not do any good IMO.

@rpjohnst
Copy link

Leaving more design space open for deref patterns is desirable. At the same time, I struggle somewhat to come up with a scenario in which adopting "& matches &mut" now restricts us meaningfully in the future.

While I'd like to avoid going too deep into hypotheticals, is there any way you could sketch out the kind of question this might restrict our choices on in the future, @rpjohnst?

It's looking like deref patterns will introduce some syntax for "deref a scrutinee of any pointer type, and then match on the result." That syntax may or may not be & itself. From the wider perspective of deref patterns, we might want these "universal & patterns" to behave in a subtly different way than whatever we come up with here, where we're only considering & and &mut.

Similarly, if we introduce "& matches &mut" now, but deref patterns choose a different syntax, we will have two different ways to match on "some kind of pointer but I don't care which." Another of the RFC's justifications for this extension of & is that it "makes refactoring less painful" - this is likely to apply to whatever deref patterns comes up with as well. It might be preferable to have only one language feature playing this role.


Regarding the "nuclear option," we would only need to reject matching on inherited references which are both A) inherited from a &mut value and B) behind &, rather than all inherited references. While rejecting & leaves space open for deref patterns, simultaneously rejecting &mut only really leaves space open to figure out the "desirable property:"

An &mut pattern is accepted if and only if removing the pattern would allow obtaining an &mut value.

Personally, I don't believe this is important. The language is full of ways to "talk about" something without being able to "get your hands on it," but these are enforced by the borrow checker, rather than by adjusting types ahead of time.

Consider that, given x: &&mut i32, the type of *x is still &mut i32 and not &i32. Imagine that & and &mut had distinct deref operators .d and .dm, like they have distinct patterns - it would still make sense to accept &x.d.dm rather than &x.d.d or x.d, and to reject &mut x.d.dm in the borrow checker rather than the type checker.

When you rely on auto(de)ref to get a &i32 out of *x, it works by desugaring to &**x. In fact, even when you try to get a &mut i32 out of *x, the error you get is a failure to reborrow **x.

To me it seems more in line with the rest of the language to accepting &mut patterns (the pattern corresponding to my made-up .dm expressions) even in places that don't permit mutation, or mutable reborrowing, or otherwise materializing a &mut.

@tmandry
Copy link
Member

tmandry commented May 29, 2024

The deciding factor is ultimately whether the lang team has considered all of the information it thinks is relevant, or will have time to do so, without either delaying the edition or risking this RFC as part of it.

For my own understanding, is there a "nuclear option" that rejects both of these and allows us to choose one later? Does that mean delaying "& and &mut can match against inherited references" entirely,

It's possible to do, we would have to forbid matching against inherited references in the specific situation that the rule affects. It would be confusing and irritating for users, though.

What about the second part of my question — delaying "& and &mut can match against inherited references" until we have a firm decision one way or the other?

We all agree this feature is desirable, and I would like to ship it as part of the story we tell for the edition. But we also have to prioritize decisions that must be made for the edition (and we are whittling those down quickly, knock on wood).

Is this feature inherently edition sensitive on its own?

@Jules-Bertholet
Copy link
Contributor Author

Jules-Bertholet commented May 29, 2024

Is this feature inherently edition sensitive on its own?

Yes, at least if we adopt the "eat-one-layer" model as proposed (and alternative models have their own issues.)

@Jules-Bertholet
Copy link
Contributor Author

I've incorporated @traviscross's suggested additions from the 2024-06-26 T-lang design meeting (minutes). We now allow let [&mut x] = &[&mut 42]. In addition, “& matches &mut” now works on all editions.

@traviscross
Copy link
Contributor

traviscross commented Jul 3, 2024

We discussed match ergonomics (part 3) in the lang design meeting on 2024-06-26 and came to a consensus on a set of rules. We discussed this further in the triage meeting today, and came to a consensus on a small amendment to Rule 4:

Inclusive of that amendment, our consensus is to adopt this slate of rules for match ergonomics:

  • Rule 1: When the DBM (default binding mode) is not move (whether or not behind a reference), writing mut on a binding is an error.
  • Rule 2: When a reference pattern matches against a reference, do not update the DBM.
  • Rule 3: If we've previously matched against a shared reference in the scrutinee (or against a ref DBM under Rule 4, or against a mutable reference treated as a shared one or a ref mut DBM treated as a ref one under Rule 5), set the DBM to ref whenever we would otherwise set it to ref mut.
  • Rule 4: If an & pattern is being matched against a non-reference type or an &mut pattern is being matched against a shared reference type or a non-reference type, and if the DBM is ref or ref mut, match the pattern against the DBM as though it were a type.
  • Rule 5: If an & pattern is being matched against a mutable reference type (or against a ref mut DBM under Rule 4), act as if the type were a shared reference instead (or that the ref mut DBM is a ref DBM instead).

These rules result in a state transition graph that appears as such.

Note that Rule 1 and Rule 2 are edition-dependent and would happen in Rust 2024. The other three rules are backward compatible and would be stabilized in all editions.

@tmandry
Copy link
Member

tmandry commented Jul 4, 2024

Thank you for the extensive work you put into this RFC, @Jules-Bertholet, and to @traviscross for framing and driving consensus on the remaining details. I'm very impressed by the attention to detail you put into the work and by how well you collaborated with one another and with @Nadrieril, @rpjohnst, and others to get it over the line. I'm extremely happy with the result and with how confident I feel in it now.

It looks like we have consensus on the rules as put forth in this RFC.

@rfcbot resolve what tradeoff are we making with the "ref mut behind &" rule

Part of the rationale of accepting this RFC as it is has now become the convergence of at least three different lines of reasoning: The one put forth in this RFC, the one put forth by @traviscross in the most recent lang design meeting on the topic, and the design axioms shared by @Nadrieril in Zulip after the meeting. I would strongly suggest including links to the latter two in the RFC itself, because not only do they strengthen its case, they provide alternative framings for understanding both the problem space of the proposal and the mindset of those around the lang team when it was accepted.

@rfcbot rfcbot added final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Jul 4, 2024
@rfcbot
Copy link
Collaborator

rfcbot commented Jul 4, 2024

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this RFC. to-announce and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. labels Jul 14, 2024
@rfcbot
Copy link
Collaborator

rfcbot commented Jul 14, 2024

The final comment period, with a disposition to merge, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

This will be merged soon.

In the lang design meeting on 2024-06-26, we adopted a slate of five
rules for match ergonomics.  We later amended *Rule 4* in our meeting
on 2024-07-03.

These five rules, as amended, form the normative description of what
the lang team means for the rules to be.  Therefore, in the
reference-level explanation, let's include these rules verbatim and
note that the remaining sections describe these rules further.
@traviscross traviscross merged commit e17e19a into rust-lang:master Jul 14, 2024
@traviscross
Copy link
Contributor

The lang team has accepted this RFC and it's now been merged.

Thanks to @Jules-Bertholet for pushing this important work forward, and thanks to @Nadrieril for earlier work here.

For further updates, follow the tracking issue:

@Jules-Bertholet Jules-Bertholet deleted the match-ergonomics-2024 branch July 14, 2024 14:06
@nikomatsakis
Copy link
Contributor

nikomatsakis commented Jul 15, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-edition-2024 Area: The 2024 edition A-patterns Pattern matching related proposals & ideas disposition-merge This RFC is in PFCP or FCP with a disposition to merge it. finished-final-comment-period The final comment period is finished for this RFC. I-lang-nominated Indicates that an issue has been nominated for prioritizing at the next lang team meeting. T-lang Relevant to the language team, which will review and decide on the RFC. to-announce
Projects
None yet
Development

Successfully merging this pull request may close these issues.