How does mutability affect lifetime inference?

The code below implements a struct MyCell to see if Rust compiler can catch possible errors. Its set method doesn't really do anything, but its prototype is interesting: The compiler is able to detect a possible lifetime error with &mut self, but not with &self. I want to know how mutability on a type can change the compiler's lifetime inference.

#![allow(unused)]

#[derive(Debug)]
struct MyCell<T> {
    value: T,
}

impl<T> MyCell<T> {
    fn new(value: T) -> Self {
        Self { value }
    }
    fn set(&mut self, _value: T) {} // compile error;
    fn set(&self, _value: T) {} // no compile error;
}

fn foo<'a>(mut cell: MyCell<&'a i32>) -> MyCell<&'a i32> {
    let val: i32 = 13;
    cell.set(&val);
    cell
}

fn main() {
    static X: i32 = 10;
    let cell = MyCell::new(&X);
    dbg!(foo(cell));
}

My analysis so far: Note that foo returns exactly its argument, that is, the only MyCell in this program. Because it is created early and used at the end of main, the generic parameter T in MyCell<T> shall have a long lifetime. But then method set shall not allow supplying an argument _value with a short lifetime, no matter its receiver is &self or &mut self. However, what actually happens is it compiles OK when its receiver is &self. Here I'm stuck.

Variance. Immutable references allow shortening the lifetime of the referent, mutable references do not.

Ie., the set(&self) method will actually receive a &'tmp MyCell<'tmp> after coercion, but the same is not possible for set(&mut self).

This would start to make more sense once you actually try to implement the methods with the obvious semantics. You can't implement the immutable version incorrectly, because you can't store the shorter-living temporary reference unless the receiver is a mutable reference to self.

1 Like

Have you tried to read documentation about that topic in the nomicon or Rust's reference?

Wouldn't be a surprise if set is a standalone function, and covariance applies. But here set is part of impl<T> MyCell<T>. As mentioned in my analysis, this T can only be a long lifetime. Doesn't monomorphization fix the lifetime of T?

I mean, when we call cell.set() where cell is MyCell<&'long i32>, wouldn't this require _value to have a long lifetime too, because _value has type T which is &'long i32? You can't supply a &'short i32 as _value to it, right?

The conversion/subtyping doesn't happen inside the impl/MyCell::set itself. It happens inside the body of foo(), only on the line where you call set().

No, that's an incorrect reasoning. It is exactly the case that lifetimes can be shortened in a covariant context, such as the referent type of an immutable reference.

I guess it depends on how you word it or from what point of view you are considering it. As always, generics look exactly the opposite when you look at them from the caller's vs. the callee's side.

In this case, inside the body of foo(), 'a is long and the temporary lifetime I called 'tmp is shorter. Thus, the compiler can coerce &'tmp MyCell<'a> to &'tmp MyCell<'tmp> because immutable references are covariant, and so anything that has access to a &'tmp Cell<'_> is known to only ever be able to read from – and not write to – it. This is a shortening of the lifetime parameter, which weakens the restriction it represents (from the caller's PoV) and making the capability it represents less useful (from the callee's PoV).

1 Like

First with fn set<'a>(self: &'a MyCell<T>, value: T). Methods are a bit harder to write signatures for, let’s see the equivalent free-standing function signature

fn set_immut<'a, T: 'a>(this: &'a MyCell<T>, value: T)

Also, we already know that we’re only using the case where T is some &i32 type, so let’s reduce the generality a bit:

fn set_immut<'a, 'b: 'a>(this: &'a MyCell<&'b i32>, value: &'b i32)

The same analysis would give us a signature of the mutable case of

fn set_mut<'a, 'b: 'a>(this: &'a mut MyCell<&'b i32>, value: &'b i32)

by the way.

Now, why does the code

fn foo<'c>(mut cell: MyCell<&'c i32>) -> MyCell<&'c i32> {
    let val: i32 = 13;
    set_immut(&cell, &val);
    cell
}

compile?

Let’s call the “lifetime” of how long the variable val exists and can be borrowed 'val. Then &val is of some type &'d i32 with 'val: 'd. (These bounds are “outlives” bounds, i.e. the lifetime 'val is longer or equal than 'd.)

This is passed to set_immut. As well as &cell. This borrow of cell is for some lifetime 'a which is not really too relevant for the issue at hand, so let’s not discuss it further; and it’s a borrow of a value of type MyCell<&'c i32>.

So the two values passed to set_immut are of type &'a MyCell<'c i32> and of type &'d i32. We call set_immut<'a, 'b> – as I’ve said we aren’t worried about 'a, so I already gave them a matching name, but what is 'b?

This is where variance comes in. Looking back at the signature

fn set_immut<'a, 'b: 'a>(this: &'a MyCell<&'b i32>, value: &'b i32)

the first argument is of expected type &'a MyCell<&'b i32>, but its actual type is &'a MyCell<&'c i32>. How can we make this match? The compiler will introduce an implicit subtyping coercion! In this case, &'a MyCell<&'c i32> is a type that is covariant in both lifetime arguments, in particular it’s covariant in 'c. So the coercion can convert our type as desired as long as 'c: 'b.

Similarly, for the second argument, we coerce &'d i32 into &'b i32 successfully as long as 'd: 'b.

The resulting complete list of lifetime constraint we gathered:

'val: 'd
'd: 'b
'c: 'b

These bounds can be trivially fulfilled by making 'b a really short lifetime, any by making 'd any lifetime between 'val and 'b. These two lifetimes we (the compiler) are completely free to choose however we want to fulfill these constraints (unlike 'c which is the parameter of our function and 'val which we defined to denote the time the variable val exists / can be borrowed for).


Now what changes with &mut?

fn foo<'c>(mut cell: MyCell<&'c i32>) -> MyCell<&'c i32> {
    let val: i32 = 13;
    set_mut(&mut cell, &val);
    cell
}

The analysis is quite similar: the two values passed to set_mut are of type &'a mut MyCell<'c i32> and of type &'d i32, with 'd being a lifetime 'val: 'd as before.

However the type &'a mut MyCell<'c i32> is covariant in 'a (a fact we don’t care much about, as we don’t look at 'a here), and invariant in 'c. This means that our implicit subtyping coercion cannot change the lifetime 'c, and 'c == 'b must be the same lifetime. Lifetime equality can also be written as a combination of two bounds: 'c: 'b and 'b: 'c.

The resulting complete list of lifetime constraint we gathered this time is:

'val: 'd
'd: 'b
'c: 'b
'b: 'c // this one is new, compared to the immutable case

This list of constraints can not be fulfilled. There is the outlives relation

'val: 'd: 'b: 'c
// which transitively implies the condition
'val: 'c

which means that the variable val must live longer than the lifetime 'c. But 'c is a caller-provided lifetime, external to our function. And this is why the compiler will complain: `val` does not live long enough, with the hint argument requires that `val` is borrowed for `'c` .

3 Likes

The conversion/subtyping doesn't happen inside the impl/MyCell::set itself. It happens inside the body of foo() , only on the line where you call set() .

This is where I made a mistake. The method call is not bound to any specific struct. Instead, as long as the compiler can find any specialization of the generic method then the code compiles (in this example it specializes set with a short lifetime and coerces the receiver). Specifically, the receiver type does not decide the generic parameter and is itself subject to subtyping coercion during a method call. This becomes obvious when we regard the receiver as an argument to the method call (after generality reduction and method-to-function conversion suggested by Steffahn).

Thank you for this long answer about the nitty-gritty. I made it clear after I reduced the generality and transformed method calls into function-like syntax:

#![allow(unused)]

#[derive(Debug)]
struct MyCell<'a> {
    value: &'a i32,
}

impl<'a> MyCell<'a> {
    fn new(value: &'a i32) -> MyCell<'a> {
        Self { value }
    }
    fn set(self: &MyCell<'a>, _value: &'a i32) {}
}

fn foo<'a>(mut cell: MyCell<'a>) -> MyCell<'a> {
    let val: i32 = 13;
    MyCell::set(&cell, &val);
    cell
}

fn main() {
    static X: i32 = 10;
    let cell = MyCell::new(&X);
    dbg!(foo(cell));
}

The source of confusion was the subtyping coercion being applied to the receiver type. Looking at the function-like syntax, this is perfectly legal, and the compiler is free to choose a specialization with coerced receiver type.

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.