Never-type fallback =================== ## Background: The stabilization of the `!` ('never') type was recently attempted in https://github.com/rust-lang/rust/pull/65355, but reverted in https://github.com/rust-lang/rust/pull/67224. The reasons for the revert has to do with a relatively obscure feature of Rust known as "never-type fallback" (just "fallback" from here on out). ### What is the `!` type? Rust allows the creation of 'empty enums' with no variants (e.g. `enum Void {}`). These types are unusual in that it is **impossible** to construct an instance of them - they are said to be 'uninhabited'. The `!` type is a sort of 'canonical' empty enum, used to represent something which is statically impossible (e.g. `panic!()` or `abort!` returning). Unlike other empty enums, the `!` type has the ability to **coerce** to any other type. For example, consider this function: ```rust fn match_it(val: Option<u8>) { let a: bool = match val { Some(_) => true, _ => panic!() }; } ``` Here, we `match` on an `Option<u8>`, producing a `bool`. Each arm of a `match` expression must produce the same type, so that the overall expression always produces the same type. The first arm produces `true` (a `bool`), while the second arm calls `panic!()`, which produces `!`. Coercion works because any code that has a `!` type available/in-scope is known to be statically unreachable. In the case of the `match` expression, we know that anything after the call to `panic!()` is unreachable. So, it's fine to "pretend" that `panic!()` produced a `bool`, since no one will be able to observe the fact that no `bool` was ever produced. ### What is fallback? In the previous example, we used a `!` type where a `bool` was expected. Inside the compiler, this is represented by replacing the `!` type with a new type variable: https://github.com/rust-lang/rust/blob/689fca01c5a1eac2d240bf08aa728171a28f2285/src/librustc_typeck/check/coercion.rs#L179 We will then unify this type variable with the expected type `bool`. Effectively, we're now pretending that `panic!()` returns a `bool`. Since `panic!()` never actually returns, this is perfectly fine. This type variable was unified with `bool` because the `match_it` function required the `match` expression to produce a `bool`. However, what if we didn't impose any such restrictions: ```rust let my_var = panic!() // What is the type of `my_var`? ``` Using some closure trickery, we can find out ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=dd70592fa0a5289a90887273222206a0)): ```rust fn output_name<R, T: FnOnce() -> R>(_: T) -> &'static str { std::any::type_name::<R>() } fn main() { let some_closure = || { let _my_var = panic!(); _my_var }; println!("Type of _my_var: {}", output_name(some_closure)); } ``` Output: ``` Type of _my_var: () ``` Surprisingly, the type of `_my_var` is not `!`, but `()` - despite the fact that `panic!()` has a return type of `!`. This is the result of **fallback**. Earlier, we described how never-type coercion works by replacing instances of `!` with fresh type variables. If such a type variable never ends up unified with another type, we need to pick what type it should be. Currently, this is `()`: https://github.com/rust-lang/rust/blob/689fca01c5a1eac2d240bf08aa728171a28f2285/src/librustc/ty/context.rs#L2289 This behavior can be surprising, and somewhat undesirable. You would expect a `!` type to stay a `!` type, unless you try to coerce it into something else. ### Why can't never-type fallback produce a `!`, instead of `()` This is exactly what the original stabilization of the `!` type tried to do. Unfortunately, this lead to undefined behavior being introduced in certain scenarios: https://github.com/rust-lang/rust/issues/66757#issuecomment-559136943 The core of the issue is code which looks like this: ```rust 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. }; } ``` There are several things going on here. The most significant is the use of an "unconstrained return type" - the function `unconstrained_return` claims to be able to produce an instance of *any* `T`. (In practive, such a function would need to `panic!()`, or be `unsafe` and require the caller to verify that the type can actually be produced somehow). This return type ends up getting unified with the return value of a `panic!()` expression, which is similarly unconstrained. Previously, fallback would result in `panic!()` getting a type of `()`. However, with the never-type fallback change, `panic()!` will now 'correctly' evaluate to `!`. This means that the call to `unconstrained_return` will also end up with a return type of `!` - despite the fact that it actually returns!