Borrow Checker Internals
Tutorial Video
Text description (accessibility)
This video demonstrates the "Borrow Checker Internals" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The borrow checker enforces two rules that together eliminate data races and use-after-free bugs: (1) you can have multiple shared references (`&T`) or exactly one mutable reference (`&mut T`), never both simultaneously; (2) references cannot outlive the data they point to. Key difference from OCaml: 1. **Aliased mutation**: Rust makes aliased mutation a compile
Tutorial
The Problem
The borrow checker enforces two rules that together eliminate data races and use-after-free bugs: (1) you can have multiple shared references (&T) or exactly one mutable reference (&mut T), never both simultaneously; (2) references cannot outlive the data they point to. These rules are based on the "aliasing XOR mutability" principle from the research community. Understanding why these rules exist — not just what they are — makes it easier to design APIs that work with the borrow checker rather than fighting it.
🎯 Learning Outcomes
rule_exclusive_mutable demonstrates that sequential pushes are always safeCode Example
// Rule 1: Multiple & OR one &mut, not both
let mut v = vec![1, 2, 3];
let r1 = &v; // shared borrow
let r2 = &v; // OK: multiple shared
// v.push(4); // ERROR: can't mutate while borrowed
// After r1, r2 last use:
v.push(4); // OK: borrows endedKey Differences
OCaml Approach
OCaml has no borrow checker. Aliased mutation is possible and used freely:
let v = ref [1; 2; 3] in
let r1 = v and r2 = v in (* two references to same list *)
r1 := 42 :: !r1; (* mutate through r1 *)
Printf.printf "%d\n" (List.length !r2) (* r2 sees the change *)
OCaml programs must use discipline and careful design to avoid bugs that Rust catches at compile time.
Full Source
#![allow(clippy::all)]
//! Borrow Checker Internals
//!
//! Understanding why the borrow checker's rules exist.
/// Rule: Cannot have &mut while & exists.
pub fn rule_shared_vs_mutable() -> Vec<i32> {
let mut v = vec![1, 2, 3];
let r1 = &v;
let r2 = &v; // multiple shared OK
let _ = (r1.len(), r2.len()); // use borrows
// r1, r2 end here (NLL)
v.push(4); // mutable borrow OK now
v
}
/// Rule: Only one &mut at a time.
pub fn rule_exclusive_mutable(v: &mut Vec<i32>) {
v.push(1);
v.push(2);
// Each push is sequential, not simultaneous
}
/// Demonstrates reborrowing.
pub fn reborrow_demo(v: &mut Vec<i32>) {
let len = v.len(); // temporary shared borrow
v.push(len as i32); // back to mutable
}
/// Working with owned vs borrowed.
pub fn ownership_rules() {
let s = String::from("hello");
let r = &s; // borrow
assert_eq!(r, "hello");
// s still owned here
drop(s); // explicit drop
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shared_vs_mutable() {
let v = rule_shared_vs_mutable();
assert_eq!(v, vec![1, 2, 3, 4]);
}
#[test]
fn test_exclusive_mutable() {
let mut v = vec![];
rule_exclusive_mutable(&mut v);
assert_eq!(v, vec![1, 2]);
}
#[test]
fn test_reborrow() {
let mut v = vec![1, 2, 3];
reborrow_demo(&mut v);
assert_eq!(v, vec![1, 2, 3, 3]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_shared_vs_mutable() {
let v = rule_shared_vs_mutable();
assert_eq!(v, vec![1, 2, 3, 4]);
}
#[test]
fn test_exclusive_mutable() {
let mut v = vec![];
rule_exclusive_mutable(&mut v);
assert_eq!(v, vec![1, 2]);
}
#[test]
fn test_reborrow() {
let mut v = vec![1, 2, 3];
reborrow_demo(&mut v);
assert_eq!(v, vec![1, 2, 3, 3]);
}
}
Deep Comparison
OCaml vs Rust: Borrow Checker
OCaml
(* No borrow checking — ref cells for mutation *)
let v = ref [1; 2; 3]
let r1 = !v
let r2 = !v
(* No restrictions *)
Rust
// Rule 1: Multiple & OR one &mut, not both
let mut v = vec![1, 2, 3];
let r1 = &v; // shared borrow
let r2 = &v; // OK: multiple shared
// v.push(4); // ERROR: can't mutate while borrowed
// After r1, r2 last use:
v.push(4); // OK: borrows ended
Key Differences
Exercises
&v borrows, uses them, then pushes — add comments showing exactly where each borrow begins and ends per NLL.&v iterator and simultaneously push to v — observe the error message and explain why it protects against iterator invalidation.fn split_first_rest(v: &mut Vec<i32>) -> (&mut i32, &mut [i32]) using v.split_first_mut() and explain why this is safe despite having two mutable references.