ExamplesBy LevelBy TopicLearning Paths
540 Intermediate

Borrow Checker Internals

Functional Programming

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

  • • The aliasing XOR mutability rule: shared borrows are fine together, but not with mutable borrows
  • • How NLL allows multiple borrows to exist sequentially in the same block
  • • How reborrowing creates a temporary shared borrow from a mutable one
  • • Why preventing aliased mutation eliminates a class of memory safety bugs
  • • How rule_exclusive_mutable demonstrates that sequential pushes are always safe
  • Code 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 ended

    Key Differences

  • Aliased mutation: Rust makes aliased mutation a compile-time error; OCaml allows it — correct concurrent programs in OCaml require careful locking discipline.
  • Data race prevention: Rust's aliasing XOR mutability rule eliminates data races statically; OCaml 5.x uses domain locks, but race conditions in user code are still possible.
  • Reborrowing: Rust's reborrow rules (shared from mutable is safe) are precisely defined; OCaml has no reborrow concept since all references are uniform.
  • Teaching tool: Understanding why the borrow checker rejects code helps design better APIs; OCaml developers rely on code review and testing for the same safety properties.
  • 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]);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • OCaml: ref cells provide interior mutability, GC safety
  • Rust: Borrow checker enforces at compile time
  • Rust: Either N readers OR 1 writer
  • Rust: Prevents data races by design
  • Both: Memory-safe, different enforcement
  • Exercises

  • Demonstrate the rule: Write code that creates two &v borrows, uses them, then pushes — add comments showing exactly where each borrow begins and ends per NLL.
  • Iterator aliasing: Attempt to create a &v iterator and simultaneously push to v — observe the error message and explain why it protects against iterator invalidation.
  • Split borrow: Implement 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.
  • Open Source Repos