2

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
  • Why the first example compiles correctly? Is it because the same reference: r is used in the outer and inner calls? Or is it because it applies the RFC 2025, Two-Phase Borrows? Or something else?
  • Why the second example fails given that the first example succeeds? Why the RFC 2025, Two-Phase Borrows does not apply 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});
vallentin
  • 23,478
  • 6
  • 59
  • 81
Angelo
  • 334
  • 4
  • 14

2 Answers2

1

In the second example, v is still mutably borrowed when you try to get its length. Vec::len takes &self, so getting its length would mean borrowing immutably while it's already borrowed mutably.

Accessing len() through r is still ok because you are borrowing through the same borrow, not borrowing it again.


Note that even the first example fails in Rust 1.30 and earlier (or 1.35 with 2015 edition), because it relies on NLL (non-lexical lifetimes). The problem with NLL is that it isn't completely intuitive to know what is allowed. Essentially, it means that borrows that don't outlive the lexical scope of the data are ok, and also several other intuitively correct cases. Your third example is one of the cases that is still not permited by NLL.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
  • I posted the same question here: https://users.rust-lang.org/t/nested-method-calls-with-existing-mutable-references/53345/2 – Angelo Dec 28 '20 at 21:22
  • I would like to know why the third example is not permitted by NLL. How does the compiler evaluate the code to consider the first example ok, while the third one not? – Angelo Dec 28 '20 at 23:42
  • That's really my point. Lexical lifetimes can be reasoned about, but NLL just means "allows more" and it's not easy to describe the limits precisely. – Peter Hall Dec 29 '20 at 10:16
  • just a clarification: when you say `NLL`, are you also referring to 2-phase borrows or not? – Angelo Dec 29 '20 at 11:18
  • 1
    The actual implementation of the borrow checker with NLL (whether it's the 2-phase model, stacked borrows or chalk) is hard to think about because it is in terms of MIR, not the code you actually wrote. It may be possible to come up with a good mental model to predict what Rust code will or won't satisfy the current borrow checker implementation, but I do not have it. In general, I have confidence in my understanding of lexical borrows and a few non-lexical cases, based on experience. I couldn't write down the rules, beyond the lexical cases, and I'm not convinced that very many people could. – Peter Hall Dec 29 '20 at 14:21
  • 1
    And I'm not convinced that the effort is even worthwhile: I would also expect more programs to be accepted in the future, either from improvements to the current implementation or from an eventual switch to chalk. – Peter Hall Dec 29 '20 at 14:21
  • Ok thanks, could you have a look at my answer and tell me if it sounds reasonable to you? – Angelo Dec 30 '20 at 10:51
1

1st example:

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

Without the 2-phase borrows the code will not compile because the outer call creates a reborrow of r: &mut *r, and the inner call a new immutable reborrow of the same value: &*r.

With 2-phase borrows, the first reborrow is converted to &mut2 *r and later activated when the second reborrow is out of scope.

2nd example:

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

Even with the 2-phase borrows it does not compile.

The inner call causes a reborrow of v: &mut v that clashes with r.

3rd example:

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

Even with the 2-phase borrows it does not compile.

The inner call requires a &mut2 reborrows of *r that is not allowed by the 2-phase borrows since the outer call already created a &mut2 reborrows of *r.

References:

Angelo
  • 334
  • 4
  • 14