Why does the "Two-phase borrows" not work for these cases?

fn main(){
   let mut v = vec![1];
   // v.push(v.len() - 1); // #1
   v.get_mut(v.len() - 1);  // #2
}

#1 is the typical case where "Two-phase borrows" applies. However, #2 will emit an error that

cannot borrow v as immutable because it is also borrowed as mutable

IMO, there is no difference between #1 and #2. #2 can be desugared to

let temp1 = & two_phase v;   // reservation point
let temp2 = v.len() - 1;
Vec::get_mut(temp1,temp2); // activation point

Between reservation point and activation point, & mut v acts as a shared borrow, which does not conflict with the shared borrow in v.len(). What's the wrong here?

Another case is

fn main(){
   let mut v2 = vec![1];
   v2[v2.len() - 1] = 2; // error
   use std::ops::IndexMut;
   *v2.index_mut(v2.len() - 1) = 2;  // ok
}

In this case, v2[v2.len() - 1] is exactly sugar of *v2.index_mut(v2.len() - 1), however, one is right and another is wrong, what's the reason here?

I have no idea how the two-phase borrow is implemented but if it doesn't activate until after auto-Deref it is probably too late. get_mut is a method of slice, not Vec.

EDIT: found this: Tracking issue for generalized two-phase borrows · Issue #49434 · rust-lang/rust · GitHub

1 Like

Do you mean, for the first case, v.get_mut(v.len() - 1) is desugared to v.deref_mut().get_mut(v.len() - 1);, which equals to the following:

let temp1 = & mut v;  // #1
let temp2 = DerefMut::deref_mut(temp1);  // no two-phase borrows
let temp3 =  v.len() - 1;  // #2
[i32]::get_mut(temp2,temp3); 

So, #2 conflicts with #1. No "two-phase borrows" applies to this case, right?

i don't know too much about Rust borrow check, but i think in the first case, the method len() is borrowing a immut ref at the same time get_mut() is borrowing a mut ref.

Yes, that is what I was thinking. I also found this github comment that suggests it is not allowed to reorder temp2 and temp3 to make the two-phase borrow work.

I think it's not about the order between temp2 and temp3.

Two-phase borrow is for the mutable (unique) reference to be temporarily shared (reservation point), and another shared reference coexist, then the mutable reference restores as mutable in the function call (activation point). It means the desugaring in the issue is incorrect. The desugar for two-phase is like

// not specific to OP or the linked issue, just stole from the documentation
let mut v = Vec::new();
let temp1 = &two_phase v; // reservation point: mutable reference is first
let temp2 = v.len(); // shared reference is second
Vec::push(temp1 /* activation point */, temp2);

For OP, I'll minimize the problem as this: Rust Playground

fn main() {
    let mut a = A { b: B { c: 123 } };
    a.mutate(a.share());
}

struct A {
    b: B,
}

impl std::ops::Deref for A {
    type Target = B;
    fn deref(&self) -> &B {
        &self.b
    }
}

impl std::ops::DerefMut for A {
    fn deref_mut(&mut self) -> &mut B {
        &mut self.b
    }
}

impl A {
    // uncomment this to see error:
    // cannot borrow `a` as immutable because it is also borrowed as mutable
    // fn mutate(&mut self, _: ()) {}
}

struct B {
    c: u8,
}

impl B {
    fn mutate(&mut self, _: ()) {}
    fn share(&self) {}
}

And the MIR related:

// success
bb0: {
    _2 = B { c: const 123_u8 };
    _1 = A { b: move _2 };

    _4 = &mut _1; // phase1: reservation point (treated as shared borrow)
    _8 = &_1;
    _7 = <A as Deref>::deref(move _8) -> [return: bb1, unwind: bb4];
}

bb1: {
    _6 = &(*_7); // shared borrow
    _5 = B::share(move _6) -> [return: bb2, unwind: bb4]; // return a value that doesn't conflict with _4
}

bb2: {
    // Each two-phase borrow is assigned to a temporary that is only used once.
    _3 = A::mutate(move _4 /* phase2: activation point (treated as unique borrow) */, move _5) -> [return: bb3, unwind: bb4];
}
// failure
bb0: {
    _2 = B { c: const 123_u8 };
    _1 = A { b: move _2 };

    _6 = &mut _1; // reservation point???
    _5 = <A as DerefMut>::deref_mut(move _6 /* activation point??? */) -> [return: bb1, unwind: bb5];
    // the temporary is used once already (the move consumes the temporary)
}

bb1: {
    _4 = &mut (*_5); // reborrow (treated as unique borrow): no longer being a two-phase borrow, otherwise code would pass
    _10 = &_1; // shared borrow: conflicts with _4 [error]
    _9 = <A as Deref>::deref(move _10) -> [return: bb2, unwind: bb5];
}

bb2: {
    _8 = &(*_9);
    _7 = B::share(move _8) -> [return: bb3, unwind: bb5];
}

bb3: {
    _3 = B::mutate(move _4, move _7) -> [return: bb4, unwind: bb5];
}

Each two-phase borrow is assigned to a temporary that is only used once. As such we can define:
https://rustc-dev-guide.rust-lang.org/borrow_check/two_phase_borrows.html

In the failure case, since _6 is used at _5 = <A as DerefMut>::deref_mut(move _6 /* activation point??? */), and after which the borrow & mut _1 acts as a mutable borrow, then _10 = &_1; conflicts with the mutable borrow, which is the reason of emitting error, right?

I think so. And IIUC as per the comment in the linked issue, it's said to fix the borrow error here, there would be an escape hatch for Deref behavior called project-deref-patterns or DerefPure. (I heard of it the first time.)

The DerefMut cannot be deferred because we guarantee that the receiver is evaluated before the other operands.
What we can do is to generalize two phase borrows (or just defer the deref) when the deref is pure. I know project-deref-patterns are considering introducing a DerefPure trait, so it may be useful here too.

A hypothetical DerefPure is what allows splitting borrows through a Box, for example.

2 Likes

Never believe documentation that claims exact sugar in Rust generally, and particularly around operators. Some cases of exact sugar exist I think, but so do many counterexamples, and the documentation is almost never precise enough to tell when something is actually equivalent.

For example, this documentation claiming equivalence with a method call is in conflict with this documentation claiming equivalence with a path based function call (which then immediately ammends itself with some comment about acting like a method in some imprecise way).


Two-phased borrows were only meant to work with method calls. The implementation slipped, but as far as I know, not that part of it.

However! I haven't done any intensive testing, maybe I'm wrong. Because there is no spec, the answers to any questions such as this topic will likely be based on testing, intentions of the original rfc, and speculation.

That said, your IndexMut examples work and don't work "as expected" relative to being a method call or not, even after removing DerefMut from the picture.

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.