Would you consider type inference discrepancies as bugs?

Hi

Given the following code:

#[derive(Copy, Clone)]
struct MyStruct;
trait MyTrait {}
impl MyTrait for MyStruct {}
fn do_smth0(_x: Option<MyStruct>) {}
fn do_smth1(_x: Option<impl MyTrait>) {}
fn do_smth2<T: Into<Option<MyStruct>>>(_x: T) {}
fn do_smth3<T: Into<Option<impl MyTrait>>>(_x: T) {} // problematic type inference

fn main() {
    let x = MyStruct {};
    do_smth0(Some(x)); // works
    do_smth1(Some(x)); // works
    do_smth2(x); // works
    do_smth2(Some(x)); // works
    do_smth3(x); // works!
    do_smth3(Some(x)); // doesn't work
    do_smth3(Some::<MyStruct>(x)); // works
}

The function do_smth3 needs extra type annotation for it to work when passing an Option, which seems inconsistent with do_smth2 and do_smth3 without an apparent Option.

Should I open an issue regarding this?

This is expected behaviour. Remember that this

fn do_smth3<T: Into<Option<impl MyTrait>>>(_x: T) {}

Is equivalent to

fn do_smth3<M: MyTrait, T: Into<Option<M>>>(_x: T) {}

And there's no way to know which M you meant -- especially in a world where there are multiple implementations of MyTrait and of Into.

4 Likes

Playing with it a bit, I think I agree with the compiler here: It doesn't know what you want to transform x into, so writing do_smth3(Some(x)) could mean:

impl Into<()> for MyStruct {
    fn into(self) -> () {}
}

fn main() {
    do_smth3(Some(x)); // Into<Option<MyStruct>> or Into<Option<()>> ?
}

Well not exactly, but that's the idea :sweat_smile:

Hmm, trying to reproduce this, for me the do_smth3(Some::<MyStruct>(x)); version doesn’t work either.

Or another scenario:

impl MyTrait for Option<MyStruct> { … }

Then, when you feed T = Option<MyStruct>, both M = MyStruct and M = Option<MyStruct> would be valid substitutions.

I could've sworn I tried it, but yes you're right.
Although I'm inclined to believe that it should work.
Also the following don't work with None either:

do_smth3(None); // doesn't work
do_smth3(None::<MyStruct>); // doesn't work
do_smth3(None::<Option<MyStruct>>); // doesn't work
do_smth3(None::<impl MyTrait>); // this isn't allowed by the compiler
do_smth3(None::<Option<impl MyTrait>>); // also not allowed

Which also brings the question of how would you pass do_smth3 a None?

I think the answer to that question is that you didn't want to write do_smth3 like that in the first place. Having two levels of generics like that is just fundamentally never going to work well -- it's like doing .into().into().

If you're going to introduce a trait for the argument and be generic over that, you don't need the Into any more -- you can just implement the trait for Option<_>s if they make sense.

1 Like

This is a very generic question, somewhat begging the answer, too. It largely depends on what you mean by "discrepancy".

The case you illustrated is clearly not a compiler bug, as others have already pointed out. It is a genuine instance of ambiguous code, which indeed cannot be inferred/typechecked.

As for the cases where the compiler is not as smart as it theoretically could be: that's still not a bug per se, although the compiler not accepting code which can otherwise be proven valid can be an actual usability problem.

Not all "obviously valid" code can be formally proven to be valid through a type system, though. Especially around generics (types and lifetimes), it's sometimes hard or even theoretically impossible to come up with the right inference step. The extent to which this is a real issue depends on how commonly it occurs and how hard it is to fix in the compiler. Certainly, due to this theoretical limitation and incompleteness property, the fact that seemingly valid code is rejected by the typechecker is most often not a bug.

What would be a real, old-fashioned type checking bug, then? Well, it's when some incorrect code type checks successfully, causing e.g. memory safety problems due to the compiler's inconsistent internal view of the code. There are indeed a number of such bugs in the compiler (again, mostly related to lifetimes and borrowing), but the above snippet of code isn't one.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.