7

I'm creating a series of data structures containing mutable references to lower level structures. I've been fairly happily working with A, B, and C below but I've attempted to add a new layer D. A, B, C, D are actually the states of a state machine for protocol decoding, but I've deleted all of that here:

struct A {}

fn init_A() -> A {
    A {}
}

struct B<'l> {
    ed: &'l mut A,
}

fn init_B(mut e: &mut A) -> B {
    B { ed: e }
}

struct C<'l> {
    pd: &'l mut B<'l>,
}

fn init_C<'l>(mut p: &'l mut B<'l>) -> C<'l> {
    C { pd: p }
}

struct D<'lifetime> {
    sd: &'lifetime mut C<'lifetime>,
}

fn init_D<'l>(mut p: &'l mut C<'l>) -> D<'l> {
    D { sd: p }
}

fn main() {
    let mut a = init_A();
    let mut b = init_B(&mut a);
    let mut c = init_C(&mut b);

    // COMMENT OUT THE BELOW LINE FOR SUCCESSFUL COMPILE
    let mut d = init_D(&mut c);
}

I get an error:

error[E0597]: `c` does not live long enough
  --> src/main.rs:38:1
   |
37 |     let mut d = init_D(&mut c);
   |                             - borrow occurs here
38 | }
   | ^ `c` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

I'm completely lacking understanding of what is happening differently for D compared to C as far as lifetimes go: I don't understand what the lifetime mismatch is.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Ben Clifford
  • 1,398
  • 1
  • 12
  • 23
  • This indeed looks surprising. For what it's worth, your code [compiles on nightly with non-lexical lifetimes enabled](https://play.rust-lang.org/?gist=ac8b62c05dd2eb79b6185a3415c1c011&version=nightly). – Sven Marnach Jan 08 '18 at 21:19
  • I'm not totally sure that the NLL version working is *correct*. It seems like it has [at least one bug](https://github.com/rust-lang/rust/issues/47279). – Shepmaster Jan 08 '18 at 21:49
  • @Shepmaster I noticed the bug as well – thanks for reporting it! I'm not sure it's related to NLL. – Sven Marnach Jan 08 '18 at 21:54
  • @SvenMarnach right. As [noted](https://github.com/rust-lang/rust/issues/47279#issuecomment-356104081), non-NLL reports both an error *and* the warning, which is what makes me think that the fact the NLL version compiles is incorrect. – Shepmaster Jan 08 '18 at 21:56
  • If it helps anyone, [here's a greatly reduced example](https://play.rust-lang.org/?gist=43602054ad031b97ad6d2f69ab81476d&version=undefined) that shows the `init_` functions aren't necessary to unify the lifetimes; a struct behaves just the same on its own. – trent Jan 09 '18 at 17:44
  • 1
    @trentcl Nice! Here's the minimal fix from my answer applied to that reduced version: [playground](https://play.rust-lang.org/?gist=6033a44a2cc9f6bc73934230180ba7f6&version=stable) – Sven Marnach Jan 09 '18 at 21:09

2 Answers2

4

I'll address the point why the code in question doesn't work.

TL;DR: Invariance over the lifetimes of types C<'l> and D<'l> and the use of a single lifetime parameter ('l) for them cause variables of those types to keep their borrows for as long as variable b exists, but variable c (borrowed by d) is dropped before b.

The borrow checker is essentially a constraint solver. It searches for the shortest lifetimes0 which satisfy various constraints: a reference must not live longer than a value it references, lifetimes must obey constraints specified in function signatures and types, and lifetimes must obey variance rules1.

0 — The shortest lifetime of a reference is the best because then the reference doesn't borrow a value for longer than necessary.

1 — Rust has a concept of variance which determines whether it is possible to use a value with longer lifetime in a place which expects a value of lesser lifetime. The Rustonomicon link explains it in detail.

The code below is a simplified version of the code in question and it fails with the same error: c does not live long enough. The blocks are marked with the lifetimes of variables. 'a is the lifetime of variable a and so on. Those lifetimes are determined by the structure of the code and they are fixed.

Lifetimes in type annotations (B(&'ar A) -> B<'ar> and so on) are variables. The borrow checker tries to find valid assignments of fixed lifetimes ('a, 'b, 'c, 'd) to these variables.

The comments below the let statements show the lifetime constraints which I'll explain below.

struct A;

struct B<'l>(&'l mut A);

struct C<'l>(&'l mut B<'l>);

struct D<'l>(&'l mut C<'l>);

fn main() {
    // lifetime 'a
    let mut a = A;
    { // lifetime 'b
        // B(&'r mut A) -> B<'ar>   
        let mut b = B(&mut a); 
        // 'r >= 'ar & 'r <= 'a
        { // lifetime 'c
            // C(&'br mut B<'ar>) -> C<'abr>  
            let mut c = C(&mut b); 
            // 'br <= 'b & 'abr = 'ar & 'br >= 'abr
            { // lifetime 'd
                // D(&'cr mut C<'abr>) -> D<'cabr> 
                let d = D(&mut c); 
                // 'cr <= 'c & 'cabr = 'abr & 'cr >= 'cabr
            }
        }
    }
}

First assignment

// B(&'r mut A) -> B<'ar>   
let mut b = B(&mut a); 
// 'r <= 'a & 'r >= 'ar

Reference to a cannot outlive a, hence 'r <= 'a.

&'r mut A is variant over 'r, so we can pass it into the type constructor of B<'ar> which expects &'ar mut A iff 'r >= 'ar.

Second assignment

 // C(&'br mut B<'ar>) -> C<'abr>  
 let mut c = C(&mut b); 
 // 'br <= 'b & 'abr = 'ar & 'br >= 'abr

Reference cannot outlive b ('br <= 'b), &mut B is invariant over B ('abr = 'ar), &'br mut B is variant over 'br ('br >= 'abr)

d's assignment is analogous to c.

Rust doesn't seem to consider lifetimes it hasn't encountered yet as possible assignments. The possible assignments for 'ar thus are 'a or 'b, the ones for 'abr are 'a, 'b, or 'c and so on.

This set of constraints boils down to 'ar = 'abr = 'cabr and the smallest allowed assignment for 'ar is 'b. Therefore the types of b, c, and d are B<'b>, C<'b>, D<'b>. That is, the variable d holds a reference to c for the lifetime 'b, but c is dropped at the end of the 'c lifetime.

If we remove d, then c still keeps b borrowed to the end of the lifetime 'b, but it isn't a problem because b doesn't outlive the lifetime 'b.

This description is still simplified. For example, while the type of c is C<'b>, c doesn't borrow b for the entire lifetime 'b, it borrows it for a part of 'b starting after definition of c, but it is something I don't have clear understanding yet.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
red75prime
  • 3,733
  • 1
  • 16
  • 22
2

The init_*() functions in your original code always return a type with a lifetime parameter equal to the lifetime of the reference you passed in. Since you build a chain of references this way, all your lifetimes will end up the same, and the types of a, b, c, d will end up being A, B<'a>, C<'a>, D<'a>. This is fine up until c, since the lifetime 'a can be the scope of b, which satisfies all constraints.

However, once you add d to the mix, there is no single lifetime 'a that would make all references valid. The lifetime 'a can't be the scope of b anymore, since c does not live long enough. Neither can it be the scope of c, since this is too short for b, so the compiler errors out.

By decoupling the lifetimes, it's possible for all variables to have their own lifetime and everything works as expected. Since the problem only starts with D, it's enough do introduce an additional lifetime at that point.

struct A;

fn init_a() -> A {
    A {}
}

struct B<'a> {
    ed: &'a mut A,
}

fn init_b(ed: &mut A) -> B {
    B { ed }
}

struct C<'b> {
    pd: &'b mut B<'b>,
}

fn init_c<'b>(pd: &'b mut B<'b>) -> C<'b> {
    C { pd }
}

struct D<'c, 'b: 'c> {
    sd: &'c mut C<'b>,
}

fn init_d<'c, 'b: 'c>(sd: &'c mut C<'b>) -> D<'c, 'b> {
    D { sd }
}

fn main() {
    let mut a = init_a();
    let mut b = init_b(&mut a);
    let mut c = init_c(&mut b);
    let d = init_d(&mut c);
}

Playground link

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • The code can be simplified. [playground](https://play.rust-lang.org/?gist=739b95c2b3e42bd797b54bda14791154&version=stable) `C<'a, 'b>` is variant over `'a` and invariant over `'b`. This allows to write `sd: &'a mut C<'b, 'b>`. – red75prime Jan 09 '18 at 06:29
  • 1
    Another way to make the original code compile: [playground](https://play.rust-lang.org/?gist=59218f6657f4b7b9db53b2d404926167&version=stable) `let (mut b, mut c, d);` makes lifetimes of `b`, `c`, and `d` exactly the same. – red75prime Jan 09 '18 at 06:42
  • @red75prime you should go ahead and make that an answer (along with the explanation of why it works) – Shepmaster Jan 09 '18 at 15:59
  • I understand that I can jiggle the code around to make it compile: for example I can also,in my real use case, stop using references at all and transfer ownership of `a` to `b`, and then of `b` to `c` and so on - so that everything goes out of scope all at once with `d`. What I was really wondering is why this compiles with `A`, `B`, and `C`, but not when I add in `D`. – Ben Clifford Jan 09 '18 at 16:51
  • 1
    @Shepmaster, I've a draft of the answer, but I need to make it clearer for everyone, me included. There's nontrivial interplay of lifetimes. – red75prime Jan 09 '18 at 17:02
  • @red75prime indeed. Perhaps [some discussion between nikomatsakis and pnkfelix](https://gitter.im/rust-impl-period/WG-compiler-nll?at=5a54ddf219147ac3231b60dd) would help? – Shepmaster Jan 09 '18 at 17:06
  • @BenClifford The function calls are making all reference lifetimes equal in your code, so you end up with the types `A`, `B<'a>`, `C<'a>`, `D<'a>`. This is fine up until `c`, since the lifetime `'a` can be the scope of `b`. However, once you add `d` to the mix, there is no single lifetime `'a` that would make all references valid, so you have to use more than one. I've gone a bit overboard by simply giving everything a separate lifetime – it's possible to make to with less, but I'm too lazy to work out the details. – Sven Marnach Jan 09 '18 at 17:22
  • @red75prime I've updated the answer with simpler code as you suggested and also added more detail to the explanation I gave. – Sven Marnach Jan 09 '18 at 21:18