I'd quite like to move forward on the stabilization of the exhaustive_patterns feature, which allows omitting match
arms for empty types, e.g.:
let x: Result<u8, !> = Ok(5);
match x {
Ok(y) => y,
// no need to add a `Err` pattern, as `!` is uninhabited.
}
Summary
I propose a lint that warns when we omit an empty arm. Unsafe users can turn it on to avoid footguns, safe users can leave it off.
Motivation
The main blocker for exhaustive_patterns
is the interaction of omitted match
arms with unsafe code. This was raised, along with a proposed solution, by @nikomatsakis here.
Here's how I understand it: an empty type is a type that has no valid values. Around unsafe code, we can have invalid or uninitialized values, so we must be careful about which code assumes/implies validity.
First example (playground):
#[derive(Copy, Clone)]
enum Void {}
union Uninit<T: Copy> {
value: T,
uninit: (),
}
unsafe {
let mut x: Uninit<(u32, Void)> = Uninit { uninit: () };
x.value.0 = 22; // initialize first part of the tuple
match x.value {
(v, _) => println!("{v}"),
}
}
Here we get a perfectly reachable arm of type (u32, Void)
, because the tuple was partially initialized. Omitting the arm could also be ok, and would assert validity of the second element of the tuple. In safe code however this arm is truly unreachable.
The second example (taken from @RalfJung here) is x: &!
. Here match x {}
implicitly dereferences x
, checks the discriminant, and declares UB because there can be no valid discriminant. Compare with match x { y => ... }
which is essentially just a let
-binding. Here the absence of an arm triggers the dereference and validity assertion. This is a footgun.
The conclusion is that something must be done to treat empty patterns specially around unsafe code.
Proposal
The original proposal proposed special patterns called "never patterns" to denote unreachable arms that assert/require validity of some empty type. No progress happened on this since however. My proposal is more humble.
I propose a new allow-by-default omitted_empty_arm
(bikesheddable name) lint, gated under exhaustive_patterns
. It warns the user if they omitted an empty arm. For example:
let x: Result<T, !> = ...;
#[allow(omitted_empty_arm)]
match x {
Ok(y) => ...,
// all good
}
#[deny(omitted_empty_arm)]
match x {
Ok(y) => ...,
// ERROR: add a "Err(_)" arm
}
In other words, whenever a match would be accepted under current exhaustive_patterns
but rejected without the feature, instead of emitting a "match is not exhaustive" error, we emit a "match should mention empty patterns explicitly" warning.
The intention is that users of unsafe would turn it on to avoid accidental soundness mishaps, and users of safe code would leave it off. If a user of unsafe does want to assert validity they can turn the lint off locally, which acts as a reminder that something fishy is going on. This is in the vein of @nikomatsakis's "let users choose what kind of help they want from the compiler".
This isn't as elegant as the full never patterns proposal, but has the advantage that I can implement it myself in a few days if this gets positive feedback. This is also forwards-compatible with never patterns.
Drawbacks
Having the lint allow-by-default risks that unsafe users forget to turn it on and shoot themself in the foot. This could be solved with profiles.
Alternatives
Instead of an optional lint, we could set it deny-by-default. This has the same behavior (modulo different diagnostics) as leaving exhaustive_patterns
off. This could allow to stabilize the feature without changing which match
statements are allowed. Warn-by-default is an interesting middle-ground too.
I don't consider never patterns as an alternative since this is forwards-compatible with it. I know of no other alternative, apart from never stabilizing exhaustive_patterns
.
Open questions
- Should the lint be allow-by-default (and unsafe users must remember to turn it on), or warn/deny-by-default (and safe users will turn it off if they don't do unsafe)?
- When the lint is off, should we warn that empty arms are unreachable?
- Instead of/in addition to setting the lint level by hand, should we turn it on automatically within unsafe blocks? If so, how do we still letusers control it?
EDIT: clarified my intentions around allow-by-default