Two-phase borrows

Two-phase borrows are a more permissive version of mutable borrows that allow nested method calls such as vec.push(vec.len()). Such borrows first act as shared borrows in a "reservation" phase and can later be "activated" into a full mutable borrow.

Only certain implicit mutable borrows can be two-phase, any &mut or ref mut in the source code is never a two-phase borrow. The cases where we generate a two-phase borrow are:

  1. The autoref borrow when calling a method with a mutable reference receiver.
  2. A mutable reborrow in function arguments.
  3. The implicit mutable borrow in an overloaded compound assignment operator.

To give some examples:

fn main() {
// In the source code

// Case 1:
let mut v = Vec::new();
let r = &mut Vec::new();

// Case 2:
std::mem::replace(r, vec![1, r.len()]);

// Case 3:
let mut x = std::num::Wrapping(2);
x += x;

Expanding these enough to show the two-phase borrows:

// Case 1:
let mut v = Vec::new();
let temp1 = &two_phase v;
let temp2 = v.len();
Vec::push(temp1, temp2);
let r = &mut Vec::new();
let temp3 = &two_phase *r;
let temp4 = r.len();
Vec::push(temp3, temp4);

// Case 2:
let temp5 = &two_phase *r;
let temp6 = vec![1, r.len()];
std::mem::replace(temp5, temp6);

// Case 3:
let mut x = std::num::Wrapping(2);
let temp7 = &two_phase x;
let temp8 = x;
std::ops::AddAssign::add_assign(temp7, temp8);

Whether a borrow can be two-phase is tracked by a flag on the AutoBorrow after type checking, which is then converted to a BorrowKind during MIR construction.

Each two-phase borrow is assigned to a temporary that is only used once. As such we can define:

  • The point where the temporary is assigned to is called the reservation point of the two-phase borrow.
  • The point where the temporary is used, which is effectively always a function call, is called the activation point.

The activation points are found using the GatherBorrows visitor. The BorrowData then holds both the reservation and activation points for the borrow.

Checking two-phase borrows

Two-phase borrows are treated as if they were mutable borrows with the following exceptions:

  1. At every location in the MIR we check if any two-phase borrows are activated at this location. If a live two phase borrow is activated at a location, then we check that there are no borrows that conflict with the two-phase borrow.
  2. At the reservation point we error if there are conflicting live mutable borrows. And lint if there are any conflicting shared borrows.
  3. Between the reservation and the activation point, the two-phase borrow acts as a shared borrow. We determine (in is_active) if we're at such a point by using the Dominators for the MIR graph.
  4. After the activation point, the two-phase borrow acts as a mutable borrow.