Description
Note
Useful background links which have been shared in comments:
- The closely related Tracking Issue for
min_exhaustive_patterns
#119612 (comment) was stabilized a couple of months ago. - The full Tracking issue for RFC 1872:
exhaustive_patterns
feature #51085 feature, particularly this post by Nadriedel summarising the subtleties. - Niko’s 2018 blog post on the never pattern for matching uninhabited types, and an "auto-never" desugaring.
- Never patterns RFC (in progress as of November 2024)
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:
match *self {}
match self { ! }
in the body as suggested by Ralf which is non-stable.
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.