rustc
, never-type
, bug
()
67225An empty type is something that never exists in the runtime as there is no way to create one and therefore, such type in code gives us a static gurantee that a piece of code will never be executed.
use std::process::exit;
fn main() {
let x: ! = exit(0);
// `x` is in scope, everything below
// here is dead code.
let y: String = match x {
// no match cases as `Never` has
// no variants
};
// we can still use `y` though
println!("Our string is: {}", y);
// we can use `x` to diverge
x
}
An HIR node is divergent if it does not exit normally, instead it return
s / break
s / panic
s leaving behind dead code.
In rustc, this is tracked using the enum Diverges.
There are different variants of it:
Maybe
- Some of the cases diverge, while others require further analysis to determine.Always
- Certainly known to diverge, so next sibling and parent are unreachable.WarnedAlways
- Same as Always
with a reachability warning always emitted.Further explanations given by Niko:
So, what would a never returning node result in ?
Ans: !
type.
Similarly, !
is also a type that can be assigned. Therefore, sometimes the compiler creates an inference variable ?X
to represent the type to which !
is being coersed.
e.g.
let x: u32 = return ...
will type-check, because the type of return will be ?X
and ?X = u32
is a valid result.
The problem is that we generally require all inference variables to have a defined value.
Which is why let x = None
won't type-check on its own and would require Option<?T>
.
What is ?T
?
Ans: Its a constraint.
But it would be annoying if we required to constrain the divergent variables as it should not matter what !
is coersed to.
Therefore,
let x = return 22;
shouldn't fail compilation if we can't figure out the type of x
. Therefore, today, we assign ?X
as the type of x
and at the end of type-checking, when we still couldn't resolve ?X
, we fallback to ()
as the type of x
.
The fallback type in this case should be !
instead of ()
.
We start out by detecting cases where that's a problem.
fn unconstrained_return<T>() -> Result<T, String> {
let ffi: fn() -> T = transmute(some_pointer);
Ok(ffi())
}
fn foo() {
match unconstrained_return::<_>() {
Ok(x) => x, // `x` has type `_`, which is unconstrained
Err(s) => panic!(s), // … except for unifying with the type of `panic!()`
// so that both `match` arms have the same type.
// Therefore `_` resolves to `!` and we "return" an `Ok(!)` value.
};
}
Walk through what goes wrong:
T
gets specified as _
– _
means "make fresh inference variable", let's call it ?X
unconstrained_return::<?X>()
is Result<?X, String>
x
in Ok(x)
is ?X
Ok(x) => x
is ?X
s
in Err(s)
is String
Err(s) => panic!(s)
is !
!
we create a fresh variable ?V
?V
as divergingM
such that both match arms can be coerced to that type M
?M
where ?X
can be coerced to ?M
?V
can be coerced to ?M
?X = ?V = ?M
?X
and ?V
are both unconstrained
?X
?V
falls back to !
, so effectively ?V
becomes !
?X
is unified, it becomes !
Where is this implemented?
fallback_if_possible
!
Slightly bigger than the set of unified variables
fn foo<T, U>() where T: Eq<U> { }
trait Eq<A> { }
impl Eq<!> for ! { }
diverging fallback
.
diverging fallback
.HIR_ID
for expressions where flag was set to warnedAlways
on entry.warn_if_unreachable
hir_id
to some set roughly on this line if self.diverges()
is set to WarnedAlways
Example of case where we would NOT issue a warning but is still UB:
// the bet is that an unconstrained return value like `T` here signals data
// that is never used, e.g. this fn could return `Ok(())`, but in this case
// that is not true:
fn unconstrained_return<T>() -> Result<(), T> {
let ffi: fn() -> T = transmute(some_pointer);
ffi(); // produces a value of type T
}
fn foo() {
match unconstrained_return::<_>() { // _ is ?X
Ok(_) => Ok(()), // type of `Ok` is `Result<(), ?A>`
Err(s) => Err(panic!(s)), // type of `Err(_)` is `Result<?B, ?V>` where `?V` is diverging
}; // type of expression is `Result<(), ?V>`
}