· 5 min read ·

Rust Syntax Without the Borrow Checker: What Lisette Is Actually Trading Away

Source: lobsters

There’s a recurring fantasy in systems programming circles: keep the expressive syntax from Rust, throw away the borrow checker, and use a garbage collector instead. Lisette is a language making that bet directly, describing itself as “Rust syntax, Go runtime.” The pitch is clean, but the details reveal a genuinely interesting set of trade-offs.

To understand what Lisette is doing, you have to first disentangle two things that Rust makes feel inseparable: its syntax and its memory model.

What “Rust Syntax” Actually Means

Rust’s syntax is expressive in ways that Go’s deliberately is not. The most valuable pieces are not about memory at all. Algebraic data types via enum, exhaustive match expressions, impl blocks for organizing methods, the trait system for ad-hoc polymorphism, Result<T, E> and Option<T> as first-class error and null handling, and pervasive type inference. None of these depend on the borrow checker. They are ergonomic and compositional features that make code readable and correct-by-construction in ways that have nothing to do with lifetimes or ownership.

Go, by contrast, made a deliberate choice to stay minimal. It has interfaces but they are structural (duck-typed), not nominal. It has no enums with associated data, no pattern matching in the algebraic sense, no generics until 1.18 (and even then they are simpler than Rust’s trait bounds). Error handling is explicit but verbose: the if err != nil pattern is everywhere. Null exists implicitly via nil pointers. None of this is accidental, it reflects Go’s design philosophy of readability through simplicity and familiarity.

So when Lisette says “Rust syntax,” what it is really offering is: enums, pattern matching, traits, Option/Result, and a more Rust-flavored type system surface, sitting on top of a runtime that was never designed to support them natively.

What Go’s Runtime Actually Provides

Go’s runtime is genuinely excellent at what it does, and it does a lot. The goroutine scheduler implements M:N threading: many goroutines (M) multiplexed across a smaller number of OS threads (N), managed by a work-stealing scheduler. Since Go 1.14, goroutines are asynchronously preemptible via signals, which solved the old problem of tight loops starving the scheduler. Creating a goroutine costs roughly 2-8KB of stack (which grows dynamically), compared to the 1-8MB typically allocated for OS threads.

The garbage collector is a concurrent tricolor mark-and-sweep design. Go’s GC has improved substantially over the years, with the runtime targeting sub-millisecond stop-the-world pauses since around Go 1.5, and actual production p99 pause times often well under that for most workloads. The GC is not free in throughput terms, but for latency-sensitive network services it works extremely well.

Channels, select statements, and the sync package round out the concurrency story. This is a mature, production-hardened runtime that runs at Google scale and in many latency-sensitive systems. Lisette gets all of this without having to build any of it.

The Borrow Checker Is Not Syntax

Here is the crux of it. The borrow checker is not a syntactic feature. Lifetime annotations ('a, 'b) appear in the syntax, but they are an expression of the ownership and borrowing rules enforced by the compiler. Remove the GC and the borrow checker enforces memory safety. Add a GC and you no longer need the borrow checker for that purpose.

What you lose is not just a compiler pass. You lose the static guarantees about aliasing and mutation that lifetimes encode. You lose the ability to reason at compile time about exactly when memory is freed. You lose the guarantee that certain classes of data races are impossible without touching unsafe. You also lose unsafe itself as a meaningful boundary, since the runtime’s GC changes what “unsafe” even means.

For Lisette’s target use case, this is a deliberate and honest trade. Concurrency bugs prevented by the borrow checker become concurrency bugs caught by the Go runtime’s race detector instead, or not caught statically at all. Memory safety comes from the GC, not from the type system. Whether that’s acceptable depends entirely on what you’re building.

This Combination Has Precedent

Swift made a nearly identical bet in a different direction: it borrowed much of Rust’s expressiveness (enums with associated values, pattern matching, a protocol system that closely mirrors traits, Optional<T>) while using ARC (automatic reference counting) instead of a borrow checker or a tracing GC. Swift 5.9 introduced ownership annotations and a borrow checker opt-in via the ~Copyable mechanism, but the core language works without them.

Kotlin put a Scala-flavored expressive type system on the JVM, accepting JVM GC semantics in exchange for the ecosystem and tooling. Dart runs on its own VM with sound null safety but no ownership model.

The pattern is consistent: take syntax and type system ergonomics from one tradition, plug in a different memory management strategy, and accept the associated trade-offs. Lisette is doing this with the specific combination of Rust’s ergonomic surface and Go’s runtime, which is interesting because Go’s runtime is unusually well-suited to network services and concurrent workloads.

The Ecosystem Question

One thing that does not come for free with “Go runtime” is the Go ecosystem. Rust syntax compiling to Go-compatible binaries presumably means some level of interoperability with Go packages and modules, but the specifics matter enormously. If Lisette can import Go packages directly, that is a significant advantage. If it requires FFI bindings or a translation layer, the value proposition weakens substantially, since you are taking on the complexity of a new language without inheriting the library ecosystem.

Go’s module system, toolchain, and standard library are major reasons people choose Go. net/http, encoding/json, database/sql, the whole x/ namespace. If Lisette treats Go libraries as first-class dependencies, it starts from a very strong position. If not, it is essentially asking developers to build a new ecosystem from scratch while also learning a new syntax.

Where This Actually Makes Sense

The honest use case for something like Lisette is someone who genuinely likes writing Rust-style code (pattern matching on results, exhaustive enums, trait dispatch) but finds the borrow checker friction too high for the domain they are working in. Network services, web backends, CLI tools, data pipelines: these are workloads where Go’s GC is perfectly adequate, where goroutines are the right concurrency primitive, and where Rust’s syntax ergonomics would be welcome but its memory model is overkill.

For systems programming in the actual sense, code that manages memory explicitly, interacts directly with hardware, needs zero-cost abstractions and predictable allocation patterns, this trade-off is wrong. The borrow checker exists because the domains that use Rust need the guarantees it provides.

But those are not the only workloads that exist. Go became extremely successful by being good enough at performance while being excellent at developer productivity for a specific class of software. If Lisette can carve out a position as “Go’s runtime, with syntax that more experienced type-system users find more pleasant,” that is a real and defensible niche.

The question is whether the implementation quality, tooling, and interoperability story can get to a point where choosing Lisette over writing Go directly is a net win for a real team shipping real software. That is a high bar. But the underlying design premise is coherent, and the combination of runtime and syntax they have chosen is not arbitrary. It reflects a genuine understanding of what each piece brings.

Was this interesting?