owned this note
owned this note
Published
Linked with GitHub
---
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.