How Rust's ownership enforcement has evolved β from the original lexical borrow checker, through Non-Lexical Lifetimes, to the next-generation Polonius engine.
All three terms describe the same job β enforcing Rust's ownership and borrowing rules at compile time. The difference is how precisely each one tracks when borrows are alive.
Original (pre-2018). Borrows live for the entire lexical scope.
Rust 2018+. Borrows end at their last use, not end of scope.
Next-gen. Uses origin analysis for even smarter flow-sensitive checking.
The original borrow checker determined the lifetime of a reference from the lexical scope
(the curly-brace block) in which it was created. A borrow lived from the let binding
until the closing } β even if the reference was never used again.
1. You can have either one &mut T (exclusive/mutable reference)
or any number of &T (shared/immutable references) β never both at the same time.
2. References must always be valid β no dangling pointers.
Because the lexical checker tied lifetimes to scopes, it rejected many programs that were actually safe:
ERROR (pre-2018) fn main() { let mut data = vec![1, 2, 3]; let first = &data[0]; // immutable borrow starts here println!("{}", first); // last use of `first` data.push(4); // β ERROR: `data` still borrowed! } // borrow of `first` ends HERE at `}`
The immutable borrow first was done being used after println!, yet the compiler
kept it alive until the end of the block. This forced developers to use ugly workarounds
like extra { } blocks to artificially shorten scopes.
WORKAROUND fn main() { let mut data = vec![1, 2, 3]; { let first = &data[0]; // borrow scoped to inner block println!("{}", first); } // borrow ends here data.push(4); // β OK now }
NLL, stabilised in Rust 1.31 (December 2018), was the single biggest ergonomics win in Rust's history. Instead of tying borrows to scopes, the compiler builds a control-flow graph (CFG) and computes the liveness region for every borrow β the set of program points where the reference might still be used.
A borrow is live at a program point if there exists a path from that point to a future use of the borrow. The borrow dies immediately after its last use β not at the end of the scope.
The previous example now compiles without any workaround:
OK (NLL) fn main() { let mut data = vec![1, 2, 3]; let first = &data[0]; // immutable borrow starts println!("{}", first); // last use β borrow ENDS here data.push(4); // β OK β no active borrow }
NLL analyses borrows in a location-insensitive way for the origin (provenance) of references. It knows when a borrow is used but not always which specific data is borrowed through a given reference at each point. This leads to false rejections in conditional borrowing patterns:
ERROR (NLL) fn get_or_insert(map: &mut HashMap<String, String>, key: &str) -> &String { if let Some(v) = map.get(key) { // immutable borrow of `map` return v; // returning the borrow } // NLL thinks `map` might still be immutably borrowed here, // because the borrow *could* have flowed to the return value. map.insert(key.to_owned(), "default".into()); // β mutable borrow map.get(key).unwrap() }
The problem: NLL cannot see that the return v branch and the map.insert branch
are mutually exclusive. If we reached insert, we didn't return, so the
immutable borrow is dead β but NLL's analysis isn't precise enough to prove that.
Polonius is the next-generation borrow checker being developed to replace NLL's internal engine. Named after Shakespeare's character from Hamlet, it flips the analysis model on its head.
It computes a lifetime region β the set of program points where the borrow must be valid. Then it checks whether any conflicting access happens within that region.
It tracks origins (also called "provenance") β which loan created a reference and where that loan's data flows. At each program point, it knows exactly which loans a reference could be carrying, and checks conflicts only for those specific loans.
This origin-sensitive, flow-sensitive analysis means Polonius can reason about conditional branches precisely. The example that fails with NLL works under Polonius:
OK (Polonius) fn get_or_insert(map: &mut HashMap<String, String>, key: &str) -> &String { if let Some(v) = map.get(key) { return v; // loan flows to return β branch exits } // Polonius knows: if we reach here, the loan from `map.get()` // did NOT flow to any live reference. It's dead. map.insert(key.to_owned(), "default".into()); // β no conflict map.get(key).unwrap() }
Polonius uses Datalog-style rules to compute three key relations over the CFG:
| Relation | Meaning |
|---|---|
origin_contains_loan_at |
At program point P, origin O may contain loan L |
loan_invalidated_at |
At point P, an action invalidates (conflicts with) loan L |
errors |
At point P, loan L is both in some live origin AND invalidated β error |
By intersecting "which loans are reachable" with "which loans are invalidated" at each point, Polonius achieves precise, flow-sensitive, origin-sensitive analysis.
| Feature | Lexical Borrow Checker | NLL | Polonius |
|---|---|---|---|
| Era | Rust 1.0 β 1.30 | Rust 1.31+ (Rust 2018) | Nightly (experimental) |
| Borrow ends at | End of lexical scope } |
Last use of the reference | Last use (with smarter flow analysis) |
| Analysis model | Scope-based | Liveness-based (CFG) | Origin/provenance-based (Datalog) |
| Flow sensitivity | None (lexical) | Partial β lifetime regions are flow-sensitive, origins are not | Full β both lifetimes and origins are flow-sensitive |
| Conditional borrows | Poor β needs workarounds | Better β but struggles with conditional return patterns | Excellent β tracks which branch a loan flows through |
| Soundness | Sound (overly conservative) | Sound (less conservative) | Sound (least conservative) |
| False positives | Many | Some | Very few |
| Performance | Fast | Fast | Actively being optimised |
OK since NLL let mut v = vec![1, 2, 3]; let last = v.last(); // immutable borrow println!("{:?}", last); // last use v.push(4); // β NLL ends the borrow above
OK with Polonius fn get_default<'a>(map: &'a mut HashMap<i32, String>, key: i32) -> &'a String { if let Some(val) = map.get(&key) { return val; // borrow exits via return } map.insert(key, String::from("default")); &map[&key] }
Needs Polonius fn update_or_insert(map: &mut HashMap<i32, Vec<i32>>, key: i32, val: i32) { if let Some(vec) = map.get_mut(&key) { vec.push(val); // mutably borrow map β get inner vec return; } map.insert(key, vec![val]); // NLL may reject; Polonius accepts }
Entry API
(map.entry(key).or_insert_with(|| ...)) to avoid these patterns entirely.
The Entry API was designed precisely to work around borrow checker limitations.
Lexical borrow checker ships. Developers learn to love (and fight) the borrow checker.
Niko Matsakis proposes Non-Lexical Lifetimes. The community rallies around the idea.
NLL is stabilised for the 2018 edition. Massive ergonomics improvement.
NLL becomes the default borrow checker for Rust 2015 edition as well (Rust 1.36).
Polonius is developed as an experimental project. Available on nightly with
-Z polonius. Active work on performance and integration.
Polonius is expected to eventually replace NLL as the default borrow checker, once performance and compatibility are fully validated.
You can experiment with Polonius on nightly Rust:
# Install nightly rustup toolchain install nightly # Compile with Polonius enabled RUSTFLAGS="-Z polonius" cargo +nightly check # Or set it in .cargo/config.toml [build] rustflags = ["-Z", "polonius"]
Borrow Checker β the umbrella concept. Rust statically enforces that references never dangle and that mutable access is exclusive. This guarantee has never changed.
NLL (Non-Lexical Lifetimes) β the current implementation. Borrows end at their last use, not at the end of the scope. Fixed the vast majority of "fighting the borrow checker" pain.
Polonius β the upcoming implementation. Tracks the origin of references through control flow, eliminating the remaining false positives that NLL cannot solve. Same safety guarantees, fewer false rejections.
All three uphold the same fundamental invariant: memory safety without garbage collection.