Compilation error when matching reference to empty enum
@Nadrieril is already working on this.
Since Nov 12, 2024.
- 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:
- The closely related https://github.com/rust-lang/rust/issues/119612#issue-2067102446 was stabilized a couple of months ago.
- The full https://github.com/rust-lang/rust/issues/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: 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.