-
-
Notifications
You must be signed in to change notification settings - Fork 14.3k
Remove the single-variant enum special case in pattern matching #150681
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
base: main
Are you sure you want to change the base?
Conversation
|
Some changes occurred in match lowering cc @Nadrieril The Miri subtree was changed cc @rust-lang/miri |
|
|
|
Looks like I messed up the syntax slightly 😅 r? @RalfJung |
|
Ah, it's just that you can only request a review from one person at a time. Makes sense. I trust that the PR will make its way to the interested parties either way, but just in case, cc @theemathas and @traviscross, who were involved in previous discussions. Preparing a sister PR to the Rust Reference is on my TODO list. |
|
I'm also not really on the review rotation, so I won't be able to do the lead review here -- sorry. I'll try to take a look at the parts relevant to Miri, but the match lowering itself is outside my comfort zone. @rustbot reroll |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
For the tests that need to be adjusted, is it possible to make the adjustments before the main changes, and still have the tests pass? That can be a nicer approach, if it's viable. |
f8d7c97 to
2ef0ade
Compare
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
Depends on which tests we're talking about. The FileCheck changes in As for the changes in |
|
👌 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
|
🚧 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
|
We talked about this in the lang call today. For my part, to move this along, I'm interested in separating this into two:
The worst flaw of the current behavior is that the opsem is crate dependent. Doing the first thing fixes that. And it makes sense conceptually. Marking an enum as non-exhaustive should cause the enum to act as if there may be other variants. There's no good reason this as if rule shouldn't apply to the defining crate too. The second one, for me, is less of a clear call. If an enum is exhaustive, then its structure has been committed to (even if not exported, this amounts to an internal commitment, for the moment, within the crate). There's a reasonableness to the programmer and the compiler relying on the details of this structure. And relying on this structure allows for more code that morally should compile to compile due to how this affects borrow checker behavior. This does then unfortunately mean that removing In any case, I'd like to break these questions apart so we can consider them separately. |
I don't see a good motivation for having such a special case for single-variant enums. People can just convert the enum to a struct if that's the behavior they want. Having fewer special cases is generally a good thing IMO. :) Note that the current code also exempts 0-variant enums in a similar way, though I am not entirely sure what the consequences of that are. |
|
Oh, and furthermore the logic in |
|
🎉 Experiment
Footnotes
|
|
@craterbot run mode=build-and-test p=1 crates=https://crater-reports.s3.amazonaws.com/pr-150681/retry-regressed-list.txt |
|
👌 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
|
🚧 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
|
🎉 Experiment
Footnotes
|
I believe this was likely considered, but mentioning it explicitly just in case: pattern matching currently also does not adhere to the as if rule in the defining crate. Should we assume that pattern matching in the defining crate should also be forced to handle a wildcard case, for the sake of the as if rule? In my opinion that would be an ergonomics regression. Of course, not doing so is also surprising — then the as if rule for non-exhaustive enums applies some times and not other times. |
If not doing so were causing us other problems, then yes. As long as it's a pure ergonomics win with no drawbacks, leaving it alone seems better. |
It seems to me that the behavior here is specifically for "what happens if we match an enum against one of its variants", and therefore it simply cannot affect 0-variant enums.
This discussion has made me realize that simply referring to long, preexisting threads to justify the change was probably a bit sloppy on my part; I have significantly expanded the frontmatter to properly argue for all three aspects of this change. See in particular "Example 1", the motivating example for the plain "just a single variant" case. |
|
Among the crates.io results of that crater report, there seem to be 2 true positives: |
|
(I'll unsubcribe from this PR, when this is ready for T-compiler review, feel free to ping or reroll) |
|
Yup, as well as some github-only projects. All of the failures are for matching on a single-variant enum without any fields, that typically seems to not implement One particularly interesting case is one where the match is being done on a value that has already been moved. Minimized: enum NonCopy {
Meow,
}
fn f(x: NonCopy) {
drop(x);
match x {
NonCopy::Meow => (),
}
} |
|
Thanks for the elaboration in the PR description. That's helpful.
This conflicts with what we documented in the Reference for #138961 in rust-lang/reference#1837. In type.closure.capture.precision.discriminants.uninhabited-variants, we say:
As you point out, this is treated as true for closure capture analysis, but as documented, we said something stronger than that. For my part, I think we should indeed make that true. I'd still like to separate out your item number 1 (one-variant rule) from numbers 2 (local non-exhaustive exception) and 3 (one-inhabited-variant partial exception). Basically, I think it's really clear that we should do 2 and 3, and I want to move those forward. I'd propose FCP merge immediately on this PR restricted to 2 and 3. For item number 1, I'd prefer to see that as a separate proposal with a self-standing motivation after 2 and 3 are fixed. Otherwise, it's too easy for the closer-call item to skate by on the compelling motivation of the easier calls. In my own head, evaluating this issue, I'm feeling the pull of that bleed-over myself. Maybe in the end it will turn out to be the right thing to do, but I see stronger reasons to be skeptical of that one, and so I'd prefer to consider it as a second step. As I understand the crater results, separating it in this way would also have the virtue of avoiding, for the moment, the observed breakage. |
Hm... we should however still emit some sort of code for the implicit
Wow, a fun example. Seems unlikely we ever actually intended that to be accepted.^^ So I feel fairly strongly that we want to do step 1 as well. But it doesn't all have to be in the same PR of course. |
The question of "when does matching an enum against a pattern of one of its variants read its discriminant" is currently an underspecified part of the language, causing weird behavior around borrowck, drop order, and UB.
Of course, in the common cases, the discriminant must be read to distinguish the variant of the enum, but currently the following exceptions are implemented:
If the enum has only one variant, we currently skip the discriminant read.
This has the advantage that single-variant enums behave the same way as structs in this regard.
However, it means that if the discriminant exists in the layout, we can't say that this discriminant being invalid is UB. This makes me particularly uneasy in its interactions with niches – consider the following example (playground), where miri currently doesn't detect any UB (because the semantics don't specify any):
Example 1
For the purpose of the above, enums with marked with
#[non_exhaustive]are always considered to have multiple variants when observed from foreign crates, but the actual number of variants is considered in the current crate.matchconsequences for#[non_exhaustive]#147722#[non_exhaustive]affecting the runtime semantics, its presence or absence can change what gets captured by a closure, and by extension, the drop order: Single-variant exception formatchconsequences for#[non_exhaustive]#147722 (comment)#[non_exhaustive]can cause borrowck to suddenly start failing in another crate.Moreover, we currently make a more specific check: we only read the discriminant if there is more than one inhabited variant in the enum.
This means that the semantics can differ between
foo<!>, and a copy offoowhereTwas manually replaced with!: Adding generics affect whether code has UB or not, according to Miri #146803Moreover, due to the privacy rules for inhabitedness, it means that the semantics of code can depend on the module in which it is located.
Additionally, this inhabitedness rule is even uglier due to the fact that closure capture analysis needs to happen before we can determine whether types are uninhabited, which means that whether the discriminant read happens has a different answer specifically for capture analysis.
For the two above points, see the following example (playground):
Example 2
In light of the above, and following the discussion at #138961 and #147722, this PR makes it so that, operationally, matching on an enum always reads its discriminant.
Note that this is a breaking change, due to the aforementioned changes in borrow checking behavior, new UB (or at least UB newly detected by miri), as well as drop order around closure captures. However, it seems to me that the combination of this PR with #138961 should have smaller real-world impact than #138961 by itself.
Fixes #142394
Fixes #146590
Fixes #146803 (though already marked as duplicate)
Fixes #147722
Fixes rust-lang/miri#4778
Fixes rust-lang/cargo#16417
r? @Nadrieril @RalfJung
@rustbot label +A-closures +A-patterns +T-opsem +T-lang