Borrowing Without Lifetimes
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