rust-lang / rust-lang/rust

Compilation error when matching reference to empty enum

Open
#131,452 26 comments 2 reactions 1 assignee View on GitHub

@Nadrieril is already working on this.

Since Nov 12, 2024.

A-exhaustiveness-checking A-patterns C-bug F-exhaustive_patterns T-compiler T-opsem T-types
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

[!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: https://github.com/rust-lang/rust/issues/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 https://github.com/rust-lang/unsafe-code-guidelines/issues/413#issuecomment-2399672350, @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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.