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

Tracking Issue for Never Patterns (never_patterns) #118155

Open
22 of 29 tasks
Nadrieril opened this issue Nov 22, 2023 · 41 comments
Open
22 of 29 tasks

Tracking Issue for Never Patterns (never_patterns) #118155

Nadrieril opened this issue Nov 22, 2023 · 41 comments
Labels
A-exhaustiveness-checking Relating to exhaustiveness / usefulness checking of patterns B-experimental Blocker: In-tree experiment; RFC pending or unneeded. B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. F-never_patterns `#![feature(never_patterns)]` S-tracking-impl-incomplete Status: The implementation is incomplete. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@Nadrieril
Copy link
Member

Nadrieril commented Nov 22, 2023

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

This feature introduces a new syntax for patterns: !. In a pattern, ! is valid for any uninhabited type and simply acknowledges that the type is empty. Because a never pattern is statically known to be unreachable, it obeys slightly different rules:

enum Void {}
let result: Result<u32, Void> = ...;
match result {
    Ok(x) => { foo(x) }
    Err(!), // no need for a `=> <expr>`
}
let (Ok(x) | Err(!)) = result; // valid

fn blah(!: Void) -> SomeType {} // the function can never be called so it doesn't need a body

This feature does not affect exhaustiveness or pattern reachability; see the exhaustive_patterns and min_exhaustive_patterns features for that.

WIP RFC text here.

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 plans/details

  1. A-async-await AsyncAwait-Triaged C-bug F-never_patterns T-compiler WG-async requires-incomplete-features

Implementation history

  1. F-never_patterns T-compiler
    compiler-errors
  2. F-never_patterns I-lang-nominated T-compiler merged-by-bors perf-regression perf-regression-triaged
    compiler-errors
  3. A-parser F-never_patterns S-waiting-on-bors T-compiler
    petrochenkov
  4. F-never_patterns S-waiting-on-bors T-compiler merged-by-bors perf-regression perf-regression-triaged
    compiler-errors
  5. A-macros F-never_patterns S-waiting-on-bors T-compiler
    compiler-errors
  6. F-never_patterns S-waiting-on-bors T-compiler
    compiler-errors
  7. I-lang-nominated S-waiting-on-bors T-compiler
    compiler-errors
  8. F-never_patterns S-waiting-on-bors T-compiler
    compiler-errors
  9. F-never_patterns S-waiting-on-bors T-compiler
    compiler-errors
  10. F-never_patterns S-waiting-on-author T-compiler
    compiler-errors
  11. F-never_patterns S-waiting-on-bors T-compiler
    compiler-errors
  12. F-never_patterns S-waiting-on-bors T-compiler
    compiler-errors
  13. F-never_patterns S-waiting-on-bors T-compiler merged-by-bors
    matthewjasper
@Nadrieril Nadrieril added T-lang Relevant to the language team, which will review and decide on the PR/issue. C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. A-exhaustiveness-checking Relating to exhaustiveness / usefulness checking of patterns F-never_patterns `#![feature(never_patterns)]` labels Nov 22, 2023
@RalfJung

This comment was marked as resolved.

@RalfJung RalfJung changed the title Tracking Issue for Never Patterns Tracking Issue for Never Patterns (never_patterns) Nov 22, 2023
@Nadrieril

This comment was marked as resolved.

bors added a commit to rust-lang-ci/rust that referenced this issue Nov 26, 2023
…take-3, r=<try>

Don't warn an empty pattern unreachable if we're not sure the data is valid

Exhaustiveness checking used to be naive about the possibility of a place containing invalid data. This could cause it to emit an "unreachable pattern" lint on an arm that was in fact reachable, as in rust-lang#117119.

This PR fixes that. We now track whether a place that is matched on may hold invalid data. This also forced me to be extra precise about how exhaustiveness manages empty types.

Note that this now errs in the opposite direction: the following arm is truly unreachable but not linted as such. I'd rather not recommend writing a `match ... {}` that has the implicit side-effect of loading the value. [Never patterns](rust-lang#118155) will solve this cleanly.
```rust
match union.value {
    _x => unreachable!(),
}
```

I recommend reviewing commit by commit. I went all-in on the test suite because this went through a lot of iterations and I kept everything. The bit I'm least confident in is `is_known_valid_scrutinee` in `check_match.rs`.

Fixes rust-lang#117119.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Nov 29, 2023
…r=compiler-errors

Add `never_patterns` feature gate

This PR adds the feature gate and most basic parsing for the experimental `never_patterns` feature. See the tracking issue (rust-lang#118155) for details on the experiment.

`@scottmcm` has agreed to be my lang-team liaison for this experiment.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Nov 29, 2023
…r=compiler-errors

Add `never_patterns` feature gate

This PR adds the feature gate and most basic parsing for the experimental `never_patterns` feature. See the tracking issue (rust-lang#118155) for details on the experiment.

``@scottmcm`` has agreed to be my lang-team liaison for this experiment.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Nov 29, 2023
…r=compiler-errors

Add `never_patterns` feature gate

This PR adds the feature gate and most basic parsing for the experimental `never_patterns` feature. See the tracking issue (rust-lang#118155) for details on the experiment.

`@scottmcm` has agreed to be my lang-team liaison for this experiment.
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Nov 29, 2023
Rollup merge of rust-lang#118157 - Nadrieril:never_pat-feature-gate, r=compiler-errors

Add `never_patterns` feature gate

This PR adds the feature gate and most basic parsing for the experimental `never_patterns` feature. See the tracking issue (rust-lang#118155) for details on the experiment.

`@scottmcm` has agreed to be my lang-team liaison for this experiment.
flip1995 pushed a commit to flip1995/rust-clippy that referenced this issue Dec 1, 2023
…r-errors

Add `never_patterns` feature gate

This PR adds the feature gate and most basic parsing for the experimental `never_patterns` feature. See the tracking issue (rust-lang/rust#118155) for details on the experiment.

`@scottmcm` has agreed to be my lang-team liaison for this experiment.
@compiler-errors compiler-errors added the I-style-nominated The issue / PR has been nominated for discussion during a style team meeting. label Dec 8, 2023
@Alonely0
Copy link

Alonely0 commented Dec 9, 2023

enum Void {}
unsafe {
   let ptr: *const Result<u32, Void> = ...;
   match *ptr {
       Ok(x) => { ... }
       Err(_) => { ... } // potentially reachable if `ptr` points to uninitialized data
   }
   match *ptr {
       Ok(x) => { ... }
       Err(!) // indicate explicitly that we ensured the empty case couldn't happen
   }
}

The first match is UB. First, in Rust, any reads on uninitialized memory are considered undefined behavior; and second, if the Err variant cannot be constructed, it can't ever exist. Thus, the check for it can (and should) be removed by the optimizer, wiping away any matching on the Err variant and the code associated with it. Basically, the match would always execute the Ok branch regardless of the existence of valid data on the inner, which is exactly what you're not trying to do there (it would always get optimized down to the second match, which is equivalent to the ones proposed that are not unsafe). As a follow-up, it is not even marked as unsafe, there are many things that can go wrong when reading from a random raw pointer, not just the discriminant of an enum... Has this gone through a pre-RFC of some sort?

I think the fact that we do not want to touch uninitialized memory directly (w/o MaybeUninit) in the language with a 10ft pole is pretty well set in stone, so I'd just remove this part of the proposal and rename it "Allow elision of unreachable patterns".

@Nadrieril
Copy link
Member Author

Hi! I thought like you not that long ago, until I digged into the subtleties of rust operational semantics. Allow me to clarify.

in Rust, any reads on uninitialized memory are considered undefined behavior

This is correct, but that match does not read from the value in the Err case, since a _ pattern does not cause a read.

if the Err variant cannot be constructed, it can't ever exist

While this is a likely possibility for rust opsem to decide, this has not been decided yet. Therefore I am erring on the side of assuming it is possible to read Err with uninitialized data. This is discussed here.

it is not even marked as unsafe

I'm confused, there's an unsafe block in the code you quote. Dereferencing a pointer always requires an unsafe block.

Has this gone through a pre-RFC of some sort?

No, this is an experimental feature gate, meant to iron out the details of the feature before we write the RFC. The lang-team took a quick look at my proposal and approved the experiment. I have discussed details of rust opsem with @RalfJung for a while before I felt confident proposing this. While I believe my approach is sound, this will have to be fully settled in the (to-be-written) RFC.

I think the fact that we do not want to touch uninitialized memory directly (w/o MaybeUninit) in the language with a 10ft pole is pretty well set in stone

This isn't quite correct. We need the possibility of uninitialized data behind a pointer. In fact to initialize a MaybeUninit<T> you have to go through as_mut_ptr which returns a *mut T pointing to possibly-uninitialized memory.

The mem::uninitialized debacle that led to the creation of MaybeUninit wasn't about uninitialized memory in general, it was about the fact that the function would return an uninitialized value, which is indeed instant UB. As long as the uninitialized data is not read (e.g. because it is behind a pointer), we're ok.

I'd just remove this part of the proposal and rename it "Allow elision of unreachable patterns".

This is the opposite of what this proposal is about. "Allow elision of unreachable patterns" is implemented in the exhaustive_patterns feature gate, which hasn't been stabilized because it is unsound, exactly because it allows elision of patterns that are reachable without UB in rust's current operational semantics. The whole point of this proposal is to fix that soundness whole.

@RalfJung
Copy link
Member

RalfJung commented Dec 9, 2023

in Rust, any reads on uninitialized memory are considered undefined behavior

This is not correct. Please consult the reference for what is and is not UB. Here is an example of code that reads uninit memory (for some definition of "reads") and has no UB.

You need to be careful with blanket statements like that.


Miri's current implementation means that reading the discriminant of a Result<u32, Void> can indeed never yield Err. The question is:

  • Do we want to commit to this and start designing surface-language-level things around it?
  • Even if we do, do we want the UB to come about implicitly by omitting match arms?

Never Patterns help because they let us make explicit when we want to exploit that a type is empty. We can then factor the exhaustive_patterns discussion into "how do Never Patterns work" and "when do we implicitly insert Never Patterns", i.e., when can they be elided.

For the case of Result<u32, Void>, given that there is an explicit Ok arm that asks for the discriminant to be read, I could imagine that we will decide "yeah okay we can add Err(!) implicitly". But maybe we don't want to do that when there are raw pointers involved, just to be extra safe.

@RalfJung
Copy link
Member

RalfJung commented Dec 9, 2023

The summary of this issue doesn't entirely capture this factoring I described though, it could be improved. Currently it mixes up the concept of Never Patterns with the concept of eliding Never Patterns, while I think these concepts should be discussed completely separately. In that sense gathering feedback on a separate writeup might be helpful. The experimental process means we don't have to wait until the RFC is all set and done, but it can still help to have the RFC at least sketched so that key stakeholders can even understand what the experimental change is doing without reverse engineering that from the implementation. It can also serve as a guideline to the reviewer.

The first example in this issue does not use a never pattern, making it a strange example for explaining never patterns.

@Nadrieril
Copy link
Member Author

Nadrieril commented Dec 9, 2023

This is not correct.

Oh well, every time I think I get it I discover I'm missing something. Thx for pointing it out.

Even if we do, do we want the UB to come about implicitly by omitting match arms?

Currently it mixes up the concept of Never Patterns with the concept of eliding Never Patterns

No, my proposal is different. I'm intentionally moving away from this "implicit/explicit" frame that was in Niko's original never patterns blog post. Maybe this will turn out to be mistaken, but my current stance is: if a branch is reachable without UB, then it can't be omitted. That would be exhaustiveness being unsound. That's it, no elided never patterns or such things. Then I add the never patterns syntax as a simpler alternative to Err(x) => match x {}.

I find that a lot easier to understand and explain than a contextual desugaring. The drawback is that exhaustiveness now depends on the source of the scrutinee place and not just its type.

it can still help to have the RFC at least sketched

I linked this sketch in the OP, have you read it?

The first example in this issue does not use a never pattern, making it a strange example for explaining never patterns.

Well yeah, that's because this proposal has two parts: there's a refinement of exhaustiveness checking, and there's the never patterns syntax. I do intend to split off the exhaustiveness part into its own feature that we might be able to stabilize sooner.

@Alonely0
Copy link

Alonely0 commented Dec 9, 2023

in Rust, any reads on uninitialized memory are considered undefined behavior

This is not correct. Please consult the reference for what is and is not UB. Here is an example of code that reads uninit memory (for some definition of "reads") and has no UB.

You need to be careful with blanket statements like that.

I consulted the Rustonomicon, sorry if I was misinformed.
https://doc.rust-lang.org/nomicon/uninitialized.html

EDIT: It seems that in this context (enums) I was right, but my statement without it wasn't, thanks for pointing that out. I wasn't aware of the following:

Note: Uninitialized memory is also implicitly invalid for any type that has a restricted set of valid values. In other words, the only cases in which reading uninitialized memory is permitted are inside unions and in "padding" (the gaps between the fields/elements of a type).

@Alonely0
Copy link

Alonely0 commented Dec 9, 2023

Hi! I thought like you not that long ago, until I digged into the subtleties of rust operational semantics. Allow me to clarify.

in Rust, any reads on uninitialized memory are considered undefined behavior

This is correct, but that match does not read from the value in the Err case, since a _ pattern does not cause a read.

But how do you know that it is an Err? Because you read the discriminant, which is either valid (Ok/Err) or invalid and UB to operate with it. You can't say "it is Err because it was not Ok", you're assuming something the optimizer hasn't yet and might not do. If, for example, it decides to use jump tables, you could be jumping to dead code for all you know. The unsafe thus should mean that at least, you ensure that the enum has a valid representation, and if it has it, it still has to be only Ok because the Err is uninhabited (more on that on the next paragraph).

if the Err variant cannot be constructed, it can't ever exist

While this is a likely possibility for rust opsem to decide, this has not been decided yet. Therefore I am erring on the side of assuming it is possible to read Err with uninitialized data. This is discussed here.

But that post is about the layout and how we manage it internally, and I'm talking about the semantics of uninhabited (never) types. According to the reference, casting uninhabited (never) types out of thin air is unsound, and that's the same as having the discriminant be Err on that value, you're saying that whatever is next (in this case, zero bytes), is a constructed uninhabited type, which is equivalent to a singularity. The Err variant is an invalid (ergo unsound) state of the program. So you have to extend the meaning of that unsafe statement to mean "I'm sure it is an Ok", but it appears to me that's exactly the opposite of what you're trying to do...

it is not even marked as unsafe

I'm confused, there's an unsafe block in the code you quote. Dereferencing a pointer always requires an unsafe block.

Now I see it, I was looking for it, but for some reason I couldn't find it yesterday, sorry for that, mea culpa.

Has this gone through a pre-RFC of some sort?

No, this is an experimental feature gate, meant to iron out the details of the feature before we write the RFC. The lang-team took a quick look at my proposal and approved the experiment. I have discussed details of rust opsem with @RalfJung for a while before I felt confident proposing this. While I believe my approach is sound, this will have to be fully settled in the (to-be-written) RFC.

I think the fact that we do not want to touch uninitialized memory directly (w/o MaybeUninit) in the language with a 10ft pole is pretty well set in stone

This isn't quite correct. We need the possibility of uninitialized data behind a pointer. In fact to initialize a MaybeUninit<T> you have to go through as_mut_ptr which returns a *mut T pointing to possibly-uninitialized memory.

Indeed, raw pointers indeed can point to uninitialized data, but that was not my point. My point is that you're reading a value that you don't know if it is initialized and in a valid state (the discriminant), and you're checking that validity using semantics that assume the value is in a valid state, so the checks may get removed altogether.

The mem::uninitialized debacle that led to the creation of MaybeUninit wasn't about uninitialized memory in general, it was about the fact that the function would return an uninitialized value, which is indeed instant UB. As long as the uninitialized data is not read (e.g. because it is behind a pointer), we're ok.

I'd just remove this part of the proposal and rename it "Allow elision of unreachable patterns".

This is the opposite of what this proposal is about. "Allow elision of unreachable patterns" is implemented in the exhaustive_patterns feature gate, which hasn't been stabilized because it is unsound, exactly because it allows elision of patterns that are reachable without UB in rust's current operational semantics. The whole point of this proposal is to fix that soundness whole.

Oh, great then.

@Nadrieril
Copy link
Member Author

Nadrieril commented Dec 9, 2023

I feel like we're talking a bit past each other, I ask your patience as we try to find what we're disagreeing on.

casting uninhabited (never) types out of thin air is unsound, and that's the same as having the discriminant be Err on that value

That doesn't seem correct. First I can very well make a ptr: *const Result<T, !>. And until opsem rules it out, I could find a way to write to ptr in a way that initializes the discriminant to Err. So far, no uninhabited type has ever been read so we're good. Then if I read the discriminant, I get Err which is still good. The case that would be UB is if I then read the data part of the enum.

Note the very important fact that all of this happens behind a pointer. If it was instead a plain let x: Result<T, !> = ...;, then x must be valid, which indeed forbids the Err case.

Also I'm not sure what we're arguing about because it is already the case on stable rust that the Err(_) case must be included. This proposal doesn't change that.

You can't say "it is Err because it was not Ok", you're assuming something the optimizer hasn't yet and might not do. If, for example, it decides to use jump tables, you could be jumping to dead code for all you know.

That is not what I'm doing. If the match expression was compiled that way I'm pretty sure it would be a bug. What the Err(_) case means is: "if we read the discriminant, and it returns Err, take this branch". If it turns out that it's UB to construct the Err case, then that's fine, the discriminant read will always return Ok and the Err branch will just never be taken.

If on the other hand I allowed match ... { Ok(_) => {} } to be exhaustive, then if Err is ever read we have UB. That would mean match exhaustiveness is unsound! I insist that requiring a branch for Err is the safer alternative. Unless that's not what you were arguing?

Thus, the check for it can (and should) be removed by the optimizer, wiping away any matching on the Err variant and the code associated with it

Until opsem decides for sure that Err is not constructible, then the optimized MUSN'T remove this case. Today's rust doesn't optimize this away, and this proposal does not change that. Moreover, if this optimization was ever correct, then what's the problem? The optimizer optimized away some dead code, I don't see how that causes any UB. I'm saying the same thing as above again, but I want to be sure we're on the same page.

@Nadrieril
Copy link
Member Author

Maybe I should be clearer about what's currently required on stable rust. In both stable rust and under the proposal, the first match below is accepted, and the second match below gives a "non-exhaustive" error.

enum Void {}
unsafe {
    let ptr: *const Result<u32, Void> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... }
    }
    match *ptr { // ERROR: non-exhaustive: `Err` not covered
        Ok(x) => { ... }
    }
}

As long as we're behind a pointer/reference/union, there's no changes compared to stable. Where it does change is in the other cases, e.g. a plain value. In stable rust, we get:

enum Void {}
let val: Result<u32, Void> = ...;
match val { // accepted
    Ok(x) => ...
    Err(_) => ...
}
match val { // ERROR: non-exhaustive: `Err` not covered
    Ok(x) => ...
}

With never_patterns, we get:

enum Void {}
let val: Result<u32, Void> = ...;
match val {
    Ok(x) => ...
    Err(_) => ... // WARN: unreachable
}
match val { // accepted
    Ok(x) => ...
}

bors added a commit to rust-lang-ci/rust that referenced this issue Dec 9, 2023
…take-3, r=compiler-errors

Don't warn an empty pattern unreachable if we're not sure the data is valid

Exhaustiveness checking used to be naive about the possibility of a place containing invalid data. This could cause it to emit an "unreachable pattern" lint on an arm that was in fact reachable, as in rust-lang#117119.

This PR fixes that. We now track whether a place that is matched on may hold invalid data. This also forced me to be extra precise about how exhaustiveness manages empty types.

Note that this now errs in the opposite direction: the following arm is truly unreachable (because the binding causes a read of the value) but not linted as such. I'd rather not recommend writing a `match ... {}` that has the implicit side-effect of loading the value. [Never patterns](rust-lang#118155) will solve this cleanly.
```rust
match union.value {
    _x => unreachable!(),
}
```

I recommend reviewing commit by commit. I went all-in on the test suite because this went through a lot of iterations and I kept everything. The bit I'm least confident in is `is_known_valid_scrutinee` in `check_match.rs`.

Fixes rust-lang#117119.
@Alonely0
Copy link

Alonely0 commented Dec 10, 2023

That doesn't seem correct. First I can very well make a ptr: *const Result<T, !>. And until opsem rules it out, I could find a way to write to ptr in a way that initializes the discriminant to Err. So far, no uninhabited type has ever been read so we're good. Then if I read the discriminant, I get Err which is still good. The case that would be UB is if I then read the data part of the enum.

But you are reading it! That's where our disagreement comes from, and it's not even the root of it. You're reading it when you dereference the pointer, because you're not reborrowing it (you then discard/drop it later with _, but that's irrelevant). The thing is, when the compiler & the optimizer see the Err variant, they see that in order for the Err to exist, a never type had to be created beforehand, because it is a dependency of Err (unlike C, where everything is wrapped in a MaybeUninit). The semantics of the never type are simple, nothing can exist after it is created; but, however, if we go back to the definition of the Err variant, the never type had to exist before, so the Err exists after the never type! This violates the semantics of the never type, so the path where the Err variant matches is unsound; Q.E.D. You can indeed write the ptr so that the data is invalid, but then even thinking about it as that enum is unsound (and even if unused, unused data must also be valid). That's proof by contradiction, this is proof by miri (the playground is down at the time of writing, so I'll just paste here the code):

fn main() {
    let mut x = Foo::<i32, Uninhabited>::X(1);
    assert_eq!(std::mem::size_of_val(&x), std::mem::size_of::<i32>() * 2); // Uninhabited is a ZST.
    assert_eq!(std::mem::size_of::<()>(), std::mem::size_of::<Uninhabited>()); // () is also a ZST.
    let mut y = Foo::<i32, ()>::X(1);
    assert_eq!(x, unsafe { std::mem::transmute(y) }); // Ergo, reprs are the same.
    y = Foo::Y(());
    let mut x = unsafe { std::mem::transmute::<_, FooLayout<i32, Uninhabited>>(x) };
    x.tag = 1; // Y's tag.
    let x = unsafe { std::mem::transmute::<_, Foo<i32, Uninhabited>>(x) };
}

#[derive(Copy, Clone, Debug, PartialEq)]
enum Uninhabited {}

#[repr(C, i32)]
#[derive(Debug, PartialEq)]
enum Foo<A, B> where A: Copy, B: Copy {
    X(A) = 0,
    Y(B) = 1,
}

#[repr(C)]
union UntaggedFoo<A, B> where A: Copy, B: Copy {
    x: A,
    y: B,
}

#[repr(C)]
struct FooLayout<A, B> where A: Copy, B: Copy {
    tag: i32,
    union: UntaggedFoo<A, B>,
}

When run with miri, notice that the write is, indeed, valid, but even if unused or unread, thinking about it as a Foo is already UB. On your initial piece of code you're checking if the discriminant is valid, and if it is not, you execute some code. However, when you do that, the potentially invalid data escapes the raw pointer, which is unsound.

@Nadrieril
Copy link
Member Author

I do not understand your explanation. Anyway, what do you concretely propose we should do differently?

@Nadrieril
Copy link
Member Author

I propose to split off the exhaustiveness-affecting changes into their own feature gate: #118803. If that gets accepted, then never_patterns will become exclusively about ! patterns (which makes a lot of sense).

@Nadrieril
Copy link
Member Author

"You don't have permission to access this resource. "

Oh shoot! Fixed it now.

But if an arm is unreachable without UB, then it can be omitted. So there's still implicit never patterns, e.g. in match (x: Void) {}, which I would say is sugar for match (x : Void) { ! }. That may not be how you plan to implement this, but conceptually it is a lot simpler IMO than having two completely separate mechanisms for matching on uninhabited types.

What "two completely separate mechanisms for matching on uninhabited types" do you mean?

@RalfJung
Copy link
Member

What "two completely separate mechanisms for matching on uninhabited types" do you mean?

There's two mechanisms that both analyze whether the match subject is uninhabited: checking whether a ! pattern is accepted, and checking whether we can omit a match arm.

@RalfJung
Copy link
Member

Oh shoot! Fixed it now.

Ah, now I can see it. :) I left some comments.

@joshtriplett
Copy link
Member

Review in the style team turned up an issue that I'd like to raise with a lang hat on:

Syntactically, the idea of never patterns like Err(!) not having any trailing delimiter seems potentially problematic for both humans and computers to parse, if never patterns appear somewhere other than the last match arm.

Consider:

match expr {
    Variant1(field) => { ... }
    Variant2(!)
    Variant3(field) => { ... }
}

Even worse, consider leading |:

match expr {
    | Variant1(field) => { ... }
    | Variant2(!)
    | Variant3(field) => { ... }
}

I think we should have a mandatory trailing , on this.

(In theory, syntactically, we don't need the trailing comma if it's the last match arm. In practice from a style perspective and for consistency with all our other uses of delimiters, we'd probably want it.)

@Nadrieril
Copy link
Member Author

Nadrieril commented Dec 13, 2023

That's how I implemented the parsing 👍 I made a comma required everywhere, except for last pattern of the match where it's optional, like we do for trailing commas in general.

@RalfJung
Copy link
Member

@RalfJung you did? On this document right? I don't see any comments, where should I be looking?

That comment disappeared. So I guess you found them? :)

@Nadrieril
Copy link
Member Author

I didn't but I realized hackmd was overall very buggy y'day for me so that was probably the cause

@RalfJung
Copy link
Member

There should be a button for "comments" at the top:

image

If you click that it shows all the comments; you can click the comment to scroll to the place in the document it refers to.

@Nadrieril
Copy link
Member Author

I see it! And it definitely wasn't there yesterday x)

@Alonely0

This comment was marked as off-topic.

@Nadrieril

This comment was marked as off-topic.

@Alonely0

This comment was marked as off-topic.

@RalfJung

This comment was marked as off-topic.

@Alonely0

This comment was marked as off-topic.

@RalfJung

This comment was marked as off-topic.

@Alonely0

This comment was marked as off-topic.

@RalfJung

This comment was marked as off-topic.

@joshtriplett joshtriplett removed the I-style-nominated The issue / PR has been nominated for discussion during a style team meeting. label Jan 10, 2024
@Nadrieril
Copy link
Member Author

Hey sorry, I quote:

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.

Please open a dedicated issue or come ask your question on Zulip.

@dead-claudia

This comment has been minimized.

@Nadrieril

This comment has been minimized.

@Nadrieril
Copy link
Member Author

Nadrieril commented Jan 18, 2024

I have split off the exhaustiveness-affecting parts of this feature to min_exhaustive_patterns. This feature is now strictly about adding the ! pattern.

compiler-errors added a commit to compiler-errors/rust that referenced this issue Jan 26, 2024
… r=compiler-errors

Add the `min_exhaustive_patterns` feature gate

## Motivation

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`](rust-lang#51085) feature allows the omission of all empty arms, but hasn't been stabilized because that was deemed dangerous around unsafe code.

## Proposal

This feature aims to stabilize an uncontroversial subset of exhaustive_patterns. Namely: when `min_exhaustive_patterns` is enabled and the data we're matching on is guaranteed to be valid by rust's operational semantics, then we allow empty arms to be omitted. E.g.:

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

If the place is not guaranteed to hold valid data (namely ptr dereferences, ref dereferences (conservatively) and union field accesses), then we keep stable behavior i.e. we (usually) require arms for the empty cases.

```rust
unsafe {
    let ptr: *const Result<u32, !> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... } // still required
    }
}
let foo: Result<u32, &!> = ...;
match foo {
    Ok(x) => { ... }
    Err(&_) => { ... } // still required because of the dereference
}
unsafe {
    let ptr: *const ! = ...;
    match *ptr {} // already allowed on stable
}
```

Note that we conservatively consider that a valid reference can point to invalid data, hence we don't allow arms of type `&!` and similar cases to be omitted. This could eventually change depending on [opsem decisions](rust-lang/unsafe-code-guidelines#413). Whenever opsem is undecided on a case, we conservatively keep today's stable behavior.

I proposed this behavior in the [`never_patterns`](rust-lang#118155) feature gate but it makes sense on its own and could be stabilized more quickly. The two proposals nicely complement each other.

## Unresolved Questions

Part of the question is whether this requires an RFC. I'd argue this doesn't need one since there is no design question beyond the intent to omit unreachable patterns, but I'm aware the problem can be framed in ways that require design (I'm thinking of the [original never patterns proposal](https://smallcultfollowing.com/babysteps/blog/2018/08/13/never-patterns-exhaustive-matching-and-uninhabited-types-oh-my/), which would frame this behavior as "auto-nevering" happening).

EDIT: I initially proposed a future-compatibility lint as part of this feature, I don't anymore.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jan 26, 2024
… r=compiler-errors

Add the `min_exhaustive_patterns` feature gate

## Motivation

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`](rust-lang#51085) feature allows the omission of all empty arms, but hasn't been stabilized because that was deemed dangerous around unsafe code.

## Proposal

This feature aims to stabilize an uncontroversial subset of exhaustive_patterns. Namely: when `min_exhaustive_patterns` is enabled and the data we're matching on is guaranteed to be valid by rust's operational semantics, then we allow empty arms to be omitted. E.g.:

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

If the place is not guaranteed to hold valid data (namely ptr dereferences, ref dereferences (conservatively) and union field accesses), then we keep stable behavior i.e. we (usually) require arms for the empty cases.

```rust
unsafe {
    let ptr: *const Result<u32, !> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... } // still required
    }
}
let foo: Result<u32, &!> = ...;
match foo {
    Ok(x) => { ... }
    Err(&_) => { ... } // still required because of the dereference
}
unsafe {
    let ptr: *const ! = ...;
    match *ptr {} // already allowed on stable
}
```

Note that we conservatively consider that a valid reference can point to invalid data, hence we don't allow arms of type `&!` and similar cases to be omitted. This could eventually change depending on [opsem decisions](rust-lang/unsafe-code-guidelines#413). Whenever opsem is undecided on a case, we conservatively keep today's stable behavior.

I proposed this behavior in the [`never_patterns`](rust-lang#118155) feature gate but it makes sense on its own and could be stabilized more quickly. The two proposals nicely complement each other.

## Unresolved Questions

Part of the question is whether this requires an RFC. I'd argue this doesn't need one since there is no design question beyond the intent to omit unreachable patterns, but I'm aware the problem can be framed in ways that require design (I'm thinking of the [original never patterns proposal](https://smallcultfollowing.com/babysteps/blog/2018/08/13/never-patterns-exhaustive-matching-and-uninhabited-types-oh-my/), which would frame this behavior as "auto-nevering" happening).

EDIT: I initially proposed a future-compatibility lint as part of this feature, I don't anymore.
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Jan 26, 2024
… r=compiler-errors

Add the `min_exhaustive_patterns` feature gate

## Motivation

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`](rust-lang#51085) feature allows the omission of all empty arms, but hasn't been stabilized because that was deemed dangerous around unsafe code.

## Proposal

This feature aims to stabilize an uncontroversial subset of exhaustive_patterns. Namely: when `min_exhaustive_patterns` is enabled and the data we're matching on is guaranteed to be valid by rust's operational semantics, then we allow empty arms to be omitted. E.g.:

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

If the place is not guaranteed to hold valid data (namely ptr dereferences, ref dereferences (conservatively) and union field accesses), then we keep stable behavior i.e. we (usually) require arms for the empty cases.

```rust
unsafe {
    let ptr: *const Result<u32, !> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... } // still required
    }
}
let foo: Result<u32, &!> = ...;
match foo {
    Ok(x) => { ... }
    Err(&_) => { ... } // still required because of the dereference
}
unsafe {
    let ptr: *const ! = ...;
    match *ptr {} // already allowed on stable
}
```

Note that we conservatively consider that a valid reference can point to invalid data, hence we don't allow arms of type `&!` and similar cases to be omitted. This could eventually change depending on [opsem decisions](rust-lang/unsafe-code-guidelines#413). Whenever opsem is undecided on a case, we conservatively keep today's stable behavior.

I proposed this behavior in the [`never_patterns`](rust-lang#118155) feature gate but it makes sense on its own and could be stabilized more quickly. The two proposals nicely complement each other.

## Unresolved Questions

Part of the question is whether this requires an RFC. I'd argue this doesn't need one since there is no design question beyond the intent to omit unreachable patterns, but I'm aware the problem can be framed in ways that require design (I'm thinking of the [original never patterns proposal](https://smallcultfollowing.com/babysteps/blog/2018/08/13/never-patterns-exhaustive-matching-and-uninhabited-types-oh-my/), which would frame this behavior as "auto-nevering" happening).

EDIT: I initially proposed a future-compatibility lint as part of this feature, I don't anymore.
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Jan 26, 2024
Rollup merge of rust-lang#118803 - Nadrieril:min-exhaustive-patterns, r=compiler-errors

Add the `min_exhaustive_patterns` feature gate

## Motivation

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`](rust-lang#51085) feature allows the omission of all empty arms, but hasn't been stabilized because that was deemed dangerous around unsafe code.

## Proposal

This feature aims to stabilize an uncontroversial subset of exhaustive_patterns. Namely: when `min_exhaustive_patterns` is enabled and the data we're matching on is guaranteed to be valid by rust's operational semantics, then we allow empty arms to be omitted. E.g.:

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

If the place is not guaranteed to hold valid data (namely ptr dereferences, ref dereferences (conservatively) and union field accesses), then we keep stable behavior i.e. we (usually) require arms for the empty cases.

```rust
unsafe {
    let ptr: *const Result<u32, !> = ...;
    match *ptr {
        Ok(x) => { ... }
        Err(_) => { ... } // still required
    }
}
let foo: Result<u32, &!> = ...;
match foo {
    Ok(x) => { ... }
    Err(&_) => { ... } // still required because of the dereference
}
unsafe {
    let ptr: *const ! = ...;
    match *ptr {} // already allowed on stable
}
```

Note that we conservatively consider that a valid reference can point to invalid data, hence we don't allow arms of type `&!` and similar cases to be omitted. This could eventually change depending on [opsem decisions](rust-lang/unsafe-code-guidelines#413). Whenever opsem is undecided on a case, we conservatively keep today's stable behavior.

I proposed this behavior in the [`never_patterns`](rust-lang#118155) feature gate but it makes sense on its own and could be stabilized more quickly. The two proposals nicely complement each other.

## Unresolved Questions

Part of the question is whether this requires an RFC. I'd argue this doesn't need one since there is no design question beyond the intent to omit unreachable patterns, but I'm aware the problem can be framed in ways that require design (I'm thinking of the [original never patterns proposal](https://smallcultfollowing.com/babysteps/blog/2018/08/13/never-patterns-exhaustive-matching-and-uninhabited-types-oh-my/), which would frame this behavior as "auto-nevering" happening).

EDIT: I initially proposed a future-compatibility lint as part of this feature, I don't anymore.
@fmease fmease added B-experimental Blocker: In-tree experiment; RFC pending or unneeded. B-unstable Blocker: Implemented in the nightly compiler and unstable. S-tracking-impl-incomplete Status: The implementation is incomplete. labels Feb 28, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-exhaustiveness-checking Relating to exhaustiveness / usefulness checking of patterns B-experimental Blocker: In-tree experiment; RFC pending or unneeded. B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: A tracking issue for an RFC or an unstable feature. F-never_patterns `#![feature(never_patterns)]` S-tracking-impl-incomplete Status: The implementation is incomplete. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

7 participants