Nested method calls with existing mutable references

The following code compiles successfully:

let mut v = vec![1];
let r = &mut v;
r.push(r.len());

while this one fails:

let mut v = vec![1];
let r = &mut v;
r.push(v.len());

with error:

error[E0502]: cannot borrow `v` as immutable because it is also borrowed as mutable
    |
    |     let r = &mut v;
    |             ------ mutable borrow occurs here
    |     r.push(v.len());
    |            ^ immutable borrow occurs here
    |     r.push(r.len());
    |     - mutable borrow later used here

I suspect that in the first example there are no errors because the compiler does not create intermediate references and it uses the same reference: r so that there are not multiple borrows.

However, if that is the case, why the following code fails to compile?

let mut v = vec![1];
let r = &mut v;
r.push({r.push(0);1});

The only explanation I see is that in the first example a new borrow does not occur because I am immutably borrowing through the same borrow (1st example).
Conversely, a new borrow occurs anyway, when I am mutably borrowing through the same borrow. (3rd example).

Since I did not find a similar rule documented anywhere, I would like to ask you if it is a correct assumption and why/relevant references.

Before asking here I posted the same question on StackOverflow: rust - Nested method calls with existing mutable references - Stack Overflow
Thanks!

1 Like

The borrow checker can't prove correctness for all possible valid code. It has some limitations, but it's constantly evolving. In older versions of Rust, it considered values to be borrowed for their entire scope in these cases; a not-so-recent improvement called "non-lexical lifetimes" lifted this restriction and made borrowck smarter. In yet another update called "Polonius", it will be even smarter. Until then, you'll sometimes need to rewrite these patterns in a way that it can prove correctness.

1 Like

Your first example compiles successfully because of RFC 2025: Nested Method Calls, which added a special case for that exact pattern.

The RFC text explains why such code would normally fail to compile. Its alternatives section explains why the RFC doesn't cover all similar patterns, like your other examples.

3 Likes

Thanks, really helpfull!

I knew about the RFC 2025 but I was not sure it applied also to references explicitly declared.
I found the alternatives in the RFC and the reason why my third example fails can be reformulated as:

Under the existing proposal, this is illegal, because v is considered "reserved" when the outer call to push is first parsed, and an additional &mut2 borrow due to the inner call to push is not permitted when the value being borrowed has been reserved.

However, I still don't understand why the second example fails since the inner push does not create a &mut2 reference but a shared reference instead: &. Could tell me which alternatives it corresponds?

In all the examples of the RFC, the mutable reference is never explicitly created but it is implicitly created by the method calls.

Can you confirm that the 2 phase borrows apply even if the reference is explicitly instantiated as a mutable reference?
Thanks!

No, I believe it only happens when a borrow is implicitly created by a call to an &mut self method. In the words of the RFC:

Two-phase borrows would be used in the specific case of desugaring a call to an &mut self method. Currently, in the initially generated MIR, calls to such methods always have a "auto-mut-ref" inserted (this is because vec.push() , where vec: &mut Vec<i32> , is considered a borrow of vec , not a move). This "auto-mut-ref" will be changed from an &mut to an &mut2 .

1 Like

Ok, thanks but this means my question is still open !

In the second example:

let mut v = vec![1];
let r = &mut v;
r.push(v.len());

On line 2, v is exclusively borrowed by r. This doesn't use a 2-phase borrow, because it's not borrowed by the desugaring of a method call, so RFC 2025 has no effect here.

Because it is exclusively borrowed, v cannot be accessed while r is live. In line 3, r is still in use, so v is still exclusively borrowed, so v.len() is not allowed.

2 Likes

perfect it makes sense. In all the three examples the 2-phase borrows do not apply.
However, for the same reason why the first example compiles successfully (re-use of the same borrow), I don't understand why the third example fails.
In the third example it looks like the nested push_back does not reuse the same borrow, but it creates a new one. But I don' t understand which rule the compiler is applying in this case

Two-phase borrows only work if the “outer” call uses an exclusive (&mut self) borrow and the “inner” call uses a shared/immutable (&self) borrow. In the third example, both the outer and inner calls use &mut self borrows:

r.push({r.push(0);1});

ok clear the two phase borrows does not apply.

in the first example, the code compiles because both the inner and the outer calls, use the same reference: r. Otherwise, since the 2-phase borrows do not apply, we would have a mutable and immutable reference at the same time. right ?

Why in the third example, instead, the same reference: r is not used both in the outer and in the inner calls ?

From my understanding, for the compiler the first example is equivalent to:

let mut v = vec![1];
let r = &mut v;
let l = r.len();
r.push(l);

Why the third example is not equivalent to this:

let mut v = vec![1];
let r = &mut v;
let b = {r.push(0);1};
r.push(b);

Arguments (including the self argument) are evaluated from left-to-right before being passed to a function. And methods that take &mut self always borrow from their first argument rather than consume it, even if it is already a reference.

So in the third example, this line:

r.push({r.push(0);1});

de-sugars to something like this:

let arg0 = &mut *r;
let arg1 = { r.push(0); 1 }
Vec::push(arg0, arg1);

Since r is exclusively borrowed in the first line, it can't be used in the second line.

1 Like

Ok thanks I truly appreciate your time, but how is it de-sugared the first example?

let arg0 = &mut *r;
let arg1 = r.len()
Vec::push(arg0, arg1); // same error of the third example

If a new reference is always created, also the first example should not compile because we would have a mutable and immutable borrow at the same time. shouldn't it?

The first example uses two-phase borrowing, so using the &mut2 notation from the RFC, it de-sugars to:

let arg0 = &mut2 *r; // `r` is reserved
let arg1 = r.len(); // okay while `r` is reserved
Vec::push(arg0, arg1); // reservation is upgraded to a borrow here
  • In the first example, why the 2-phase borrowing applies even with the explicit reference: r ?

If that is the case why the 2-phase borrowing does not apply to the second example ? Something like that:

let arg0 = &mut2 *r; // `r` is reserved
let arg1 = v.len(); // okay while `r` is reserved
Vec::push(arg0, arg1); // reservation is upgraded to a borrow here

In the second example, r is reserved, but v has already been exclusively borrowed on the previous line. So at that point, r can still be accessed but v cannot.

1 Like