Viscosity
Review
Part IX · Rust for TypeScript developers · Day 34

133. Lifetimes

Chapter 126 told you to ignore the ’src in drip-rs’s Token. Here is what it says: a token may point into the source text only for as long as that text is alive. Name that span, and the compiler can prove that no token ever dangles.

About 18 minutes, including the exercises and quiz
By the end you will be able to
  • Explain what a lifetime is, and read the E0597 and E0106 errors.
  • Write lifetime annotations on structs, impls and functions, such as Token<'src> and fn longer<'a>.
  • Apply the three elision rules, and see when the default they pick is the wrong one.
  • Read the lifetimes in drip-rs's scanner and compiler, and say why they let scanning run without allocating.

The tick you skipped

Chapter 126 showed you drip-rs’s token type and asked you to ignore one piece of it:

pub struct Token<'src> {
    pub kind: TokenKind,
    pub lexeme: &'src str,
    pub line: usize,
}

A token’s lexeme is a &str, a borrowed slice of the program’s source text. It isn’t a copy. So what happens if you delete the 'src and write the struct the way you’d write a TypeScript interface? Rust 1.97 on this machine (trimmed):

error[E0106]: missing lifetime specifier
2 |     lexeme: &str,
  |             ^ expected named lifetime
  |               parameter
help: consider introducing a named
  lifetime parameter
1 ~ struct Token<'a> {
2 ~     lexeme: &'a str,

Your chapter 28 scanner gave each token source.slice(start, current), a new string kept alive by the garbage collector. A Rust token holds a pointer into someone else’s text, which nobody keeps alive for it. So the compiler asks: whose text, and for how long?

Intuition

A reference is a cloakroom ticket, worth something only while the cloakroom is open. A lifetime is the opening hours printed on it. Keep the ticket as long as you like, but don’t try to collect your coat after closing: the compiler checks every ticket against the hours before the program runs.

How long is a borrow valid?

Here is the smallest way to get it wrong, in the diagram below. The scanner is cut down to split on spaces, but its signatures are the real ones (adapted from companion/rust/src/scanner.rs). src owns the text and is dropped at the closing brace (chapter 124), and t is used after that. Step through what the compiler sees:

Step throughWho outlives whom
Code lines on the left, the life of each variable as a bar on the rightsrcsct'src1let t;2{3    let src = String::from("let x");4    let mut sc = Scanner::new(&src);5    t = sc.next();6}7println!("{}", t.lexeme);
t is declared but has no value yet. Rust lets you fill it in later, once.
1/7

The real error, from compiling that program (trimmed):

error[E0597]: `src` does not live long
  enough
51 | let mut sc = Scanner::new(&src);
   |                  ^^^^ borrowed value
   |                  does not live long
   |                  enough
53 | }
   | - `src` dropped here while still
   |   borrowed
54 | println!("{}", t.lexeme);
   |                -------- borrow later
   |                         used here

Chapter 125’s three points again: where the borrow started, where the owner died, where the borrow was still needed. And note what is not an error: sc dies at the same brace, but t borrows the source, not the scanner.

Definition

A lifetime is the stretch of a program during which a reference must stay valid. The borrow checker gives every reference one, and checks that it fits inside the life of the value it points at (from creation to drop). A reference that could outlive its value would dangle; preventing that is the main job of lifetimes[1].

Naming lifetimes

Inside a function body the compiler works lifetimes out by itself. It needs your help at the boundaries: a struct that holds a reference, and a signature that returns one. 'src is a lifetime parameter, a name for “however long the borrowed source is valid”, declared in angle brackets like a type parameter (chapter 132):

struct Scanner<'src> {
    src: &'src str,
    pos: usize,
}

impl<'src> Scanner<'src> {
    fn next(&mut self) -> Token<'src> {
        // ... skip spaces, find the word
        let s = &self.src[at..self.pos];
        Token { lexeme: s, line: 1 }
    }
}

Read next’s signature aloud: “borrow the scanner mutably for a moment, and hand back a token that is valid as long as the source is”. Same name, same span.

A plain function with two inputs, longer, returns whichever string is longer. Without annotations, Rust 1.97 asks which input the answer borrows from (trimmed):

error[E0106]: missing lifetime specifier
1 | fn longer(a: &str, b: &str) -> &str {
  |              ----     ----     ^
  = help: this function's return type
    contains a borrowed value, but the
    signature does not say whether it is
    borrowed from `a` or `b`

The fix it suggests is fn longer<'a>(a: &'a str, b: &'a str) -> &'a str: the result is valid only while both inputs are.

Definition

A lifetime annotation names a lifetime with an apostrophe, such as 'a or 'src. It is declared as a parameter (struct Token<'src>, impl<'src>, fn f<'a>) and attached to references (&'a str) or to types holding them (Token<'a>). Annotations never make anything live longer; they state how lifetimes relate, so callers can be checked.

The rules that let you leave them out

Chapter 126’s exercise was fn take_ident(src: &str) -> &str, with no ticks at all. That works because of three elision rules, which fill in lifetimes when the answer is obvious[2]:

  1. Each reference parameter gets its own lifetime.
  2. If there is exactly one input lifetime, every output reference gets it.
  3. In a method, if one input is &self or &mut self, every output reference gets self’s lifetime.

If an output is still unnamed after that, you get E0106, as longer did. The compiler never looks at the body to decide[1].

Rule 3 is where drip-rs has to be explicit. Take a word splitter shaped like the scanner, and write its method the short way: fn next_word(&mut self) -> Option<&str>. Rule 3 ties the result to the borrow of the splitter. Then holding two words at once, which the compiler does with previous and current all the time, is refused (from a test program, trimmed):

let a = w.next_word();
let b = w.next_word();
println!("{:?} {:?}", a, b);
error[E0499]: cannot borrow `w` as mutable
  more than once at a time
35 | let a = w.next_word();
   |         - first mutable borrow
36 | let b = w.next_word();
   |         ^ second mutable borrow

While a is alive, the splitter counts as mutably borrowed, so there can’t be a second call. drip-rs’s -> Token<'src> says the token borrows the source instead, so the scanner is free again as soon as the call returns.

The longest lifetime: ‘static

One lifetime is built in. 'static means “valid for the whole run of the program”. String literals have it, because their text is stored in the program’s binary, and a &'static str can be used wherever a shorter lifetime is wanted. drip-rs’s error tokens rely on that:

fn error(&self, message: &'static str)
    -> Token<'src> {
    Token {
        kind: TokenKind::Error,
        lexeme: message,
        line: self.line,
    }
}

The message outlives any source, so it fits in a Token<'src>. If a compiler hint suggests 'static to silence a borrow error, the Rust book advises asking first whether the reference really should live that long[1].

Try itTokens outlive the scanner
#[derive(Clone, Copy, Debug)]
struct Token<'src> {
    lexeme: &'src str,
    line: usize,
}

struct Scanner<'src> {
    src: &'src str,
    pos: usize,
}

impl<'src> Scanner<'src> {
    fn new(src: &'src str) -> Self {
        Scanner { src, pos: 0 }
    }

    // The token borrows the source,
    // not the scanner.
    fn next(&mut self) -> Token<'src> {
        let b = self.src.as_bytes();
        while self.pos < b.len()
            && b[self.pos] == b' '
        {
            self.pos += 1;
        }
        let at = self.pos;
        while self.pos < b.len()
            && b[self.pos] != b' '
        {
            self.pos += 1;
        }
        if b.get(at) == Some(&b'@') {
            return self.error();
        }
        let s = &self.src[at..self.pos];
        Token { lexeme: s, line: 1 }
    }

    // A &'static str fits in a
    // Token<'src>: it lives longer.
    fn error(&self) -> Token<'src> {
        let m = "Unexpected character.";
        Token { lexeme: m, line: 1 }
    }
}

fn main() {
    let src = String::from("let x @ 1");
    let mut toks = Vec::new();
    {
        let mut sc = Scanner::new(&src);
        for _ in 0..4 {
            toks.push(sc.next());
        }
    } // sc is dropped here
    for t in &toks {
        println!("{:?}", t);
    }
}
Open in Rust Playground ↗
Under the hood

Follow 'src through drip-rs. The compiler is struct Compiler<'src, 'a>, with two lifetimes for two different loans: 'src for the source text, and 'a for the heap, the GC roots and the error list it borrows from its caller. It keeps current and previous as Token<'src>, and each local variable’s name is a &'src str pointing into the source, so neither a token nor a local copies any text; the TypeScript VM calls slice for every lexeme. Lifetimes exist only for the checker: at run time a &'src str is still a pointer and a length (chapter 126). engine/rust meets lifetimes from a library: oxc builds the JavaScript AST inside an Allocator, so its nodes have types such as Program<'a>. That allocator is a local variable of the JS plugin’s lower, so nothing that borrows the AST can be returned from it, and the port records what the emitter needs in a side table instead (its README: “No live AST after lowering”).

Your turn

ExerciseA word splitter that never copies

Write next_word, which returns the next space-separated word of the source, or None at the end. It must return a slice of the source, not a new String. The starter’s signature uses the elided form; the tests hold two words at once and keep a word after the Words value is gone, so you’ll need to fix the signature too.

struct Words<'src> {
    src: &'src str,
    pos: usize,
}

impl<'src> Words<'src> {
    fn new(src: &'src str) -> Self {
        Words { src, pos: 0 }
    }

    fn next_word(&mut self)
        -> Option<&str> {
        todo!()
    }
}
Open in Rust Playground ↗
Extra practiceWhich input does it borrow?

Fix both signatures and write both bodies. Span::text returns the part of src between start and end; the result must stay usable after the Span is gone. longer returns whichever of two strings is longer (the first if they tie).

struct Span {
    start: usize,
    end: usize,
}

impl Span {
    fn text<'s>(&self, src: &'s str)
        -> &str {
        todo!()
    }
}

fn longer(a: &str, b: &str) -> &str {
    todo!()
}
Open in Rust Playground ↗

What you learned

  • A lifetime is the span during which a reference must stay valid. The borrow checker makes it fit inside the life of the value borrowed, or reports E0597.
  • Lifetime annotations like 'src name lifetimes on structs, impls and signatures. They describe relationships; they never extend a value’s life.
  • Three elision rules fill in the obvious cases. When they can’t, E0106 asks you which input an output borrows from.
  • Rule 3 ties a method’s result to self; drip-rs’s Token<'src> ties it to the source instead.
  • 'static lasts the whole run; string literals have it.

Check your understanding

0/6
  1. 1.

    Why does struct Token { lexeme: &str } fail to compile?

  2. 2.

    A Token borrows from src, src is dropped at the end of a block, and the token is printed after the block. What happens?

  3. 3.

    What does adding <'a> and &'a to fn longer change at run time?

  4. 4.

    Which elision rule gives fn get(&self, k: &str) -> &str its output lifetime?

  5. 5.

    A word splitter has fn next_word(&mut self) -> Option<&str>. Why does let a = w.next_word(); let b = w.next_word(); fail when a is used later?

  6. 6.

    Why does drip-rs never allocate while scanning?

References

  1. [1] Steve Klabnik, Carol Nichols, Chris Krycho (2026). The Rust Programming Language (3rd edition). No Starch Press; free online at doc.rust-lang.org/book. doc.rust-lang.org/book/ (checked 2026-10-31)
  2. [2] The Rust Project Developers. The Rust Reference: Lifetime elision. The Rust Reference (rust-lang/reference on GitHub, rendered at doc.rust-lang.org/reference). doc.rust-lang.org/reference/lifetime-elision.html (checked 2026-10-31)