Never type initiative
What is this?
This page tracks the work of the never type initiative! To learn more about what we are trying to do, and to find out the people who are doing it, take a look at the charter.
Current status
The following table lists of the stages of an initiative, along with links to the artifacts that will be produced during that stage.
Stage | State | Artifact(s) |
---|---|---|
Proposal | ✅ | Tracking issue |
Experimental | 🦀 | Evaluation |
RFC | ||
Development | 💤 | Explainer |
Feature complete | 💤 | Stabilization report |
Stabilized | 💤 |
Key:
- ✅ -- phase complete
- 🦀 -- phase in progress
- 💤 -- phase not started yet
How Can I Get Involved?
- Check for 'help wanted' issues on this repository!
- If you would like to help with development, please contact the owner to find out if there are things that need doing.
- If you would like to help with the design, check the list of active design questions first.
- If you have questions about the design, you can file an issue, but be sure to check the FAQ or the design-questions first to see if there is already something that covers your topic.
- If you are using the feature and would like to provide feedback about your experiences, please [open a "experience report" issue].
- If you are using the feature and would like to report a bug, please open a regular issue.
We also participate on Zulip, feel free to introduce yourself over there and ask us any questions you have.
Building Documentation
This repository is also an mdbook project. You can view and build it using the following command.
mdbook serve
✏️ Updates
Lang-team initiatives give monthly updates. This section collects the updates from this initiative for posterity.
To add a new update:
- Create a new file
updates/YYYY-mmm.md
, e.g.updates/2021-nov.md
- We recomend basing this on the update template
- Link it from the
SUMMARY.md
2022-01: Lang team update
Summary
- Work has started on inlining problems/evaluations from issues, gists, etc. See no inference changes, PRs/issues; the no inference changes page is likely mostly complete. Some other pages are also available but in more draft status.
Goals for this month
- Continue to fill out scenarios, particularly explanations of current feature-gated state and the effects (breakage, expectations for next steps).
Questions for discussion, meeting proposals
- No particular all-lang questions yet. Current trajectory suggests that we will have a complete-ish set of documents for lang review in 1-2 months, depending on bandwidth. Current expectation is that we have a roughly ready nightly implementation, but more work is needed to document it and the tradeoffs it makes.
📜 {{INITIATIVE_NAME}} Charter
Proposal
Membership
Role | Github |
---|---|
Owner | ghost |
Liaison | ghost |
🔬 Evaluation
The evaluation surveys the various design approaches that are under consideration. It is not required for all initiatives, only those that begin with a problem statement but without a clear picture of the best solution. Often the evaluation will refer to topics in the design-discussions for more detailed consideration.
PRs/Issues
This is intended to give a relatively quick link list to relevant issues/PRs or individual comments, for possible referencing/inclusion elsewhere.
Necessity of fallback changes
Canonical example of inference failure with no-inference-change scenario.
#![allow(unused)] fn main() { struct E; impl From<!> for E { fn from(_: !) -> E { E } } #[allow(unreachable_code)] fn foo(never: !) { <E as From<!>>::from(never); // Ok <E as From<_>>::from(never); // Inference fails here } }
Discussion in:
Conditional fallback
See v1 explainer for the details of the core algorithm.
Most of this code is implemented, gated on never_type_fallback
, landed in
#88149 (primarily refactoring) and
#88804 (most of the changes).
Pain point / confusing behavior
- Closure return types are
()
even if body!
- Missing
From<!> for T
- Conflicts with
From<T> for <T>
- After stabilizing
!
, a crate can addFrom<!> for MyType
preventing a std impl.
- Conflicts with
- Uninhabited enums (e.g., for errors)
Miscellaneous
- Proposed (and rejected) change of fallback to
!
rust-lang/rust#40801. - Propagating coercions more 'deeply' rust-lang/rust#40924 May help avoid some of the current problems of over-eagerly coercing into inference variables, for example.
- Prohibit coercion to
!
from other types in trailing expressions after a diverging expression rust-lang/rust#46325- Affected rust-lang/rust#40224, removing the type-check success case discussed therein.
resolve_trait_on_defaulted_unit
lint rust-lang/rust#39216
This discusses the impact of just stabilizing !
syntax and changing
std::convert::Infallible
to be an alias for !
. (That is, no inference
algorithm changes).
Regression: inference failures
This leads to the regression identified in rust-lang/rust#66757.
#![allow(unused)] fn main() { struct E; impl From<!> for E { fn from(_: !) -> E { E } } #[allow(unreachable_code)] fn foo(never: !) { <E as From<!>>::from(never); // Ok <E as From<_>>::from(never); // Inference fails here } }
Without changes to type inference fallback, code such as the above with
explicit !
type now fails to compile. This happens because with !
, unlike
with std::convert::Infallible
, !
can coerce to any variable. That means that
the coercion from !
to _
succeeds, leading to the inference failure here,
as there's no constraint placed on the argument.
The coercion to inference variables is in general desirable, because of code like this:
#![allow(unused)] fn main() { if foo { loop {} } }
if
expressions without else currently desugar such that the "block" is typed
at an inference variable (later eq'd with ()
), so the coercion to an inference
variable is necessary to make this work with the current compiler
implementation. Further, changing the compiler to avoid this seems relatively
hard (FIXME: just how hard?). This regression is prominent enough that it makes
fully avoiding inference changes not possible.
Over-eager ()
fallback
This leads to the painpoint (amongst others) identified in
rust-lang/rust#66738. This may
also be a regression in some cases if users start more explicitly writing !
in
some places, though is not directly one at just stabilization time.
fn magic<R, F: FnOnce() -> R>(f: F) -> F { f } fn main() { let f2 = magic(|| loop {}) as fn() -> !; }
The (presumed) expected behavior is that loop {}
, having type !
, means that
the closure above will have a return type of !
. However, that's not what
actually happens: the closure's return type is set to an inference variable,
?T
. Since expressions in the return position are coerced to the return type,
we end up with no requirement on the return type's inference variable. (The cast
does not participate in inference, generally). This means that the inference
variable is unconstrained and so falls back to ()
.
Note that this behavior is currently relied upon by some libraries. For example,
code like this is relatively common, which requires that closures return ()
or
u32
. This means that if the fallback doesn't go to ()
, we will end up with
an error.
trait Bar { } impl Bar for () { } impl Bar for u32 { } fn foo<R: Bar>(_: impl Fn() -> R) {} fn main() { foo(|| panic!()); }
Take a snippet from warp 0.1.11:
#![allow(unused)] fn main() { wr_rx.map_err(|()| { unreachable!("mpsc::Receiver doesn't error"); }) .forward(tx.sink_map_err(|_| ())) .map(|_| ()); }
This code fails if the first closure is typed as impl Fn(()) -> !
, because
forward
requires that !: From<()>
(i.e., that the error types are compatible). That
impl is not currently available, so this code doesn't work
out.
This kind of interaction is relatively common, which makes changes to closure return types that "make sense" at an intuitive level actually commonly result in errors.
Unsizing fails due to eager fallback
See rust-lang/rust#49593.
#![allow(unused)] fn main() { fn foo(x: !) -> Box<dyn std::error::Error> { Box::new(x) } }
This is essentially the same as the previous case, where we have x
generate a
coercion at the function call site, meaning that we have Box::<?T>::new(...)
with no constraints on ?T
, which means ?T = ()
with no fallback changes.
Since (): !Error
, this fails to compile.
📚 Explainer
The "explainer" is "end-user readable" documentation that explains how to use the feature being deveoped by this initiative. If you want to experiment with the feature, you've come to the right place. Until the feature enters "feature complete" form, the explainer should be considered a work-in-progress.
Never type fallback proposal
This is a proposal for an alternative scheme for never type fallback. This scheme, while not fully backwards compatible, sidesteps the problems we've encountered so far in attempting to stabilize the !
type:
- Unsound type inference changes from changing fallback, as described in #66173. These problems result from changing the fallback from
()
to!
for type variables wind up interacting with "live code", resulting in!
values being created. - Regressions from having no fallback at all, as described in #67225 and #66757.
Background
The current fallback scheme is based on the concept of "diverging" type variables. In short, a !
type can be coerced to any other type. But when it is inferred to an unknown type ?T
(a type variable), the way that we handle it is to create a diverging type variable ?D
and unify the two. Once type-checking is complete, we walk over any unbound type variables. If ?D
has not yet been unified with any concrete type (it may have been unified or related to other general type variables, but none of those type variables have yet been assigned a type), then it will "fallback" to a specified type. In current Rust, that type is ()
. The !
type RFC proposed changing that fallback type to !
(as part of introducing the concept of !
as a standalone type). The idea behind this fallback is that since ?D
represents the type of an expression that is known to diverge, what actual type it is assigned to doesn't matter, since it can never affect live code. Unfortunately, this premise is false.
Bug 66173: Unsoundness introduced by changing fallback from !
to ()
As described in #66173, we have found in practice that the fallback of diverging type variables can impact the types assigned to live code. The most common problem involves match patterns. Consider the following pattern:
#![allow(unused)] fn main() { let x = match something() { pattern1 => Default::default(), pattern2 => panic!("..."), }; }
In a case like this, the type of the match is ultimately represented by a type variable ?M
. The first arm is assigned a type variable ?T
and the second arm (which panics) gets the type !
. Both ?T
and !
are coerced into ?M
:
?T -> ?M
! -> ?M
The first coercion creates a subtyping relationship (?T <: ?M
) because the two types are unknown. The second coercion creates a diverging type variable ?D
and a subtyping relationship ?D <: ?M
.
The problem now is that if ?D
falls back to !
, then this winds up causing ?M
and ?T
to both be assigned the type !
. In this particular example the result is a compilation error, because Default
is not implemented for !
, but in other examples the result was unsound execution.
This example prompted us to hold off on changing the fallback from ()
to !
. The result is in some way no less surprising: the type of Default::default
winds up falling back to ()
, rather than (say) requiring an explicit type annotation. However, at least using ()
didn't produce unsound behavior in previously sound code.
Bug 66757: Regressions introduced by NOT changing fallback
Unfortunately, if we don't change the fallback to !
, then we also trigger other sorts of regressions (at least if we want to also redefine the Infallible
type in the stdlib). As described in #66757, the fundamental problem is that we have a From<!>
impl for any type E
, but we don't have a From<()>
impl. So when we have code that requires From<?D>
where ?D
is a diverging type variable, falling back to !
is preferred.
This is related to the fact that !
is in many cases the right fallback! If you have code like Some(return)
, you would prefer that the type of this expression (if not otherwise constrained) is Option<!>
, not Option<()>
. In the case of #66757, we had similar code like From::from(never)
where never: Infallible
. If Infallible
is an alias for !
, this ought to "fallback" to !
.
Proposal: fallback chooses between ()
and !
based on the coercion graph
So we've seen that changing the fallback from ()
causes unsoundness, but keeping the fallback as !
can cause failed compilations. The proposal in this PR is to cause the fallback to be more subtle: diverging type variables prefer to fallback to !
but sometimes fallback to ()
(in cases where they may leak out into live code, in particular).
The idea is based on a "coercion graph". Roughly speaking, each type that an unbound type variable ?A
is coerced into another unbound type variable ?B
, we create a Coercion(?A -> ?B)
relation (instead of immediately falling back to subtyping). At the end of type-checking, we can take any such relations that remain (because neither ?A
nor ?B
was constrained to another type) and create a graph where an edge ?A -> B
indicates that ?A
is coerced into ?B
.
Similarly, we can identify those type variables ?X
where we have a coercion ! -> ?X
. We call those diverging type variables. Each diverging type variable will either fallback to !
or ()
depending on the coercion graph:
- Let
D*
be the set of type variables that are reachable from a diverging type variable via edges in the coercion graph. These are therefore the variables where the!
type "flows into" them (or would, if it didn't represent the result of a diverging execution). - Let
N
be the set of type variables that are (a) unresolved and (b) not a member ofD*
. - Let
N*
be the set of type variables that are reachable fromN
. - Each diverging type variable in
D
will fallback to()
if it can reach a variable inN*
in the coercion graph, and otherwise fallback to!
.
The intuition here is: if there is some group of unconstrained type variables ?X
that are all dominated in the coercion graph by the type !
, then they fallback to !
. If there are type variables in the coercion graph that are the target of a !
coercion but also flow into variable that are the target of other coercions, they fallback to ()
.
Effect on #66173
Recall the example from #66173:
#![allow(unused)] fn main() { let x: /*?X*/ = match something() { pattern1 => Default::default(), // has type `?T` pattern2 => panic!("..."), // has type `!` }; // the match has type `?M` }
Here we have a coercion graph as follows:
?T -> ?M
! -> ?M
?M -> ?X
In this case, applying the rules above:
- The set
D
of diverging variables is[?M]
- The set
D*
of variables reachable fromD
is[?M, ?X]
- The set
N
of non-diverging variables is[?T]
- The set
N*
of variables reachable fromN
is[?X, ?X, ?T]
- Since the diverging variable
?M
can reach a variable inN*
, it falls back to()
, and the unsoundness is averted.
Effect on #66757
Recall this example much like #66757:
#![allow(unused)] fn main() { <?R as From<?F>>::from(return) }
Here we have a coercion graph as follows:
! -> ?F
In particular, the type of the argument is ?F
and it is the target of a coercion from !
(the type of the return
expression). Since ?F
is only reachable from a !
, it falls back to !
as desired.
Weird cases
There are some "weird cases" where ()
fallback can result even in dead code.
#![allow(unused)] fn main() { let mut x; x = return; x = Default::default(); }
Here, the type ?X
of x
will have an "incoming edge" from the result of Default::default
which will cause it to fallback to ()
. Seems ok.
Backwards incompatibilies
Changing the fallback from always preferring ()
to sometimes preferring !
can still cause regressions:
trait Foo { } impl Foo for () { } impl Foo for i32 { } fn gimme<F: Foo>(f: F) { } fn main() { gimme(return); }
Here, the type argument of gimme(return)
will fallback to !
and stop compiling.
It can also cause changes in behavior, though that is relatively difficult to engineer. An example might be:
match true {
true => Cell::new(Default::default()),
false => Cell::new(return),
}
In this case, the type variable for Default::default
is never directly coerced into the type variable for return
, so the latter would still fallback to !
. In this example that would cause a compilation failure but one could imagine variants, similar to #66173, which would be unsound. (It's worth noting that the lint which @blitzerr and I were experimenting with would detect cases like this, though unfortunately it also detected its fair share of false warnings.)
Future extensions
I would like to deprecate the ()
fallback. I believe that these cases ought to be hand-annotated and are quite surprising. Consider Example 1 from this github comment:
#![allow(unused)] fn main() { parser.unexpected()?; }
Here the ?
desugaring winds up causing this to fall back to ()
, but I think it should require manual annotation, as removing the ?
would require manual annotation. We can consider this separately but this branch should make it possible to do such a transition over an edition, perhaps.
To !
This discusses the semantics for coercion into !
from some expression.
Currently, these rules largely come down to coercion to !
being possible
"after" an expression with !
type is encountered, such as return
or break
.
FIXME: It seems like this may have subtle differences as the divergence checking is done on HIR(?) but MIR may end up lowered into a different order -- should write out the rules and check more thoroughly. Relevant code:
-
compiler/rustc_typeck/src/check/fn_ctxt/checks.rs:720
If we have definitely diverged, then a missing tail expression does not force the return type to be()
(i.e., the implicit expression is not present).#![allow(unused)] fn main() { fn foo() -> u32 { return 0; // would normally be an error as function returns u32, not (), but accepted. } }
FIXME: the above example does not feel obviously related to !
, there's
probably some more reading and digging to do here.
Examples pulled from src/test/ui/coercion/coerce-to-bang.rs
and src/test/ui/coercion/coerce-to-bang-cast.rs
.
#![allow(unused)] fn main() { fn foo(x: usize, y: !, z: usize) { } fn call_foo_a() { foo(return, 22, 44); //~^ ERROR mismatched types } fn call_foo_b() { // Divergence happens in the argument itself, definitely ok. foo(22, return, 44); } fn call_foo_c() { // This test fails because the divergence happens **after** the // coercion to `!`: foo(22, 44, return); //~ ERROR mismatched types } fn call_foo_d() { // This test passes because `a` has type `!`: let a: ! = return; let b = 22; let c = 44; foo(a, b, c); // ... and hence a reference to `a` is expected to diverge. //~^ ERROR mismatched types } fn call_foo_e() { // This test probably could pass but we don't *know* that `a` // has type `!` so we don't let it work. let a = return; let b = 22; let c = 44; foo(a, b, c); //~ ERROR mismatched types } fn call_foo_f() { // This fn fails because `a` has type `usize`, and hence a // reference to is it **not** considered to diverge. let a: usize = return; let b = 22; let c = 44; foo(a, b, c); //~ ERROR mismatched types } fn array_a() { // Return is coerced to `!` just fine, but `22` cannot be. let x: [!; 2] = [return, 22]; //~ ERROR mismatched types } fn array_b() { // Error: divergence has not yet occurred. let x: [!; 2] = [22, return]; //~ ERROR mismatched types } fn tuple_a() { // No divergence at all. let x: (usize, !, usize) = (22, 44, 66); //~ ERROR mismatched types } fn tuple_b() { // Divergence happens before coercion: OK let x: (usize, !, usize) = (return, 44, 66); //~^ ERROR mismatched types } fn tuple_c() { // Divergence happens before coercion: OK let x: (usize, !, usize) = (22, return, 66); } fn tuple_d() { // Error: divergence happens too late let x: (usize, !, usize) = (22, 44, return); //~ ERROR mismatched types } fn cast_a() { let y = {return; 22} as !; //~^ ERROR non-primitive cast } fn cast_b() { let y = 22 as !; //~ ERROR non-primitive cast } }
See some background/discussion in rust-lang/rust#40800.
From !
Any coercion site with !
as the "from" type always succeeds, with the
destination type being the new type. This includes inference variables.
✨ RFC
- Feature Name: never_type, never_type_fallback
- Start Date: TBD
- RFC PR: Past rust-lang/rfcs#1216
- Rust Issue: TBD
Summary
Promote !
to be a full-fledged type equivalent to an enum
with no variants,
and adjust default inference fallback to avoid breakage.
Guide level explanation
While empty types exist already in Rust today, for example in the form of enum Foo {}
, most Rust users will not be directly familiar with them, so this RFC
provides an explanation of the properties these types hold (and that !
will
hold).
-
They never exist at runtime, because there is no way to create one.
-
Code that handles them cannot execute, because there is no value that it could execute with. Therefore, having an empty type in scope is a static guarantee that a piece of code will never be run.
-
They represent the return type of functions that don't return. For a function that never returns, such as
std::process::exit
, the set of all values it may return is the empty set. That is to say, the type of all values it may return is the type of no inhabitants, ie.Never
or anything isomorphic to it. Similarly, they are the logical type for expressions that never return to their caller such asbreak
,continue
andreturn
. -
They can be converted to any other type. To specify a function
A -> B
we need to specify a return value inB
for every possible argument inA
. For example, an expression that convertsbool -> T
needs to specify a return value for both possible argumentstrue
andfalse
:#![allow(unused)] fn main() { let foo: &'static str = match x { true => "some_value", false => "some_other_value", }; }
Likewise, an expression to convert
() -> T
needs to specify one value, the value corresponding to()
:#![allow(unused)] fn main() { let foo: &'static str = match x { () => "some_value", }; }
And following this pattern, to convert
Never -> T
we need to specify aT
for every possibleNever
. Of which there are none:#![allow(unused)] fn main() { let foo: &'static str = match x { }; }
Reading this, it may be tempting to ask the question "what is the value of
foo
then?". Remember that this depends on the value ofx
. As there are no possible values ofx
it's a meaningless question and besides, the fact thatx
has typeNever
gives us a static guarantee that the match block will never be executed.
Here's some example code that uses Never
. This is legal rust code that you
can run today.
use std::process::exit; // Our empty type enum Never {} // A diverging function with an ordinary return type fn wrap_exit() -> Never { exit(0); } // we can use a `Never` value to diverge without using unsafe code or calling // any diverging intrinsics fn diverge_from_never(n: Never) -> ! { match n { } } fn main() { let x: Never = wrap_exit(); // `x` is in scope, everything below here is dead code. let y: String = match x { // no match cases as `!` is uninhabited }; // we can still use `y` though println!("Our string is: {}", y); // we can use `x` to diverge diverge_from_never(x) }
This RFC proposes that we allow !
to be used directly, as a type, rather than
using Never
(or equivalent) in its place. Under this RFC, the above code
could more simply be written.
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 }
Motivation
There are several key motivators for adding !
, despite the existence of empty
types in the language already.
It creates a standard empty type for use throughout Rust code
Empty types are useful for more than just marking functions as diverging.
When used in an enum variant they prevent the variant from ever being
instantiated. One major use case for this is if a method needs to return a
Result<T, E>
to satisfy a trait but we know that the method will always
succeed.
For example, here's a possible implementation of FromStr
for String
than
currently exists in libstd
.
#![allow(unused)] fn main() { impl FromStr for String { type Err = !; fn from_str(s: &str) -> Result<String, !> { Ok(String::from(s)) } } }
This result can then be safely unwrapped to a String
without using
code-smelly things like unreachable!()
which often mask bugs in code.
#![allow(unused)] fn main() { let r: Result<String, !> = FromStr::from_str("hello"); let s = match r { Ok(s) => s, Err(e) => match e {}, } }
The Try
trait family is written with never type in mind, and currently
relies on the enum Infallible {}
defined in libcore as a substitute.
Empty types can also be used when someone needs a dummy type to implement a
trait. Because !
can be converted to any other type it has a trivial
implementation of any trait whose only associated items are non-static
methods. The impl simply matches on self for every method.
Example:
#![allow(unused)] fn main() { trait ToSocketAddr { fn to_socket_addr(&self) -> IoResult<SocketAddr>; fn to_socket_addr_all(&self) -> IoResult<Vec<SocketAddr>>; } impl ToSocketAddr for ! { fn to_socket_addr(&self) -> IoResult<SocketAddr> { match self {} } fn to_socket_addr_all(&self) -> IoResult<Vec<SocketAddr>> { match self {} } } }
All possible implementations of this trait for !
are equivalent. This is
because any two functions that take a !
argument and return the same type
are equivalent - they return the same result for the same arguments and
have the same effects (because they are uncallable).
Suppose someone wants to call fn foo<T: SomeTrait>(arg: Option<T>)
with
None
. They need to choose a type for T
so they can pass None::<T>
as
the argument. However there may be no sensible default type to use for T
or, worse, they may not have any types at their disposal that implement
SomeTrait
. As the user in this case is only using None
, a sensible
choice for T
would be a type such that Option<T>
can ony be None
, i.e.
it would be nice to use !
.
While the trait author or user could define their own empty type and implement the trait themselves, it is useful to avoid the hassle of needing to import the appropriate type in order to specify it in cases like this.
Better dead code detection
Consider the following code:
#![allow(unused)] fn main() { let t = std::thread::spawn(|| panic!("nope")); t.join().unwrap(); println!("hello"); }
Under this RFC: the closure body gets typed !
instead of ()
, the unwrap()
gets typed !
, and the println!
will raise a dead code warning. This
requires a canonical empty type to fallback to for the type of expressions
like panic!()
.
To be clear, !
has a meaning in any situation that any other type does. A !
function argument makes a function uncallable, a Vec<!>
is a vector that can
never contain an element, a !
enum variant makes the variant guaranteed never
to occur and so forth. It might seem pointless to use a !
function argument or
a Vec<!>
, but that's no reason to disallow it. And generic code sometimes
requires it.
It's also worth noting that the !
proposed here is not the bottom type that
used to exist in Rust in the very early days. Making !
a subtype of all types
would greatly complicate things as it would require, for example, Vec<!>
be a
subtype of Vec<T>
. This !
is simply an empty type (albeit one that can be
cast to any other type).
Reference-level explanation
Add a new primitive type !
to Rust. !
behaves like an empty enum except that
it can be implicitly cast to any other type. For example, code like the
following is acceptable:
#![allow(unused)] fn main() { let r: Result<i32, !> = Ok(23); let i = match r { Ok(i) => i, Err(e) => e, // e is cast to i32 } }
We also adjust the type inference algorithm used by rustc to fallback to !
rather than ()
when an inference variable ends up unconstrained. This change
is motivated by the desire to provide a better experience for Rust users since
expressions typed at !
can be coerced into other types. If we avoid making
this change, we have to specify that expressions like loop {}
and panic!()
continue to "return" ()
, which seems obviously wrong.
Adjusting type inference to fallback to !
also helps set us on a path towards
removing some of the fallback and requiring explicit types in the future, as
we can be more confident that the code involved is somewhat faulty.
Ensuring soundness
However, this change to fallback is not generally sound in the presence of
unsafe code that is present in the wild today. The following is a piece of
example code that was previously OK, but with the proposed change has undefined
behavior. It's a stripped down example -- the code is already unsound in the
sense that unconstrained_return
must only be called with valid T
, but in
practice code like this exists and does not cause UB (as it never gets
instantiated with an incorrect T
) with today's compiler.
#![allow(unused)] fn main() { fn unconstrained_return<T>() -> Result<T, String> { let ffi: fn() -> T = std::mem::transmute(some_pointer); Ok(ffi()) } fn foo() { let _a = match unconstrained_return::<_>() { Ok(a) => a, Err(s) => panic!("failed to run"), }; } }
The match here has type ?M
and the two arms have types ?T
and !
, as
panic!
has type !
with these changes. ?T
here is effectively
unconstrained, so it falls back to !
with the proposed change to type
inference. As a result, unconstrained_return
's call to ffi()
has a return
type of !
, which means that the compiler is free to assume that the function
does not return. Since ffi()
in practice does return, this leads to UB.
So we cannot simply change fallback in all cases.
We propose an algorithm which indicates whether it is safe to fallback to !
.
For each inference variable still present during fallback (i.e. those which are
unconstrained), we look at the relationships between these inference variables
to determine whether to keep the fallback to ()
or fallback to !
.
We construct a directed graph where the vertices are inference variables, and
add edges ?A -> ?B
if ?A
is coerced into ?B
.
Let D
be the set of type variables ?X
which were created as a result of the
coercion of ! -> ?X
being proposed. These can arise from functions returning
!
or loop {}
expressions, which can coerce into any type.
For each type variable ?T
, we fallback to:
!
if the variable is dominated in the graph by the setD
(that is, there is no path from a non-diverging type variable to it).()
otherwise.
The intuition here is that variables reachable only from diverging type
variables must be in dead code, but if they are also reachable from some
non-diverging type variable, they may be in live code, and so fallback to !
is
unsound.
Avoiding regressions
The proposed graph is sufficient for soundness, but unfortunately is insufficient to avoid a good number of regressions. We add an additional heuristic that mitigates most of the remaining breakage (~100 crates per Crater), leaving ~25 regressions.
This heuristic adds another case of fallback of ?T
to ()
when we encounter the
following:
?T: Foo
andBar::Baz = ?T
and(): Foo
This pattern is commonly encountered in code which constrains the return type of
some closure, and where closure instances frequently contain panic!()
. With
the previously proposed fallback, there is no additional constraint on ?T
(it's not flowing into any other inference variables), so it is reasonable for
it to fallback to !
. However, in practice, that causes breakage, as the
closure's return type is restricted to Bar
, which is only implemented for ()
(and u32
).
// Crate A: pub trait Bar { } impl Bar for () { } impl Bar for u32 { } pub fn foo<R: Bar>(_: impl Fn() -> R) {} // Crate B: fn main() { foo(|| panic!()); }
Alternatives
Leave Infallible
as the standard empty type
We could abandon the effort to stabilize !
as a dedicated return type, instead
leaving the currently existing Infallible
as the standard empty type. There
are several downsides to this:
- Users need to learn two different canonical names (
!
if standalone in a function return types andInfallible
for other places) Infallible
is either special-cased to coerce to other types or users need to writematch foo {}
to get that behavior.
Edition-based fallback?
We considered whether the fallback algorithm could be adjusted over an edition boundary to permit stabilizing never type. In practice, this doesn't work because the never type is a vocabulary type and so is present in the public API of various crates (including core/std, after stabilization).
This means that while the fallback within a library might be edition-dependent,
we'd still need to stabilize full use of !
across all editions. That basically
puts prior editions on the same grounds as just stabilizing !
, which is
known to introduce regressions.
Generally it seems pretty clear that stabilizing never type (or fallback changes) only on a subset of editions would lead to a more challenging landscape for Rust users. For example, as we've seen, fallback has implications for soundness, so we'd still need all of the complexity in the fallback algorithm in order to prevent cases like the one described above -- this seems to just make things more complicated, rather than improving them.
Future work
Removing (parts of) fallback
We can consider removing some of the cases of fallback, particularly those which
cause problems for this RFC, in the future. There were several examples
encountered in Crater and elsewhere that required effort to deduce the inferred
type. Additionally, we believe that it's likely that cases of fallback to ()
in new code are likely better written in other ways and we may wish to force the
user to indicate a type rather than silently providing one that may cause
trouble or hurt diagnostic quality.
We can leverage editions to remove parts of fallback. Unlike the phase-in of never type fallback, removing fallback only prevents code from compiling and does not change the behavior of existing code. This is fairly standard for cross-edition changes, and so is much more straightforward to design.
However, this RFC does not propose a particular path towards removing/improving our fallback algorithms, beyond noting that it may be advantageous to do so. If we do so, we can likely end up avoiding the more complex fallback algorithm proposed by this RFC being a permanent feature of future Rust editions.
Special casing trait impls for !
!
is useful as a standard marker inside types which may derive various traits.
Implementing a trait for !
is frequently fairly annoying, as you need to write
out many functions which are largely inconsequential as they can never run (if
they have a self
parameter).
For now, the expectation is that library authors will want to add manual
impls for standard traits (including in the standard library), but a future
extension could let users write (potentially) overlapping trait impls for !
so
long as the body of the trait is "obviously equivalent" as the methods can't be
called.
Unresolved questions
- Pending (needs review of the above)
😕 Frequently asked questions
This page lists frequently asked questions about the design. It often redirects to the other pages on the site.
What is the goal of this initiative?
See the Charter.
Who is working on it!
See the Charter.