Skip to content

Instantly share code, notes, and snippets.

@matthewjasper
Last active March 10, 2019 14:46
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save matthewjasper/1e48c300e96aaf69e9f56a2d9f969c87 to your computer and use it in GitHub Desktop.
Two phase borrow notes

Two-Phase Borrows

This is a write-up of the current state of two-phase borrows, and some potential future changes.

Current state

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:

// In the source code
// Case 1:
let mut v = Vec::new();
v.push(v.len());
let r = &mut Vec::new();
r.push(r.len());
// Case 2:
std::mem::replace(r, vec![1, r.len()]);
// Case 3:
let mut x = std::num::Wrapping(2);
x += x;

// Partially lowered 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);

Note that each two-phase borrow is assigned to a temporary that is used only twice:

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

We borrow check two-phase borrows as follows:

  1. There is only one lifetime associated to the borrow. It is required to outlive the lifetime in the created reference.
  2. At the reservation point we only error if there are conflicting live mutable borrows.
  3. Between the reservation and the activation point, the two-phase borrow acts as a shared borrow.
  4. At the activation point we error if there are any conflicting live borrows.
  5. After the activation point, the two-phase borrow acts as a mutable borrow.

Interesting cases

No activation

If the activation point is unreachable, then the lifetime of the two phase borrow ends immediately.

Additional adjustments

Consider:

use std::{
    num::Wrapping,
    ops::AddAssign,
};

let mut x = std::num::Wrapping(2);
let y = &mut x;
<Wrapping<usize> as AddAssign>::add_assign(y, *y);
<dyn AddAssign<Wrapping<usize>>>::add_assign(y, *y);

The first call will successfully borrow-check but the second call will not. This is not because of the lack of a two-phase borrow, but because there is an unsizing coercion for the second call. This results in MIR like:

temp1 = &mut *y                         // Two phase borrow reservation
temp2 = move temp1 as &mut dyn Trait    // Activation

Even though the borrow is two-phase, the borrow gets activated before it's possible to observe that it was a two-phase borrow. This is slightly surprising, because there is not visible difference from the users point of view.

Matches

There used to be some two-phase borrows involved in matches. They were removed by rust-lang#57609.

Alternative approaches for two-phase borrows

These are some alternatives that I could think of for implementing two-phase borrows. With some brief notes on potential problems.

Delay the mutable borrow

One option might be to evaluate the mutable borrow later: so EXPR1.m(EXPR2) would

  1. Evaluate EXPR1 to a place, P
  2. Shared borrow P
  3. Evaluate EXPR2 as an operand
  4. Fake read the shared borrow
  5. Mutably borrow P
  6. Call m

This would avoid the need for a two-phase borrow, but it would give incorrect results if P was *x, say, and EXPR2 assigned to x (unlikely, and forbidden in AST borrowck, but allowed with NLL).

Make reservations conflict with shared borrows

This weakens two-phase borrows, effectively making them "three-phase". This is easier to model with stacked borrows. Unfortunately, this results in 3 errors when building the compiler, which hasn't had 2PB active for very long.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment