Mojo Lifetimes

A lifetime is the span of time during which a value exists in memory. Mojo tracks lifetimes to guarantee that you never hold a reference to memory that has already been freed. The compiler enforces lifetime rules automatically — you do not manage them manually, but understanding them helps you write code that the compiler accepts without struggle.

The Hotel Room Analogy

  Value = Hotel room
  Owner = Guest who booked the room
  Lifetime = Check-in date to check-out date

  You can give a friend your room key (borrow).
  Your friend must leave before you check out.
  If your friend keeps the key after you check out,
  the key opens an empty (or someone else's) room → BUG.

  Mojo prevents this by checking at compile time that
  all borrows end before the owner's lifetime ends.

Basic Lifetime Concept

fn main():
    var x = 42          ← x's lifetime begins here
    {
        var y = x       ← y's lifetime begins (copy of x)
        print(y)        ← y used
    }                   ← y's lifetime ENDS here (out of scope)
    print(x)            ← x still valid, lifetime continues
                        ← x's lifetime ENDS here (end of main)

Lifetime of References

A reference (borrow) must never outlive the value it refers to. The compiler checks this at compile time using lifetime analysis.

Correct — reference lives shorter than the value:

  fn get_first(borrowed lst: List[Int]) -> Int:
      return lst[0]    ← returns a copy, not a reference to lst's memory

  fn main():
      var numbers = List[Int](10, 20, 30)
      var first = get_first(numbers)    ← first is an Int copy
      print(first)   # 10
      # numbers still valid here

Lifetime Annotations

For advanced patterns where a function returns a reference into one of its arguments, Mojo uses lifetime parameters to express the connection between the input and output lifetimes.

# 'a is a lifetime parameter name
fn first_element[a: AnyLifetime](ref [a] lst: List[Int]) -> ref [a] Int:
    return lst[0]

This signature tells Mojo: "the returned reference lives exactly as long as the list that was passed in." Without this annotation, the compiler cannot verify safety.

Stack vs Heap Lifetimes

Stack lifetime:                     Heap lifetime:
  ┌────────────────────────┐          Allocated with UnsafePointer
  │ fn main():             │          Lives until you explicitly free it
  │   var x = 42  ←born    │          No automatic cleanup
  │   ...                  │
  │   print(x)             │          Stack is automatic and fast.
  │   ...                  │          Heap is manual and flexible.
  │                 dead→  │          Mojo structs live on the stack
  └────────────────────────┘          by default (unlike Python objects).

Struct Field Lifetimes

When a struct holds a reference to external data (not its own copy), the struct's lifetime must not exceed the referenced data's lifetime.

struct Slice:
    var data: UnsafePointer[Int]
    var length: Int

    fn __init__(inout self, ptr: UnsafePointer[Int], length: Int):
        self.data = ptr
        self.length = length

    fn get(self, index: Int) -> Int:
        return self.data[index]
Lifetime relationship:

  ┌─────────────────────────────────────────────┐
  │  Original array lives here (the owner)      │
  │                                             │
  │    ┌─────────────────────────────┐          │
  │    │  Slice struct               │          │
  │    │  (reference into array)     │          │
  │    │  Must not outlive the owner │          │
  │    └─────────────────────────────┘          │
  └─────────────────────────────────────────────┘

The Drop Order

When multiple variables in a scope end at the same time, Mojo destroys them in reverse declaration order (last declared, first destroyed). This mirrors the call stack's natural unwinding.

fn main():
    var a = Resource("A")   ← declared first
    var b = Resource("B")   ← declared second
    var c = Resource("C")   ← declared third
    print("Using resources")
                            ← scope ends:
                               c destroyed first
                               b destroyed second
                               a destroyed last
Drop order diagram:
  Declaration: A → B → C
  Destruction: C → B → A  (LIFO — last in, first out)

Lifetime and Function Calls

fn helper(borrowed data: String):
    print(data)
    # data borrow ends when helper returns

fn main():
    var text = String("Lifetime example")
    helper(text)          ← borrow starts and ends within this call
    print(text)           ← text still fully owned by main, usable here
                          ← text destroyed here (end of main)

Why Lifetimes Make Mojo Unique

Language      | Memory Safety Strategy
--------------|----------------------------------------------
C / C++       | Manual — you manage, bugs are your problem
Java / Python | Garbage collector — safe but slow/unpredictable
Rust          | Lifetime checker + ownership — safe + fast
Mojo          | Lifetime checker + ownership — safe + fast
              | (but simpler ergonomics than Rust in many cases)

Key Takeaways

A lifetime spans from when a value is created to when it is destroyed. References (borrows) must end before the value they reference is destroyed. The Mojo compiler checks lifetimes at compile time — no runtime overhead. Stack values are destroyed in reverse declaration order when their scope ends. Lifetime annotations on functions express the relationship between input and output reference durations. Understanding lifetimes lets you write performant, safe code that the compiler accepts without guessing.

Leave a Comment

Your email address will not be published. Required fields are marked *