Simple borrow error

I find it a little surprising this does not compile:

fn f(_v: &mut Vec<i32>, _n: usize) {}

fn main() {
    let mut v = vec![1, 2, 3];
    f(&mut v, v.len());
}

Error is "error[E0502]: cannot borrow v as immutable because it is also borrowed as mutable".

( Rust Playground )

My reasoning is that although v is borrowed mutable "earlier", the mutable reference cannot be "used" until the function is called, the mutable borrow is not yet "fully alive" when v.len() is called.

It is interesting the if the order of the two parameters is switched around, the program then compiles ( see Rust Playground ).

This is the actual problem. Your mental model does not match what is happening here. The mutable reference IS live from the moment you declare it, not from the moment you use it.

RFC 2025 (Nested Method Calls) added a special case to the borrow checker so this does work as long as both functions use method call syntax:

trait MyTrait {
    fn f(&mut self, _n: usize) {}
}

impl MyTrait for Vec<i32> {}

fn main() {
    let mut v = vec![1, 2, 3];
    v.f(v.len());
}

Similar rules could be added to make your example work too, but this was not done for reasons discussed in the Alternatives section.

2 Likes

Well the mutable reference "exists" but it is not yet accessible. It appears that at least in this simple case the program is "ok", and the compiler could allow it to compile without there being a data race ( as I see it, perhaps someone can explain why this isn't true ).

As far as I understand the problem is that v.len() is syntactic sugar for Vec::len(v). That is calling a method whose signature is Vec::len(&v). Please excuse any syntactic sloppiness there.

So when you do:

f(&mut v, v.len());

v is disappearing into f as a mutable borrow, and is then expected to be passed in as an immutable borrow at the same time. Which violates the borrowing rules.

You can reduce your example to this.

There's a balance to be struct between ergonomics and the ability to reason about the program -- especially when things like unsafe get involved, where the borrow checker won't save you (e.g. where the programmer needs rely on &mut's exclusivity promises for soundness).

The way things work now, the exclusivity promise kicks in when you create the &mut... for this example.


The two-phase borrow RFC linked above was an attempt to aid ergonomics by adding some complexity in specific situations. The intent was for the applicability to be limited and well defined so as to not impair the ability to reason too much, and also for the backwards compatibility reasons discussed in the Alternatives section.

How successful it was can be debated. The RFC says it should only work when the borrow is in argument position to a method call:

let mut v = vec![0];
v.push(v.len());

But the implementation slipped, so for example this also works today:

let mut v = vec![0];
let shared = &v;
v.push(shared.len());

Moreover, what we're left with today has no specification and there's even resistance to providing one right now.

So ergonomics may have improved, but complexity has increased in undocumented ways with no blessed way to reason about exactly when exclusivity kicks in[1] (a bad place to be, IMO). Hopefully the situation eventually improves with the specification RFC/initiative.

(It would be a breaking change to dial two-phased borrows back to the originally intended scope.)


  1. e.g. syntactically ↩︎

3 Likes