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.