Skip to content

Compilation error when matching reference to empty enum #131452

Open
@dhedey

Description

@dhedey

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.

Metadata

Metadata

Assignees

Labels

A-exhaustiveness-checkingRelating to exhaustiveness / usefulness checking of patternsA-patternsRelating to patterns and pattern matchingC-bugCategory: This is a bug.F-exhaustive_patterns`#![feature(exhaustive_patterns)]`T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-opsemRelevant to the opsem teamT-typesRelevant to the types team, which will review and decide on the PR/issue.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions