Skip to content

Conversation

@meithecatte
Copy link
Contributor

@meithecatte meithecatte commented Jan 4, 2026

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:

  1. 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
      #![allow(dead_code)]
      use core::mem::{size_of, transmute};
      
      #[repr(u8)]
      enum Inner {
          X(u8),
      }
      
      enum Outer {
          A(Inner),
          B(u8),
      }
      
      fn f(x: &Inner) {
          match x {
              Inner::X(v) => {
                  println!("{v}");
              }
          }
      }
      
      fn main() {
          assert_eq!(size_of::<Inner>(), 2);
          assert_eq!(size_of::<Outer>(), 2);
          let x = Outer::B(42);
          let y = &x;
          f(unsafe { transmute(y) });
      }
  2. 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.

  3. 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 of foo where T was manually replaced with !: Adding generics affect whether code has UB or not, according to Miri #146803

    • Moreover, 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
      #![allow(unused)]
      
      mod foo {
          enum Never {}
          struct PrivatelyUninhabited(Never);
          pub enum A {
              V(String, String),
              Y(PrivatelyUninhabited),
          }
          
          fn works(mut x: A) {
              let a = match x {
                  A::V(ref mut a, _) => a,
                  _ => unreachable!(),
              };
              
              let b = match x {
                  A::V(_, ref mut b) => b,
                  _ => unreachable!(),
              };
          
              a.len(); b.len();
          }
          
          fn fails(mut x: A) {
              let mut f = || match x {
                  A::V(ref mut a, _) => (),
                  _ => unreachable!(),
              };
              
              let mut g = || match x {
                  A::V(_, ref mut b) => (),
                  _ => unreachable!(),
              };
          
              f(); g();
          }
      }
      
      use foo::A;
      
      fn fails(mut x: A) {
          let a = match x {
              A::V(ref mut a, _) => a,
              _ => unreachable!(),
          };
          
          let b = match x {
              A::V(_, ref mut b) => b,
              _ => unreachable!(),
          };
      
          a.len(); b.len();
      }
      
      
      fn fails2(mut x: A) {
          let mut f = || match x {
              A::V(ref mut a, _) => (),
              _ => unreachable!(),
          };
          
          let mut g = || match x {
              A::V(_, ref mut b) => (),
              _ => unreachable!(),
          };
      
          f(); g();
      }

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. introduces the following changes to this behavior:

  • matching on a #[non_exhaustive] enum will always introduce a discriminant read, regardless of whether the enum is from an external crate
  • uninhabited variants now count just like normal ones, and don't get skipped in the checks

As per the discussion below, the resolution for point (1) above is that it should land as part of a separate PR, so that the subtler decision can be more carefully considered.

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 parts of #147722
Fixes rust-lang/miri#4778

r? @Nadrieril @RalfJung

@rustbot label +A-closures +A-patterns +T-opsem +T-lang

@rustbot
Copy link
Collaborator

rustbot commented Jan 4, 2026

Some changes occurred in match lowering

cc @Nadrieril

The Miri subtree was changed

cc @rust-lang/miri

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jan 4, 2026
@rustbot
Copy link
Collaborator

rustbot commented Jan 4, 2026

Nadrieril is not on the review rotation at the moment.
They may take a while to respond.

@rustbot rustbot added A-closures Area: Closures (`|…| { … }`) A-patterns Relating to patterns and pattern matching T-lang Relevant to the language team T-opsem Relevant to the opsem team labels Jan 4, 2026
@meithecatte
Copy link
Contributor Author

Looks like I messed up the syntax slightly 😅

r? @RalfJung

@rustbot rustbot assigned RalfJung and unassigned Nadrieril Jan 4, 2026
@meithecatte meithecatte changed the title Always discriminate Remove the single-variant exception in pattern matching Jan 4, 2026
@meithecatte
Copy link
Contributor Author

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.

@RalfJung
Copy link
Member

RalfJung commented Jan 4, 2026

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

@rustbot rustbot assigned JonathanBrouwer and unassigned RalfJung Jan 4, 2026
@meithecatte meithecatte changed the title Remove the single-variant exception in pattern matching Remove the single-variant enum special case in pattern matching Jan 4, 2026
@rust-log-analyzer

This comment has been minimized.

@rust-log-analyzer

This comment has been minimized.

@Zalathar
Copy link
Member

Zalathar commented Jan 5, 2026

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.

@rustbot

This comment has been minimized.

@meithecatte
Copy link
Contributor Author

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.

Depends on which tests we're talking about. The FileCheck changes in mir-opt/unreachable_enum_branching.rs could be turned into a separate commit that'd pass when put first. For the rest of the changes in mir-opt and codegen-llvm, it is at the very least an iffy idea, as it is at the very least quite difficult to perform the canonicalization that's necessary within FileCheck.

As for the changes in tests/ui, it is also not viable, because the semantics are being changed and the tests reflect that.

@theemathas theemathas added the needs-crater This change needs a crater run to check for possible breakage in the ecosystem. label Jan 5, 2026
@traviscross traviscross added needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. I-lang-nominated Nominated for discussion during a lang team meeting. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. labels Jan 5, 2026
@rustbot
Copy link
Collaborator

rustbot commented Jan 15, 2026

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.

@meithecatte meithecatte changed the title Remove the single-variant enum special case in pattern matching Make operational semantics of pattern matching independent of crate and module Jan 15, 2026
@rust-log-analyzer

This comment has been minimized.

@meithecatte
Copy link
Contributor Author

I've modified the PR to only do parts 2 and 3 for now. While I was at it, I made sure to first introduce the ui tests, so that the diffs for the later commits clearly show the semantics changes using these tests as examples. I decided against doing the same for the miri tests, as moving the tests between pass/ and fail/ is a little unwieldy.

(btw, sorry for the extra roundtrip on tidy, I've finally tried out Jujutsu for doing this patchstack surgery, and haven't yet figured out how to get the pre-push hook working with that)

Do we want crater again? In theory all the breaking changes found by the previous crater run should be resolved by postponing point 1, but in practice, there tend to be some differences between theory and practice sometimes...

@traviscross
Copy link
Contributor

traviscross commented Jan 15, 2026

Thanks @meithecatte.

Proposal: Let's adopt item numbers 2 (removing local non-exhaustive exception) and 3 (removing one-inhabited-variant partial exception).

@rfcbot fcp merge lang,opsem

Let's think about Reference text (cc @rust-lang/lang-docs). Probably we'll change type.closure.capture.precision.discriminants.non_exhaustive in this way:

r[type.closure.capture.precision.discriminants.non_exhaustive]
If [#[non_exhaustive]][attributes.type-system.non_exhaustive] is applied to an enum defined in an external crate, the enum is treated as having multiple variants for the purpose of deciding whether a read occurs, even if it actually has only one variant.

For item 3, the Reference was already correct, I think. Anything else?

@rustbot labels +S-waiting-on-documentation

We'll run crater over the list from the last crater run.

@bors try

Also CC @rust-lang/fls.

@rust-rfcbot
Copy link
Collaborator

rust-rfcbot commented Jan 15, 2026

Team member @traviscross has proposed to merge this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns.
See this document for info about what commands tagged team members can give me.

@rustbot rustbot added the S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging label Jan 15, 2026
@rust-rfcbot rust-rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Jan 15, 2026
@rust-bors

This comment has been minimized.

rust-bors bot pushed a commit that referenced this pull request Jan 15, 2026
Make operational semantics of pattern matching independent of crate and module
@CAD97
Copy link
Contributor

CAD97 commented Jan 15, 2026

Resolving points (2) and (3) as proposed seems like a clear improvement to me.

@rustbot reviewed

FWIW, my assumption on point (1) would have been that we elide reading the discriminant if and only if the discriminant can be fully elided from the layout (and thus reading it is a no-op w.r.t. opsem). This feels like a special case optimization(?) for single-variant enums that was overlooked when introducing repr(int), as IIUC prior to that addition, all single-variant enums didn't store a discriminant. IIRC eliding the discriminant read was first done before MIR borrowck, even, meaning the "optimization" wouldn't even have served to make matching non-capturing until MIR borrowck came along.

Certainly smells to me like an "optimization" to avoid emitting a "useless" MIR statement that turned out to be not so meaningless later on.

@rust-bors
Copy link
Contributor

rust-bors bot commented Jan 15, 2026

☀️ Try build successful (CI)
Build commit: 36577a2 (36577a2b3cb42b8f23dec84e9203116bad6051e8, parent: 7704328ba5ae8d6ce0ac303c9d5a1a1605906766)

@traviscross
Copy link
Contributor

@craterbot
Copy link
Collaborator

👌 Experiment pr-150681-2 created and queued.
🤖 Automatically detected try build 36577a2
🔍 You can check out the queue and this experiment's details.

ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more

@craterbot craterbot added S-waiting-on-crater Status: Waiting on a crater run to be completed. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging labels Jan 15, 2026
@Zalathar
Copy link
Member

It looks like a lot of the preliminary test changes could potentially be merged on their own, separately from the main changes in this PR.

Would it make sense to extract some of those into a separate PR to land first, to make this PR smaller and more focused on the actual proposed changes?

@meithecatte
Copy link
Contributor Author

Yeah, feels like the first four commits could probably be a separate PR. I've created #151207.

Zalathar added a commit to Zalathar/rust that referenced this pull request Jan 17, 2026
…thar

Preliminary match/capture test cleanup for PR 150681

Review for rust-lang#150681 requested that this cleanup gets extracted to a separate PR.

r? @Zalathar
rust-timer added a commit that referenced this pull request Jan 17, 2026
Rollup merge of #151207 - always-discriminate-prelim, r=Zalathar

Preliminary match/capture test cleanup for PR 150681

Review for #150681 requested that this cleanup gets extracted to a separate PR.

r? @Zalathar
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-closures Area: Closures (`|…| { … }`) A-patterns Relating to patterns and pattern matching disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. I-lang-nominated Nominated for discussion during a lang team meeting. I-lang-radar Items that are on lang's radar and will need eventual work or consideration. needs-crater This change needs a crater run to check for possible breakage in the ecosystem. needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. P-lang-drag-1 Lang team prioritization drag level 1. https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. S-waiting-on-crater Status: Waiting on a crater run to be completed. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team T-opsem Relevant to the opsem team

Projects

None yet