ExamplesBy LevelBy TopicLearning Paths
547 Intermediate GitHub

Polonius Borrow Checker Concepts

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Polonius Borrow Checker Concepts" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. NLL (Non-Lexical Lifetimes) dramatically improved Rust's borrow checker but still rejects some provably safe code. Key difference from OCaml: 1. **NLL limitation**: Rust's NLL rejects get

Tutorial

The Problem

NLL (Non-Lexical Lifetimes) dramatically improved Rust's borrow checker but still rejects some provably safe code. The most famous example: looking up a key in a map, and if absent, inserting and returning a reference to the new value — the classic "get-or-insert" pattern. NLL conservatively rejects this because the mutable borrow for the lookup overlaps with the mutable borrow for the insert, even though they cannot both be active simultaneously. Polonius, the next-generation borrow checker based on Datalog constraints, accepts this pattern and other currently-rejected safe code.

🎯 Learning Outcomes

  • • What patterns NLL conservatively rejects that are provably safe
  • • The classic "get-or-insert" pattern and why NLL's two-phase borrow check fails on it
  • • How to work around NLL limitations using contains_key + separate get, or the entry API
  • • What Polonius is and how it improves over NLL using Datalog-based constraint solving
  • • Where Polonius matters most: HashMap get-or-insert, conditional borrow returns
  • Code Example

    // NLL rejects: borrow in match conflicts with insert
    // Workaround: check first, then mutate
    pub fn get_or_insert<'a>(
        map: &'a mut HashMap<String, String>,
        key: String,
    ) -> &'a str {
        if !map.contains_key(&key) {
            map.insert(key.clone(), format!("default_{}", key));
        }
        map.get(&key).unwrap()
    }
    
    // Or use entry API
    map.entry(key).or_insert_with(|| compute())

    Key Differences

  • NLL limitation: Rust's NLL rejects get-or-insert patterns even when provably safe; OCaml's lack of borrow checking means no such limitation exists.
  • Entry API: Rust's HashMap::entry was specifically designed to work around NLL limitations — it is the idiomatic solution; OCaml has no equivalent API pattern needed.
  • Polonius timeline: Polonius is a research project that will replace NLL — it accepts all NLL-accepted programs plus additional safe programs; OCaml's model is orthogonal.
  • Workaround cost: NLL workarounds for get-or-insert require double lookups (one contains_key, one get) — a performance cost that Polonius would eliminate; OCaml pays no such cost.
  • OCaml Approach

    OCaml's Hashtbl and functional Map have no borrow checker limitations — get-or-insert is direct:

    let get_or_insert tbl key default =
      match Hashtbl.find_opt tbl key with
      | Some v -> v
      | None -> Hashtbl.add tbl key default; default
    

    No workaround needed — the hash table is always mutably accessible through its reference.

    Full Source

    #![allow(clippy::all)]
    //! Polonius Borrow Checker Concepts
    //!
    //! Patterns where current NLL is conservative; Polonius accepts.
    
    use std::collections::HashMap;
    
    /// Classic Polonius example: get-or-insert.
    /// NLL-friendly workaround using contains_key.
    pub fn get_or_insert<'a>(map: &'a mut HashMap<String, String>, key: String) -> &'a str {
        if !map.contains_key(&key) {
            map.insert(key.clone(), format!("default_{}", key));
        }
        map.get(&key).unwrap()
    }
    
    /// Another workaround: use entry API.
    pub fn get_or_insert_entry<'a>(map: &'a mut HashMap<String, String>, key: String) -> &'a str {
        map.entry(key.clone())
            .or_insert_with(|| format!("default_{}", key))
    }
    
    /// Pattern that Polonius would accept directly.
    /// We work around by returning Option differently.
    pub fn find_or_create(items: &mut Vec<String>, target: &str) -> usize {
        for (i, item) in items.iter().enumerate() {
            if item == target {
                return i;
            }
        }
        items.push(target.to_string());
        items.len() - 1
    }
    
    /// Conditional return pattern.
    pub fn get_cached<'a>(cache: &'a mut HashMap<i32, String>, key: i32) -> &'a str {
        if !cache.contains_key(&key) {
            cache.insert(key, format!("computed_{}", key));
        }
        cache.get(&key).unwrap()
    }
    
    /// Split the logic to help the borrow checker.
    fn compute_if_missing(cache: &mut HashMap<i32, String>, key: i32) {
        if !cache.contains_key(&key) {
            cache.insert(key, format!("value_{}", key));
        }
    }
    
    pub fn get_with_helper<'a>(cache: &'a mut HashMap<i32, String>, key: i32) -> &'a str {
        compute_if_missing(cache, key);
        cache.get(&key).unwrap()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_get_or_insert() {
            let mut map = HashMap::new();
            let v1 = get_or_insert(&mut map, "key1".into());
            assert!(v1.contains("default_key1"));
        }
    
        #[test]
        fn test_get_or_insert_entry() {
            let mut map = HashMap::new();
            let v1 = get_or_insert_entry(&mut map, "key2".into());
            assert!(v1.contains("default_key2"));
        }
    
        #[test]
        fn test_find_or_create_existing() {
            let mut items = vec!["a".into(), "b".into(), "c".into()];
            let idx = find_or_create(&mut items, "b");
            assert_eq!(idx, 1);
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_find_or_create_new() {
            let mut items = vec!["a".into(), "b".into()];
            let idx = find_or_create(&mut items, "c");
            assert_eq!(idx, 2);
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_get_cached() {
            let mut cache = HashMap::new();
            let v = get_cached(&mut cache, 42);
            assert!(v.contains("42"));
        }
    
        #[test]
        fn test_get_with_helper() {
            let mut cache = HashMap::new();
            let v = get_with_helper(&mut cache, 7);
            assert!(v.contains("7"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_get_or_insert() {
            let mut map = HashMap::new();
            let v1 = get_or_insert(&mut map, "key1".into());
            assert!(v1.contains("default_key1"));
        }
    
        #[test]
        fn test_get_or_insert_entry() {
            let mut map = HashMap::new();
            let v1 = get_or_insert_entry(&mut map, "key2".into());
            assert!(v1.contains("default_key2"));
        }
    
        #[test]
        fn test_find_or_create_existing() {
            let mut items = vec!["a".into(), "b".into(), "c".into()];
            let idx = find_or_create(&mut items, "b");
            assert_eq!(idx, 1);
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_find_or_create_new() {
            let mut items = vec!["a".into(), "b".into()];
            let idx = find_or_create(&mut items, "c");
            assert_eq!(idx, 2);
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_get_cached() {
            let mut cache = HashMap::new();
            let v = get_cached(&mut cache, 42);
            assert!(v.contains("42"));
        }
    
        #[test]
        fn test_get_with_helper() {
            let mut cache = HashMap::new();
            let v = get_with_helper(&mut cache, 7);
            assert!(v.contains("7"));
        }
    }

    Deep Comparison

    OCaml vs Rust: Polonius / Advanced Borrow Checking

    OCaml

    (* No borrow checking — hashtbl mutation is direct *)
    let get_or_insert tbl key =
      try Hashtbl.find tbl key
      with Not_found ->
        let v = "default_" ^ key in
        Hashtbl.add tbl key v;
        v
    

    Rust (NLL workaround)

    // NLL rejects: borrow in match conflicts with insert
    // Workaround: check first, then mutate
    pub fn get_or_insert<'a>(
        map: &'a mut HashMap<String, String>,
        key: String,
    ) -> &'a str {
        if !map.contains_key(&key) {
            map.insert(key.clone(), format!("default_{}", key));
        }
        map.get(&key).unwrap()
    }
    
    // Or use entry API
    map.entry(key).or_insert_with(|| compute())
    

    Key Differences

  • OCaml: No borrow tracking, direct mutation
  • Rust NLL: Conservative in some patterns
  • Rust Polonius: More precise flow analysis (experimental)
  • Rust: Entry API is idiomatic workaround
  • Both: Achieve same functionality, different ergonomics
  • Exercises

  • Entry API mastery: Rewrite get_or_insert using only the Entry API with or_insert, or_insert_with, and or_default variants — compare the verbosity of each.
  • Two-pass workaround: Implement a fn get_or_compute<'a>(map: &'a mut HashMap<i32, i32>, key: i32, f: impl Fn(i32) -> i32) -> &'a i32 using the NLL workaround (two lookups).
  • Find-or-add dedup: Write fn dedup_insert(v: &mut Vec<String>, s: String) -> usize that returns the index of s if already present, or inserts it and returns the new index — without cloning s unnecessarily.
  • Open Source Repos