owned this note changed a year ago
Published Linked with GitHub

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


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):

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:

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:

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:

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:

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:

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.

Select a repo