Variable Still Borrowed Outside Scope

Hi all. I'm just learning rust, and experimenting with the borrow checker with the code below. This code doesn't compile. Somehow, the self.table.insert operation fails because it is trying to mutably borrow self.table while the self.table itself is being immutably borrowed by the self.table.get in the if statement. Why does the self.table is still borrowed when it already in the outside of the if scope?

Thank you.

use std::collections::HashMap;

struct BP {
    table: HashMap<u32, u32>,
}

impl BP {
    fn load<'a>(&'a mut self, index: u32) -> &'a u32 {
        if let Some(val) = self.table.get(&index) {
            return val;
        }
        
        let x = 10;
        self.table.insert(index, x);
        self.table.get(&index).unwrap()
    }
}

fn main() {
    println!("Hello, world!");
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0502]: cannot borrow `self.table` as mutable because it is also borrowed as immutable
  --> src/main.rs:14:9
   |
8  |     fn load<'a>(&'a mut self, index: u32) -> &'a u32 {
   |             -- lifetime `'a` defined here
9  |         if let Some(val) = self.table.get(&index) {
   |                            ---------------------- immutable borrow occurs here
10 |             return val;
   |                    --- returning this value requires that `self.table` is borrowed for `'a`
...
14 |         self.table.insert(index, x);
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` due to previous error

I'm having a hard time tracking down the relevant GitHub issue right now, but this is a known issue with the current borrow checker.

Basically when you return a reference, the borrow checker considers the reference borrowed for the entire function body, even though it obviously isn't borrowed if the if branch isn't taken.

Fortunately in this case HashMap has the entry API which both gets rid of this problem and makes the code simpler

Playground

use std::collections::HashMap;

struct BP {
    table: HashMap<u32, u32>,
}

impl BP {
    fn load<'a>(&'a mut self, index: u32) -> &'a u32 {
        let entry = self.table.entry(index);

        entry.or_insert(10)
    }
}

fn main() {
    let mut bp = BP {
        table: HashMap::new(),
    };

    // Inserts the default value
    bp.load(0);
    assert_eq!(bp.table[&0], 10);

    // Change the value to something else
    *bp.table.get_mut(&0).unwrap() = 123;
    // This time the call will do nothing because there is already a value for that key
    bp.load(0);
    assert_eq!(bp.table[&0], 123);

    println!("{:?}", bp.table);
}
2 Likes

Ah, good old NLL Problem Case #3 again.

If you're just experimenting, compile with -Z polonius on nightly to see that it actually does work. If you need this to work in production, use the Entry API instead (as semicoleon also suggests).

3 Likes