Ketan Singh

Collection of musing and ramblings

Limitations of Rust Borrow Checker

Posted at — Aug 20, 2023

Borrow checker is one of the most defining feature of the Rust programming language. We can even say it’s the primary mechanism which makes the language memory safe, while also being of the thing which can frustrate people new to the language because they have to fight the “rules” of the borrow checker. Let me give a quick introduction to these rules as a refresher.

All the values in rust have an owner. The owner has the responsibility to drop the value once it is no longer needed. Owner can lend the value to different places in the code using references, and the rules to lend out these values is governed by the borrow checker.

There are two kinds of borrow (lending) that can happen, first is reference and second is mutable reference. Reference is used to read from underlying value (not completely true, look interior mutability) while mutable reference is used to read and update underlying value. Now the most important rule here is that (a) there can be any number of immutable references to the item (b) a single mutable reference to the item. Compiler will enforce these rules and will not allow code to compile if this rule is violated.

Breaking the borrow checker

I ran into an instance where the borrow check does not respect the early return of the mutable reference and extends checks to the end of the function. To give you an example, check the following code

use ::std::collections::HashMap;

fn main() {
    let mut m = HashMap::new();
    println!("{}", get_or_insert(&mut m, 42));
}

fn get_or_insert (
    map: &mut HashMap<u32, String>,
    key: u32,
) -> &String {
    if let Some(v) = map.get(&key) {
        return v;
    }
    map.insert(key, String::from("default_value"));
    &map[&key]
}

This code seems to follow the rules at first glance. If there’s a key in the map, we return reference to the value from map and return early. If not, then we insert a value and return reference to the newly added value. All the rust’s borrow rules are satisfied but upon compiling we see the following error

error[E0502]: cannot borrow `*map` as mutable because it is also borrowed as immutable
  --> src/main.rs:15:5
   |
9  |     map: &mut HashMap<u32, String>,
   |          - let's call the lifetime of this reference `'1`
...
12 |     if let Some(v) = map.get(&key) {
   |                      ------------- immutable borrow occurs here
13 |         return v;
   |                - returning this value requires that `*map` is borrowed for `'1`
14 |     }
15 |     map.insert(key, String::from("default_value"));
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

Link: https://godbolt.org/z/rj81obWEP

Workarounds

Polonius

Above code will compile if we provide -Z polonius compiler flag and use the nightly compiler. This limitation is actually a well known issue and has been reported several times already. The typical solution is to use nightly build and use polonius borrow checker hence the flag name but unfortunately work for polonius is not complete and is only provisionally integrated into rustc. It only works with nightly rust for now.

Alternative API

Sometimes there’s an alternative API to accomplish this, such as entry() in this case which can be used to mutate the map. However, this may not always be the case.

map.entry(42).or_insert_with(|| String::from("default_value"))

Unsafe

Last resort is using unsafe and doing pointer dereference (and other shenanigans), which I am not particularly a fan of, but if there’s no other option it’s something to be considered.

let map_ptr = map as *const HashMap<u32, String>;
if let Some(v) = unsafe { (*map_ptr).get(&key) } {
    return v;
}
map.insert(key, String::from("default_value"));
&map[&key]

References