Inconsistent exhaustiveness check on enum uninhabited variants

Depending on the context, the compiler requires (or not) that I match Foo::Baz. What's the difference here? Why does this compile, while the version without the Baz branch in the fmt body doesn't compile?

This looks very much like a compiler bug :sweat_smile:

(tested both on stable and nightly)

enum Foo {
    Bar(i32),
    Baz(Infallible)
}

impl fmt::Debug for Foo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            Self::Bar(x) => write!(f, "Bar is {x}"),
            // Doesn't compile without this branch, yet it does for upcast. what's up?
            Self::Baz(infallible) => match infallible {},
        }
    }
}
impl Foo {
    pub fn upcast(self) -> Foo {
        match self {
            Self::Bar(x) => Self::Bar(x+1),
        }
    }
}

(Playground)

That is intentional (was discussed when exhaustive_patterns was stabilized). The reason is that while having a value of an uninhabited type is UB, having a reference to it might not be (it's undecided). It's still unsound to expose it to safe code (it might just dereference it), in other words this is a safety invariant and not a validity invariant, but it was decided that allowing to omit the match arm (which will be UB if no arm is reached) is too easy way to promote this "dangerous" type to actual UB, and we want to be more explicit. The "more explicit" is the still-unstable never pattern, spelled as:

#![feature(never_patterns)]

match *self {
    Self::Bar(x) => write!(f, "Bar is {x}"),
    Self::Baz(!)
}

If omitting the match arm is disallowed, then why does the upcast function compile? We are omitting the arm right there.

As said, upcast compiles because having a value of an uninhabited type is certainly UB.

When you match *self, the thing matched is not a value expression; it’s a place expression. Converting a place expression to a value requires actually reading from that place, but whether that conversion needs to happen or not depends on the pattern used to bind self or parts of self.

Self::Baz(infallible) does require an actual read, but Self::Baz(ref infallible) would not. So, if references to uninhabited types were to have broken safety invariants but not broken validity invariants, then the match arm for Self::Baz in the fmt example does need to be included to perform an actual dereference.

Here’s an example using match {*self} (blocks are always value expressions, not place expressions): Rust Playground
(I wouldn’t recommend this compared to the extra match arm, since matching {*self} seems quite weird.)

Notice that in your example, Self: !Copy and yet you managed to dereference self. In my modified version, I had to add derive(Clone, Copy) to get it to compile.

Thank you! That's very clear!

I keep hearing about "place expression" vs "value expression" and I didn't quite understand the distinction. This explanation just clarified both why the inconsistent exhaustiveness check and place expression :sweat_smile: .

I was wondering about the compiler not complaining about "Foo doesn't implement Copy", you also manage to clarify that.

Something to add to https://this.quiz.is.fckn.gay . I'll probably do that Sunday

You might want to read What is a place expression?.