← Back to Blog

Writing the First Lateralus Compiler

February 1, 2025 historycompiler

The story of building the first Lateralus compiler — from idea to working prototype in three months. This is the tale of late nights, syntax debates, and the moment "Hello, World" finally compiled.

◉ The Genesis

It started with frustration. Every time I wanted to write a data transformation, I'd end up nesting function calls five levels deep. Why couldn't code just flow like data naturally does? That question became Lateralus.

The initial prototype was hacked together in Python over a weekend — just enough to prove the pipe operator could work. By Monday, I had this compiling:

// This was the first Lateralus program that ever ran
let nums = [1, 2, 3, 4, 5]
nums |> filter(fn(x) { x > 2 }) |> map(fn(x) { x * x }) |> println

Seeing [9, 16, 25] print to the console was electric. The pipeline vision was real.

◉ Bootstrapping

The first "real" compiler was written in Python. I know, I know — but Python's AST module made prototyping trivial, and we needed to iterate fast on syntax.

The architecture was simple:

The Python backend was the fastest path to "working." We could compile Lateralus to Python, run it, and see if the semantics were right. Only later did we add the bytecode VM for performance.

◉ The Type System Wars

Type inference was contentious. Should we require annotations everywhere (like Rust)? Allow full inference (like Haskell)? We landed on a middle ground:

// Function signatures need annotations
fn add(x: int, y: int) -> int {
    return x + y
}

// But locals are inferred
let result = add(1, 2)  // result: int, inferred

The Hindley-Milner algorithm handles let-polymorphism, which means this works:

fn identity(x) { return x }

let a = identity(42)      // a: int
let b = identity("hello") // b: str - same function, different type!

◉ Design Decisions

Why pipelines? Because x |> f |> g |> h reads left-to-right, like natural language. Compare: h(g(f(x))) — you have to read inside-out.

Why Hindley-Milner? Because inference reduces boilerplate while preserving type safety. No runtime type errors, but also no annotation gymnastics.

Why Python as first backend? Pragmatism. Python is everywhere. By targeting Python, Lateralus immediately inherits access to NumPy, Pandas, requests, and 400,000 other packages.

◉ The First Real Program

The first non-trivial Lateralus program was a CSV transformer:

import io
import strings

fn main() {
    io.read_file("sales.csv")
        |> strings.split("\n")
        |> filter(fn(line) { len(line) > 0 })
        |> map(fn(line) {
            let parts = strings.split(line, ",")
            return { name: parts[0], amount: float(parts[1]) }
        })
        |> filter(fn(row) { row.amount > 1000 })
        |> sort_by(fn(row) { row.amount })
        |> reverse()
        |> take(10)
        |> each(fn(row) { println(row.name + ": $" + str(row.amount)) })
}

It worked. The entire ETL pipeline, readable top-to-bottom. No temporary variables needed.

◉ Lessons Learned

  1. Ship something. The Python prototype was ugly. It worked. We iterated.
  2. Syntax matters. We spent weeks debating |> vs >> vs ->. The pipe won because it looks like data flowing through a pipe.
  3. Test with real programs. Unit tests caught bugs, but real programs caught design flaws.
  4. Type errors should help, not punish. We rewrote error messages three times until they actually told you what to fix.

◉ What's Next

The compiler today is vastly more sophisticated — LLVM backend, LSP server, debugger support — but the core insight from day one remains: code should flow like data flows.

Want to try it? The playground is running the latest compiler. Write some pipelines and see for yourself.