Why does the instantiated method permite an argument with a shorter lifetime?

struct A<'a>(&'a i32);
impl<'a> A<'a>{
    fn call(&self, v:&'a i32){
        
    }
}
fn main(){
   let i = 0;
   static b:A<'static> = A::<'static>(&0);
   b.call(&i);
}

I'm confused by the above example. As shown in the code, the lifetime parameter explicitly specified for the type of b is 'static, the instantiated method should have the function type fn(& A<'static>, &'static i32), obviously, the borrowing of i cannot outlive 'static, however, the call of the method is ok. What's the reason here? If I explicitly call the method with the form

A::<'static>::call(&b, &i);

The compiler will complain:

argument requires that i is borrowed for 'static

That's constant promotion

Since you're borrowing a constant expression, the compiler is basically transforming your code into

fn main(){
   let i = 0;
   const PROMOTED: i32 = 0;
   static b:A<'static> = A::<'static>(&PROMOTED);
   b.call(&i);
}

A<'x> is covariant in 'x, and so b: A<'static> can coerce to A<'local>.

7 Likes

Is it? Wouldn't that mean A::<'static>::call(&b, &i) would work?

I thought this is the doing of subtyping for lifetimes. A reference with a lifetime 'b where 'b outlives 'a is a subtype of 'a. The 'static lifetime is a subtype of any lifetime 'a. So I think this makes A<'a> covariant to A<'static> and therefore allows the compiler to implicitly coerce &'static self to &'a self in the call method's arguments.

1 Like

b.call(&i); is equivalent to A::<'static>::call(&b, &i);, isn't it? Why the second form is ill-formed?

I didn't ask A::<'static>(&PROMOTED), I asked why &i can be passed to the method that receive &'static i32.

It's more like A::<'_>::call(&b, &i);, but I'm not sure if this is explicitly documented anywhere.

(Also whenever I see "equivalent" in Rust documentation, I take it with a large grain of salt.)

4 Likes

If it is, It appears to me the lifetime in the method wouldn't have any restriction that we expect. For example, I would expect b.call(&i); should restrict that the lifetime of &i should outlive the lifetime in A<'a> that is the type of b, However, with the transformation form A::<'_>::call(&b, &i);, the restriction will be gone since the call just need to infer the lifetime to make both arguments coerce to the common type.

In general Rust opts for practicality, and I think most programmers expect lifetimes to "just work" wherever possible.

So as a counter-point, here's a case where subtype coercion is possible but isn't automatically applied. If you haven't witnessed the situation and aren't experience enough to understand it should be allowed, you'd probably never figure out to manually force the coercion.


References as arguments have to automatically subtype or reborrow anyway in order for the language to be tolerable, so after thinking on it for a moment, this would be the relevant documentation.

3 Likes

Does it mean, for every method call, the call expression receiver.method(...)will be transformed to T::method(&receiver, ...) where T is the general type if it was.

You haven't realized the covariance matters a lot.

The method signature is a contract that says kinda call<'a, 'call>(&'call A<'a>, &'a i32), but

The secret is the covariance on A<'a> after the instantiation.

So consider a stricter contract call_strict<'a>(a: &'a A<'a>, v: &'a i32) which is discouraged though. Rust Playground

If you disregard the point on covariance, the second line would also be a surprise for you:

let mut b: A<'static> = A::<'static>(&0);
call_strict(&b, &i); // Note: this line is not `call_strict(&'static A<'static>, &'static i)`
&mut b; // without the covariance after the instantiation, you couldn't do this

The code compiles fine.

The link shows it also works for A::<'_>(&0) a type with inferred probably non-static lifetime.

What's the trick you're missing or misunderstanding?

Invariance. Consider to confine the power of covariance that would happen after the instantiation: Rust Playground

struct A<'a>(*mut &'a i32);

let mut b: A<'static> = A::<'static>(&mut &0);
b.call(&i);  // error: `i` does not live long enough

This time, you're probably not stunned any more. Once you have a A<'static>, the lifetime just stays 'static and everything seems easy to understand. Except for the following working case:

let mut b = A::<'_>(&mut &0);
b.call(&i);

Oops, you haven't confined the power of covariance that has happened before the instantiation :slight_smile:

let mut b = A::<'_>(&mut &0);
b.call(&i); //           ^
//     ^-----------------↑ lifetimes are tied together according to the call method
//                         but the compiler makes `&'long 0` shorter beforehand

The secret is the covariance on A<'a> after the instantiation.

The secret is b.call(&i) is not equivalent to A::<'static>::call(&b, &i); even though it looks like we want to call method call on the instance b that has type A<'static>, instead, the method call is equivalent to A::<'_>::call(&b, &i), that is, the compiler will infer a lifetime for '_ such that 'static:'_ and 'i: '_, then use the inferred lifetime '_ to instantiate the method, then covariant happens after this instantiation.

Any transformation isn't happening on the AST level.

Some light testing seems to indicate that lifetimes are inferred but type and const parameters are not.

The transformation is only used to think of how the lifetime will be inferred for a method call. In other words, in a method call expression, the lifetime parameter of the enclosing type in the method signature is not designated by the receiver expression, the lifetime instead should be re-inferred together with other arguments in order to find out the common lifetime they can be coerced to. Is this understanding right?

Seems that way. I think all lifetimes are replaced with inference variables and then checked against constraints.

Concrete lifetimes don't matter for method resolution either... effectively anyway. (I don't think there's a way to implement for a non-overlapping pair of lifetimes, technically, so it must be this way.)

However, this also remains an issue, the Rust Reference says:

For method calls, the receiver (self parameter) can only take advantage of unsized coercions.

In this case, the subtype coercion is expected to happen at the receiver position, how to interpret this point?

I've been confused by that phrasing forever. I dug into the history of the reference and didn't find any explanation in the PR.

My best guess as to what it's trying to say is that function arguments don't automatically unsize coerce except that method resolution may unsize coerce a method receiver.

(Then the next place the reference falls short is that only slice unsizing takes place for method resolution, and not say, dyn Trait unsizing.)

2 Likes

Maybe, you may want to post an issue to Rust Reference to fix that error.

I linked to an existing issue (which I also commented on), but PRs have a better hit rate, so probably I should try that at some point. Although I've seen those stall out too sadly.

What Rust really needs is an actual spec.

What Rust really needs is an actual spec.

That would be better, however, Rust is a high-speed evolve language, so it may be too early to have a standard specification. Anyway, it would be meaningful if there was a book that can commonly interpret borrowing checker, lifetime, coercion, and some obscure concept.