References and Borrowing

Borrow Checker Fundamentals

The borrow checker (mir_borrowck) is the static analysis pass inside rustc that enforces Rust's ownership, borrowing, and lifetime rules. Operating on the compiler's Mid-Level Intermediate Representation (MIR) control-flow graph, it proves at compile time that every memory access is valid, properly initialized, and free of aliasing violations.

What the Borrow Checker Tracks

For every variable, struct field, and reference in a function's MIR control-flow graph, the borrow checker tracks three pieces of state at every program point (statement):

Tracked Property Question Answered by the Borrow Checker Error Codes Emitted on Violation
1. Initialization & Move State Is this variable/field currently initialized, uninitialized, or moved out of? E0381 (uninitialized), E0382 (moved), E0384 (reassigning immutable)
2. Active Borrows (Loans) Which shared (&) or mutable (&mut) loans of this place are currently live (will be used on a reachable future path)? E0502 (& + &mut), E0499 (&mut + &mut), E0505 (move while borrowed), E0506 (assign while borrowed)
3. Region / Lifetime Liveness Does the owner of every borrowed place stay alive for at least as long as every reference that can reach a use site? E0597 (does not live long enough), E0515 (returning ref to local), E0716 (temporary dropped while borrowed)
How the Borrow Checker Analyzes a Borrow ("Loan"):

  1. Loan Created (`let r = &mut x;`)
          │
          ├──> Path-sensitive scan along MIR Control-Flow Graph
          │    (Checks every statement between creation and last use of `r`)
          │    - Any read/write/borrow/move of `x` along this path? --> ❌ Error!
          │    - Does `x` go out of scope before `r`'s last use?    --> ❌ Error!
          v
  2. Last Use of `r` (`println!("{r}");`)  --> Loan expires! `x` is free again!

Path-Sensitive Control-Flow Analysis

Because the borrow checker runs on the MIR control-flow graph rather than simple text lines, it is path-sensitive: it knows that the if branch and the else branch are mutually exclusive execution paths!

fn main() {
    let mut message = String::from("initial");
    let condition = true;

    if condition {
        let r1: &mut String = &mut message;
        r1.push_str(" (branch A)");
        println!("{r1}");
    } else {
        // Allowed! Even though `r1` was `&mut message` above, `r1` and `r2`
        // live on mutually exclusive control-flow branches!
        let r2: &mut String = &mut message;
        r2.push_str(" (branch B)");
        println!("{r2}");
    }

    println!("Final: {message}");
}
initial (branch A)
Final: initial (branch A)

Why the Borrow Checker Is Conservative (Soundness Over Completeness)

In computer science, no static analyzer can decide with 100% accuracy whether two arbitrary runtime computations will alias at runtime (Rice's Theorem / the Halting Problem). Therefore, the borrow checker is designed to be sound (conservative):

  • Zero false negatives: It never accepts a safe-Rust program that could cause a use-after-free or data race at runtime.
  • Occasional false positives: It rejects some programs that happen to be safe at runtime if that safety depends on runtime values (such as knowing that i != j when taking &mut slice[i] and &mut slice[j], or knowing that a helper method &mut self only touches one field).

Whenever you encounter a conservative rejection, use standard library escape hatches that encode the invariant in types—such as .split_at_mut(mid), disjoint struct field borrows (&mut self.field), indices (usize), or interior mutability (RefCell<T>, Cell<T>, Mutex<T>).