Why does conditional assignment of a matched mutable reference cause borrow errors?

Viewed 410

I'm having trouble understanding why this causes an error:

#[derive(Debug)]
pub struct Node {
    next: Option<Box<Node>>,
}

pub fn print_root_or_next(root: &mut Node, try_next: bool) {
    let mut current = root;
    match &mut current.next {
        Some(node) => {
            if try_next {
                current = &mut *node;
            }
        }
        None => return,
    }

    println!("{:?}", current);
}
error[E0502]: cannot borrow `current` as immutable because it is also borrowed as mutable
  --> src/lib.rs:17:22
   |
8  |     match &mut current.next {
   |           ----------------- mutable borrow occurs here
...
17 |     println!("{:?}", current);
   |                      ^^^^^^^
   |                      |
   |                      immutable borrow occurs here
   |                      mutable borrow later used here

I can't see how there are conflicting borrows; even the error message seems to indicate that two borrows are one and the same. Is this an issue in with the borrow checker or is this example actually flawed in some way?

I am very interested in this limitation. I don't know much about the implementation, but considering that a basic if:

if try_next {
    current = &mut *current.next.as_mut().unwrap();
}

and basic match:

match &mut current.next {
    Some(node) => {
        current = &mut *node;
    }
    None => return,
}

and even inverting them:

if try_next {
    match &mut current.next {
        Some(node) => {
            current = &mut *node;
        }
        None => return,
    }
}

all work, there must be some thing that the borrow checker is or is not considering that conflicts with my understanding of why the original form doesn't work.

1 Answers

I think that the problem is that current is a conditional reborrow of itself.

Consider this simpler code that removes the condition but uses two variables:

#[derive(Debug)]
pub struct Node {
    next: Option<Box<Node>>,
}

pub fn print_root_or_next(root: &mut Node) {
    let a = root;
    let b;
    match &mut a.next {
        Some(node) => {
            b = &mut *node;
        }
        None => return,
    }

    println!("{:?}", a);
    println!("{:?}", b);
}

This fails, as you would expect with the error message:

error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
  --> src/lib.rs:16:22
   |
9  |     match &mut a.next {
   |           ----------- mutable borrow occurs here
...
16 |     println!("{:?}", a);
   |                      ^ immutable borrow occurs here
17 |     println!("{:?}", b);
   |                      - mutable borrow later used here

That is, a is mutably borrowed and that borrow is kept alive because of b. So you cannot use a while b is alive.

If you reorder the last two println! lines, it compiles without issues, because of the lexical lifetime: b is printed and then forgotten, releasing the borrow and making a available again.

Now, look at this other variant, similar to yours but without the if:

pub fn print_root_or_next(root: &mut Node) {
    let mut c = root;
    match &mut c.next {
        Some(node) => {
            c = &mut *node;
        }
        None => return,
    }
    println!("{:?}", c);
}

It compiles fine, too, because when c is reborrowed it is reassigned. From this point on, it works as the b of the previous example. And you could use that b freely, what was forbidden was using a, and that is no more available here.

Back to your code:

pub fn print_root_or_next(root: &mut Node, test: bool) {
    let mut c = root;
    match &mut c.next {
        Some(node) => {
            if test {
                c = &mut *node;
            }
        }
        None => return,
    }
    println!("{:?}", c);
}

The issue here is that when c is reborrowed, it is done conditionally, the compiler does not know which one it will be, like a or like b from the example above. So it must assume that it is both at the same time! But from the example we saw that you cannot use a while b is alive, but since they are both the same value, this value just cannot be used any longer.

When the compiler complains with this weird error message:

17 | , current);
   | ^^^^^^^
   | |
   | immutable borrow occurs here
   | mutable borrow later used here

It actually means:

17 | , current);
   | ^^^^^^^
   | |
   | immutable borrow occurs here if try_next is false
   | mutable borrow at the same time used here if try_next is true

I don't know if this is a limitation of the compiler and using this reference that conditionally reborrows itself is actually safe. Maybe it is a limitation of the borrow checked, or maybe there is some subtlety that I don't understand... I suspect that allowing this might be unsound if you include more than one condition or more than one reference.

Related