There is a recurring tension in language design between what a language looks like and what it does at runtime. Rust’s syntax has accumulated a well-deserved reputation for expressiveness: algebraic data types, exhaustive pattern matching, trait bounds, no implicit null. Go’s runtime has its own reputation for operational simplicity: goroutines that scale to millions, a garbage collector tuned for low pause times, a scheduler that handles blocking syscalls without pinning OS threads. These strengths live in different places. Lisette is a new language that tries to have both, using Rust’s syntax as its surface language while running on Go’s runtime.
That is a deliberate design position, and it carries specific consequences worth thinking through.
What Rust’s Syntax Actually Gives You
When people say they want Rust’s syntax, they usually mean a few concrete things.
First, algebraic data types. Rust’s enum is nothing like C’s or Java’s. You can encode state machines, error cases, and optional values directly in the type:
enum ConnectionState {
Connecting { timeout_ms: u32 },
Connected(Socket),
Failed(IoError),
}
The compiler forces you to handle every variant. There is no implicit fall-through, no forgotten case. This is not just aesthetic; it eliminates entire classes of bugs that appear in code written with nullable references or integer status codes.
Second, pattern matching. Rust’s match is structural, exhaustive, and composable. You can destructure nested types, bind variables, and add guards in a single expression. Languages like Haskell pioneered this, ML dialects refined it, and Rust brought it to systems programming.
Third, the trait system. Traits in Rust serve as both interfaces and a mechanism for bounded polymorphism. They are not runtime vtables by default; the compiler monomorphizes generic code at compile time, generating specialized implementations for each concrete type. The result is zero-cost abstraction in the literal sense: generics compile down to the same machine code you would write by hand.
Fourth, and probably most important for everyday ergonomics: Option<T> and Result<T, E> in place of null and exceptions. The type system refuses to let you ignore the possibility of failure. The ? operator makes propagating errors almost as lightweight as exceptions while keeping them visible in the type signature.
None of this requires Rust’s memory model. These are syntactic and type-system features that could, in principle, compile to any runtime.
What Go’s Runtime Actually Gives You
Go made a set of runtime bets in 2009 that have held up remarkably well. The most significant is the goroutine scheduler.
Goroutines are multiplexed onto OS threads by the Go runtime using a work-stealing scheduler. The canonical model is G-M-P: goroutines (G) run on processors (P), which are scheduled onto OS threads (M). When a goroutine blocks on a syscall, the runtime detaches its P and gives it to another M, so other goroutines can continue running. This means you can spawn hundreds of thousands of goroutines with manageable overhead; each starts with a small stack (around 8KB by default) that grows dynamically as needed.
The consequence for application code is significant. You write straightforward blocking code and the runtime handles the multiplexing. There is no async/await coloring problem, no explicit callback chains, no distinction between sync and async contexts that propagates through your function signatures. A goroutine that blocks on a network read looks identical to one doing CPU work.
Go’s garbage collector has also improved substantially since the language launched. By Go 1.21 and later releases, typical GC pause times are well under a millisecond in latency-sensitive applications. The collector uses a tri-color concurrent mark-and-sweep algorithm, running most of its work concurrently with the application. It is not zero-pause, but it is production-proven at enormous scale.
Channels give you typed communication between goroutines with backpressure built in. Buffered channels let you decouple producers from consumers; unbuffered channels give you synchronization points. The select statement lets you wait on multiple channel operations simultaneously, covering the common patterns that would require complex callback registration in other models.
The Deliberate Omission
The most consequential design decision in Lisette is what it does not carry over from Rust: the borrow checker.
Rust’s ownership system is the mechanism by which the compiler guarantees memory safety without a garbage collector. It is also the primary source of the “fighting the borrow checker” experience that drives developers away from Rust. Self-referential data structures, shared mutable state, callbacks that capture references across async boundaries: all of these require careful choreography under the borrow checker, and sometimes reach for Rc<RefCell<T>> or Arc<Mutex<T>> wrappers that erode the ergonomic benefits the syntax was supposed to provide.
Lisette sidesteps this entirely by using Go’s GC for memory management. Heap allocation, reference cycles, shared mutable state: the runtime handles reclamation. You lose the compile-time memory safety guarantees that Rust provides, but you gain the ability to write the kinds of code that the borrow checker resists most.
The trade-off deserves clear accounting. Rust’s memory safety guarantees eliminate use-after-free, data races, and iterator invalidation at compile time. Lisette, running on Go’s runtime, gives up some of those guarantees in exchange for a much lower barrier to writing concurrent code. Go’s race detector can catch data races at runtime, and it is an effective testing tool, though it provides no compile-time guarantees. Whether that exchange is worth making depends entirely on what you are building.
Prior Art in Hybrid Language Design
Lisette is not the first project to ask whether you can separate a language’s syntax from its runtime.
Gleam runs on Erlang’s BEAM runtime under a Rust-influenced, type-safe syntax. BEAM gives you actor-model concurrency, preemptive scheduling, and fault tolerance; Gleam provides a sound type system and ML-style pattern matching on top. The combination is compelling for the same reasons Lisette is: you get the operational characteristics of a battle-tested runtime with the ergonomics of a more expressive syntax.
Elixir took a similar approach earlier, layering a Ruby-influenced syntax on BEAM. It is now one of the more popular functional languages in production, partly because BEAM’s fault tolerance is genuinely hard to replicate elsewhere and partly because Elixir’s syntax is approachable from day one.
Moving closer to the Rust-Go space, Mojo goes in the opposite direction: it keeps Python’s surface syntax while introducing Rust-style ownership and explicit memory management underneath. The goal is Python ergonomics with systems-level performance. Lisette takes the inverse approach, keeping Rust’s surface syntax while replacing the memory model with a GC runtime.
All of these projects are making the same underlying observation: runtime characteristics and syntax ergonomics are separable concerns, and the combinations most languages offer are historical accidents as much as principled choices.
The Concurrency Angle
Where Lisette’s combination becomes most interesting is concurrency. Rust’s async story, while powerful, carries real complexity. The async/await model requires that function signatures propagate their async nature throughout call chains; mixing sync and async code requires care; the ecosystem has historically split between tokio, async-std, and other runtimes, though tokio has largely won out. The function color problem remains a genuine friction point.
Go’s goroutine model avoids this architecture entirely. If Lisette gives you Rust’s expressive type system for encoding data and error states while using goroutines and channels for concurrency rather than futures and async runtimes, that is a genuinely ergonomic combination. You could write fan-out patterns with straightforward blocking code, collect results through a typed channel, and propagate errors through Result types, all without an async runtime, an executor, or a pinning discussion.
The Go runtime’s concurrency primitives are mature in a way that matters for production code. The scheduler has been tuned over fifteen-plus years of use at Google and across the broader ecosystem. Channel semantics are stable, well-understood, and have extensive tooling around deadlock detection. Grafting Rust’s type expressiveness onto that foundation addresses Go’s most common complaint (limited type system) without discarding what Go’s runtime does well.
Who This Is For
Lisette occupies a specific spot in the language landscape. It is not aimed at applications where compile-time memory safety guarantees are non-negotiable; Rust exists for that. It is not aimed at projects that need the simplicity and tooling maturity of standard Go; Go already exists and its ecosystem is large. The target is the developer who finds Go’s type system constraining, who reaches for interface{} or any and loses static guarantees, who wants pattern matching and algebraic types but finds the goroutine concurrency model genuinely well-suited to what they are building.
The frustration with Go’s type system, particularly before generics landed in Go 1.18, drove real interest in alternatives. Generics helped, but the lack of sum types and exhaustive pattern matching remains a genuine gap for code that needs to model complex state. A connection status, a message type in a protocol implementation, a task queue entry with different execution modes: these all benefit from a type system that can enforce completeness at the match site.
Lisette is a bet that Rust’s syntax improvements are separable from Rust’s runtime requirements, and that Go’s runtime is worth keeping. Given how many developers already use both languages in different contexts and reach for each one’s specific strengths, that bet is grounded in real experience rather than purely theoretical motivation.