Signature mismatch caused by weird variance inference

Foreword:

Original case I tries to implement is: "Replace all signature's types with associated types in impl block".

Synopsis:

Variance inference looks weird in case described above. When &'s T type is replaced with Self::AssocT<'s> it drives a signature mismatch.

trait Test
where
    Self: for<'a> Lookup<'a>,
{
    fn test<'s>(_: &'s String) -> &'s String;
}

trait Lookup<'s> {
    type Arg;
}

impl<'a> Lookup<'a> for () {
    type Arg = &'a String;
}

impl Test for () {
    fn test<'s>(_: <Self as Lookup<'s>>::Arg) -> <Self as Lookup<'s>>::Arg { // Error: `'s` is not `'s`
        todo!()
    }
}

According to nomicon and dev-guide, by default 'a should be late-bounded and contravariant in fn(&'a T) signature, so Self::AssocT<'a> probably changes variance of 'a in some cases and its doesn't looks like a normal behavior.

trait CovariantLate: Lookup {
    fn test<'a>(arg: i32) -> &'a i32;
}

trait CovariantEarly: Lookup {
    fn test<'a: 'a>(arg: i32) -> &'a i32;
}


trait ContrvariantLate: Lookup {
    fn test<'a>(arg: &'a i32) -> i32;
}

trait ContrvariantEarly: Lookup {
    fn test<'a: 'a>(arg: &'a i32) -> i32;
}


// or it is bivariant?
trait InvariantLate: Lookup {
    fn test<'a>(arg: &'a i32) -> &'a i32;
}

trait InvariantEarly: Lookup {
    fn test<'a: 'a>(arg: &'a i32) -> &'a i32;
}


trait Lookup {
    type T1<'a>;
    type T2<'a>;
    type T3;
}

impl<T: ?Sized> Lookup for T {
    type T1<'a> = &'a i32;
    type T2<'a> = i32;
    type T3 = i32;
}

impl CovariantLate for () {
    fn test<'a>(arg: Self::T2<'a>) -> Self::T1<'a> {
        todo!()
    }
}

impl CovariantLate for ((), ) {
    fn test<'a>(arg: Self::T3) -> Self::T1<'a> {
        todo!()
    }
}

impl CovariantEarly for () {
    fn test<'a>(arg: Self::T2<'a>) -> Self::T1<'a> {
        todo!()
    }
}

impl CovariantEarly for ((), ) {
    fn test<'a>(arg: Self::T3) -> Self::T1<'a> {
        todo!()
    }
}

// errors
// impl ContrvariantLate for () {
//     fn test<'a>(arg: Self::T1<'a>) -> Self::T2<'a> {
//         todo!()
//     }
// }

impl ContrvariantLate for ((), ) {
    fn test<'a>(arg: Self::T1<'a>) -> Self::T3 {
        todo!()
    }
}

impl ContrvariantEarly for () {
    fn test<'a>(arg: Self::T1<'a>) -> Self::T2<'a> {
        todo!()
    }
}

impl ContrvariantEarly for ((), ) {
    fn test<'a: 'a>(arg: Self::T1<'a>) -> Self::T3 {
    //      ^^^^^^ required
        todo!()
    }
}


// errors
// impl InvariantLate for () {
//     fn test<'a>(arg: Self::T1<'a>) -> Self::T1<'a> {
//         todo!()
//     }
// }

impl InvariantEarly for () {
    fn test<'a>(arg: Self::T1<'a>) -> Self::T1<'a> {
        todo!()
    }
}

The workaround is specify 's: 's bound in method definition, but this drives another mismatch in "normal" implementations.

trait Test
where
    Self: for<'a> Lookup<'a>,
{
    fn test<'s>(_: &'s String) -> &'s String
    where
        's: 's; // Now it is early-bounded.
}

trait Lookup<'s> {
    type Arg;
}

impl<'a> Lookup<'a> for () {
    type Arg = &'a String;
}

impl<'a> Lookup<'a> for ((),) {
    type Arg = &'a String;
}

impl Test for () {
    fn test<'s>(_: <Self as Lookup<'s>>::Arg) -> <Self as Lookup<'s>>::Arg {
        todo!()
    }
}

impl Test for ((),) {
    fn test<'s>(_: &'s String) -> &'s String { // Error: `'s` is not `'s`
        todo!()
    }
}

Trait lifetime parameters are invariant (as you could define Lookup::<'a>::Arg to be fn(&'a String) instead of &'a String for example).

The "'s isn't 's unless early bound in both declaration and definition" thing is weird [1], and I would file a bug if you can't find an existing issue.


  1. probably normalization related but even then the error should point at the projection and not the lifetime IMO ↩︎

1 Like

Thanks for explanation. I tried to replace the reference type with a fn(&'a T) (and even just pass the type as-is through generic parameter) and seems like it still doesn't work.

trait Test: Lookup {
    fn test<'s>(_: fn(&'s String)) -> fn(&'s String);
}

trait Lookup {
    type Arg<T>;
}

impl Lookup for () {
    type Arg<T> = T;
}

impl Test for () {
    fn test<'s>(_: Self::Arg<fn(&'s String)>) -> Self::Arg<fn(&'s String)> { // error
        todo!()
    }
}

Also checked the difference between early-bounded and late-bounded lifetimes in MIR output. Late-bounded lifetimes represented as for<'lt>, and early-bounded passes the actual lifetime directly. Maybe this for<'lt> breaks/replaces the lifetime passed to a trait somewhere under-the-hood?

This is the issue with how Rust currently treats early and late bounded lifetimes and not intended to be changed in the near future: Typecheck fails when providing explicit types instead of GAT · Issue #87803 · rust-lang/rust · GitHub

To hack around this we can transform late-bounded lifetime into an early-bounded one with useless 'a: 'a bound:

trait Test {
    fn test<'a: 'a>(arg: &'a str) -> &'a str;
}

trait Lookup {
    type Arg<'a>;
}

impl<T: ?Sized> Lookup for T {
    type Arg<'a> = &'a str;
}

impl Test for () {
    fn test(arg: <Self as Lookup>::Arg<'_>) -> <Self as Lookup>::Arg<'_> {
        arg
    }
}
1 Like

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.