Try   HackMD

Exposition

There is a desire to have a canonical uninhabited type (a type with no values). Since 2016[1] Rust has such type: ! (also known as "never").

However, this type is only fully available on nightly Rust (via #![feature(never_type)]) or via hacks (see Appendix E). There were numerous attempts to stabilize the never type, however they all failed, mainly because every time it turned out to be a significant breaking change.

I've read through the tracking issue and various discussions linked there and tried collecting the most important things here. This document describes the problems that stand between us and the stabilization of !, and proposes a way forward.

Note though that I'm but a human and probably lost some context in the hurry of things. Still, I believe this document captures the most important events in the hisroty of the never type and has enough context to discuss the plan for the future.

Goals

This is what we wish to achieve alongside the stabilization:

  1. Spontaneous decay rules are easy to teach and remember
  2. Limit spontaneous never type decay as much as possible (fallback to ! most of the time) (spontaneous decay is weird and unintuitive, more on this in Spontaneous decay)
  3. No existing code is broken (we still have our backwards compatibility guarantees!)
    • Avoid unsoundness or at least lint in existing code, not easy to shoot yourself in the foot
  4. Make ! unify with Infallible
  5. Common situations don't get unbound inference variables

(list adopted from a recent T-lang meeting with slight modifications)

Those are roughly ordered in "most important to least important" (in the opinion of the document's author). I think it's important to have a simple and easy to understand design, never type as a concept is already slightly confusing for users, ideally we wouldn't add unnecessary complexity (points 1 and 2). Breaking code is obviously undesirable (point 3). Infallible was always supposed to be !, but in my opinion it's not the most important thing (point 4). Lastly, point 5 is just a usability concern, unnecessary errors are not nice (it is satisfied by all ideas in this document though, so is the least important).

Characters

!, the never type

The never type is the canonical uninhabited type. It can be used to disable enum variants (Result<T, !> can only be Ok) and in general to represents the type of computations which never resolve to any value at all (like an infinite loop or a panic).

On stable Rust it can be used only as a function return type (although that allows to get it as a type via hacks, see Appendix E):

fn abort() -> ! { ... } // stable

A lot of things that concern control flow have ! type. For example: return, break, continue, panic!(), loop{}[2].

never-to-any coercion

! can be implicitly coerced to any other type. This is sound because ! is uninhabited — we know the code which has a value of ! is unreachable.

This is an important coercion used everywhere on stable Rust:

match res {
    Ok(x) => x as u32,
    Err(_) => panic!() /* never-to-any */,
}

panic!() has type !, but since all arms of a match must have the same type, it is coerced to u32. This is extremely handy.

std::convert::Infallible

This is an uninhabited enum which is used to make a Result which can only be Ok in std. This is useful when a trait demands a Result<_, _> but your specific implementation is infallible.

Since its implementation there always was a desired to change Infallible to an alias to ![3]. However, this caused problems in the past.

Edition 2024

An upcoming edition where we could introduce a breaking change[4]. Will this allow us to finally do something about the never type?

Never type spontaneous decay (aka never type fallback)

The main culprit and the reason why so many attempts to stabilize ! failed.

Because of backward compatibility issues, introduction of ! had to introduce a hack spontaneous never type decay.

This effect is more commonly known as the never type fallback. However, in this document I refer to it as "spontaneous never type decay", because I think it makes it easier to explain. (it also removes the ambiguety of "what the fallback is")

Spontaneous decay makes ! coerce to (), even when this is not required. For example (play):

fn print_ret_ty<T>(_: impl FnOnce() -> T) {
    dbg!(std::any::type_name::<T>());
}

print_ret_ty(|| loop {});

One would expect it to print "!" loop {} is ![2:1], so the closure should return ! too. However reality is different:

[src/main.rs:2:5] std::any::type_name::<T>() = "()"

As you can see the never "returned" by loop {} spontaneously decayed to (), making the closure return () and the function print "()". This is quite confusing, as nothing in the program required this coercion. To my knowledge never-to-any is currently the only coercion that can happen spontaneously.

The "fallback" termionology commonly used for this refers to the fact that compiler sees this program more like this:

fn never_to_any<T>(never: !) -> T { ... }
fn print_ret_ty<T>(_: impl FnOnce() -> T) { ... }

print_ret_ty(|| never_to_any(loop {}));

With explicitly added coercion, you can notice that this won't type check. There is nothing allowing the compiler to infer T it is unbounded. The current compiler has a fallback to () if the "return type" of a never-to-any coercion cannot be inferred, it is set to (). Ideally we would set it to !, making !'s behavior more intuitive (i.e. passing it to a generic function keeps the type, as with any other type). However this breaks some code that depends on the fallback being ().

Previous attempts

This section contains a best-effort recollection of the past attempts to stabilize !, with links to related PRs/issues/comments and reasons why they did not succeed.

Attempt 1 (2018-01)

So the reason the first attempt was reverted was this example (sometimes generated by a macro):

pub fn a(x: Infallible) -> Box<dyn Error> {
    Box::new(x)
}

That is fine (Infallible: Error), but when making Infallible = ! this example used to not compile because compiler decided to coerce x to dyn Error causing errors because Box::new only accepts sized types.

Currently it fails for a different reason (play):

#![feature(never_type)]

pub fn b(x: !) -> Box<dyn Error> {
    Box::new(x)
    //~^ error[E0277]: the trait bound `(): Error` is not satisfied
}

This fails because x spontaneously decays to (), which does not implement error. The error is extremely confusing, if you don't know about the spontaneous decay.

Disabling spontaneous decay fixes the issue once more (play) (see Appendix C for notes about never_type_fallback feature):

#![feature(never_type, never_type_fallback)]

pub fn c(x: !) -> Box<dyn Error> {
    Box::new(x)
}

Attempt 2 (2018-12)

This attempt proposed to stabilize ! & disable the spontaneous decay. However, it was eventually closed in favour of the next one.

Attempt 3 (2019-10)

Originally this attempt was identical to the previous one, but later tried to only stabilize ! without the decay/fallback change.

The (minimized) issue looks like this:

struct E;
impl From<!> for E { fn from(x: !) -> E { x } }

fn f(never: !) {
    <E as From<_>>::from(never);
}

The problem is that spontaneous decay of never makes the call require E: From<()> rather than E: From<!>. This problem disapears if we disable spontaneous decay.

This issue occured in normal code because of the ? desugaring. However, the desugaring has been changed, and the new one does not reproduce this issue[5].

Attempt 4 (2020-11)

This was an attempt at "conditional decay", where the idea would be that we only decay ! to () when necessary. This PR still broke a few crates and was deemed not good enough, particularly because it required too complex and hard to explain rules.

Attempt 5 (2021-09)

This attempt to work on ! seems to have died out for similar reasons to the previous one:

  • Unclear motivation of ! and coercion behavior
  • Complex rules needed to keep backwards compatibility

Analysis

Most of the issues look to be resolved. However, there is still an inherent conflict:

  • Spontaneous decay is being depended on
  • Infallible does not sponteneously decay, so making Infallible = ! requires ! to also not decay

So we can't both

  • Not break code by keeping spontaneous decay (do we even want this? spontaneous decay is very confusing)
  • Make Infallible = !

Options

For the next (2024) edition, I think the only thing that makes sense is to disable spontaneous decay (aka "always fallback to !"). This is the easiest behavior to teach and understand. This ideally achieves goals of limiting spontaneous decay and making it easy to understand (there is none!), while not breaking any code (opt-in, new edition)[6]. For completeness there is the checklist:

  1. ✅ Ideal for teachability & simplicity
  2. ✅ No spontaneous decay
  3. ✅ Does not break any code
  4. ✅ Allows Infallible = !
    • By itself, to actually make Infallible = ! we need all editions to allow that

(our 5-th goal of not getting unbound inference variables in common code is satisfied by all proposals here, so I will not mention it)

For previous editions everything is much more complicated. Since we are bound by the backwards compatibility of ! spontaneous decay and of Infallible not spontaneously decaying, there aren't any perfect options. Still, there are options:

  1. Also fully disable spontaneous decay
    1. ✅ Ideal for teachability & simplicity
    2. ✅ No spontaneous decay
    3. ❌ Breaks existing code which depend on the decay
    4. ✅ Allows Infallible = !
  2. Keep the current spontaneous decay rules (always decay)
    1. ⚠️ Bad for teachability & simplicity, but not the worst
    2. ❌ Spontaneous decay always
    3. ✅ Does not break existing code
    4. Infallible is not !
  3. Keep the current spontaneous decay rules (always decay) but still make Infallible = !
    1. ⚠️ Bad for teachability & simplicity, but not the worst
    2. ❌ Spontaneous decay always
    3. ❌ Breaks some code where Infallible spontaneously decaying causes problems
    4. Infallible = !
  4. Come up with a spontaneous decay rules which break no code and allow Infallible = !
    1. ❌ Very bad for teachability & simplicity
    2. ⚠️ Some spontaneous decay
    3. ✅ Does not break existing code
    4. ✅ Allows Infallible = !

Proposal

I think we need to commit to disabling spontaneous decay in the next edition. No matter what we decide for other editions, in my opinion, it's a good choice. It's a simple change that can be implemented quickly. That allows us to not block 2024 edition on any other decisions about the never type.

For previous editions I want to propose going with the option 1: fully disabling spontaneous decay everywhere. I think that the simplicity of the language rules and their teachability is very important. All other options seem significantly worse with this, but also with other goals too.

From what I can tell this was the original plan (Niko's comment), which was then abondaned because of the issues described in the history section (all resolved now) and the fact that it can break unsafe code, which is scarry.

I believe that we can make a lint which would catch the unsafe code breaks[7]. It's relatively easy to catch the fact that fallback goes into mem::transmute or ptr::read, etc.

Non-unsafe breaks are few and far between, from what I can tell in the discussions (although we'll have to remesure of course). And I want to argue that this falls into the "technically a breaking change, but allowed" category (you can always specify the () type explicitly). Still, this is a breaking change and we should look into warning people in advance (with lints and/or blog posts).

We have tried to tune the decay rules to not break any code, however this causes very complex and confusing rules, which also fallback to () most of the time.

I think it is crucial for the stabilization of ! to disable the spontaneous decay, because that makes ! a lot more intuitive to work with. I think that the spontaneous decay of ! to () is so counterintuitive, that it's unreasonable to believe that people depended on it consciously. IMO it's a miracle that their code worked in the first place.

Proposed next steps

  • Adjust #![feature(never_type_fallback)] to never spontaneously decay ! to ().
    • That is, if the output type of a never-to-any coercion is unbounded (it cannot be inferred), then always assume ! rather than ().
    • Or add a new feature to that effect (like #![feature(no_spontaneous_decay)]).
  • Enable (the equivalent of) #![feature(no_spontaneous_decay)] in Rust 2024.
  • Add a future-incompat lint in all editions anywhere that this change when stabilized would make code fail to compile (e.g. the match .. { x => Default::default(), y => panic!(), } scenario).
  • Add a lint in all editions anywhere that fallback would flow into scary places like mem::transmute, ptr::read, etc.
  • Eventually enable (the equivalent of) #![feature(no_spontaneous_decay)] in all editions.
  • Stabilize !, alias Infallible = !.

Mark that you're done reading when reaching this point.


Appendices

Here are some relevant, but not absolutely required, topics.

Appendix A: another reason against spontaneous decay

Another reason why we want to disable spontaneous decay / fallback to !, noted here by Taylor Crammer, is that it leads to better dead code detection:

let t = std::thread::spawn(|| panic!("nope"));
t.join().unwrap();

// this is dead code, but currently rust does not see it.
// `!` from the panic immediately decays to `()` and nothing
// in `t.join().unwrap();` shows that it's diverging
println!("hello");

Appendix B: From<!> for T

Since ! can be coerced to any type, it would make sense to have the following From impl:

impl<T> From<!> for T {
    fn from(never: !) -> T {
        never
    }
}

However, it conflicts with the identity impl when T = !.

Additionally adding such an impl after stabilization of ! is a breaking change, because users can write their own conflicting impls.

Oli believes we can still add this impl as a builtin, ignoring the conflict with the identity impl.

Turns out we can't actually do that, current type checker the trait solver cannot support such an impl, since it is incoherent. (edit: errs)

Appendix C: current feature(never_type_fallback)

It is of note that currently feature(never_type_fallback) does not (fully) disable spontaneous decay. Instead it tries to still decay when it's required.

The behavior is documented in rustc code: [link], although it is quite complicated.

Appendix D: blocks become never

When a block has a statement which has ! type and there is no last expression (i.e. it ends in a statement) that block returns !, not ().

let a = {
    loop {};
    ();
};

let b = {
    loop {};
    ()
};

// a: !
// b: ()

This is not terribly bad, although a bit confusing and, IMO, unnecessary.

Maybe we can also disable this in the next edition? That is, maybe we can make them both () in the next edition, to make the rules simpler & more intuitive (the result of a block will always be the last expression or () if there is no expression).

The current rules are not that complex (at least to my knowledge) to warrant a breaking change, but would be nice to get rid of this special case at least in the next edition.

Note that this has an implication on return;:

let x = if cond {
    y
} else {
    // currently: allowed (the else block has a diverging statement,
    //            so it's made to result in ! and then coerced to y's type)
    //
    // if we remove that rule: error (expected `typeof(y)`, found `()`)
    return;
};

let x = if cond {
    y
} else {
    // you'll have to write this instead
    return
}

I was confused for years, why return; is allowed in such cases, and even though now I know the rules it's still irking me ^^'

Either way, we must document the behavior: https://github.com/rust-lang/reference/issues/1033.

N.B. This is very explicitly not part of the current proposal and should be viewed as a future possibility.

Appendix E: the hack to get stable never

You can exploit the fact that fn() -> ! pointers are stable and the fact that FnOnce is stable (in some capacity) to just <fn() -> ! as FnOnce<()>>::Output. In reality you need to create a separate trait, because you can't specify an FnOnce bound without specifying Output to a specific type.

Still, this is fairly easy (play):

trait F {
    type Out;
}

impl<T> F for fn() -> T {
    type Out = T;
}

type Never = <fn() -> ! as F>::Out;

fn test_never_to_any<A>(never: Never) -> A {
    never
}

There is an existing (joke?) implementation: lib.rs/never-say-never.

I'd say that this is clearly a hack and is outside of our stability guarantees.

This is tracked in #58733 (although I don't think there is anything to be done, but to stabilize the never type already).

Appendix F: function pointers allow impls to mention !

Similary to the previous one, you can implement traits for fn() -> ! and fn() -> Infallible, depending on the fact that those are different types (play):

trait Trait {}
impl Trait for fn() -> ! {}
impl Trait for fn() -> Infallible {}

We can never make Infallible into an alias for ! without breaking this.

I'd assume that this does not happen in practice no crater runs showed breakage due to this & it's fairly uncommon to implement traits for specific function pointers like this.

Appendix G: bad desugaring of ?

As it turns out, ? actually skews inference:

// currently:          compiles (T = ())
// with this proposal: compiles (T = !)
Err(())?;

// currently:          compiles (Self = ())
// with this proposal: fails (Self = !, which does not implement the trait)
serde::Deserialize::deserialize(deserializer)?;

This is because ? desugars to a match with a return:

// Simplified: current version uses `Try`, `ControlFlow`, ...
match expr {
    Ok(val) => val,
    Err(err) => return Err(err),
}

This unifies type of val with type of return (which is !) which causes fallback to kick in. Ideally ? would not do that (this is clearly a bug in the desugaring).

Issue: https://github.com/rust-lang/rust/issues/51125.
PR with a simple fix: https://github.com/rust-lang/rust/pull/122412.

Appendix H: breakage from disabling spontaneous decay

There is real breakage from disabling sponteneous decay. It happens when something like panic!() is passed to a generic function, which would work with (), but not !:

struct X;
impl From<X> for () { ... }

fn generic(_: impl From<X>) {}

generic(panic!());

The fix is generally to just replace the panic with a unit:

panic!();
generic(());

A possibly more common example involves a closure:

struct X;
impl From<X> for () { ... }

fn call<R: From<X>>(_: fn() -> R) {}

generic(|| panic!());

And is fixed similarly:

generic(|| {
    panic!();
    ()
});

(alternatively you can explicitly specify the type via turbofish)

Real example: [comment].


Discussion

Attendance

  • People: TC, tmandry, Nadri, scottmcm, Waffle, Lukas Wirth, Josh, pnkfelix, nikomatsakis, Oleksandr Babak

Meeting roles

  • Minutes, driver: TC

! or ?Fresh

scottmcm: it mentions:

A lot of things that concern control flow have ! type. For example: return, break, continue, panic!(), loop{}[2].

Do they have literally ! type, or unbound-type-variable type?

waffle: Right now they do actually have ! type, to the best of my knowledge.

Note on Infallible

scottmcm: There's a bunch of implementations in the wild that use Infallible today. The biggest reason to wish for the merge is that it's not a breaking change for those libraries including core! to change to real-! instead. As well as all the error types that have implemented MyError: From<Infallible> to work with ?, where it would be nice for things to immediately work with ! as well once that's stable, rather than needing the ecosystem to go add a bunch more identical Froms.

If we make Infallible = !, then "Appendix B" already applies

nikomatsakis: A point that was raised in the past is that there are already existing impl From<Infallible> for MyType impls out there. If we alias Infallible = !, then it is already a coherence failure to add an impl like impl<T> From<!> for T. Discussion around this is what led us to the current "reservation" system, which was intended to ensure that we can create the blank impl if we had specialization. I forget exactly what case it was trying to prevent.

NM: Whether we can add this conflicting impl is probably an orthogonal point.

pnkfelix: This feels like a major point.

Josh: We don't have that problem impl. We'd like to add impl From<!> for T but we can't because it'd conflict with impl From<T> for T.

No "spontaneous decay" = "always fallback to !", correct?

nikomatsakis: The terminology of "spontaneous decay" is new to me, but I believe that in terms of the type system implementation, it means "if we convert ! to a type variable, that type variable falls back to !". One of the reasons I am not super keen on this is code like the following

trait Deserialize {
    fn deserialize(input: &str) -> Self;
}

// Reasonable
impl Deserialize for () {
    fn deserialize(input: &str) -> Self {
        ()
    }    
}

// Reasonable
impl Deserialize for ! {
    fn deserialize(input: &str) -> Self {
        panic!()
    }
}

fn main(input: Option<&str>) {
    // In past at least, resulted in `x: !` which would mean a guaranteed panic.
    // I'd prefer an error for unconstrained type variable.
    let x = match input {
        None => return, // has type `!`
        Some(x) => Deserialize::deserialize(input),
    };
}

Do we have a plan for how to address cases like this?

With never_type_fallback today, it appears to generate (). playground

I guess this is Appendix G.

Waffle: Yes, the proposal is to always fallback to !. It would be unfortunate to issue an unbound error here.

scottmcm: This gets to my control-flow lowering question. Something like that may be a way around these sort of problems.

Josh:

nikomatsakis: The aligns with what I'd expect.

Nadri: This would make it work less like a normal type. IS there a sensible use of Infallible where changing this would be surprising?

Waffle: There are two perspectives here. One is seeing ! as a type. The other is seeing the control-flow effects of that return. Not sure what to do with this contradiction.

scottmcm: The backcompat restriction is on functions that return ! directly. I'm wondering if we could treat the -> ! case somewhat specially.

NM: I agree with Waffle. I came to the same conclusion awhile back that there is this tension.

NM: No other type has fallback (except integers, which are weird), so there is specialness here. If we just treated ! as another type, much of the concern goes away, but that raises some ergonomics questions.

NM: I've outlined some scenarios here of cases that may feel similar to the user but may be different to the compiler:

let x = match input {
    None => return,
    Some(x) => ...
};

let x = match input {
    None => panic!(),
    Some(x) => ...
}

let x = match input {
    None => my_panic(),
    Some(x) => ...
}

let x = match input {
    None => wrapper(|| return),
    Some(x) => ...
}

let x = match input {
    None => wrapper(|| my_panic()),
    Some(x) => ...
}

fn my_panic() -> ! { }

fn wrapper<R>(_: impl FnOnce() -> R) -> R { }

NM: My recollection is that by not falling back to (), some of these cases stop working.

Waffle: The way that I see the always fallback to ! behavior is that it's equivalent to not having fallback and only coercing to ! when required. So in that sense, it's the same as other types.

nikomatsakis: I feel like this example is one where we would either not coerce or give an unconstrained error (though after having said this, Niko is rethinking, it's just kind of hard to draw a comparison I guess). I guess Waffle's point is, if that return arm returned Infallible, it would behave the same (but, on the other hand, we wouldn't be able to have any type in the other arm, specifically because).

Josh: If we generate scenarios where code panics that didn't, that is something we probably need to address. If it's that it only causes compiler failure, that's probably not blocking.

TC: This is a change to type inference, and any change to type inference can cause arbitrary changes to runtime behavior. This is true even for the changes we characterize as minor in RFC 1122.

scottmcm: nit: I wouldn't focus on the panic! in the example it might be Err or something instead. Worst is transmute cases, since going from transmute::<_, ()> to transmute::<_, !> is much much much worse than panic and ! is ZST so the size check doesn't notice.

Waffle: It seems like a fairly narrow case where behavior might change. Probably this doesn't happen too often.

Nadri: can we experiment with the control flow thing above?

Nadri: How did we pick up on the objc case, and can we find other cases like that?

NM: In that case, code started to crash, and people investigated. We have tried to lint lints for this and it's challenging. The inference chain doesn't necessarily all happen in a single function.

NM: On the question of experimentation, we tried very hard on this, but perhaps we were operating under too many constraints.

NM: For example, what if we only allow coercions from ! to all types?

let x = from(return);

fn from<T>(t: T) { }

NM: Under my version I realized this would not compile. So I added heuristics that turned out to be complex.

https://gist.github.com/nikomatsakis/7a07b265dc12f5c3b3bd0422018fa660

Does this need to happen on all editions?

pnkfelix: The text says "By itself, to actually make Infallible = ! we need all editions to allow that"

pnkfelix: I interpret that as saying "if any edition makes Infallible = !, then all editions need to make Infallible = !"

pnkfelix: is that the correct interpretation?

pnkfelix: assuming it is the correct interpretation why is that the case? E.g. could we not have ! denote one type (that is interchangable with Infallble) for editions >= 2024, and well, ! just won't denote a first-class type on editions < 2024, right? I feel like I'm missing something about why this equivalence cannot be edition-specific, if the ! type itself is already (potentially?) edition-specific.

pnkfelix: (Or are all those bullet points about pre-2024 edition implicitly including a goal that we stabilize ! as a first-class type for all editions?)

pnkfelix: (maybe there is a concern re passing around values with types like fn () -> ! and such on old editions)

scottmcm: I think the place this gets hard is when you're using core traits like the TryFrom blanket that has Error = Infallible. That probably has to be the same type across inference that can involve multiple editions.

Waffle: The questions: Can we stabilize ! only on one edition?

Waffle: I don't think so; it doesn't seem feasible to me.

pnkfelix: Could we make ! one type in Rust 2024 and another type in other editions?

tmandry: We could treat it as a type alias in newer editions

Nadri: We could allow ! as a syntax in more places in Rust 2024 and not previous editions. Then there's a discussion about changing the behavior of it. And there's the problem of equating it with Infallible which I think has to be done in all editions at once.

scottmcm: It might be scary to do this based on the span.

Coercing all uninhabited types to !

tmandry: Rather than Infallible = !, how much would we gain by making all uninhabited types coercible to !? This would also apply to existing empty enums in the ecosystem that are used for the same purpose.

nikomatsakis: Also, how we would we do it. In the past I was semi-unkeen on this but I've softened, especially as we gained the idea of "privately uninhabited". We might just do it for e.g. empty enums specifically, even.

Nadri: only empty enums please :D

scottmcm: Anywhere that coercions don't apply because we're doing trait resolution would be a problem here. Because of generics, many places are not coercion sites.

Waffle: Infallible is often used inside of a Result, and it's not going to coerce here.

nikomatsakis: e.g., Result<X, Infallible> cannot be coerced to Result<X, !> (at least, not today).

tmandry: It makes we wonder if we could solve this problem independently. Maybe would could come up with rules to allow us to coerce here.

Waffle: It seems undesirable to add special behaviors to all uninhabited types here.

nikomatsakis: In the same way that () feels distinct from struct Foo, I feel like it's prob useful to distinguish ! from empty enums at least in some cases.

Nadri: remember that (T, !) is not a ZST (it can store a T today), so definitely I wouldn't want to do anything about all uninhabited types.

NM: I agree with Waffle that it's useful to be able to distinguish "the canonical empty type" from other, newtype'd flavors of it. But I also agree with tmandry that people have used empty enums in places where they would have wanted to use the never type. But there would be weird interactions in treating all uninhabited types in this way, I could maybe imagine some special case around empty enums but not other kinds of uninhabited.

tmandry: I'd be okay with restricting to empty enums, perhaps even requiring an attribute to be added to the enum to make it behave more like !.

Pondering: lowering to control flow?

scottmcm: Since the only thing we need to keep working for back-compat is -> !, is there a way we could detect that somehow and treat it like control flow, like how we changed the && desugaring recently for let chains? Were let x = if c { expr } else { returns_never() } to not actually have a value edge back from the else block, maybe that'd let us dodge the need for decay?

Nadri: to be precise, this has to do with never_to_any, not decay I think.

scottmcm: I guess this is harder than grammar things, since we'd have to do it in the middle of name-resolution-and-type-checking since that's when we'd find out things are -> !. But we only have to support concrete -> !, at least, I think?

Appendix H and "The fix is generally to just replace the panic with a unit"

Josh: The most common way I'd expect this to happen: code_in_progress(xyz, todo!()).

Josh: It wouldn't be satisfying to change the todo!() to ().

Waffle: The only way this changes is if you're passing this to a function with a generic with a trait bound that is implemented for () but not !.

NM: This is a good example of where the spontaneous decay is surprising.

scottmcm: This is already an error, for example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=3ef16be86bddfb9d4e609aac2a099a61

fn code_in_progress(x: &str, y: impl Into<String>) {}

fn main() {
    let xyz = "asdf";
    code_in_progress(xyz, todo!());
    //~^ ERROR: the trait bound `String: From<()>` is not satisfied
}

NM: Even today, this works less often than I wish it did.

scottmcm: This doesn't compile today, but arguably we have the syntax-level knowledge to know that the call is comhpletely unreachable, and thus could potentially skip the need to determine a type. (But then we're at "are types checked in dead code?", where I think we've said they should before.)

fn code_in_progress(x: &str, y: impl Into<String>) {}

fn main() {
    let xyz = "asdf";
    code_in_progress(xyz, return todo!());
}

nikomatsakis: Consider:

fn code_in_progress(x: &str, y: impl Into<String>) {}

fn main() {
    let xyz = "asdf";
    code_in_progress(xyz, wrapper(|| todo!()));
}

Spontaneous decay, why do we care?

nikomatsakis: I see the argument for unifying Infallible and !. I don't fully understand the reason to fallback to !. I'm sure there are advantages to it, I'd like to know why it's useful though.

scottmcm: One I recall is something like Err(e)?, which today is () and that feels weird to people because it's unreachable. Of course, adding throw e would be the nice way around that particular case.

scottmcm: Ok(v) giving you Result<T, ()> instead of Result<T, !> makes a major difference.

Nadri: because you could have a let x: Infallible that unexpectedly turns into a () when you try to use it. Waffle had examples IIRC. Or do you mean having fallback at all?

nikomatsakis: I suppose

let x: ! = ...;
let y = Some(x); /* infers to `Option<()>` ?

?

TC: cramertj described some motivation for this as follows:

As for why we should make this change, the RFC lists a couple of motivating factors, the most relevant of which is (IMO) better dead-code detection, such as in cases like this:

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. There's no way current rust can detect cases like that.

This change makes it possible to catch more bugs via dead-code warnings, expand the reachability analysis of the compiler allowing for smarter optimizations, and helps to provide "more correct" types like Result<(), !> rather than Result<(), ()> in more places (which may turn previously manual user assertions into no-ops). It also helps introduce the user to the concept of ! by giving a "more correct" return type to diverging expressions.

NM: Yes, that's a good one. When I see threats to reliability I get very concerned.

Waffle: There are there canonical examples:

  1. The case above.
  2. You can't make Infallible into the never type.
  3. Intuition, in particular error messages

nikomatsakis: Another example:

#![feature(never_type)]

fn main() {
    let x: Option<!> = None;
    
    let y: Option<_> = x.map(|x| x);
    println!("sizeof={}", std::mem::size_of_val(&y)); // prints 1

    let z: Option<!> = x.map(|x| x);
    println!("sizeof={}", std::mem::size_of_val(&y)); // prints 0
}

nikomatsakis: Example that failed to compile when we unified Infallible = ! and we kept "fallback to ()":

struct E;
impl From<!> for E { fn from(x: !) -> E { x } }

fn f(never: !) {
    <E as From<_>>::from(never);
    // never: gets coerced to `()`
}

Next steps

TC: Where do we want to go from here?

NM: This was a fantastic document. I do want to unblock this going forward. I do have reliability concerns. It seems we need to explore whether we want to consider more complicated rules.

NM: I'm curious who is worried about this match example:

trait Deserialize {
    fn deserialize(input: &str) -> Self;
}

// Reasonable
impl Deserialize for () {
    fn deserialize(input: &str) -> Self {
        ()
    }    
}

// Reasonable
impl Deserialize for ! {
    fn deserialize(input: &str) -> Self {
        panic!()
    }
}

fn main(input: Option<&str>) {
    // In past at least, resulted in `x: !` which would mean a guaranteed panic.
    // I'd prefer an error for unconstrained type variable.
    let x = match input {
        None => return, // has type `!`
        Some(x) => Deserialize::deserialize(input),
    };
}

nikomatsakis: I semi-block the proposal because of the above I find it a blow to reliability to fallback to !. I'd like to see this addressed. I'm curious what other people think.

scottmcm/tmandry: can we use lints?

TC: NM, is there anything the edition can help with here on your concern?

NM: Yes, e.g. with my proposal, we could give a hard error on fallback to ().

Waffle: One of the main points of this proposal is that a solution that will make everyone happy is going to be very complex. So my proposal is that doing something simple and having lints to help the unhappy people may be the best path. What do people think about that?

pnkfelix: The transmute examples are really unfortunate. I don't know what to do about that. Maybe a lint would help.

NM: If we turned my gist into a lint, that would catch those examples, as I recall.

scottmcm: The big takeaway for me on this document is understanding how the spontaneous decay is really a different thing. I may be willing to risk some of the UB cases to get rid of that.

NM: Waffle, I'd be up to talk this over later.

(The meeting ended here.)

Allowing overlapping impls for !

tmandry: Strawman proposal: This would literally compile:

impl<T> From<!> for T {}

We treat every trait item that depends on having a value of ! as having an implicit where T: Inhabited. Also, we make an exception to the overlap check for ! and possibly other publicly-uninhabited types.

This would still require traits to opt in to the impl, rather than doing something probably-unwise like automatically implementing traits for !.

Nadri: would this be consistent? Sounds like we could need specialization

tmandry: The intuition is that any item we allow overlap with wouldn't be reachable. We probably couldn't make it work for traits which have associated types.

Nadri: similar to how we allow overlap for marker traits maybe? I think we do at least.

tmandry: Exactly.. in the ! case some traits effectively have no items, so they act more like marker traits.

Topic

name: Question/comment.


  1. Tracking issue for promoting ! to a type (RFC 1216) was opened on 2016-07-29 ↩︎

  2. A loop {} without breaks inside is !-typed, but one with breaks will have the same type as the values passed to the breaks ↩︎ ↩︎

  3. [comment] ↩︎

  4. Not all breaking changes are created equal; we'll still have to support the old code using the old edition ↩︎

  5. The new desugaring passes Result<!, E>, instead of E to a generic function (Try::from_residual), so since ! does not apear in non-generic code, there is no place for spontaneous decay to happen. (zulip discussion) ↩︎

  6. It might not have ideal interopability with old editions, since macros expanded in a new edition, but written with spontaneous decay in mind, will break (and vice-versa). But "macros which depend on spontaneous decay of !" seems like a very niche usecase (And again: this is not breaking any existing code, only adding theoretical issues with cross edition interactions). There is also precedent with NLL: [comment]. ↩︎

  7. I did most of the implementation, but then stopped because it was unclear what T-lang actually wants out of the lint which breaks we want to do. ↩︎