· 7 min read ·

Separating the Notation from the Runtime: What Lisette Reveals About the Rust-Go Design Space

Source: lobsters

Programming language design has a recurring pattern: take a proven runtime and pair it with a different surface syntax. Erlang’s BEAM got Gleam, a statically typed language with ML-influenced syntax that compiles to Erlang bytecode. JavaScript got TypeScript, CoffeeScript, and a dozen others. The JVM has accumulated more guests than any single-family home can reasonably support. Lisette applies the same logic to a pairing nobody had explicitly committed to: Rust’s notation on top of Go’s runtime.

The premise is direct enough to state in four words, but it contains a meaningful bet about where the difficulty actually lives in both languages.

What Go’s Runtime Actually Is

When someone says “Go runtime,” they usually mean goroutines, and goroutines are worth understanding precisely before discussing what it means to build another language on top of them.

A goroutine starts with a 2 KB stack, which grows automatically as needed. The runtime multiplexes thousands or millions of goroutines onto a small pool of OS threads using an M:N scheduler: M goroutines onto N threads, where N is typically set to the number of CPU cores via GOMAXPROCS. The scheduler is work-stealing; idle threads pull goroutines from the queues of busy threads. Since Go 1.14, the scheduler is also preemptive at safe points, so a CPU-bound goroutine can be interrupted rather than monopolizing a thread.

Channels are typed message queues with configurable buffer sizes. The select statement lets a goroutine wait on multiple channel operations simultaneously, taking whichever one is ready first, or executing a default case if none are ready:

select {
case msg := <-work:
    process(msg)
case <-shutdown:
    return
default:
    // nothing ready, continue
}

This machinery has been refined across roughly fifteen years of production use at Google and across the broader ecosystem. The GC is a concurrent tri-color mark-and-sweep collector that has had its pause times driven down to sub-millisecond range in recent versions, with pause time targets configurable via GOGC and GOMEMLIMIT. The memory allocator is derived from tcmalloc, with size classes and per-thread caches to reduce contention.

This is not a simple thing to build. The Go team has been tuning this runtime since 2009. A new language that targets it, rather than reimplementing it, gets that investment for free.

What “Rust Syntax” Means Without the Borrow Checker

The natural question is what Rust syntax means when you remove the thing most people associate with Rust: the borrow checker and its lifetime annotations. The answer is that a surprising amount remains.

Rust’s enum system is substantially more expressive than what Go offers. Go has iota-based integer enums and type switches on interfaces. Rust has algebraic data types where each variant can carry different data:

enum Response {
    Ok(String),
    NotFound,
    Error { code: u32, message: String },
}

Pattern matching on this with match is exhaustive by default. The compiler requires you to handle every variant, or explicitly opt out with a wildcard. This is not just convenient; it prevents an entire class of bugs where a new variant is added to an enum and existing match sites silently fall through.

match response {
    Response::Ok(body) => send(body),
    Response::NotFound => send_404(),
    Response::Error { code, message } => log_and_send(code, message),
}

Go’s switch statement does not give you this. You can write a type switch, but the compiler does not tell you when you’ve missed a case. Adding a new type to an interface in Go is a refactoring problem you solve with grep and discipline; in a language with exhaustive matching, it’s a compile error.

Traits add another layer. Go’s interfaces are structurally typed and satisfied implicitly, which is elegant in its own way, but they cannot carry associated types, they cannot have default method implementations, and they cannot be used as bounds on type parameters in ways that carry behavioral guarantees. Rust’s trait system enables a style of generic programming where the bounds on a function signature are a machine-enforced contract: the function body is limited to what the bounds declare, and adding a new bound is a breaking API change.

Type inference via Hindley-Milner, closures with flexible capture semantics, ? for ergonomic error propagation, destructuring in let bindings: these are properties of Rust’s surface language that have nothing to do with ownership. A language can have all of them under a garbage collector.

The Borrow Checker as the Rubicon

Rust’s borrow checker is not ornamental. It enforces that every piece of data has a single owner, that references cannot outlive what they point to, and that mutable references are exclusive. These rules make the language memory-safe without a garbage collector: no use-after-free, no double-free, no data races at the language level.

Removing the borrow checker to add a GC is not a simplification so much as a swap. You trade one complexity budget for another. With a GC, you no longer have to think about ownership explicitly; the runtime tracks reachability and reclaims memory when nothing points to it. The cost is pause latency, throughput overhead, and the loss of a class of compile-time guarantees. Go’s GC is among the best available, but “best available GC” and “no GC” are not the same thing, and in latency-sensitive systems programming contexts, that difference shows up in tail latencies and heap pressure.

The other thing you lose is the borrow checker’s guarantee against data races. Go addresses this with its race detector (go build -race), a runtime tool that instruments memory accesses and reports concurrent unsynchronized reads and writes. It is not free: instrumented binaries run roughly 2-20x slower and use more memory. It catches races dynamically, meaning it only catches races that actually occur during the instrumented run. Rust catches them statically, meaning they cannot compile. A language built on Go’s runtime inherits Go’s approach to this problem.

For many applications, none of this matters. Web services, data pipelines, Discord bots, CLI tools, internal infrastructure: these are not programs where sub-millisecond GC pauses or static data race prevention are meaningful constraints. Go has proven this convincingly across more than a decade of production deployments. The design bet Lisette is making is that the programs Rust’s syntax is best suited for are not necessarily the programs where Rust’s runtime model is required.

Prior Art in This Design Space

The pattern of decoupling syntax from runtime has been tried across several dimensions.

Gleam is the closest structural parallel to what Lisette is attempting. It runs on the BEAM, the Erlang virtual machine, which provides preemptive lightweight processes (analogous to goroutines), message-passing concurrency, and hot code reloading. Gleam’s syntax draws heavily from ML and Rust: algebraic data types, exhaustive pattern matching, type inference, and a type system with no nulls. The BEAM is a forty-year-old runtime built for telecommunications-grade reliability; Gleam gets that for free and offers a modern syntax over it. Gleam also compiles to JavaScript, which extends the same syntax to a second runtime.

Lobster takes a different approach: Rust-inspired syntax with an ownership inference system that eliminates most explicit lifetime annotations while using a form of reference counting. It targets game development specifically and compiles to C++. Inko is another language in this neighborhood: Rust-like syntax, a concurrent GC with separate heaps per lightweight process, explicit concurrency. Both make the same observation that Lisette is making, that Rust’s syntax solves real ergonomic problems independently of whether the runtime is GC-based.

Nim is older and broader: Python-adjacent syntax, multiple GC strategies including arc (automatic reference counting) and orc (cycle-collecting arc), and compilation to C. Its approach to concurrency has historically been less elegant than Go’s goroutines, but the language demonstrates that good syntax and GC-based safety are not in conflict.

What This Pairing Targets

The practical audience for a language like Lisette is developers who find Go’s type system insufficient but find Rust’s ownership model more overhead than their applications require. This is not a small group. Go’s type system, despite the addition of generics in 1.18, still lacks sum types and exhaustive matching. Rust’s ownership model, despite tooling improvements and the ? operator and NLL (non-lexical lifetimes), still requires careful thought about data flow that GC languages simply do not demand.

The people who genuinely want Rust’s ADTs and pattern matching but are building network services or background processors that already live comfortably in a GC world have had limited options: accept Go’s less expressive type system, accept Rust’s ownership complexity, or learn a more obscure language. Lisette, if it delivers on its premise, offers a fourth option.

What remains to be seen is whether it can build enough library and tooling ecosystem to be practical, and whether the Go runtime’s interoperability story extends usably to a separate-syntax compiler. Gleam’s trajectory is instructive here: it took several years to reach the point where the ecosystem was useful for production work, and it benefited from full interoperability with Erlang and Elixir libraries from the start. A Rust-syntax Go-runtime language that can import Go packages would have a significant head start; one that cannot would be starting from scratch in a market where starting from scratch is expensive.

Was this interesting?