Understanding Mutable Borrowing Behavior in Rust

Hello Rust Community,

I'm new to Rust and came across a behavior I'm trying to understand regarding mutable borrowing. Here's the code:

let mut s = String::from("Does work");
let t: &mut String = &mut s;
s.push('?');
println!("{}", s);

From my learning, I thought this code shouldn't compile because s is being modified while it's mutably borrowed by t. However, it compiles and runs without errors. Does Rust's borrow checker enforce rules only when the borrowed variable (t in this case) is used, not just declared? Or is there something I'm missing about how borrowing works?

Yes, that’s indeed the case. And interestingly enough, it’s not always been this way. Lifetimes used to be more “lexical”, i.e. focusing on the lexical scope a variable is declared in. If you take your code to an older version on Compile Explorer, you can indeed find an error message

error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> <source>:5:5
  |
4 |     let t: &mut String = &mut s;
  |                               - first mutable borrow occurs here
5 |     s.push('?');
  |     ^ second mutable borrow occurs here
...
8 | }
  | - first borrow ends here

error[E0502]: cannot borrow `s` as immutable because it is also borrowed as mutable
 --> <source>:6:20
  |
4 |     let t: &mut String = &mut s;
  |                               - mutable borrow occurs here
5 |     s.push('?');
6 |     println!("{}", s);
  |                    ^ immutable borrow occurs here
7 | 
8 | }
  | - mutable borrow ends here

error: aborting due to 2 previous errors

And to fix it, you’d have to write

let mut s = String::from("Does work");
{
    let t: &mut String = &mut s;
    // …can use `t` here
}
s.push('?');
println!("{}", s);

The newer system, often called “non-lexical lifetimes” is more clever than this though. For types like &mut String for which the compiler knows they cannot possibly don’t do anything to the reference target when being dropped, it’s now the last usage that’s important to determine how long s is borrowed by t; and when t is implicitly dropped at the end of its scope, it’s allowed to be already dead / pointing to invalid memory / etc.

Only if you add a usage of t at a later point, you get an error. E.g.

let mut s = String::from("Does work");
let t: &mut String = &mut s;
s.push('?');
// and now use `t` again
let _ = &t;
println!("{}", s);
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> <source>:5:1
  |
4 | let t: &mut String = &mut s;
  |                      ------ first mutable borrow occurs here
5 | s.push('?');
  | ^ second mutable borrow occurs here
6 | // and now use `t` again
7 | let _ = &t;
  |         -- first borrow later used here

error: aborting due to previous error

This improvement to the borrow checker means a significant improvement in usability. It’s hard to imagine nowadays the amount of additional struggle when programming with Rust that came from the need to add extra blocks or otherwise limit the scope of variables, just to make the borrow checker happy with code that wasn’t “really” any problematic at all. I cannot imagine it either since when I’ve learned Rust, we already had the new borrow checking stuff (which was introduced in about 2018, roughly at the same time as the first newer edition, I believe).

Even nowadays, people still manage to run into borrow checker limitations regularly, and some of them are even already improved upon in the next WIP upgrade called “polonius”.

5 Likes

Thank you , it is much clearer now.