Skip to content

Tracking Issue for min_exhaustive_patterns #119612

Closed

Description

@Nadrieril

This is a tracking issue for the experimental "minimal exhaustive patterns" feature (as per the T-lang experimental feature process).
The feature gate for the issue is #![feature(min_exhaustive_patterns)].

Proposal

Pattern-matching on empty types is tricky around unsafe code. For that reason, current stable rust conservatively requires arms for empty types in all but the simplest case. It has long been the intention to allow omitting empty arms when it's safe to do so. The exhaustive_patterns feature allows the omission of all empty arms, but hasn't been stabilized because that was deemed dangerous around unsafe code.

This feature aims to stabilize an uncontroversial subset of exhaustive_patterns. Namely: when we're matching on data by-value, then we allow empty arms to be omitted. E.g.:

let x: Result<T, !> = foo();
match x { // ok
    Ok(y) => ...,
}
let Ok(y) = x; // ok

In other cases (behind references, pointers or unions) we keep stable behavior i.e. we require arms for the empty cases. This is because these cases may be reachable without UB (either now or in the future, until T-opsem decides otherwise).

unsafe {
    let ptr: *const Result<u32, !> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... } // still required
    }
}
let foo: Result<u32, &!> = ...;
match foo {
    Ok(x) => { ... }
    Err(&_) => { ... } // still required
}

Note: never_patterns will make these remaining cases more convenient to handle. The two features nicely complement each other.

Comparison with stable rust

This proposal only affects match checking of empty types (i.e. types with no valid values). Non-empty types behave the same with or without this feature. Note that everything below is phrased in terms of match but applies equallly to if let and other pattern-matching expressions.

To be precise, an empty type is:

  • an enum with no variants;
  • the never type !;
  • a struct with a visible field of empty type (and no #[non_exhaustive] annotation);
  • en enum with all variants empty (and no #[non_exhaustive] annotation).

For normal types, exhaustiveness checking requires that we list all variants (or use a wildcard). For empty types it's more subtle: in some cases we require a _ pattern even though there are no valid values that can match it. This is where the difference lies regarding min_exhaustive_patterns.

Stable rust

Under stable rust, a _ is required for all empty types, except specifically: if the matched expression is of type ! (the never type) or EmptyEnum (where EmptyEnum is an enum with no variants), then the _ is not required.

let foo: Result<u32, !> = ...;
match foo {
    Ok(x) => ...,
    Err(_) => ..., // required
}
let foo: Result<u32, &!> = ...;
match foo {
    Ok(x) => ...,
    Err(_) => ..., // required
}
let foo: &! = ...;
match foo {
    _ => ..., // required
}
fn blah(foo: (u32, !)) {
    match foo {
        _ => ..., // required
    }
}
unsafe {
    let ptr: *const ! = ...;
    match *ptr {} // allowed
    let ptr: *const (u32, !) = ...;
    match *ptr {
        (x, _) => { ... } // required
    }
    let ptr: *const Result<u32, !> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... } // required
    }
}

min_exhaustive_patterns

With min_exhaustive_patterns, a pattern of an empty type can be omitted if (and only if):

  • the match scrutinee expression has type ! or EmptyEnum;
  • or the empty type is matched by value.

In all other cases, a _ is required to match on an empty type.

let foo: Result<u32, !> = ...;
match foo {
    Ok(x) => ..., // `Err` not required
}
let foo: Result<u32, &!> = ...;
match foo {
    Ok(x) => ...,
    Err(_) => ..., // required because `!` is under a dereference
}
let foo: &! = ...;
match foo {
    _ => ..., // required because `!` is under a dereference
}
fn blah(foo: (u32, !)) {
    match foo {} // allowed
}
unsafe {
    let ptr: *const ! = ...;
    match *ptr {} // allowed
    let ptr: *const (u32, !) = ...;
    match *ptr {
        (x, _) => { ... } // required because the matched place is under a (pointer) dereference
    }
    let ptr: *const Result<u32, !> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... } // required because the matched place is under a (pointer) dereference
    }
}

About tracking issues

Tracking issues are used to record the overall progress of implementation.
They are also used as hubs connecting to other relevant issues, e.g., bugs or open design questions.
A tracking issue is however not meant for large scale discussion, questions, or bug reports about a feature.
Instead, open a dedicated issue for the specific matter and add the relevant feature gate label.

Steps

Unresolved Questions

### Implementation history
- [ ] https://github.com/rust-lang/rust/pull/118803
- [ ] https://github.com/rust-lang/rust/pull/120775
- [ ] https://github.com/rust-lang/rust/pull/120742
- [ ] https://github.com/rust-lang/rust/pull/122255
- [ ] https://github.com/rust-lang/rust/pull/122792

Activity

  1. added
    T-langRelevant to the language team
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    A-exhaustiveness-checkingRelating to exhaustiveness / usefulness checking of patterns
    on Jan 5, 2024
  2. Nadrieril commented on Jan 30, 2024

    @Nadrieril
    MemberAuthor

    Hi T-lang! I would like to move forward with this experiment.

    Summary of the feature

    This feature allows users to omit match arms on empty types when the empty type is matched by-value. This is a guaranteed-sound subset of the exhaustive_patterns feature. This allows:

    enum Void {}
    let x: Result<T, Void> = foo();
    match x { // ok
        Ok(y) => ...,
    }
    let Ok(y) = x; // ok

    If x: &Result<T, Void> or x: Result<T, &Void> or we're matching behind a pointer/union then this changes nothing (i.e. we require a Err(_) arm like on stable rust). In contrast, exhaustive_patterns allows omitting match arms on empty types in all cases, which can cause UB. For more detail see the OP.

    Because it's a well-behaved and useful subset, I'd like to move to stabilize it soon. We can think about the rest of exhaustive_patterns later. Never patterns are a likely part of the final plan, and they need more time (and would benefit from this being stabilized).

    Proposal

    If T-lang approves, I will:

    • mark the feature gate as non-incomplete (that's the part I feel I need approval for);
    • use min_exhaustive_patterns instead of exhaustive_patterns in rustc, std and tests wherever possible;
    • call for the testing of this feature;
    • make a stabilization PR, and ask for T-lang approval again.
  3. Nadrieril commented on Feb 7, 2024

    @Nadrieril
    MemberAuthor

    For the triage meeting I prepared step 1 and 2 here: #120742

    It appears that the vast majority of in-tree use cases of exhaustive_patterns are covered by min_exhaustive_patterns, as hoped.

  4. nikomatsakis commented on Feb 7, 2024

    @nikomatsakis
    Contributor

    @rfcbot fcp merge

    This covers the majority of cases I've encountered myself. I'd be very excited to see this subset move forward. Let's do it!

  5. rfcbot commented on Feb 7, 2024

    @rfcbot

    Team member @nikomatsakis 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.

  6. added
    proposed-final-comment-periodProposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off.
    disposition-mergeThis issue / PR is in PFCP or FCP with a disposition to merge it.
    on Feb 7, 2024
  7. scottmcm commented on Feb 8, 2024

    @scottmcm
    Member

    This sounds great. By-value-only covers the two places that I immediately think of for this:

    1. let Ok(x) = foo.try_into(); where it's using Error = !, and
    2. The matches that are used for residuals in the try_trait_v2 stuff.

    So I'd be quite happy to start with that and worry about the rest afterwards.

    @rfcbot reviewed

  8. removed
    proposed-final-comment-periodProposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off.
    on Feb 8, 2024
  9. 34 remaining items

  10. added a commit that references this issue on Aug 10, 2024
  11. Nadrieril commented on Aug 10, 2024

    @Nadrieril
    MemberAuthor

    This is now stabilized!

  12. added this to the 1.82.0 milestone on Aug 10, 2024
  13. removed this from the 1.82.0 milestone on Oct 2, 2024
  14. added a commit that references this issue on Mar 31, 2025
  15. added a commit that references this issue on Mar 31, 2025
  16. added a commit that references this issue on Apr 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-exhaustiveness-checkingRelating to exhaustiveness / usefulness checking of patternsC-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCF-exhaustive_patterns`#![feature(exhaustive_patterns)]`F-min_exhaustive_patterns`#![feature(min_exhaustive_patterns)]`T-langRelevant to the language teamdisposition-mergeThis issue / PR is in PFCP or FCP with a disposition to merge it.finished-final-comment-periodThe final comment period is finished for this PR / Issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions