--- title: "Design meeting 2024-04-05: Never type" tags: ["T-lang", "design-meeting", "minutes"] date: 2024-04-05 discussion: https://rust-lang.zulipchat.com/#narrow/stream/410673-t-lang.2Fmeetings/topic/Never.20type.20meeting.202024-04-05 url: https://hackmd.io/92bYs_v4QjWwOTVedK1u5w --- # Never type discussion The revised proposal is: 1. Change fallback to `!` on 2024 edition. 2. Add a lint against fallback affecting a generic that is passed to an `unsafe` function. - Perhaps make this lint `deny-by-default` or a hard error in Rust 2024. 3. Add a future incompat lint for some/most of the code that will be broken by fallback to `!`. 4. Add documentation/migration tools for people adopting 2024 edition. 5. At a later date when 2024 edition adoption is widespread, make the breaking change to fall back to `!` always everywhere. 6. Change `Infallible = !`. 7. Stabilize `!` (!). Analysis of that `objc` bug: [Playground link](https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=6315e3fc6e22ced34a783c64022863ba) --- # Discussion ## Attendance - People: TC, Niko, Waffle, Urgau ## Meeting roles - Minutes, driver: TC ## Fallback to type you can't write NM: In Rust 2024, this would mean we'd fall back to a type you can't write. Is that a problem? NM: You can always coerce it to any other type. I don't see any immediate problems. ## What does it mean to pass a type to an unsafe function? NM: What does it mean to pass a type to an unsafe function? TC: Here's the `objc` example (minimized): ```rust! pub unsafe fn send_message<R>() -> Result<R, ()> { dbg!(core::any::type_name::<R>()); Ok(unsafe { core::mem::zeroed() }) } macro_rules! msg_send { () => { match send_message::<_ /* ?0 */>() { Ok(x) => x, Err(_) => loop {}, } }; } fn main() { // If we call `send_message` directly and constrain the type // variable, this works as we would expect: let _: Result<(), _> = unsafe { send_message() }; //~^ OK! // // ...similarly, calling it through `msg_send` with the type // variable constrained always works: let _: () = unsafe { msg_send!() }; //~^ OK! // // If instead we call `send_message` directly and do not constrain // the type variable, this fails to compile as we would expect: //let _: Result<_, _> = unsafe { send_message() }; //~^ ERROR type annotations needed //~| NOTE cannot infer type // // However, if we call `msg_send` without constraining the type // variable, this magically(!) compiles (in current Rust): let _: _ = unsafe { msg_send!() }; //[fb_unit]~^ OK! //[fb_never]~^ CRASH illegal instruction //[no_fb]~^ ERROR type annotations needed // // Changing Rust to fall back to `!` results in UB, and changing // Rust to disable fallback entirely causes the compilation error // we would expect. } ``` NM: Is the lint just for the top-level types or also nested? Waffle: Also nested. NM: How much code are we expecting this to affect? Waffle: Probably not much. NM: I suppose that we could tune this over time. It's just a lint after all. NM: E.g., if the type appears in the input arguments, then it should be fine, because you must have produced a value of it. Waffle: That sounds fine, but I don't want to only look at the return type because you could still create UB by creating a value of the type only within the function. NM: I'm wondering about a case like this: ```rust unsafe fn create<T>() -> T where T: Default, { /* do something unsafe here that is unrelated */ T::default() } let mut x = something_optional.map(|x| panic!()); unsafe { x = create(); } ``` Waffle: There may be something like this possible, but I have a hunch that this is rare. NM: Probably, and it'd be better to tune based on real examples. ## Missing unsafe NM: Let's talk about this: ```rust pub fn send_message<R>() -> Result<R, ()> { dbg!(core::any::type_name::<R>()); Ok(unsafe { core::mem::zeroed() }) } ``` NM: This is buggy, of course, but we wouldn't detect this. NM: Maybe we could lint to say this function should be `unsafe`. But how? Waffle: What the lints would say is that since you're producing a value that is unbounded and the function is safe... Waffle: It's clear to a human that this function is unsound because you can give it an `R` that causes UB. Not sure how we teach the compiler to do this in general. NM: We're trying to say: * If your return value includes a value of type `R` where... * ...`R` was produced by an unsafe function that can return any value, perhaps one from a known set (e.g., zeroed, transmute)... * ...and `R` is a type parameter without any unsafe trait in the bound... * ...then your function must be unsafe. NM: This isn't a blocker; but it's a good idea for a lint. ## The "deserialize" pattern and `?` interactions nikomatsakis: My other concern has been the interaction with `?`, which imo is deeply surprising, and things like: ```rust Deserialize::deserialize(); // error, unconstrained Deserialize::deserialize()?; // "ok", falls back to `!`, panics when it executes match Deserialize::deserialize() { Ok(x) => x, Err(e) => return e, } ``` TC: The proposal to change the `?` desugaring: https://github.com/rust-lang/rust/pull/122412 NM: What occurs to me is that in arms where control-flow is dead, they don't contribute to the type.... but of course they do right now. Waffle: It's an interesting idea, but I'm not sure it fits into Rust. I'm not sure we have any control-flow specific things like that. NM: I'm not sure that's true, but nonetheless I agree. It's not as good as something that could be more type based. E.g. I'd prefer if this worked the same way, and there are many other variations: ```rust match Deserialize::deserialize() { Ok(x) => x, Err(e) => log(|| panic!()), } fn log<R>(f: impl FnOnce() -> R) -> R { f() } ``` NM: It may be possible to change `coerce_many` to handle many but not all cases. It wouldn't work when we needed information that is only available later in the process. If we could find a way to solve this I'd have basically no qualms. NM: The first lint make total sense and we should definitely do it (flowing the unboutd variable into `unsafe`). The second lint (missing `unsafe`) is less important but also makes sense and isn't a blocker. Perhaps we could put it in clippy, e.g. Waffle: Regarding `?`, the first step is changing that desugaring. Then we could add a lint. NM: Seems like we have two tenets: * Simple and unsurprising desugaring, type rules. * Correctness. ...and you are saying you prefer them in that order and for correctness to be addressed via lint. Why then not apply same reasoning to `?`? It has a simple desugaring... Waffle: Probably for me, the `?` case feels like a leaky abstraction. NM: I value a simple desugaring. I'm probably OK with the lint only. Let's just do the lint only. If we want to change `?` desugaring, we could do that later after we collect data. NM: Probably I want a hard error here. I think we can achieve that with the lint approach. This may require improvements in the implementation of our type system. Waffle: How would the lint work? NM: What we're looking for is that you have a type variable that is the target of a coercion from both a fallback variable and a non-fallback variable *and* that type variable's type is determined by fallback. Graphically: ```mermaid flowchart LR V1 --> V3 V2 --> V3 V1["Non-fallback variable V1"] V2["Fallback variable V2"] V3["Non-fallback variable V3,\nwhose type is determined by V2"] ``` ...and V3's type is determined by fallback. NM: Would I block on this? I don't know. I would like to see this explored. ## "Add a future incompat lint for some/most of the code that will be broken by fallback to `!`." NM: What would this lint look like? Waffle: I'm hoping to think about the details of this later. We have a crater run going now that turns off fallback entirely. We're hoping to go through some of the examples of breakage here to come up with ideas. TC: That crater run is: https://crater.rust-lang.org/ex/no-never-type-fallback NM: ...you had a type variable, and there was a trait matched against it, and its value was determined by fallback. I.e.: * type variable `?X` * you had a trait obligation that involved `?X` * e.g., `?X: Foo` is a subset but probably the most common * could also be `Option<?X>: Foo` or `(): Foo<?X>` etc etc * type of `?X` is determined by fallback Two possibilities: * trait obligation is still solvable with `?X = !` * and leads to the same impl * huzzah, no difference * and does not lead to the same impl * compiles in Rust 2024 but potentially changes behavior * example above is this case * and may not lead to the same impl * not sure if this can happen * trait obligation is not still solvable with `?X = !` * would not compile in Rust 2024 Other extraneous cases: * what I wrote above is missing some cases because of indirection * `?X: IsEq<?Y>` (`impl IsEq<()> for ()`, `impl IsEq<!> for !`) * `?X` falls back to `()`, now `?Y` becomes `()` as a result of second round of trait solving * was not *directly* implicated by fallback * `?X: Something<Output = ?Y>` ## Next steps TC: So the next steps are: 1. Implement lint against fallback flowing into an `unsafe` function. 2. Implement lint against fallback flowing into a trait obligation (?) Other things: 1. Maybe alter `?` desugaring 2. Explore whether it can be generalized to match coercion behavior. 3. Explore the lint about missing `unsafe`. Waffle: The main work seems to be to implement these lints and to run crater on them to see how code that they flag. ## Hard error or `deny-by-default` in Rust 2024? TC: What are your thoughts about making the lint against fallback flowing into an `unsafe` into a hard error or a `deny-by-default` lint in Rust 2024? NM: Makes sense to me. We have a policy on `deny-by-default` being when something is almost certainly a bug, and that seems to be the case here. ## OK on plan? NM: The plan sounds good to me.