← Back to Blog

Borrowing Without Lifetimes

April 2026 ownershipcompiler

Rust taught an entire generation of programmers to reason about lifetimes. It also taught them that typing <'a, 'b: 'a> is a tax you pay to get memory safety without a garbage collector. Lateralus asks: what if you paid less of that tax?

We ship a borrow checker. We do not ship lifetime syntax. The whole story is in compiler/borrowck/regions.ltl.

◉ Regions, not lifetimes

Every reference gets a *region* — a compiler-internal token the user never sees. Regions have an outlives relation the checker discovers by walking control flow. When you write:

fn longest(a: &String, b: &String) -> &String {
    if len(a) > len(b) { a } else { b }
}

the checker infers: the returned reference's region is the intersection of a's region and b's region. No annotation required. No 'a: 'b to explain to the user.

◉ Why does this work?

Two design choices.

Whole-program annotations are rare

Rust's explicit lifetimes pay off on public library APIs, where a caller deep in the ecosystem benefits from knowing "this result does not outlive the first argument." For application code — the 95% — the annotations carry no information the compiler couldn't infer locally. Lateralus does the local inference by default and lets you *opt into* explicit regions on library boundaries where you care.

Pipelines are linear

In a pipeline-heavy language, the value flow is already visible:

bytes
    |> parse_headers()
    |> route()
    |> handle(ctx)
    |> write_response(sock)

Each stage consumes its input. The region of any intermediate reference is bounded by the stage that produces it and the stage that consumes it. The checker can see that from the pipeline structure without a programmer drawing arrows.

◉ What the checker rejects

The same programs Rust rejects. We just report errors differently.

let mut v = [1, 2, 3]
let r = &v[0]        // shared borrow starts
v |> push(4)          // ERROR: `v` is mutably borrowed here
println(r)           // shared borrow ends

Lateralus says:

error: borrow of `v` conflicts with earlier borrow
  --> main.ltl:2:9
2 | let r = &v[0]
  |         ^^^^^ shared borrow starts here
3 | v |> push(4)
  | ^ mutable access while shared borrow is live

No lifetime names in the message. The spans are enough.

◉ Pipelines get a specialized rule

Because the pipeline operator is first-class syntax, the checker has a dedicated case: each stage's output region must outlive the next stage's input region. That's two lines in the implementation and it makes pipeline error messages strictly more localized than the general case.

◉ What you lose

Mostly: the ability to encode "this iterator borrows from both structures" at the type level. For that pattern you write:

#[region(r)]
fn zip_refs<'r>(a: &'r [T], b: &'r [U]) -> ZipIter<'r, T, U> { ... }

which looks suspiciously like Rust. That's fine — region annotations are always *available*, just never *required* for code the compiler can figure out on its own. The rule of thumb: if your function returns a reference derived from multiple inputs, write a region. Otherwise, don't.

◉ Why we're confident this scales

We've been running the checker against the full stdlib (~90 files, now mirrored at lateralus-stdlib) and the OS kernel in lateralus-os (200+ files). Zero explicit region annotations exist in either tree. Every module type-checks. Every test passes.

The conclusion: lifetime syntax was a tool for a problem that mostly has a local solution. When the compiler can see the whole function, it can infer what used to be typed by hand.

Try it in the playground

Every snippet on this page runs in your browser. No install, no signup.

▶ Open Playground Star on GitHub