fallback and !

Background

  • We introduced the ! type in RFC XXX
  • Currently, for "abrupt" expressions like return and break, result is a type inference variable that (if otherwise unconstrained) falls back to ()
  • RFC proposed to change that to !
  • However, it was found that this change caused a certain amount of breakage in practice
    • some of this manifested as failed compilations
    • but other cases (notably the objc crate) resulted in silent crashes

When does the fallback mechanism apply

  • the type of an expression like panic!() is an inference variable
  • if that variable winds up being unconstrianed, it will "fallback" to either () (today) or ! (proposed)
  • it is unusual for variables to be fully unconstrained but certainly possible, especially around dead code

Example:

fn foo() {
    let x: _ = panic!();
    bar(&x);
}
    
fn bar<T: Debug>(t: T) { }

The type of x here is an unconstrained inference variable ?X the only constraint is that whatever ?X winds up being, it must implement Debug.

Arguments in favor of changing the fallback

The ! type is more likely to be implemented and to integrate with impl Trait,
as scottmcm pointed out here:

pub fn demo() -> impl std::error::Error {
    unimplemented!()
}

Examples where problems arise

Compilation failures

I'm not sure if we saw in the wild, but it's possible to have a compilation failure if you have fallback and pending trait obligations that ! cannot satisfy:


Runtime errors

Code that uses mem::zeroed or mem::uninitializd can sometimes be "tricked"
into synthesizing a ! value. This presently results in a lint and a runtime panic, as Centril notes here:

The former might not result in an error. It does however result in a warning (invalid_value) as well as a run-time panic (playground):

warning: the type `!` does not permit zero-initialization
 --> src/main.rs:8:13
  |
8 |             std::mem::zeroed()
  |             ^^^^^^^^^^^^^^^^^^
  |             |
  |             this code causes undefined behavior when executed
  |             help: use `MaybeUninit<T>` instead
  |
  = note: `#[warn(invalid_value)]` on by default
  = note: The never type (`!`) has no valid value

    Finished dev [unoptimized + debuginfo] target(s) in 1.12s
     Running `target/debug/playground`
thread 'main' panicked at 'Attempted to instantiate uninhabited type !', /rustc/1423bec54cf2db283b614e527cfd602b481485d1/src/libcore/mem/mod.rs:461:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

As you can see from ^, there's no UB in sight.

The "Deserialize" pattern (XXX rust issue number (issue 39297?)

One pattern that was found worked like this:

fn foo() -> Result<(), ()> {
    let x = try!(Deserialize::deserialize())?
    println!("{:?}", x);
    Ok(())
}

when try! is expanded, the result is

let x = match Deserialize::deserialize() {
  Ok(v) => v, // Ok match arm has type `?OK`
  Err(err) => return err.into(), // Err match arm has type `?ERR`
}

The problem here is that the type of the both match arms are inference variables, let's call them ?OK and ?ERR. Those two variables are constrained to be the same, but neither of them are constrained to anything in particular. At fallback point, ?ERR falls back to !, and hence ?OK also falls back to !. Therefore, the final type of x is inferred to ! but deserializing a ! value doesn't make sense in particular, the deserialize trait was not (at the time) implemented for !, so we got an error.

Had deserialize been implemented for !, the only possible behavior would be to panic, and hence this code would go from deserializing a Result<(), ()> (which could well succeed) to deserializing a Result<!, ()> (which panics). (In the particular case where this code was found, though, the value was known to be the Err variant so that would not in fact have happened.)

Nonetheless, it was surprising that code which is not obviously dead winds up with the type ! (that is, the variable x).

objc crate

The core of the objc crate error is the same pattern as the Deserialize error, as SSheldon noted here.

let _ = if false {
    panic!("panic")
} else {
    mem::zeroed()
};

However, the specific case involved a fn() -> ! value, as SimonSapin clarified here:

[objc] transmutes a pointer to fn(…) -> R and calls it. R is a generic type parameter that is inferred, and in the cases discussed here affected by the fallback change.

Data on expected breakage

XXX links to old crater runs and summaries of their results

XXX objc fallout results from thread

Mitigation options

One thing to consider is whether we can mitigate the fallout through a warning period. The question is how one would design a suitable lint and how precise it would be. If the lint is overly coarse, we might make it opt-in (i.e., allow by default).

Option 1: use "taint tracking" on the () value that results from fallback. This is what we attempted at first. It was complex and we are not keen to attempt it again.

Option 2: when falling back a variable ?X to (), search the outstanding trait obligations and see whether any of them reference ?X. If so, issue a lint warning or perhaps get more precise, for example by "evaluating" the trait obligation with () and ! to see if the result differ. (To check: Would this actually capture the "semantic" violations from cases like objc?)

Options

  • Stabilize ! type but leave fallback temporarily unresolved
    • this doesn't resolve the question, so it's really rather orthogonal to the document, but it may be worth considering regardless
    • the main concern when this was previously proposed was that we would have insufficient motivation to pursue a change to fallback (which does seem plausible, unless someone commits to seeing it through)
  • Leave the fallback as ().
  • Transition to !:
    • Can we do some sort of warning period?
    • How much do we have to prepare the ecosystem?
  • Edition boundary
    • no code breaks: good!
    • but it is complex for us to manage and may result in surprising interactions
      • presumably the edition of the return or break statement would be used to determine the fallback
      • depending how precise we are, we may see code breakage anyway
Select a repo