What is the diffrence between such two example for borrowing checker?

An example sourced from Polonius update | Inside Rust Blog

struct Thing;

impl Thing {
    fn maybe_next(&mut self) -> Option<&mut Self> { None }
}

fn main() {
    let mut temp = &mut Thing;

    loop {
        match temp.maybe_next() {
            Some(v) => { temp = v; }
            None => { }
        }
    }
}

The borrowing checker reports an error

error[E0499]: cannot borrow `*temp` as mutable more than once at a time

I tried to simplify above example for researching

struct Thing;
impl Thing {
    fn next(&mut self) -> &mut Self {
        self
    }
}
fn main() {
    let mut temp = &mut Thing;
    loop {
        let r = temp.next();
        temp = r;
    }
}

This code is compiled. What's the difference here? By using NIL to analysis the first example

fn main() {
    let mut temp = &/* 'tmp */ mut Thing;

    loop {
       /* &'ln mut (*temp) */ 
      /* loan ('ln, mut, *temp) */
        match temp.maybe_next() {
            Some(v) => { temp = v; }
            None => { }
        }
    }
}

Assuming the control flow repeatedly steps into the Some branch several times. According to reborrowing constraints: 'tmp :'l1 :'l2 ... Since v is assigned to v(i.e. 'l1:'tmp, 'l2:'l1, ...), 'tmp == 'l1 == 'l2 .... However, because of the assignment to temp, the following rules applies to this example:

For a statement at point P in the graph, we define the “transfer function” – that is, which loans it brings into or out of scope – as follows:

  • [...]
  • if this is an assignment lv = <rvalue>, then any loan for some path P of which lv is a prefix is killed.

So, any former in 'l1 == 'l2 ... is killed after temp = v as if it were to look like:

let mut temp = &'tmp mut Thing;
let v = &'l1 mut *temp; // ('l1, mut, *temp)  #1
temp = v; // kill ('l1, mut, *temp)

//#1 is not a relevant borrowing here
let v = &'l2 mut *temp; // ('l2, mut, *temp) 
temp = v; // kill ('l2, mut, *temp)

// ...

This is why I thought the first example is equivalent to the second one, however, the second example is compiled. Is there something I misunderstood?

Oh, Seems I see the problem, the Some branch make the borrowing borrowed for lifetime 'tmp, if the control flow repeatedly steps into None, the loan is not killed, such that the borrowing is equivalent to

&'l1 mut *temp;  // ('l1, mut, *temp) #1
// next loop
&'l2 mut *temp;  // #1 is relevant here

'l1 == 'l2 == 'tmp, such two borrowings conflicting.

The way I think of it[1] is that on the None path, 'ln can be arbitrarily short, but on the Some path, 'ln must be 'tmp. And that would be fine (sound) because temp gets overwritten on the Some path. But the current borrow checker can't handle a single lifetime that "differs in duration" based on some conditional, so the 'ln is always 'tmp, even when temp is not overwritten, resulting in the error.

I'm not sure this way of thinking about it covers everything though. It doesn't cover everything discussed in 47680, but many early examples stated to work in that issue do not. I presume because they were from before all location sensitive lifetimes were removed from NLL. (Probably I won't put in the effort to understand those example better though, since it's probably only of historical interest; I'd rather put time into how Polonius sovles it or fails to solve some cases.)


  1. without deep-diving by "running NLL on paper" anyway ↩︎

1 Like

A bit question about this snippet code in NIL RFC

fn access_legal(lvalue, is_shallow, is_read) {
    let relevant_borrows = select_relevant_borrows(lvalue, is_shallow);

    for borrow in relevant_borrows {
        // shared borrows like `&x` still permit reads from `x` (but not writes)
        if is_read && borrow.is_read { continue; }
        
        // otherwise, report an error, because we have an access
        // that conflicts with an in-scope borrow
        report_error();
    }
}

Where is the detail about whether borrow.is_read is true? I seem not to find a relevant description of this. Or, it is a consensus that mutable and unique borrowing is considered is_write and shared borrowing is is_read, which is not necessary to document?

I've always assumed that's what it meant.

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.