This is a fresh take based on just the current nightly 2018 edition behavior, (what I recall of) this blog post, and (what I recall of) the discussion around an access-based model.
I think the important thing to note is that types like (u32, !)
are very unlikely to show up in non-generic code. Thus, the most important thing when considering how the concrete type should be when its used in a generic context, which is where it’s likely to be.
We want the !
to effectively make any code after it dead after it exists. We need it to not change anything before it exists. And we want to allow partial initialization to work in generic code, as that is something people are currently doing and we have no reason to break. Furthermore, we want to allow, by methodology of !
-pattern elaboration, to allow match
s to ignore !
-dead enum variants.
To start with, let us say that a match
statement contains an automatically-inserted !
pattern which will catch any “strongly uninhabited” pattern as unreachable. I will define this term along with what it accesses and means for code as we go along.
The trivial base case is the !
type and the empty enum – the types that you can write an empty match over today.
let never = panic!();
match never { }
// elaborated
let never: ! = panic!();
match never {
!,
}
The obvious generic case is enum Either<Left, Right> { Left(Left), Right(Right) }
. We want to be able to use Either<_, !>
to delete the Either::Right
variant.
let x = either(|| 10, || panic!());
let Either::Left(x) = x;
// elaborated
let x: Either<i32, !> = either::<i32, !>(|| -> i32 { 10 }, || -> ! { panic!() });
let x: i32 = match x {
Either::Left(x) => x,
!,
};
From this behavior we derive that a newtype variant around a strongly uninhabited type must also be strongly uninhabited. If a newtype variant is just a 1-tuple variant, then that would also imply that n-tuple variants containing a !
should be strongly uninhabited, but let’s treat the newtype as special currently.
Here the !
pattern destructures the 1-tuple variant into a strongly uninhabited type. As this cannot exist, that branch is dead code.
Nothing so far should be contentious. This is all strongly desired behavior from the !
type.
Now let’s consider the common pattern of *mut !
to fill the same use case as C void*
. Obviously, moving forward an extern type
should be preferred – an extern type is not considered uninhabited, just unintrospectable by Rust code. However, *mut !
is not problematic – a pointer can be null or dangling and Rust doesn’t care. Rust only cares about the validity of a raw pointer when you dereference it. Dereferencing a *mut !
is unsafe and instant UB, as it produces a strongly uninhabited type.
But what about &!
? The trivial types-as-contracts position is that &!
should be strongly uninhabited as well – as there cannot be a valid !
, there cannot be a valid reference to !
. However, the access-based model that we’re leaning towards currently says that the validity of the reference only matters when it is used. (Note that passing it into a safe function definitely counts as a use, as does returning it from any safe function.)
But how much does this matter in practice? In safe code, it should be impossible to get to a reference-to-!
, as that would require having a !
in the first place. This author is of the position that &!
can be considered strongly uninhabited safely – unsafe code authors should use a pointer type or extern types. We should probably lint against creating uninhabited enums anyway, and suggest using !
or an extern type when possible.
The beneficial result of this classification is that Either<&_, &!>
continues to have it’s never side deleted.
let x = either(|| 10, || panic!());
let x = x.as_ref();
let Either::Left(x) = x;
// elaborated
let x: Either<i32, !> = either::<i32, !>(|| -> i32 { 10 }, || -> i32 { panic!() });
let x: Either<&i32, &!> = x.as_ref();
let x: &i32 = match x {
Either::Left(x) => x,
!,
};
This can, however be accomplished without deciding that &!
is definitely strongly uninhabited – by relying on match binding modes!
At this point I feel like I’ve just restated the blog post, honestly. (I’m a bit too tired to be thinking this hard.) I think !
-elaboration is a good way to talk about how the compiler “catches” the unreachable patterns, and that the auto-never rules can describe which patterns are considered strongly uninhabited.
I think it’s desirable that a struct containing a !
field should be considered strongly uninhabited. Consider a routine that provides additional error information:
type ParseResult<T, E> = Result<T, ParseError<E>>;
struct ParseError<E> {
cause: E,
location: (u32, u32),
}
I’d still want to be able to use !
to delete the error case. If !
-uninhabitedness passes through struct (and struct variant) membership, I’d expect the same from tuple (and tuple variant) membership.
I’ve reasoned myself into a corner and I’m not sure what the best way out is anymore. Considering these uninhabited and allowing !
-elaboration to catch and delete these arms feels right. Probably any !
elaboration should raise a lint “in and around” unsafe blocks.
Whatever the solution is, it shouldn’t rely on leaking implementation details of the generic function, however. Whether the function does its internal workings via piecewise initialization or separate stack values put together at the end of the function should not matter for what the generic arguments are allowed to be.
Acquiring the !
is the point of UB where code can start getting optimized out. The question is what are the requirements to observe that the !
exists, and thus invoke UB and optimize out code.