Why the variable is still borrowed when all the borrows are out of scope?

Hello.

I was implementing a trie, and encountered a common borrow checker error "cannot borrow *a as mutable more than once at a time".
However, all the references were out of scope when another borrow occurred.

Here is a minimal repruducible example:

fn main() {

    let mut a = &mut 1;

    {
        if let Some(next_node) = Some(&mut *a) {
            a = next_node;
            return;
        }
    }
    
    let _ = &mut *a;
}

The code fails to compile:

error[E0499]: cannot borrow `*a` as mutable more than once at a time
  --> src/main.rs:12:13
   |
6  |         if let Some(next_node) = Some(&mut *a) {
   |                                       ------- first mutable borrow occurs here
...
12 |     let _ = &mut *a;
   |             ^^^^^^^
   |             |
   |             second mutable borrow occurs here
   |             first borrow later used here

Commenting out the a = next_node line prevents the error.
Also, directly assigning the &mut *a to a, without putting it in Some, also seems to prevent the error.
The following example compiles successfully:

fn main() {
    let mut a = &mut 1;

    {
        a = &mut *a;
        return;
    }

    let _ = &mut *a;
}

As far as I can tell, all the references to the variable a should be gone when in the outer scope, are they not?
Why the error happens?
And why the second example works?

Thanks!

I am not an expert, but I will try to explain.

First, let's say the type of a is &'a i32 and the type of next_node is &'b i32.

Now before we work out the relation between 'a and 'b let's see what is happening inside your if let block.

There you have two mutable references (namely a and next_node) to the same object. To make that work, the compiler has to make sure that a cannot be used whenever next_node may still be around. However It does not matter whether the variable next_node is in scope, but its value (the reference) is important. You could assign it to other variables, use it as argument of function, have it returned from a function, etc. the compiler needs to track what happens to the value.

To do that tracking the compiler (and the language) uses lifetimes. It does not do exact tracking, (such a thing would be undecidable) it's just an estimation of what may happen.

What it all means is that the use of 'a' is disabled (it is considered re-borrowed) for the full duration of lifetime 'b (even if the next_node is out of scope, its original value may still be in another variable or somewhere).

So what is 'b? Since next_node is a re-borrow of a, 'b must be not longer than 'a. But also because of the assignment a = next_node, 'b must be not shorter than 'a. So we have 'a == 'b[1] which means that variable a cannot be used for all of its existence.

Removal of the assignment allows 'b' to be shorter than 'a, which allows you to use a when the next_node is out of scope.

In your second example the re-borrow is a temporary. Tracking of temporaries is much easier as they are fully managed by the compiler and you cannot do with them whatever you want. Alternatively we can say that there is always only one mutable reference (namely a), no others around.

Hope it makes sense.


  1. Maybe the assignment alone is sufficient force the equality, I am not sure if there is an implicit re-borrow in assignments ↩︎

4 Likes

You should restructure your code to avoid it...

1 Like

The scopes of references rarely matter. In particular you can have a reference with a lifetime longer than the scope it's created in, no problem. You do this every time you have a local &'static (like a literal "str").

I believe the analysis is pretty much as @Tom47 wrote out. I will note that you can use a inside the if let, because the compiler understands it holds the reborrow from the Some(&mut *a). But beyond that block, the assignment may not have happened.

Compared to the example that works, the non-working version is more like:

    let mut a = &mut 1;

    // Reborrow "forever" unconditionally
    let b = &mut *a;
    if true {
        // assign back to `a` conditionally
        a = b;
        *a = 0; // usable here
    }

    // error here (`a` unusable at this point)
    let _ = &mut *a;

You need the borrow checker to have enough flow awareness to let the reborrow of *a have different lifetimes depending on which branch is taken, or something like that. I believe the next generation borrow checker (Polonius) will accept it.

(In the compiling version, the assignment is unconditional.)


That is a different limitation around returning borrows, which the OP is not doing (and the error remains when the return is removed).

It does.

1 Like