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

Compilation error when matching reference to empty enum #131452

Open
dhedey opened this issue Oct 9, 2024 · 26 comments
Open

Compilation error when matching reference to empty enum #131452

dhedey opened this issue Oct 9, 2024 · 26 comments
Assignees
Labels
A-exhaustiveness-checking Relating to exhaustiveness / usefulness checking of patterns A-patterns Relating to patterns and pattern matching C-bug Category: This is a bug. F-exhaustive_patterns `#![feature(exhaustive_patterns)]` T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-opsem Relevant to the opsem team T-types Relevant to the types team, which will review and decide on the PR/issue.

Comments

@dhedey
Copy link

dhedey commented Oct 9, 2024

Note

Useful background links which have been shared in comments:

Summary

I tried this code:

enum A {}

fn f(a: &A) -> usize {
    match a {}
}

I expected to see this happen: This compiles successfully.

Instead, this happened: (E0004 compiler error, expandable below)

E0004 Compiler Error

error[E0004]: non-exhaustive patterns: type `&A` is non-empty
 --> src/lib.rs:4:11
  |
4 |     match a {}
  |           ^
  |
note: `A` defined here
 --> src/lib.rs:1:6
  |
1 | enum A {}
  |      ^
  = note: the matched value is of type `&A`
  = note: references are always considered inhabited
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern as shown
  |
4 ~     match a {
5 +         _ => todo!(),
6 +     }
  |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `playground` (lib) due to 1 previous error

Searching for "Rust E0004" links to docs that don't explain why references to uninhabited types need to be matched: https://doc.rust-lang.org/error_codes/E0004.html - you have to search for "references are always considered inhabited" which takes you to this issue from 2020 - more on this history in the Background section below.

Motivating example

This comes up commonly when creating macros which generate enums, e.g. a very simplified example (playground link):

macro_rules! define_enum {
    (
        $name: ident,
        [
            $(
                $variant: ident => $string_label: expr
            ),* $(,)?
        ]$(,)?
    ) => {
        enum $name {
            $($variant,)*
        }

        impl $name {
            fn label(&self) -> &'static str {
                match self {
                    $(Self::$variant => $string_label,)*
                }
            }
        }
    }
}

// This compiles
define_enum!{
    MyEnum,
    [Foo => "foo", Bar => "bar"],
}

// This fails compilation with E0004
define_enum!{
    MyEmptyEnum,
    [],
}
Compiler Error

error[E0004]: non-exhaustive patterns: type `&MyEmptyEnum` is non-empty
  --> src/lib.rs:16:23
   |
16 |                   match self {
   |                         ^^^^
...
31 | / define_enum!{
32 | |     MyEmptyEnum,
33 | |     [],
34 | | }
   | |_- in this macro invocation
   |
note: `MyEmptyEnum` defined here
  --> src/lib.rs:32:5
   |
32 |     MyEmptyEnum,
   |     ^^^^^^^^^^^
   = note: the matched value is of type `&MyEmptyEnum`
   = note: references are always considered inhabited
   = note: this error originates in the macro `define_enum` (in Nightly builds, run with -Z macro-backtrace for more info)
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern as shown
   |
16 ~                 match self {
17 +                     _ => todo!(),
18 +                 }
   |

For more information about this error, try `rustc --explain E0004`.
error: could not compile `playground` (lib) due to 1 previous error

The fact that this doesn't work for empty enums is quite a gotcha, and I've seen this issue arise a few times as an edge case. In most cases, it wasn't caught until a few months after, when someone uses the macro to create an enum with no variants.

But why would we ever use such a macro to generate empty enums? Well this can fall out naturally when generating a hierarchy of enums, where some inner enums are empty, e.g.

define_namespaced_enums! {
    Foo => {
        Fruit: [apple, pear],
        Vegetables: [],
    }
    Bar => {
        Fruit: [banana],
        Vegetables: [carrot],
    }
}

Workarounds

Various workarounds include:

  • Add a blanket _ => unreachable!("Workaround for empty enums: references to uninhabited types are considered inhabited at present")
  • In some cases where the enum is copy, use match *self
  • Use an inner macro which counts the number of variants, and changes the code generated based on whether the enum is empty or not. And e.g. outputs one of:

Background

This was previously raised in this issue: #78123 but was closed as "expected behaviour" - due to the fact that:

... the reason that &Void (where Void is an uninhabited type) is considered inhabited is because unsafe code can construct an &-reference to an uninhabited type (e.g. with ptr::null() or mem::transmute()). I don't know enough to be sure though.

However, when I raised this as a motivating example in rust-lang/unsafe-code-guidelines#413 (comment), @RalfJung suggested I raise a new rustc issue for this, and that actually the match behaviour is independent of the UB semantics decision:

So please open a new issue (in the rustc repo, not here) about the macro concern. That's a valid point, just off-topic here. Whether we accept that match code is essentially completely independent of what we decide here.

Meta

rustc --version --verbose:

rustc 1.81.0 (eeb90cda1 2024-09-04)
binary: rustc
commit-hash: eeb90cda1969383f56a2637cbd3037bdf598841c
commit-date: 2024-09-04
host: aarch64-apple-darwin
release: 1.81.0
LLVM version: 18.1.7

Also works on nightly.

@dhedey dhedey added the C-bug Category: This is a bug. label Oct 9, 2024
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label Oct 9, 2024
@RalfJung
Copy link
Member

RalfJung commented Oct 9, 2024

For more background (but partially outdated), also see this blog post by @nikomatsakis.

Cc @rust-lang/opsem @rust-lang/types @Nadrieril

My comment referred to the fact that the safety invariant is uncontroversial in your example, and sufficient to ensure that without unsafe code, nothing weird can happen -- in the specific case considered here, where the match is on a reference.

There are related, more subtle cases though. For instance, what if ptr: *const !/*const Void, and now we do match *ptr {}? Such a pointer can be constructed in safe code, and let _ = *ptr; is not UB and might even be(come) safe. Similarly, match *ptr { _ => () } is not UB. But with an empty match arm now suddenly this is elevated to UB, even though no safety invariant was violated going into the match. IMO that's a footgun we want to avoid: the absence of code introduces UB into a program that doesn't ever obviously violate any validity invariant. (Constructing a place of type ! is fine. It's reading the discriminant that's a problem, but whether and when a match reads the discriminant depends on many factors, and in this case it is strictly the absence of a match arm that is the only thing this code did wrong.)

Also for references, we may allow code to temporarily break the &Enum safety invariant (by not making it a validity invariant for the discriminant to be valid). Not sure if we should be concerned about that, but it is something to consider.

So my position on this heavily depends on whether the place we are matching on is "safe" or not. A place is "unsafe" if it involves * on a raw pointer or a union field. For unsafe places, I think we need to be careful. I think we want to introduce ! patterns, not accept anything we don't already accept, and lint against existing code like this to enforce the use of never patterns any time "empty type" reasoning is done on an unsafe place.

For safe places, I am much less concerned. Maybe we want an opt-in lint that recommends never patterns to make this explicit. Maybe we want the lint to be warn-by-default when there is unsafe code nearby? But that seems hard and we don't have any other lint like this.

There's also the question of irrefutable patterns exploiting empty types, such as let OK(foo) = place. There we can't suggest never patterns as that would destroy the entire point of these patterns. So IMO we should not do empty type reasoning here for unsafe places, i.e. this and this should keep being rejected. Though arguably by writing Ok(_) the programmer did explicitly request a discriminant to be read, and Result<(), !> simply doesn't have a discriminant for its Err variant, so this is less bad in my view than the match *ptr {} case.

@ijackson
Copy link
Contributor

ijackson commented Oct 9, 2024

Can't we make a distinction between unsafe and safe places and auto-scrutinise the discriminant of safe places?

IOW declare that &Void is unsound and say that *Void is allowed but you can't do the empty match on it.

@RalfJung
Copy link
Member

RalfJung commented Oct 9, 2024 via email

@compiler-errors
Copy link
Member

It is a new distinction between two kinds of places that I don't think we already have, so it's not an entirely trivial extension of the status quo.

Well, since min_exhaustive_patterns was stabilized, we considers some places to be "exhaustive" and some not to be, depending on their expression and their type... so we kinda already do this, at least in the THIR.

@Nadrieril
Copy link
Member

+1 to most of what Ralf just said. I'm in favor of making the & case convenient at the cost of footguns for users of unsafe references (that's full of footguns already anyway).

Result<(), !> simply doesn't have a discriminant for its Err variant

As far as I remember we hadn't yet committed to that, at least in more complex cases like Result<(), (!, T)>.

let OK(foo) = place. There we can't suggest never patterns

We can suggest let (Ok(foo) | Err(!)) = place.

@saethlin saethlin added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. A-patterns Relating to patterns and pattern matching T-types Relevant to the types team, which will review and decide on the PR/issue. T-opsem Relevant to the opsem team and removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels Oct 12, 2024
@stephanemagnenat
Copy link

stephanemagnenat commented Nov 11, 2024

Note that from a user perspective, this issue happens quickly when one wants to implement the "Trees that Grow" compiler AST pattern in Rust:

enum Never {}

trait Phase {
    type PhaseData;
}

struct PhaseWithInhabited;

impl Phase for PhaseWithInhabited {
    type PhaseData = Never;
}

enum Ast<P: Phase> {
    Identifier(&'static str),
    MaybeSomething(P::PhaseData)
}

fn main() {
    let ast = Ast::<PhaseWithInhabited>::Identifier("hi");
    let s = match &ast {
        Ast::Identifier(s) => s
    };
}

Tada:

error[E0004]: non-exhaustive patterns: `&Ast::MaybeSomething(_)` not covered

Note, as a user, I'll be happy if the compiler would fill the inhabited pattern with panics, in case someone does something strange with unsafe code. What is painful is the need to manually book-keep inhabited match arms (which defeats the static type safety provided by the "Tree that Grow" pattern in the first place, for example) and face the surprising change of behaviour that happens when (ast in this case) is changed from passed by value to passed by reference.

@RalfJung
Copy link
Member

With rust-lang/rfcs#3719, you could write this as

    let s = match &ast {
        Ast::Identifier(s) => s
        ! // explicitly mark other cases as impossible
    };

@stephanemagnenat
Copy link

stephanemagnenat commented Nov 11, 2024

That would of course be better than the status quo! It would still be a bit unexpected for new users that match by value and match by reference have different semantics regarding inhabited cases, but if there is a good reason, I guess everyone can live with that!

@RalfJung
Copy link
Member

For matching on references I'd say this is deep in the undecided territory. For matching on *ptr where ptr is a raw pointer, IMO it'd be a mistake to do reasoning like this completely implicitly.

@stephanemagnenat
Copy link

Yes, my concern is only about references, as they are very common in safe code (as in my AST example). For raw pointers and unsafe code, I have no opinion, as people writing unsafe code are (hopefully) typically way more advanced into Rust than people writing safe code, so they could certainly bear the increased cognitive load of a more complex mental model.

@dhedey
Copy link
Author

dhedey commented Nov 12, 2024

The explicit never patterns RFC is exciting 👍

I just wanted to quickly explore (below) how it might relate to satisfying the discussed safe-rust use cases benefit from stabilisation of explicit never patterns, without an auto-never sugaring (or something equivalent such as exhaustive-patterns).

To summarize in the introduction, never patterns are mostly intended for use in unsafe code, but they won't by themselves allow "safe land" to feel seamless without something like the auto-never rules or the exhaustive_patterns feature.

Latest example

In this latest example (playground link), according to the incomplete never_patterns feature, ! isn't enough, you have to explicitly mention the variants which contain the !:

    let _s = match &ast {
        Ast::Identifier(s) => s,
        Ast::MaybeSomething(!),
    };

Although I think there is discussion on the current RFC about possibly loosening that? I've posted a comment on the RFC clarifying.

Macro-generated nested enum example

Looking at the nested enum example from the Motivating example in the original post:

  • Under the existing incomplete never_patterns feature:
    • This can't currently be solved just by sticking a raw ! at the root of the match.
    • I thought that perhaps you could use the never pattern at each layer of the nesting - e.g. by simply inserting a ! pattern in each of the sub-matches (playground link) - we get the compile error a never pattern must be used on an uninhabited type.
    • This makes sense, from the point of view of the never pattern making it more explicit what accesses the compiler will make - but it's still a problem for macro code, as it still needs to output different code, branched on if there are 0 variants, or fall back to a workaround like "always output a _ => unreachable!() pattern".
  • I'm unsure how this changes (if at all) with the RFC

Side note - newcomer introduction to the complexities

I wanted to share my own (ineloquent - and hopefully not too inaccurate) explanation of some of the complexities on display here inspired by this great one by Nadrieril to attempt to help future me and other newcomers come up to speed on the complexities of this:

In "safe land" dealing with references, the verbosity of never patterns feel unergonomic because references to uninhabited variants cannot be constructed in safe code (they don't satisfy the "safety" invariant of a type which can be assumed in safe code). So, in safe code, the match patterns feel like boilerplate to satisfy the compiler. The code compiling with a lack of patterns would communicate the same meaning as explicit never patterns more succinctly.

But this isn't quite true in "unsafe land":

  • In unsafe land, we care about whether something is valid (i.e. whether all code including unsafe code has to uphold this invariant - and therefore that the compiler can assume it holds and optimize using this knowledge).
  • In most cases, constructing an invalid value is UB, although as an extra complexity, raw pointers and inside union fields use an access-based validity model where we only get UB if an invalid thing is accessed. For these types, if you are matching on pointers / references to partially valid values, whether we get UB is dependent on exactly how the compiler processes the match statement, and whether it "accesses" certain pointers/references and asserts the validity of them (or not).
  • There is a separate thread on whether references to uninhabited types can always be assumed invalid, but pointers to them can be valid (and even constructed in safe code).
  • So there is clarity benefit to making the resolution of the match statement more explicit, so the accesses (and so the performed validity assertions) are clear from the structure (e.g. through never patterns)

@RalfJung
Copy link
Member

That sounds pretty good. :)

Personally I am undecided at how far we want to go with "automatic never patterns" in safe code. But I can see how having to write ! for a match arm that safe code could never possibly hit can feel like boilerplate.

@stephanemagnenat
Copy link

So there is clarity benefit to making the resolution of the match statement more explicit

In the case you mentioned, and in interaction with unsafe code, yes. But when using never types to make some enum variants vanish, which is the whole idea of the "Tree that Grow" mentioned above (in that pattern one could have several compiler phases and more than one branch that vanish in each phase), that is a maintenance hindrance. So I guess there are tradeoffs between various use cases.

@dhedey
Copy link
Author

dhedey commented Nov 12, 2024

Personally I am undecided at how far we want to go with "automatic never patterns" in safe code. But I can see how having to write ! for a match arm that safe code could never possibly hit can feel like boilerplate.

In my mind, there's a compelling argument in terms of user impact - I imagine ~2 orders of magnitude more people are writing matches or destructurings with partially uninhabited types / references (e.g. infallible results) than matching on pointers to partially valid objects.

I generally like explicitness/awkwardness where it prompts the user with a necessary choice they need to handle / consider; but in safe-land code, never patterns just give the awkwardness, and don't have any pay-off in terms of improved handling or understanding.

From a rust project organisation perspective, do you think this is the right place to discuss what auto-never could look like? Or is this best left to a different channel / medium?

@dhedey
Copy link
Author

dhedey commented Nov 12, 2024

Musings I have around a potential auto-never feature include:

  • What are the rules around when an auto-never sugaring might apply? Is it purely about the type? Or about whether we’re in an unsafe block?
  • Do we add a lint for circumstances where auto-never will apply, but could conceivably be a footgun? Such a lint could be silenced by explicitly writing out never patterns (e.g. outside of an unsafe block but near unsafe code or code involving pointers, where a user might be matching on a reference to an invalid value. Such a reference is likely meets the validity variant (see discussion; related detail), but not the safety variant outside of “unsafe land” but could exist between unsafe blocks in unsafe land)
  • Could we allow auto-never to be explicitly enabled/disabled (say, with an attribute on the match statement)? How would it handle e.g. the partially filled union fields example (u32, !)?
    • On reflection, I imagine it might be not useful in practice. Perhaps an additional lint of "always require explicit never patterns" could be added instead, which would be more flexible, and could achieve the same aim if a user puts it on a setting which prevents compilation if set.

@RalfJung
Copy link
Member

From a rust project organisation perspective, do you think this is the right place to discuss what auto-never could look like? Or is this best left to a different channel / medium?

I think it makes more sense to either make this part of the never pattern RFC, or a dedicated companion RFC, depending on what @Nadrieril prefers.

@dhedey
Copy link
Author

dhedey commented Nov 12, 2024

So there is clarity benefit to making the resolution of the match statement more explicit

In the case you mentioned, and in interaction with unsafe code, yes. But when using never types to make some enum variants vanish, which is the whole idea of the "Tree that Grow" mentioned above (in that pattern one could have several compiler phases and more than one branch that vanish in each phase), that is a maintenance hindrance. So I guess there are tradeoffs between various use cases.

Yes, sorry my post was unclear. The clarity benefit was supposed to just be a benefit in unsafe land, I've edited the post to make this clearer.

For what it's worth I agree with you - my personal view is that in safe code, requiring explicit never patterns would result in slightly lower clarity, because the boilerplate is irrelevant, and is therefore a distraction from actual code/logic that the reader might need to care about.

@ijackson
Copy link
Contributor

my personal view is that in safe code, requiring explicit never patterns would result in slightly lower clarity, because the boilerplate is irrelevant, and is therefore a distraction from actual code/logic that reader might need to care about.

Quite so. Additionally, it sets macro authors up for failure. Every macro-generated match statement needs to have a spurious ! arm, or the macro will fail to compile, sometimes, with an empty enum.

@Nadrieril
Copy link
Member

I agree with all of this. I would prefer for this to be a separate discussion from never patterns: never patterns are useful on their own, and once we have them we can choose how much to elide them. I think this here issue is a good place to discuss this, with the option of going to the rust-lang Zulip to discuss finer points.

@Nadrieril Nadrieril self-assigned this Nov 12, 2024
@Nadrieril Nadrieril added the A-exhaustiveness-checking Relating to exhaustiveness / usefulness checking of patterns label Nov 12, 2024
@Nadrieril Nadrieril added the F-exhaustive_patterns `#![feature(exhaustive_patterns)]` label Nov 12, 2024
@clarfonthey
Copy link
Contributor

clarfonthey commented Nov 15, 2024

I'm removing this comment under the acknowledgement that it's not helpful, but also not using the built-in hide feature because I feel like it would be confusing for understanding the existing discussion. It's preserved here for posterity.

Forgive me for not fully understanding the circumstances, but what's confusing me is this:

There are related, more subtle cases though. For instance, what if ptr: *const !/*const Void, and now we do match *ptr {}? Such a pointer can be constructed in safe code, and let _ = *ptr; is not UB and might even be(come) safe. Similarly, match *ptr { _ => () } is not UB. But with an empty match arm now suddenly this is elevated to UB, even though no safety invariant was violated going into the match. IMO that's a footgun we want to avoid: the absence of code introduces UB into a program that doesn't ever obviously violate any validity invariant. (Constructing a place of type ! is fine. It's reading the discriminant that's a problem, but whether and when a match reads the discriminant depends on many factors, and in this case it is strictly the absence of a match arm that is the only thing this code did wrong.)

Even inside a match, shouldn't dereferencing any pointer be unsafe, and that be the source of the UB? To me, the UB here isn't that we're matching on an uninhabited reference, but rather that we've created an uninhabited reference from an invalid pointer.

Like, I get that this may not be how the compiler works in reality (still treating references as pointers, and thus having real pointers to nonexistent types even if there's no way to actually form them) but that just feels like the proper takeaway from this.

I also checked to make sure I'm not being incorrect, and the following code requires unsafe still:

enum Void {}
const VOID: *const Void = core::ptr::dangling();
fn main() { match *VOID {} }

@RalfJung
Copy link
Member

RalfJung commented Nov 15, 2024 via email

@dhedey
Copy link
Author

dhedey commented Nov 15, 2024

Even inside a match, shouldn't dereferencing any pointer be unsafe, and that be the source of the UB? To me, the UB here isn't that we're matching on an uninhabited reference, but rather that we've created an uninhabited reference from an invalid pointer.

I think there's possibly a bit of confusion in your message between e.g. pointers and references; and the difference between unsafe and UB.

If the deep complexities of these topics are new to you (as they were to me a month or two ago - and I'm still coming up the curve), I'd suggest the following reading:

  • https://www.ralfj.de/blog/2018/08/22/two-kinds-of-invariants.html - captures the difference between a safety invariant and a validity invariant (broadly - the difference between "these are properties which a developer can always expect to hold in 'safe land' code" and "these are properties that always must hold (on pain of UB)". In particular, types can sit in the "should not exist or be operated on in safe code" space but still appear (and possibly be useful) in unsafe code. When paired with match statements, because how a match expands can add "accesses" to sub-parts of the place in the match expression, resulting in assertions of validity against these sub-parts. Roughly speaking this can turn unsafe types into invalid types, i.e. turn unsafe but possibly habitable values into UB.
  • https://www.ralfj.de/blog/2018/08/07/stacked-borrows.html - an introduction to thinking about access-based validity (which is used for pointer accesses and union field accesses)
  • https://www.ralfj.de/blog/2024/08/14/places.html - an introduction to places

In terms of how it relates to this thread - a lot of this detail is thankfully irrelevant in safe-land code dealing with references (assuming unsafe-land code maintains its safety invariants on its interfaces!) so an ideal solution would have safe-land code not care about this complexity, and unsafe-land code have intuitive tools to handle this. Roughly speaking, the current direction looks to be:

The right order of tackling these appears a bit unclear, as any separate discussion ends up becoming a bit tangled up. But I'm sure Nadrieril will update us when they have a plan.

@clarfonthey
Copy link
Contributor

clarfonthey commented Nov 17, 2024

I'm removing this comment under the acknowledgement that it's not helpful, but also not using the built-in hide feature because I feel like it would be confusing for understanding the existing discussion. It's preserved here for posterity.

I guess that my source of confusion here is still that I was under the impression that coercing a pointer into a reference comes with a few contracts that must be upheld, on pain of UB. Even though &*ptr doesn't read the pointer, it requires an unsafe block because it's coercing a pointer to a reference, and because safe code makes particular assumptions about the reference, you must ensure those still hold when creating the reference.

For example, I was under the assumption that simply creating a reference from an unaligned pointer is UB whether you read that reference or not. I figured that creating a reference from an uninhabited type, which literally cannot exist, also fell into this situation. But I guess my confusion here is that there's a case where unsafe code can both create an uninhabited reference and would want to avoid treating this immediately as UB, which feels confusing.


Reading the 2018 post, it makes a little more sense, but I'm still not entirely convinced. The specific case concerns unions, particularly MaybeUninit, which are already unsafe to match in general. Again, the key issue here, to me, is that we're not just dealing with pointers, but actual references, and which come with their own validity constraints.

Like, I'm fine with the idea of creating references not being the source of UB, and instead accessing them. All this is fine. What's particularly bothersome to me is the implication that match x { ! } or even match x { &! } is not clearly an access, since it feels clear that it's an assertion that the type is empty which could invoke UB if it's not.

Like, don't get me wrong, I love never patterns as an explanation tool, but I'm still not convinced they should be added as a language syntax, because the positives don't really outweigh the negatives. Like, you already need to consider several non-obvious aspects of types in unsafe code, liki ZSTs for example, and I'm not sure that uninhabitedness assertions are weird enough that they deserve gating reference patterns behind an entirely new, custom syntax for it.


Also rereading this, it's clear I flipped between being against and for access-based UB in my explanation, which admittedly was one of the sources of confusion before. But I'm not convinced that this deserves blocking basic ergonomics behind a special syntax that will complicate the fundamentals of match blocks just because it might be unintuitive that a match accesses a place and can thus invoke UB if it's invalid.

@RalfJung
Copy link
Member

RalfJung commented Nov 17, 2024

For example, I was under the assumption that simply creating a reference from an unaligned pointer is UB whether you read that reference or not.

That is correct. It is also entirely irrelevant in your example since that example does not involve any references.

there's a case where unsafe code can both create an uninhabited reference and would want to avoid treating this immediately as UB, which feels confusing.

No, that is not the case.

Unsafe code can create a place to an uninhabited type, or an unaligned place. This is already the case today. The following code is well-defined:

let ptr = &raw const *(23 as *const i32);
let ptr = &raw const *(23 as *const !);

But places and references are very different things. Please carefully ready the resources that were recommended to you above, in particular my recent blog post about places.

You keep talking about references. Please explain why, since as I already said above, it really doesn't make sense in the context of this example, where there are no references involved:

enum Void {}
const VOID: *const Void = core::ptr::dangling();
fn main() { match *VOID {} }

@Nadrieril
Copy link
Member

Nadrieril commented Nov 17, 2024

What's particularly bothersome to me is the implication that match x { ! } or even match x { &! } is not clearly an access, since it feels clear that it's an assertion that the type is empty which could invoke UB if it's not.

I'm confused: the whole point of ! is that they are an access. That's their only purpose.

it's clear I flipped between being against and for access-based UB in my explanation

There's nothing to be for or against, at least in the context of this issue: the kind of "access-based" that we're talking about is a core fact of rust unsafe semantics, it's not up for debate here.

It seems to me that you're not familiar with the specific operational semantics context that's assumed when talking about this issue. This is not the place to explain this context, would you mind continuing this conversation on Zulip?

@clarfonthey
Copy link
Contributor

clarfonthey commented Nov 18, 2024

I have a particular aversion to using Zulip for discussion for a number of reasons, although I will stop discussing this here since you're right that this isn't the place. However, I will add that I think that providing the operational semantics context specifically for this issue is an important piece of justifying why this particular change is blocked on some kind of never patterns, and I don't think that this issue nor the proposed RFC do a good enough job of explaining that. A collection of blog posts, while very helpful, does not constitute a sufficient motivation either: it should be combined together in a single explanation and I would like to see that.


Also, FWIW, in hindsight I don't think that either of my explanations was particularly good, so, I'm editing them to make that clear while preserving them since they're otherwise important for understanding the discussion that happened here.

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 A-patterns Relating to patterns and pattern matching C-bug Category: This is a bug. F-exhaustive_patterns `#![feature(exhaustive_patterns)]` T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-opsem Relevant to the opsem team T-types Relevant to the types team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

9 participants