Rust's Type System Without the Borrow Checker: What Lisette Is Actually Proposing
Source: lobsters
The conventional framing when comparing Rust and Go is that Rust gives you control and Go gives you simplicity. That framing collapses a lot of nuance. Go’s concurrency model is genuinely sophisticated; its runtime is one of the most battle-tested pieces of software infrastructure in production today. And Rust’s complexity is not uniformly distributed: most of the learning curve is concentrated in the borrow checker and lifetime system, while the rest of the language is a fairly ergonomic statically-typed ML derivative.
Lisette makes this separation explicit. Its tagline is “Rust syntax, Go runtime,” which is a compressed design statement with real implications. If the project succeeds on its own terms, it would mean that Go’s main weaknesses are in its type system and ergonomics, not its runtime, and that Rust’s main strengths are in its type system and ergonomics, not its memory model. Those are contestable positions, but they are not obviously wrong.
What the Go Runtime Actually Provides
The Go runtime is not a thin layer. Every Go binary embeds a goroutine scheduler, a garbage collector, a stack management system, and a set of synchronization primitives. These are not optional dependencies; they are compiled into the output binary and are always present.
The scheduler uses an M:N threading model, formally described as GMP: goroutines (G) are user-space green threads, OS threads (M) are the physical execution contexts, and logical processors (P) manage the assignment between them. GOMAXPROCS defaults to the number of available CPU cores since Go 1.5, so a program gets genuine parallelism without any configuration. Goroutines start with 2KB stacks (reduced from 8KB in Go 1.4) and grow via contiguous stack copying, which makes spawning hundreds of thousands of them practical. The scheduler has been fully preemptive since Go 1.14, using signal-based interruption at safe points rather than relying solely on cooperative yielding at function call boundaries.
The garbage collector is a concurrent tri-color mark-and-sweep design. Stop-the-world pauses dropped below 1ms with Go 1.8 and have continued improving since. The GC runs mostly concurrently with user goroutines on background threads, which keeps pause times bounded regardless of heap size. There is no generational collection, so the full heap is scanned each cycle, but the concurrency design keeps this from being user-visible in most workloads.
Channels are typed, goroutine-safe communication primitives embedded in the language and runtime. An unbuffered channel synchronizes sender and receiver directly; a buffered channel with capacity n allows n sends before blocking. The select statement multiplexes over multiple channel operations with random fair scheduling among ready cases. These are not library features bolted onto a threading model; they are runtime primitives with known memory model semantics.
This is what Lisette inherits. A language that runs on the Go runtime does not need to implement any of this. That is a substantial starting position.
The Rust Features That Live Outside the Borrow Checker
Rust’s reputation is largely defined by its ownership and borrowing system. That system is what eliminates use-after-free bugs, data races, and dangling pointers at compile time without a garbage collector. It is also the feature that generates the most friction for new users. Learning when and why the borrow checker rejects code takes real time.
But Rust has a set of features that do not depend on the borrow checker at all. Sum types via enum with associated data give you algebraic data types that most systems languages lack. Option<T> replaces nullable pointers with a type-level distinction between present and absent values. Result<T, E> makes error paths explicit in the type signature and composable with the ? operator. match expressions with exhaustive pattern matching let you handle every constructor of an enum at the call site, with the compiler enforcing completeness. Traits provide bounded polymorphism. Generics with trait bounds allow code reuse without the runtime overhead of Go’s pre-1.18 empty interface approach.
// This code has nothing to do with ownership or lifetimes.
// It's just an ergonomic type system.
fn parse_config(input: &str) -> Result<Config, ParseError> {
let lines: Vec<&str> = input.lines().collect();
match lines.first() {
None => Err(ParseError::Empty),
Some(header) => parse_from_header(header, &lines[1..]),
}
}
None of the above requires a borrow checker to be sound. If you have a garbage collector handling memory, Option and Result still encode important information in the type. Pattern matching exhaustiveness is still a useful compile-time check. Traits still give you principled polymorphism. These features work independently of how memory is managed.
Lisette’s bet is that this set of features is valuable enough to justify a new language, and that the Go runtime is a good foundation to run that language on.
The Transpilation Challenge: Encoding Sum Types in Go
The most technically interesting problem Lisette faces is encoding its richer type system into whatever it compiles to. If the compiler targets Go source (the most pragmatic approach for an early-stage project, and the one used by comparable experiments like Oden), it must represent Lisette’s enums in terms of Go’s type system, which has no native algebraic data types.
The standard Go idiom for sum-type-like patterns is an interface with an unexported marker method:
// Encoding a Lisette enum Result<T, E> into Go
type Result interface{ isResult() }
type Ok struct{ Value interface{} }
type Err struct{ Error string }
func (Ok) isResult() {}
func (Err) isResult() {}
// Pattern matching becomes a type switch
switch r := result.(type) {
case Ok:
process(r.Value)
case Err:
log.Println(r.Error)
}
This works but has costs. Interface dispatch adds indirection. The generated code is not idiomatic Go; a Go programmer reading it would find it unfamiliar. Type information is partially erased into interface{}. And crucially, the Go compiler cannot verify exhaustiveness: if you add a new variant to the Lisette enum and forget to update a match, the Go type switch silently falls through.
The Lisette compiler must do exhaustiveness checking itself during its own type-checking pass, before emitting Go. This is straightforward to implement but it means the safety guarantee lives in the Lisette compiler, not in the Go toolchain. Any debugging of the generated output requires understanding two layers of representation.
This pattern of two-layer development is familiar from other source-to-source compilers. TypeScript has it with JavaScript. Fable has it with F# targeting JavaScript. The tradeoff is usually considered acceptable for languages early in their lifecycle, since the compilation target provides a working runtime and ecosystem immediately.
Prior Art and Why It Mostly Didn’t Stick
Lisette is not the first attempt at this design space. Oden, developed around 2016 to 2018, was an ML-style functional language that compiled to Go and ran on the Go runtime. It had sum types, pattern matching, and a Hindley-Milner type system. It also had very few users and was eventually abandoned. The author cited difficulty with encoding the type system cleanly in Go’s type system, and the friction of maintaining a compiler for a single developer.
Crystal took a different approach: Ruby-like syntax, its own runtime, but Go-inspired channels and fibers (Channel(T) is a generic type in Crystal’s standard library). It found a community among Ruby developers who wanted static types and performance but did not want to switch paradigms. Nim similarly has its own runtime but can target C, JavaScript, or LLVM, giving it flexibility that a Go-runtime-locked language lacks.
The lesson from these projects is not that Lisette’s design is doomed, but that language adoption is an ecosystem problem as much as a language design problem. Go succeeded partly because Google shipped it with a standard library, a toolchain, and internal adoption. Rust succeeded partly because Mozilla used it for Firefox’s CSS engine, providing credibility. A language with Rust syntax and Go semantics needs a compelling enough story to pull developers away from both.
The Trade-Off in Plain Terms
If you are a Go developer frustrated by Go’s type system, Lisette’s pitch is coherent. You would keep everything you like about Go operationally: goroutines, channels, fast compilation, small self-contained binaries, a GC that does not demand manual tuning. You would gain Option, Result, pattern matching, and sum types. You would lose direct access to the Go ecosystem without a bridge layer, and you would accept that your code’s stack traces and profiling output show generated Go, not your source.
If you are a Rust developer, Lisette removes the feature that differentiates Rust from every other language. The borrow checker is not incidental to Rust’s design; it is the reason Rust can make guarantees that no GC language can match. Programs that need deterministic memory layout, sub-millisecond tail latency without GC pauses, or truly fearless concurrency without a garbage collector are not the target. Lisette is not trying to be Rust without the pain; it is trying to be a better-typed Go.
That is a narrower but more honest pitch. Go’s ergonomic weaknesses are real. The if err != nil pattern at scale generates noise. The absence of sum types requires discipline to substitute. Nil pointer dereferences are runtime errors that a type system could catch at compile time. If Lisette can address those weaknesses while keeping the Go runtime’s operational properties intact, it has a case to make, even if that case is only compelling to a slice of Go developers who care about these things enough to adopt a new toolchain.
Whether the project gets there is an open question. The compiler and runtime infrastructure are not trivial to get right, and every language that has tried to transplant Rust’s type system onto a different runtime has learned that the devil is in the encoding details. But the design question Lisette is asking is a legitimate one, and the answer is not predetermined.