I’m trying to understand a lifetime behavior in Rust and would appreciate any insight:
fn main() {
let outer = 1;
let mut r = &outer;
{
let inner = 2;
r = &inner;
let r_inner = r;
r = &outer;
println!("{}", r_inner);
}
println!("{}", r);
}
The compiler complains that &inner does not live long enough. This confuses me because r_inner is only used inside the inner scope, so &inner seems like it should be valid.
Interestingly, if I swap these two lines:
r = &outer;
println!("{}", r_inner);
the code compiles successfully.
Why does simply reordering these lines affect the lifetime analysis? How does the borrow checker determine the lifetime of &inner when reassigning references?
That's because of the Non-Lexical Lifetime feature of the current borrow checker, if reassigning to r happens before you read it, the borrow checker will know that the previous value won't be used somewhere else, but if you read it before reassigning to it, the current borrow checker won't be sure about that, but the next-gen borrow checker polonius is control-flow sensitive so it can get the same conclusion.
Is it correct to understand that the lifetime in type of r is kind of extended by r_inner?
But how to explain the swap between r = &outer; and println("{}", r_inner);?
let r: &'r T = ...
let r_inner: &'r_inner T = r;
...
Then subsequent uses of r_inner keep 'r_inner alive, which keeps 'r alive. We write this as 'r: 'r_inner.
But that's not the complete story: further analysis may cause the lifetimes to be dead, creating "gaps" in the lifetimes which borrows do not propagate across. And reference overwriting in particular is a special case in the analysis. If the analysis is deep enough, relationships like 'r: 'r_inner can effectively be more limited in scope.
(My comments in the playgrounds were based on my understanding of the NLL analysis. Polonius has a deeper analysis and accepts both versions of the code. I might or might not try to explain that later.)
For the example with NLL it mostly comes down to liveness. This video has an introduction to how its used in Rust which may help. Basically, once you overwrite r with &outer, there are no borrows that can read inner anymore. NLL understands this when you perform the overwrite before the 'r: 'r_inner relationship forms, because 'r has a gap in it. But if you perform the overwrite after that relationship forms, it doesn't, because the approximation it uses (liveness of the 'r lifetime) isn't accurate enough.
Ah! This video helps a lot. I wasn't aware of the continuity of lifetimes, and it implies that the lifetime in type of r (when assigned to r_inner) must cover the lifetime in type of r_inner, which is the following 2 lines, and it extends until the last use of r.