Rust's Type System Without the Borrow Checker: The Design Logic Behind Lisette
Source: hackernews
Go and Rust occupy adjacent but very different space in the systems-adjacent programming world. Go’s pitch is simplicity, fast compile times, goroutines, and a garbage collector that is good enough that you rarely think about memory. Rust’s pitch is compile-time memory safety, zero-cost abstractions, and a type system expressive enough to make illegal states unrepresentable.
The gap between them is well-documented and genuinely felt by practitioners on both sides. Go developers write a lot of defensive code around nil pointers, perform type assertions at runtime, and work around the absence of algebraic data types with interface nesting that reads more like a workaround than a solution. Rust developers, meanwhile, spend the first several months fighting the borrow checker, and even experienced users hit friction when the ownership model does not map cleanly onto the problem at hand.
Lisette, which surfaced on Hacker News to meaningful community interest, sits squarely in this gap. It takes Rust’s type system ideas, strips out the ownership and borrowing model, and compiles to Go. The result is a language aiming to be more expressive than Go without being as demanding as Rust.
What “Inspired by Rust” Actually Means Here
When language designers say “inspired by Rust,” they usually mean one of two things: surface syntax, or type system design. Lisette leans toward the latter, which is the more substantive choice.
Rust’s most valuable contribution to programming language design is not the borrow checker. It is the combination of algebraic data types (enums with associated data), exhaustive pattern matching, and the Result/Option types that eliminate entire categories of runtime errors. These features do not require an ownership model. They require a richer type system than Go currently provides.
Go has interfaces, but interfaces are structural and implicit, which differs from Rust’s traits in ways that matter for correctness. Go has no native sum types. You can simulate them with interface embedding and type switches:
type Shape interface{ area() float64 }
type Circle struct{ Radius float64 }
type Rect struct{ Width, Height float64 }
func describe(s Shape) string {
switch v := s.(type) {
case Circle:
return fmt.Sprintf("circle r=%f", v.Radius)
case Rect:
return fmt.Sprintf("rect %fx%f", v.Width, v.Height)
default:
return "unknown"
}
}
The problem is that default branch. Go gives you no way to tell the compiler that this interface has exactly these two implementations and no others. Adding a new Shape does not trigger compile errors at every switch statement that handles shapes. Rust’s enum solves this at the type level, turning missed cases into compile errors rather than silent bugs. Lisette brings that same guarantee to a Go-backed runtime.
The practical payoff is real. Pattern matching over sum types is one of those features that, once you have it, makes you notice every place you’re working around its absence. Go developers write if err != nil checks on every other line partly because the language has no Result type to make the happy path and error path structurally distinct in the type system itself.
The Go Backend as a Strategic Choice
Compiling to Go rather than to native code or LLVM IR is a deliberate tradeoff, and it deserves to be evaluated as one.
The upside is substantial. Go’s runtime is mature and ships with a garbage collector that has improved dramatically since Go 1.5’s low-latency rewrite. Go’s goroutines provide lightweight concurrency that would take years to reimplement reliably from scratch. The Go standard library and module ecosystem become immediately available to Lisette programs. Go’s fast compilation speed means Lisette inherits a quick edit-compile-run loop that native-targeting languages often struggle to maintain at scale.
The readable output matters too. One of the persistent problems with transpiled languages is that debugging becomes harder when the generated code is unreadable. If Lisette generates idiomatic Go, you can drop into the Go layer when you need to, use Go’s tooling (including pprof, go vet, and the race detector), and reason about performance in terms you already understand.
The downside is that you give up what Rust is most celebrated for: predictable memory layout, zero-cost abstractions, and the ability to write code where you know exactly what is happening at the allocation level. Compiling to Go means compiling to a GC-managed runtime. Performance-critical embedded systems or kernel work is not the target.
For the workloads where Go is already the right choice, though, that is not a meaningful loss. Most Go developers are writing network services, command-line tools, and distributed systems infrastructure. For those workloads, Go’s GC is fine. The friction is in the type system, and that is exactly what Lisette addresses.
Precedents in the Transpiled Language Space
Lisette is not the first language to take “better type system, compile to existing runtime” as its thesis, and studying the precedents illuminates what tends to work.
Gleam is the clearest analogue. It is a statically typed language with ML-style algebraic data types, pattern matching, and Result/Option semantics that compiles to Erlang bytecode (and JavaScript). Gleam developers get exhaustive matching and type-safe error handling on top of the BEAM runtime, with access to the entire OTP ecosystem. The parallel with Lisette targeting Go is almost exact: you are taking a richer type system and placing it on top of a runtime you would rather not rewrite. Gleam has found a real audience, which suggests the appetite for this tradeoff is genuine.
TypeScript is the most commercially successful example of the pattern. It layered a structural type system on top of JavaScript’s runtime, and the ecosystem broadly accepted the tradeoff. The generated JavaScript is sometimes uglier than handwritten JavaScript, but it does not matter if you are not reading it. What matters is that the type system catches errors before you ship them.
Elm took a stricter approach: no runtime exceptions, enforced purity, algebraic types throughout, compiles to JavaScript. It is more opinionated than TypeScript but also more reliable in the domains where it applies. Lisette’s emphasis on Rust-inspired types suggests a philosophy closer to Elm than to TypeScript: stronger guarantees, narrower applicability.
The Absent Borrow Checker Is a Feature, Not a Gap
The conspicuous absence in Lisette is Rust’s ownership and borrowing system. This is the right call, and it deserves to be recognized as a design statement rather than a shortcut.
The borrow checker enforces memory safety at compile time. It prevents use-after-free, dangling references, and data races on shared mutable state. These guarantees matter enormously in systems programming, where you are managing raw memory and where bugs at that layer are both expensive and hard to reproduce.
For the workloads Lisette targets, those guarantees are less critical. You are not managing raw memory; Go’s GC is handling that. What you lose is Rust’s data race prevention, which is genuinely valuable. Go’s answer to data races is conventions, channel-based communication, and the runtime race detector. That is a weaker guarantee than Rust’s compile-time enforcement, but it is the guarantee Go developers already work with.
The borrow checker is also what makes Rust hard to learn. Removing it lowers the barrier to entry significantly. The remaining type system features, sum types, pattern matching, explicit error handling via Result, are conceptually accessible to most developers with any functional programming background, and increasingly to developers who have absorbed Rust-adjacent ideas through languages like Swift or Kotlin.
What This Signals About Go’s Ecosystem
Languages like Lisette rarely replace their target platform languages. TypeScript did not kill JavaScript. Gleam has not replaced Erlang. But they apply pressure on the host language to address the gaps they fill. Gleam’s growth has made the BEAM community more attentive to type safety. TypeScript’s ubiquity pushed JavaScript itself toward better type annotation proposals.
If Lisette finds an audience, it becomes a concrete demonstration that Go’s type system gaps are significant enough to push developers toward transpilation. The Go team has been cautious about adding complexity to the language, and that caution has preserved Go’s signature legibility. But generics arrived in 1.18 after years of resistance, because the community case became undeniable.
Sum types and exhaustive pattern matching are the next obvious frontier. They are not exotic features; they have been standard in ML-family languages since the 1970s and have made their way into Swift, Kotlin, Scala, and TypeScript. A language at 235 points on Hacker News that compiles Rust-style enums to Go is, among other things, a vote on what Go is missing.
Whether Lisette itself becomes a production tool or remains an experiment, the design question it is asking is the right one.