Skip to content

← Ownership II: Borrowing and the Borrow Checker step 18 of 24

Hard Primitives

Two-phase borrows: why v.push(v.len()) compiles

pub fn build_ledger(n: i64) -> Vec<i64>

Build a vector by these four steps, exactly:

  1. Start empty.
  2. n times (zero times if n <= 0): push the vector’s current length.
  3. Push the current length once more.
  4. Set the last element to the current length.
n = 0  -> [1]
n = 1  -> [0,2]
n = 2  -> [0,1,3]
n = 5  -> [0,1,2,3,4,6]
n = -3 -> [1]

(Check n = 2 by hand: step 2 gives [0,1], step 3 gives [0,1,2], step 4 overwrites the last element with the length, 3, giving [0,1,3].)

The starter implements exactly those steps and gets two E0502s. One of the three lines in it compiles fine. Working out why is the whole item.

The observation that destroys trust

Every learner meets this eventually:

v.push(v.len());

By the rule you were taught, that cannot work. push needs &mut v for the whole call. v.len() needs &v. Shared and exclusive at the same time is precisely what item 3.3 forbids.

It compiles. And when it does, the natural conclusion is that the rules you were taught are approximately true at best — which is much worse than not knowing, because now you cannot predict anything. Naming the mechanism restores the model and explains why the boundary is where it is.

Two-phase borrows

RFC 2025. The mutable borrow created by an autoref in a method call is not born exclusive. It starts as a reservation, which behaves like a shared borrow, and only activates into a full exclusive borrow at the moment of the call. The rustc dev guide desugars it like this:

let tmp = &two_phase v;   // reservation: acts like a shared borrow
let n = v.len();          // legal — shared reads are fine during a reservation
Vec::push(tmp, n);        // activation: NOW it is exclusive

During the reservation phase, other reads of v are permitted; other writes are not. At activation, the borrow becomes fully exclusive as usual. So the argument expression may read the receiver, which is the entire point.

This exists because v.push(v.len()) is obviously fine to a human, was rejected under early NLL, and generated an enormous amount of complaint. It is a targeted ergonomic patch, not a general weakening of the rule.

Exactly three sites get it

This is the part to memorise, because it is what makes the behaviour predictable:

  1. the autoref for &mut self in a method call — v.push(...);
  2. a mutable reborrow in a function argument position;
  3. the implicit borrow in an overloaded compound assignment.

Nothing else. In particular:

  • An &mut you write out in the source is never two-phase. The starter’s Vec::push(&mut v, v.len() as i64) is the same call as v.push(...), desugared by hand — and it is E0502, because you wrote the &mut yourself and the compiler takes it at face value. That is a genuinely startling demonstration: method-call sugar is not merely sugar here.
  • Index/IndexMut does not benefit. v[v.len() - 1] = 7; is E0502: the index expression v.len() - 1 is evaluated as part of the place, while the IndexMut borrow is being taken, and there is no reservation phase to shelter it. Hoist it into a local and it is fine.

Note carefully what is not on that list of failures: a plain v[0] = v.len() as i64; does compile — not because of two-phase borrows, but because for an assignment expression Rust evaluates the right-hand side before the left-hand place, so the two never overlap. Two things that look alike, working for two different reasons. This is why the item exists.

Treat it as an explanation, not a rule to lean on

Be careful with how much weight you put on this. The precise boundary is implementation-defined, it has shifted, and the diagnostics around it are poor (rust#77826 is the standing complaint). Two-phase borrows are the right answer to “why did that compile?” and a bad basis for “so I can write this.”

When a line surprises you, hoist the sub-expression into a local. It always works, it never depends on a subtle rule, and it is usually clearer:

let last = v.len() - 1;
v[last] = v.len() as i64;

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…