🎓 A1Learner
🔍

Rust Non-Lexical Lifetimes

Non-lexical lifetimes are lifetimes measured by use, not by block. A borrow stays live from its first use to its last use.

The old way: a borrow filled its whole block

Before 2018, this shape was refused. The borrow of data lasted until the end of the block, even though slice is never used after the call:

let slice = &mut data[..];
capitalize(slice);
data.push('d'); // refused before 2018: the block was not over

Now: a borrow ends at its last use

The same program compiles today. The borrow is measured by where the reference is used, not by where the block ends:

let mut names = vec![String::from("ana"), String::from("bo")];
let first = &names[0];
println!("first = {}", first);
names.push(String::from("new"));
println!("count = {}", names.len());

A borrow can live in one match arm only

Here the borrow of the map exists only in the Some arm. In the None arm there is no borrow at all, so the map itself may be used:

match counts.get_mut("apples") {
    Some(n) => *n += 1,
    None => { counts.insert("apples", 1); }
}

Walking a &mut down a list

This linked-list walk borrows one node's value, then moves the &mut on to the next node. Early Rust refused the assignment, with error E0506. It compiles now, because the borrowed value sits behind the reference, not inside the variable being reassigned:

result.push(&mut list.value);
if let Some(n) = list.next.as_mut() {
    list = n;
}

Errors point at the later use

A modern borrow error shows its working. It names the first borrow and the second borrow. It also names the line where the first one is used again. That last line is the one to fix:

let a = &mut x[0];
let b = &mut x[1]; // error E0499: borrowed as mutable more than once
println!("{} {}", a, b); // ...and the error adds: first borrow later used here

Example

use std::collections::HashMap;

fn capitalize(data: &mut [char]) {
    for c in data.iter_mut() {
        c.make_ascii_uppercase();
    }
}

fn main() {
    let mut data = vec!['a', 'b', 'c'];
    let slice = &mut data[..];
    capitalize(slice);
    data.push('d');
    println!("{:?}", data);

    let mut counts: HashMap<&str, i32> = HashMap::new();
    counts.insert("apples", 3);
    match counts.get_mut("apples") {
        Some(n) => *n += 1,
        None => { counts.insert("apples", 1); }
    }
    match counts.get_mut("pears") {
        Some(n) => *n += 1,
        None => { counts.insert("pears", 1); }
    }
    println!("apples = {} pears = {}", counts["apples"], counts["pears"]);
}

Key points

  • Non-lexical: a borrow's span is worked out from the flow of the program.
  • It does not come from the { } block it sits in.
  • Lexical would mean "by block".
  • A borrow is live from the line that makes it.
  • It stays live until the last place it may still be used.
  • After that line, the value is free again.
  • Two borrows of one value may follow each other inside one function.
  • No extra block is needed between them.
  • The first must finish before the second begins.
  • Code in old books and posts sometimes wraps a borrow in its own { } block.
  • The block ends the borrow early.
  • That was a workaround for the older checker.
  • It is rarely needed now.
  • Rust has measured borrows this way since the 2018 edition.

In this example, two formerly-refused shapes compile side by side. First, slice borrows the whole of data mutably and capitalize shouts each letter. The borrow's last use is that call. So the very next line may push onto data itself. It prints ['A', 'B', 'C', 'd']. Below, each match borrows the map only inside its Some arm. apples is found and raised to 4. pears is missing. So the None arm inserts it as 1. No borrow exists in that arm. Before non-lexical lifetimes, both halves of this program needed restructuring to please the checker.

Note

The borrow rules themselves never changed — many readers OR one writer still holds.

Only the measuring became precise: the checker now watches uses, not blocks.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

In `let slice = &mut data[..]; capitalize(slice); data.push('d');`, why does the push compile on modern Rust?