:#/ GSLF Journal
← :#/ GSLF Journal

Rustlers Atom 2.6: NLL (non-lexical lifetimes)

On this page

<figure class="video" style="margin:0;">
<div style="position:relative;width:100%;padding-bottom:56.25%;height:0;overflow:hidden;">
<iframe src="https://www.youtube.com/embed/lMpyGQTAPzE?si=HXN3ogYJp99GFOxO"
title="YouTube video player"
style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen></iframe>
</div>
</figure>

For a long time, Rust’s concept of a "lifetime" was strictly tied to lexical scope—the curly braces {}. If you created a reference inside a block, the compiler assumed that reference lived until the closing brace }, period.

This was safe, but annoying. It often rejected valid code because the compiler wasn't smart enough to see that you were done with a variable before the block ended.

Since the 2018 edition, Rust uses Non-Lexical Lifetimes (NLL). The borrow checker now looks at the control flow graph of your program. It knows that a reference’s life ends the last time it is used, not necessarily when it goes out of scope.

The "Interleaved" Borrows#

Before NLL, the following code would fail to compile because mut_ref would "lock" the data variable until the end of the function. With NLL, Rust sees that mut_ref is used for the last time in the push line, so it releases the borrow immediately after.

fn main() {
    let mut data = vec![1, 2, 3];

    // 1. Create a mutable reference
    let mut_ref = &mut data; 
    mut_ref.push(4); 
    // --- `mut_ref` is no longer used after this point ---
    // Under NLL, the mutable borrow effectively ends HERE.

    // 2. Create an immutable reference
    // This is now allowed because the mutable borrow is "dead"
    let imm_ref = &data; 
    println!("{:?}", imm_ref); 
}

This feature reduces the need for "fighting the borrow checker." You don't need to artificially wrap code in extra {} blocks just to kill a borrow early. You can write code that flows naturally, and if the logic is sound, Rust will likely accept it.

Important Nuance: Drop vs. Liveness#

NLL changes the liveness of references (when a borrow is active). It does not change when a value is dropped.

  • Borrows end at the last usage.
  • Owned values are dropped at the closing brace }.
struct LoudDrop;
impl Drop for LoudDrop {
    fn drop(&mut self) { println!("Dropped!"); }
}

fn main() {
    let x = LoudDrop;
    let y = &x; 
    
    println!("Used y"); 
    // y's borrow ends here (NLL)
    
    println!("End of main");
} // x is dropped here (Lexical Scope)