Why does the order of generation of loan cause the error?

Consider this example:

struct S<'a>{
	a:&'a u8
}
fn show<'a>(this:& mut S<'a>){
	(& *(*this).a,& mut *this);  // #1
	(& mut *this,& *(*this).a);  // #2
}

In this example #1 is OK but #2 causes an error. This is a bit weird. The only difference between #1 and #2 is the order of producing borrowing. I think it may be subtle in 2094-nll - The Rust RFC Book. However, I don't know if my understanding is right, here.

For case #1, the borrowing & *(*this).a produce a loan ('a, shared, *(*this).a), and consider the borrowing & mut *this produces an action that is a deep write to *this, the former loan is not a relevant borrowing.

For case #2, the borrowing & mut *this that would be an action at #1 instead becomes the loan ('a, mut, *this), whereas the borrowing & *(*this).a that would be the loan at #1 instead becomes the action at #2, the action is a deep read to *(*this).a, and *this is a prefix of *(*this).a according to

For deep accesses to the path lvalue, we consider borrows relevant if they meet one of the following criteria:

  • [...]
  • there is a loan for some prefix of the path lvalue;

Hence, the second case has an error. Is this the correct interpretation of the example? The borrowing checker is sensitive to the order of loans?

It actually doesn't matter if you copy or reborrow the shared one. This is the same:

fn show<'a>(this: &mut S<'a>) {
    (this.a, &mut *this); // #1
    (&mut *this, this.a); // #2
}

For both copying and reborrowing, you need read access, and you don't have that while a mut reference exists. Once the &u8 is copied or reborrowed, it is disconnected from this, so getting a mut reference after is allowed. And I guess tuples make all their members exist at once (i.e. there is no NLL within a single tuple).

Let's simplify things a bit.

struct T { i: i32 }

fn example(this: &mut T) {
    let x = this.i;
    let y = &mut *this;
    (x, y);
    
    let x = &mut *this;
    let y = this.i;
    (x, y);
}

The first tuple is ok. We copied out a value from *this, and then we took an exclusive reborrow of *this. Then we used the copied thing, and the exclusive reborrow. No problem.

The second tuple is not ok. We took an exclusive reborrow of *this, and copied out a value from *this (but not through the exclusive reborrow), and then tried to use the exclusive reborrow (and the copied thing). So the exclusive reborrow must be active while we tried to copy out of *this, and that's a borrow error.

Do you find this weird?

The reborrowing of shared references has similar logic, because shared references implement Copy.

1 Like

Your simplified example is a more intuitive interpretation. If we don't consider whether a type is Copy or something else, we just start with whether borrowing and action conflicts, is my understanding right?

Moreover, the control flow seems to follow the specified order of initialization? Is it true for struct?

struct T { i: i32 }
struct A<'a>{a:&'a mut T,b:i32}
struct RA<'a>{b:i32, a:&'a mut T}
fn example(this: &mut T) {
    let _ = RA{b:this.i,a:& mut *this};  //Ok
    
    let _ = A{b:this.i,a:& mut *this,};  //Ok

    let _ = A{a:& mut *this,b:this.i};  // Error
}

The same is true for the initialization of function arguments

struct T { i: i32 }
fn example(this: &mut T) {
    order_arg(& mut *this,this.i);  // Error
    order_arg2(this.i, & mut *this); // Ok
}

fn order_arg<'a>(a:&'a mut T,b:i32){}

fn order_arg2<'a>(b:i32, a:&'a mut T){}

For function arguments it's mostly true, except there's some special handling for methods:

let mut v = vec![1, 2];
v.remove(v.len() - 1); // works
Vec::remove(&mut v, v.len() - 1); // doesn't work

But I don't think you can actually observe this. It's just for convenience. Everything else inside functions happens in the order you write it.

1 Like

The example you mentioned here is about Two-phase-borrowing. I think this is irrelevant here. I asked how the control flow that determines an action is associated with the corresponding initialization here.

Borrow checking does treat shared references differently though.

The supporting prefixes for an lvalue are formed by stripping away fields and derefs, except that we stop when we reach the deref of a shared reference. Inituitively, shared references are different because they are Copy – and hence one could always copy the shared reference into a temporary and get an equivalent path.

And that definition is used when seeing if a deep access conflicts.

You're right that the order of borrows matters. (The particulars in your OP aren't exactly right (the loans don't have to be for 'a if nothing else), but I haven't gone through the time to walk through everything.)

It's generally left-to-right, top to bottom. There are some weird or suprising corner cases though. I'd call two-phrase borrows one of those corner cases. The note about primitives here is another one.

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.