We would like:
Some(_)
when matching on x: Option<!>
;match *x {}
with x: *const !
has the side-effect of asserting validity of the data at place *x
, which is not clearly indicated by the match; this proposes match *x { ! }
instead;An empty type or uninhabited type is a type that has no valid value, like !
, Result<!, !>
, or Result<(i32, !), !>
. (For this purpose, we pretend that private fields in foreign structs are inhabited).
An empty pattern for a type is a pattern that can be written but does not match a valid value of the type. E.g. Some(_)
for the type Option<!>
. (For this purpose, we pretend that private fields in foreign structs are inhabited).
Given a match expression, an arm is:
Most of the time, usefulness and reachability coincide. It is however possible to get an arm which is useful && !reachable
, with the combination of empty patterns and partially-initialized values. This is the trickiness that I want to address in this proposal.
Example (under conservative assumptions, see Unresolved Questions):
We add !
as a pattern that is allowed for any uninhabited type (e.g. !
or (u32, !)
). The never pattern means the !
pattern. A never pattern means any pattern that contains !
.
An arm whose pattern is a never pattern must omit its body, e.g.:
Semantically, a never pattern acts as a load of a nonexistent value from the place. A Some(!)
arm is semantically indistinguishable from Some(a) => match a {}
. In particular, a never pattern is always unreachable in the sense above (but can be useful).
We compute usefulness and reachability by tracking validity of the data at each place on top of the existing exhaustiveness algorithm.
A place is known_valid
if it is known to contain data valid for its type. We consider a place known_valid
except if it is inside a union field or a deref (raw ptr or &T
). We check this syntactically for the scrutinee, then recursively as we dig into patterns.
If a place is known_valid
, then empty patterns are unreachable and unuseful. If a place is !known_valid
, then an empty pattern can be reachable and/or useful, depending. If it has a !
or a binding on an empty type, it is unreachable. Other cases build on the usual exhaustiveness algorithm.
The diagnostics I propose are:
useful && reachable
, we don't lint anything;!useful && !reachable
, we suggest the user remove it;useful && !reachable
and it isn't already a never pattern, we suggest the user replace it with a corresponding never pattern;_ => {}
at the bottom of the match would be reachable, the match is not exhaustive. The user must add arms to make it exhaustive. If a never pattern would make sense we suggest that, otherwise we suggest a normal pattern.Under this proposal:
let Ok(x) = y
if y: Result<T, !>
;
y: Result<T, &!>
, see discussion of safety below.match *x {}
is not allowed; we require match *x { ! }
or match *x { _ => {} }
.Phrased in terms of validity, the current stable implementation of exhaustiveness assumes all places invalid, except specifically the scrutinee if its type is !
or an empty enum.
The current implementation of the exhaustive_patterns
feature essentially assumes all places valid (with a small inconsistency around &!
).
Both of these warn unreachable on arms that are useful, which is pretty bad; see e.g. https://github.com/rust-lang/rust/issues/117119.
Both of these also allow some empty matches that have side-effects. Specifically match ... {}
is allowed on stable if the scrutinee has type enum Void {}
or !
, even if the place comes from a raw ptr/union field/reference. We'll need to carve out this exception for backwards-compatibility, at least at first. Ideally we can deprecate this in favor of a never pattern. Note: this exception does not concern composite types like (u32, !)
or Result<!, !>
; only specifically non-nested !
and empty enums.
minimal_exhaustive_patterns
that does the diagnostics I proposed, except we don't suggest never patterns.
let Ok(x) = y
for y: Result<T, !>
.match ... { ! }
.exhaustive_patterns
in favor of never_patterns
.match ... { ! }
.What is or isn't valid depends on opsem decisions. I tried to err on the side of future-compatibility. These questions include:
&T
must point to a valid T
(https://github.com/rust-lang/unsafe-code-guidelines/issues/413);Option<T>
can have variant Some
with not-fully-valid data for T
(https://github.com/rust-lang/unsafe-code-guidelines/issues/443);Most users would like to write let Ok(x) = y;
when y: Result<T, &!>
. This is when y
is safe for its type. This is in tension with the desire to be explicit about side-effects, since this implicitly asserts that y
is not Err(&uninit)
. It's not unthinkable that there'd be a middle way; for example it seems correct to assume safety if there are no unsafe
blocks anywhere in the crate.
The current proposal is forward-compatible with assuming safety later. Note also that we can write let Ok(x) | Err(&!) = ...;
which isn't too bad.