What mistake with the compiler made that can result in the lifetime being extended from a small one?

Of course, this is a classic issue 25860. However, I'm just curious about what mistake the compiler made to make the example passed here. Given a simplified example:

fn extend<T>(input: &T) -> &'static T {
    struct Bounded<'a, 'b: 'static, T>(&'a T, &'b i32);
    fn n<'c,U>(x:&'c U)-> Bounded<'static, 'c, U>{  // #1
        Bounded(x, &0)
    }
    //n(input).0;   // #2
    let n: &dyn for<'c>  Fn(&'c T) -> Bounded<'static, 'c, T> = &n::<T>;
    n(input).0  // #3
}
fn main(){

}

In the definition of n at #1, it is presumably that the implied bound is 'c:'static due to the trait bound in the definition of Bounded(i.e. 'b:'static). If uncomment #2, the compiler will report the lifetime of input does not satisfy 'static.

However, For the type annotation &dyn for<'c> Fn(&'c T) -> Bounded<'static, 'c, T>, it should also impose the similar requirement 'c:'static with the same reason as above, which looks like as if it were &dyn for<'c:'static> Fn(&'c T) -> Bounded<'static, 'c, T>.

However, the invocation at #3 is just accepted by the compiler, why doesn't it deserve a compiled error as that of #2?

What mistake does the compiler make here? Is the mistake that the compiler does not impose the implicit trait bound 'c:'static for dyn for<'c> Fn(&'c T) -> Bounded<'static, 'c, T>?

If you make it explicit;

fn n<'c,U>(x:&'c U)-> Bounded<'static, 'c, U> where 'c: 'static {
error[E0308]: mismatched types
 --> src/main.rs:8:65
  |
8 |     let n: &dyn for<'c>  Fn(&'c T) -> Bounded<'static, 'c, T> = &n::<T>;
  |                                                                 ^^^^^^^ one type is more general than the other
  |
  = note: expected trait `for<'c> Fn<(&'c T,)>`
             found trait `Fn<(&T,)>`

error: implementation of `FnOnce` is not general enough
 --> src/main.rs:8:65
  |
8 |     let n: &dyn for<'c>  Fn(&'c T) -> Bounded<'static, 'c, T> = &n::<T>;
  |                                                                 ^^^^^^^ implementation of `FnOnce` is not general enough
  |
  = note: `fn(&'2 T) -> extend::Bounded<'static, '2, T> {extend::n::<'2, T>}` must implement `FnOnce<(&'1 T,)>`, for any lifetime `'1`...
  = note: ...but it actually implements `FnOnce<(&'2 T,)>`, for some specific lifetime `'2`

At #2, your n is a function, whereas at #3 you've redefined it as a closure.

Lifetimes for functions and closures have slightly different rules. I'm not expert enough on Rust's type system or its development to explain exactly how and why, but someone on this forum should be able to help you.

Also, this open issue might give some insight:

Yes, or alternatively, that it doesn't otherwise carry the bound forward using something like

// Hypothetical higher-ranked-with-obligations syntax
//  vvvvvvvvvvvvvvvvvvvvvvvvv
dyn for<'c where 'c: 'static> Fn(&'c T) -> Bounded<'static, 'c, T>
1 Like

However, as pointed out by @jonh in his comment, why add the explicit trait bound will make a compiled error? Don't the explicit trait bound just do the same things as the implicit one?

No. It makes the function parameterized by the lifetime, for one. It can't meet the higher-ranked bound (even in cases where it's sound).

You can regain the unsoundness when you have the 'static bound like so.

let n: &dyn for<'c>  Fn(&'c T) -> Bounded<'static, 'c, T> = &|x| n::<T>(x);
    fn n<'c,U>(x:&'c U)-> Bounded<'static, 'c, U>{  // #1
        Bounded(x, &0)
    }

So, this function didn't impose the trait bound 'c:'static? However, we can create a reference in the body like let rf:&'static &'c () = &&();. This embodies that 'c shall satisfy 'static.

For example

struct B<'a:'static>(&'a i32);
fn foo<'a>(v:&'a i32)->B<'a>{  // impose 'a:'static
    let rf:&'static &'a () = &&();
    todo!()
}
fn foo2<'a>(v:&'a i32){   // impose nothing
    let rf:&'static &'a () = &&();
    todo!()
}

The second one that does not impose the trait bound by B<'a>(i.e. 'a:'static) will emit an error in its body.

Function bodies can assume all bounds are met and all types in the signature are well formed, which in this case, means 'a: 'static.

The actual checking has to happen someplace in compilation though; usually it's at the call site. There the compiler checks that everything is well-formed and all the bounds are met.

The coercion leaves you with something where not all the things that need to be checked, do get checked. With dyn Fn it's probably because the output is an associated type, but I'd have to spend a lot of time playing around to say that confidently, probably.

That is, the implicit trait bound by signature is just conceptually equivalent to the explicit one, but they are not equivalent when checking types? As in your example:

fn foo<'a, T>(_: &'a T) {}  // #1
fn bar<'a, T: 'a>(_: &'a T) {}  // #2

The signature in #1 implicitly requires that T:'a while we explicitly requires the trait bound in #2, however, when doing type checking, they behave different.

They effect how the function item is internally defined, and explicit bounds tend to not be higher-ranked compatible, so far.

foo is like a nominal FooFn<T> struct, while bar is like a BarFn<'a, T: 'a> struct, to some extent.

// FooFn<T>: for<'a> Fn(&'a T)
impl<'a, T> Fn(&'a T) for FooFn<T> { ... }

// BarFn<'a, T>: Fn(&'a T)
// But there's no way for `Bar<'_, T>` to meet the higher-ranked bound
impl<'a, T: 'a> Fn(&'a T) for BarFn<'a, T> { ... }

Do you mean the explicit trait bound T: 'a in bar makes the lifetime 'a is not enough general, because that 'a cannot be any lifetime but just be those lifetimes that T will satisfy?

Instead, the implicit trait bound will be processed by the compiler differently even though the implicit imposes the same requirement as if we were using an explicit one, which results that foo is ok but bar is an error?

I mean it changes how the function item is defined, and that has an impact on what how the function item has to implement traits, what it can be cast to, and so on. It's like... the function item itself is less general.

But it is the case that explicit bounds are treated as less general than implicit bounds. Let's consider a case where there's no substantial difference in the implementing types. Here's an example where the implementations are semantically the same, but the explicit version is less general as far as the compiler is concerned.

trait Foo<T> {}

struct Implicit;
struct Explicit;

impl<'a, T> Foo<&'a T> for Implicit {}
impl<'a, T: 'a> Foo<&'a T> for Explicit {}

fn witness<'s, F: for<'a> Foo<&'a &'s str>>() {}

fn check<'s>() {
    // compiles
    witness::<'s, Implicit>();
    // fails
    witness::<'s, Explicit>();
}

Or another one, this actually came up indirectly in another thread today.

These examples demonstrate that it's not just function item types being different that causes problems, it's the presence of explicit bounds in some cases. The compiler does treat them differently.

Please confirm whether my understanding of your example is correct.

First, F: for<'a> Foo<&'a &'s str> means F must satisfy Foo<&'a &'s str> for any lifetime 'a.

Then, this imposed requirement will make the blanket implementations try to implement the trait.

For the blanket implementation of Implicit, we have the implementation:
impl<'a, 's> Foo<&'a &'s str> for Implicit {}, this implementation means we implement trait Foo<&'a &'s str> for Implicit for any lifetime 'a even though there is an implicit trait bound 's:'a.

Instead, For the blanket implementation of Explicit, we have the implementation:
impl<'a> Foo<&'a &'s str> for Explicit where 's:'a, this implementation means we implement the trait Foo<&'a &'s str> only for those lifetimes 'a that satisfy 's:'a. So, from this perspective, the former is more general than the latter even though they are semantically the same, Is my understanding right?

It's something close to that (but the implementations only have one lifetime, the other generic is a type variable).

Based on what I've observed anyway. If what actually goes on is specified or documented, I don't know where. Compiler dev docs maybe.

1 Like

OK, we just need to remember an informal principle that the explicitly written trait bound is less general than the implicit trait bound inferred by the compiler for some cases, even though they are semantically the same.

The OP might be one of the issues linked here, if you want to pursue some compiler dev discussion about what goes on.

@quinedot As a simplified example in your above comment, consider this example

use std::future::Future;

fn show<'a>(v: &'a i32) -> impl Future<Output = ()> + 'a {
    async move {}
}
fn caller<F: for<'b> MyTrait<'b,i32,()>>(f: F) {  // #1
    let a = 0;
    f.call(&a);
}

trait MyTrait<'a, A, T> {
    async fn call(self, _: &'a A) -> T;
}
impl<'a, F, A:'a, T, Fut> MyTrait<'a, A, T> for F  // #2
where
    F: Fn(&'a A) -> Fut,
    Fut: Future<Output = T>,
{
    async fn call(self, v: &'a A) -> T {
        self(v).await
    }
}
fn main() {
    caller(show);
}

The trait bound in #1 says F should satisfy MyTrait<'b,i32,()> for any lifetime 'b, however, in the blanket implementation at #2, it just implements MyTrait<'a, A, T> for F for some lifetimes that satisfy A:'a, which has similar constraints as we have done for the Explicit implementation, that is, the expected compiled error would be something like this:

The implementation is not general enough

however, the call caller(show) is ok. How to interpret this example?

My best guess that the existence of

impl<'a> Fn<(&'a i32,)> for fn#show

is somehow enough to satisfy the higher-ranked bound on MyTrait despite the explicit bound.

However, fn#show may satisfy the trait bound F: for<'b> MyTrait<'b,i32,()> only if it uses the blanket implementation at #2, and #2 is not a higher-ranked implementation due to the trait bound A:'a that limits what these 'a can be, instead, in the signature of caller, the relevant trait bound is a higher-ranked trait bound, which requires the implementation satisfy all lifetimes. This is the issue I think the call should be an error.

Clearly, yes.

Here I already pause -- you're making assumptions about how things are implemented in the compiler.

If I understand,[1] it finds the implementation and fills in some parameters, and then translates the bounds on the implementation into higher-ranked bounds.

Query: Does fn#show: for<'b> MyTrait<'b, i32, ()>
  Use placeholder '0
  Query: Does fn#show: MyTrait<'0, i32, ()>
    Candidate: impl<'a, F, A, T, Fut> MyTrait<'a, A, T> for F
    Matches with 'a = '0, F = fn#show, A = i32, T = (), Fut: ?Fut
      i32: '0 -- sure
      fn#show: Fn<(&'0 i32,)> -- sure (`?Fut = fn#show::Output`)
      fn#show::Output: Future<Output = ()> -- sure
    Plug holes
      for<'a> i32: 'a -- sure
      for<'a> fn#show Fn<(&'a i32,)> -- sure
      for<'a> fn#show::Output: Future<Output = ()> -- sure

So perhaps this permits higher-ranked bounds if A: 'static. I'm not 100% on that though.

I don't know enough to say if or how it relates to the compiler's current unsoundness holes.


  1. I only went looking due to this topic ↩ī¸Ž

1 Like