11

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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • How are the borrows one and the same? You are borrowing `current` mutably with `&mut current.next`, and then borrowing it immutably with `println!` – Ibraheem Ahmed Nov 02 '20 at 03:50
  • Weird, remove the if check (`if try_next`), and the code compiles – Ibraheem Ahmed Nov 02 '20 at 03:51
  • 3
    For the @IbraheemAhmed's comment on that code compiles if you don't reinitialize `current` -- this might be because if you only assign a variable once, compiler can track it in compile time and thus it'll be able to shrink lifetime of the mutable borrow to the end of `match` statement, because this is the last time it's needed to be mutable. On the other hand if you reassign the variable, compiler probably doesn't even try to shrink the lifetime as it's runtime dependent, and it will not compile as you can possibly have mutable borrow of `root` when `println` is being executed. – Alexey S. Larionov Nov 02 '20 at 05:54
  • This question is a duplicate of [Returning a reference from a HashMap or Vec causes a borrow to last beyond the scope it's in?](https://stackoverflow.com/q/38023871/155423); TL;DR the duplicate: the borrow checker doesn't know that the borrow isn't held in the `None` case. – Shepmaster Nov 07 '20 at 03:22
  • 1
    Thanks for the link @Shepmaster! Though I'm still confused; most of the explanations there are around returning references where this is not. I first thought you were saying the `return` somehow *extends* the borrow, however its the same error if the `None` yields `()` instead of returning. And if it "doesn't know that the borrow isn't held in the `None` case", why does it have no issue with a basic match that doesn't include the if? – kmdreko Nov 07 '20 at 04:39
  • If you add an explicit lifetime to the reference (`<'a>(root: &'a mut Node` ... `current: &'a mut Node`) you get this additional message: "type annotation requires that `current.next` is borrowed for `'a`" which seems to imply that rustc thinks the borrow of `current.next` lasts for the whole function for some reason – Tavian Barnes Nov 12 '20 at 15:46

1 Answers1

5

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.

rodrigo
  • 94,151
  • 12
  • 143
  • 190
  • 3
    For the record the original code compiles fine with `-Zpolonius`, so I suspect it is sound to allow this. It's similar to "[case #3](https://rust-lang.github.io/rfcs/2094-nll.html#problem-case-3-conditional-control-flow-across-functions)" that was originally supposed to be fixed by NLL but wasn't for compiler performance reasons. – Tavian Barnes Nov 12 '20 at 20:24