I'm trying to better understand how reborrowing works.
For example, consider the following two examples:
struct S {}
impl S {
fn dup(&mut self) -> &mut S {
return self;
}
}
pub fn main() {
let mut s = S {};
let r1 = &mut s;
let r2 = r1.dup();
*r1 = S {};
*r2 = S {};
}
fails to check with:
|
12 | let r2 = r1.dup();
| -- `*r1` is borrowed here
13 | *r1 = S {};
| ^^^^^^^^^^ `*r1` is assigned to here but it was already borrowed
14 | *r2 = S {};
| ---------- borrow later used here
struct S {}
struct C<'a> {
s: &'a mut S
}
impl S {
fn cap(&mut self) -> C {
return C { s: self };
}
}
pub fn main() {
let mut s = S {};
let r1 = &mut s;
let c = r1.cap();
*r1 = S {};
*c.s = S {};
}
fails to check with:
|
16 | let c = r1.cap();
| -- `*r1` is borrowed here
17 | *r1 = S {};
| ^^^^^^^^^^ `*r1` is assigned to here but it was already borrowed
18 | *c.s = S {};
| ----------- borrow later used here
Removing the assignment in the last statement makes the code typecheck.
How does the borrow checker determine that s is still borrowed after the function calls, how does it reject the assignments, which do not explicitly mention s? Is there a relationship between parameters and return values?
Nit: The examples don't have any significant reborrows, just borrows.
Using the returned value keeps the borrow of the input active, So long as r/c are in use, s remains exclusively borrowed. If you unelide the lifetimes, the relationship is more clear:
Ok, now *r1 is reborrowed to make the method call. Beyond that point, every use of s has been replaced by a use of *r1, and the logic is pretty much the same: uses of r2 keep *r1 exclusively borrowed, which conflicts with being overwritten.
The reborrow is of the path to the place (*r1). Use of the reborrow keeps r1's borrow of s active transitively (though that didn't happen to enter into the error in this case).
Incidentally, the closest thing I know of to official documentation of reborrows is the NLL RFC. The portions about reborrow lifetimes is pretty consumable, I think:
Thank you for the explanation and the links to the documentation!
So the borrow checker tracks the origin of what is borrowed (when r1 is passed to the method, it's *r1, i.e. s), and a lifetime constraint is added: the lifetime of the original borrow (r1, let's say 'a) must outlive the lifetime of the reborrow (let's say 'b), 'a: 'b.
When only locals are involved, I can see how the mechanism works. For example, rewriting my first example:
struct S {}
fn main() {
let mut s = S {};
let r1 = &mut s;
// explicit reborrow, could have also used
//
// let r2: &mut _ = r1;
//
// which implicitly reborrows,
// But could have not used
//
// let r2 = r1;
//
// because that's a move.
let r2 = &mut *r1;
*r1 = S {};
*r2 = S {};
}
However, I'm still confused about method calls: How does the borrow checker locally reason (i.e. without having to analyze the function) that r2, the result of the method call, borrows *r1?
fn foo<'a>(x: &'a mut i32) -> &'a mut i32 {
return x
}
fn main() {
let mut x = 1;
let r1 = &mut x;
let r2 = foo(&mut*r1);
// assignments to either r1 or to r2 work, but not both
}
Given that r2 and r1 have the same lifetime (lifetime of the return value of foo is the same as lifetime of parameter via lifetime parameter), does the borrow checker then also assume that r2 borrows/aliases what r1 borrows?
They don't necessarily have the same lifetime. You reborrowed r1 and passed that reborrow to foo, so this 'a is allowed to be (but is not necessarily) shorter than the lifetime in the type of r1.
does the borrow checker then also assume that r2 borrows/aliases what r1 borrows?
The borrow checker sees that, for a particular lifetime 'a belonging to this specific call to foo(),
r2 is of type &'a mut i32, and
the expression &mut *r1 exclusively borrows r1 for some lifetime 'a which must not exceed r1's validity.
So, the borrow checker knows that r1 is borrowed for 'a and therefore is borrowed until all uses of 'a end, i.e. until r2 is dropped or otherwise no longer used in the future.
Regarding aliasing: The borrow checker does not care about aliasing at all. It only checks lifetime rules, and the rest of the language (and libraries written in Rust, including but not limited to the standard library) are designed to ensure that as long as the lifetime rules are upheld, so are the aliasing rules.