Skip to content

Lobster — Compile-Time Reference Counting

A pragmatic language by Wouter van Oortmerssen that elides 95% of refcount ops at compile time through flow-typed lifetime analysis. Cycles handled by a cleanup at program exit.

§1 Provenance

§2 Mechanism

Lobster’s compiler does flow-sensitive lifetime analysis on the AST. Every refcounted value has a known “owner” at every program point. The compiler then:

  1. Elides inc on move. When ownership transfers (assignment, function-arg pass) without aliasing, no refcount op is emitted. This is the “moved-from doesn’t need destruct” property.
  2. Elides dec on consumed values. A value passed into a sink (e.g. the receiver of a method that drops it) is not separately dec’d.
  3. Elides everything for non-escaping function values. Lobster’s closures are non-capturing — they are bare code pointers; free variables must still be in scope at the call site. This makes them zero-cost and removes a whole class of allocations.
  4. Inserts dec at the lexical scope exit for local owners that haven’t been moved out.

Cycle detection runs once at program exit, doing a single mark-sweep over what’s left. Cycles never break correctness; they merely accumulate until shutdown. The author argues this is fine for game-style programs (Lobster’s target domain).

The author claims ~95% of refcount operations are removed by the analysis. The remaining 5% (the ones that can’t be statically resolved — usually heterogeneous container elements) run at runtime as ordinary RC.

§3 Memory-safety property

Spatial + temporal safety from RC + bounds checks. No UAF because RC drops happen at known lexical points. No double-free because moves are tracked.

The compile-time analysis doesn’t add safety on top of what RC provides — it just removes the runtime cost. Cycles are not collected during execution, so genuinely leaking programs leak until exit; this is a memory-pressure issue, not a safety one.

§4 Production status (May 2026)

Lobster is single-author, MIT-licensed, actively maintained on GitHub (aardappel/lobster). It has a real user base in game-jam and educational contexts but no industrial deployments visible from public sources. It is most useful as an existence proof: a single person can build a language whose memory management is fast, predictable, and correctness-checked at compile time without going full Rust.

No mainstream benchmark suite published. The author has shared microbenchmarks where Lobster’s compiled C++ output approaches Lua-JIT in scripting workloads and matches naive C++ in tight loops.

§5 Cost

  • Throughput. Comparable to Lua or modern Python for the interpreted backend; close to naive C++ for the LLVM/C++ backend, because the RC ops vanish.
  • Memory. Low. The runtime is tiny (a header + a small allocator).
  • Latency. Predictable — no GC pauses ever, except a single cleanup at exit.
  • Compile time. Slightly increased by lifetime analysis but Lobster compiles fast in absolute terms.
  • Hidden cost. AST-based analysis is “limiting/complex” — Rust moved to CFG-based NLL for the same reason. Lobster’s author concedes it’s flow-typed but acknowledges the limitation.

§6 Mochi adaptation note

Lobster’s lesson for vm3 is the bound: how much RC traffic can be statically eliminated. If even half of vm3’s planned OP_DUP_HANDLE / OP_DROP_HANDLE ops can be elided at compile time, the steady-state alloc overhead from MEP-40 Phase 1 (1.4-1.7× of make()) drops further.

Concrete smallest patch:

  1. Compiler3 ownership pass (MEP-40 §7.3). Add an ownership.go pass that runs after type-driven lowering. For each IR value, compute:
    • consumed_by_call: is this value passed by-value into a function whose body consumes it?
    • escapes_scope: does this value flow into a global / closure capture / longer-lived parent?
    • aliased: are there multiple live names for this slot at the same program point?
  2. Emit-time elision. When a Last-Use(x) node has consumed_by_call && !aliased, do not emit OP_DROP_HANDLE; the callee will drop. When two adjacent uses are (Use, Move) with no intervening alias, do not emit OP_DUP_HANDLE.
  3. Non-capturing closures. Mochi already distinguishes closures with captures from those without; the former allocate a kArenaClosure cell. Adopt Lobster’s stricter rule: in tight inner loops, prefer non-escaping function literals (no closure cell at all). This is a compiler hint, not a language change.

There is no Go-hosting conflict. Go’s GC is unaffected because we are not changing what’s on the heap, only how often the slab Free path runs.

§7 Open questions for MEP-41

  • Should Mochi’s analysis be AST-based (like Lobster) or CFG-based (like Rust NLL)? CFG is more accurate but is a larger lift for compiler3.
  • What fraction of dup/drop ops in the Mochi corpus can be statically eliminated? Need a measurement before committing to the pass.
  • Does the “cycle collector runs only at exit” trade-off work for long-running Mochi server processes (web handlers, daemons)? Maybe an opt-in periodic cycle pass is needed.
  • Lobster non-escaping closures are a language-level restriction. Mochi closures today do escape (used in map, filter, reduce pipelines and stored in lambdas). Can we get the win for the common case by detecting non-escape statically?

Sources