I'm curious about the compiled Go output though. The Result desugaring gets pretty verbose, which is totally fine for generated code, but when something breaks at runtime you're probably reading Go, not Lisette. Does the LSP handle mapping errors back to source positions?
Also wondering about calling Lisette from existing Go code (not just the other direction). That feels like the hard part for adoption in a mixed codebase.
Is the goal here to eventually be production-ready or is it more of a language design exploration? Either way it's a cool project.
I know it is Rust inspired, but why write it in Rust and not Go?
Lisette brings you the best of both worlds.
I do think there may be a limit to how far it can be improved, though. Like typed nil means that a variable of an interface type (say coming from pure Go code) should enter Lisette as Option<Option<http.Handler>>. Sure, one can match on Some(Some(h)) to not require two unwrapping steps, but it becomes a bit awkward anyway.
Lisette also doesn't remove the need to call defer (as opposed to RAII) in the very awkward way Go does. E.g. de facto requiring that you double-close on any file opened for write.
Typescript helps write javascript, but that's because until WASM there was no other language option to actually run in the browser. So even typescript would be a harder sell now that WASM can do it. Basically, why try to make Go more like Rust when Rust is right there? And fair enough, the author may be aiming for somewhere in between. And then there's the issue of existing codebases; not everything is greenfield.
So this seems best suited for existing Go codebases, or when one (for some reason) wants to use the Go runtime (which sure, it's at least nicer than the Java runtime), but with a better language. And it does look like a better language.
So I guess what's not obvious to me (and I mentioned this to the author) is what's the quick start guide to having the next file be in Lisette and not Go. I don't think this is a flaw, but just a matter of filling in some blanks.
[1] https://blog.habets.se/2025/07/Go-is-still-not-good.html
But I can't help wondering:
If it is similar to Rust why not make it the the same as Rust where it feature-matches?
Why import "foo.bar" instead of use foo::bar?
Why Bar.Baz => instead of Bar::Baz =>? What are you achieving here?
Why make it subtlety different so someone who knows Rust has to learn yet another language?
And someone who doesn't know Rust learns a language that is different enough that the knowledge doesn't transfer to writing Rust 1:1/naturally?
Also: int but float64?
Edit: typos
What would be actually nice is running async Rust on the Go green threads runtime.
I love Rust for what it is, but for most of my projects, I canβt justify the added complexity. Sure, there are a bunch of things I miss from the Rust world when Iβm working on large-scale distsys services in Go, but introducing Rust in that space would be a recipe for disaster.
I guess the Go team knows that if they start adding everyoneβs favorite Rust features, the language would become unrecognizable. So weβre not getting terser error-handling syntax or enums. Having union types would be nice too.
But I work in platform engineering, so my needs are quite different from someone writing business logic in Go. I understand that having a more expressive syntax is nice when youβre writing complex business code, but in reality, that almost always comes with a complexity/fragility tradeoff. Thatβs part of the reason no one wants to use Rust to write their business logic, despite it being so much more expressive.
For distsys, programming ergonomics matter far less compared to robustness and introspectability. So the Go runtime with Go syntax is perfect for this. But of course, thatβs not true for all use cases.
Sorry for the rant - completely uncalled for. This is a cool project nonetheless :)
Go gives you access to a compute- and memory-efficient concurrent GC that has few or no equivalents elsewhere. It's a great platform for problem domains where GC is truly essential (fiddling with spaghetti-like reference graphs), even though you're giving up the enormous C-FFI ecosystem (unless you use Cgo, which is not really Go in a sense) due to the incompatibilities introduced by Go's weird user-mode stackful fibers approach.
Then realized Rust wasn't that hard.
Rust, of course superbly achieves its goals within its niche! But it is a niche, is my meaning here.
What I actually want is code that's correct, but ergonomic to write. So my ideal language (as strange as it sounds) would be Rust with a GC.
I don't want to worry about what string type I'm using. I want it to just work. But I want it to work correctly.
Lisette looks like it's in this exact category! It seems to combine the best aspects of both Rust and Go, which is a very promising endeavour. I'll have to take a proper look :)
if I can incorporate Lisette into my golang projects for example, (Invoking rust code within Golang to me feels like a larger problem and Invoking C might be easier from my tinkering experiments) I feel like you are viewing this from a pure performance metric but to be honest, most things aren't necessary to be the fastest, the type system of rust/rust-alike languages can be beneficial to people as-it-is
Check out gleam, its based on erlang so it has a runtime involved, people love gleam because it gives them a bit more expressiveness in the type system from what I've heard.
I feel like these experiments are genuinely nice, Also perhaps a project like this can then slowly also invoke tinyGo (there was a recent discussion about it too) and could be compiled into tinyGo in future iterations to have no runtime essentially as well. People who love rust, love it, but most people really find it hard to get-into as compared to golang, I really love golang for its simplicity but I wish to tinker with rust too, so if Lisette combines both of these things and atleast makes me familiar with more rust without having to jump into too many hoops
As for int and float64, this comes from Go's number type names. There's int, int64, and float64, but no float. It's similar to how Rust has isize but no fsize.
Last commit was 9 years ago though, so targets Python 2.7.
isize is the type for signed memory offsets, fsize is completely nonsensical.
β : I've tried to transpile Rust code through WASM into Go assembly, and I've also explored how to inject trampolines into Go binaries (which involves generating Go assembly too).
When did OCaml get affine types? Or unique references?
In C/C++ you have the #line preprocessor directive. It would be nice if Go had something similar.
[1] https://github.com/ivov/lisette/blob/main/tools/bindgen/READ...
Having great tools. Excellent documentation. Being friendly to new users.
Yes, it's also a systems language without a runtime. But that's not the novel part. You could write horrors in C++ that approximate ML even without language support. There are eldritch libraries where some kind of pattern matching is done via generic lambdas.
The main difference is developper UX. Good tools, good error messages, quality of life. The novelty is making ML not painful.
Why would there be? Goβs assembly might be lacking ways to make them optimally efficient, but thatβs probably a given either way without an optimizing compiler backend.
Being memory safe without being managed is what makes rust a truly novel and interesting language for which it rightfully gets the hype.
Low level strong correctness was absolutely a novel part. In fact itβs exactly why many people glommed onto early rust, and why it was lowered on the stack.
Although learnability and weirdness budgets were also extremely novel in low level contexts which had been subsumed by C and C++.
> horrors in C++
Yes, horrors in C++. Half baked jerry-rigged and barely usable nonsense. Not an industrial strength langage with a reliable type system and a strong focus on safety through types.
There's also the support for concurrency and parallelism, which has started to improve recently, but is still years behind what is available in Go (but still better in my opinion than what is available in Rust).
Still, where absolute max performance or realtime are not required, I'd choose OCaml as it is elegant & a pleasure to code in (personal opinion, ymmv).
I can expand on any of those if you disagree with them.
Algebraic data typesβΒ·βPattern matchingβΒ·βNo nilβΒ·β
Hindley-Milner type systemβΒ·βImmutable by default
Interoperability with Go's ecosystem
> cargo install lisette
import "go:fmt" import "go:io" import "go:os"
fn load_config(path: string) -> Result<Cfg, error> { let file = os.Open(path)? defer file.Close() let data = io.ReadAll(file)? parse_yaml(data) }
fn main() { match load_config("app.yaml") { Ok(config) => start(config), Err(e) => fmt.Println("error:", e), } }
Enums & pattern matching
enum Message { Ready, Write(string), Move { x: int, y: int }, }
fn handle(msg: Message) -> string { match msg { Message.Ready => "ready", Message.Write(text) => f"wrote: {text}", Message.Move { x, y } => f"move to ({x}, {y})", } }
Structs & impl blocks
import "go:math"
struct Point { x: float64, y: float64, }
impl Point { fn distance(self, other: Point) -> float64 { let (dx, dy) = (self.x - other.x, self.y - other.y) math.Sqrt(dx*dx + dy*dy) } }
Expression oriented
fn describe(score: int) -> string { let grade = if score >= 90 { "A" } else if score >= 70 { "B" } else { "C" }
let stars = { let count = score / 20 strings.Repeat("*", count) }
f"{grade} {stars}" }
Chaining and lambdas
fn server_url() -> string { let scheme = os.LookupEnv("HTTPS") .filter(|s| s == "1") .map(|_| "https") .unwrap_or("http")
let host = os.LookupEnv("HOST") .map(|s| strings.TrimSpace(s)) .filter(|s| s.length() > 0) .unwrap_or("localhost")
let port = os.LookupEnv("PORT") .unwrap_or("8080")
scheme + "://" + host + ":" + port }
Interfaces & generics
interface Metric { fn label(self) -> string fn value(self) -> float64 }
fn report(metrics: Slice<Metric>) { for m in metrics { fmt.Println(m.label(), m.value()) } }
fn max<T: Metric>(metrics: Slice<T>) -> T { metrics.fold(metrics[0], |a, b| if a.value() > b.value() { a } else { b } ) }
if let & let else
type Headers = Map<string, string>
fn handle_headers(h: Headers) -> Result<(), string> { if let Some(token) = h.get("Authorization") { let user = authenticate(token)? authorize(user)? } else { return Err("missing credentials") }
let Some(id) = h.get("X-Request-ID") else { return Err("missing request ID") }
process(id) }
π΄ match is not exhaustive ββ[example.lis:4:3] 2 β enum Severity { Low, High, Critical } 3 β fn should_alert(s: Severity) -> bool { 4 β match s { Β· ββββ¬βββ Β· β°ββ not all patterns covered 5 β Severity.Low => false, 6 β Severity.High => true, 7 β } β°ββββ help: Handle the missing case Severity.Critical, e.g. Severity.Critical => { ... }
π΄ nil is not supported ββ[users.lis:3:12] 1 β fn find(name: string) -> Option { 2 β if name.is_empty() { 3 β return nil Β· ββ¬β Β· β°ββ does not exist 4 β } 5 β db.lookup(name) 6 β } β°ββββ help: Absence is encoded with Option in Lisette. Use None to represent absent values
π‘ Result is silently discarded ββ[files.lis:7:3] 5 β fn cleanup() -> Result<(), error> { 6 β os.RemoveAll("cache_dir")? 7 β os.Remove("temp.txt") Β· βββββββββββ¬ββββββββββ Β· β°ββ failure will go unnoticed 8 β } β°ββββ help: Handle this Result with ? or match, or explicitly discard it with let _ = ...
π‘ Private type Config in public API ββ[config.lis:6:24] 5 β struct Config { host: string, port: int } 6 β pub fn new_config() -> Config { Β· ββββ¬ββ Β· β°ββ private 7 β Config { host: "localhost", port: 8080 } β°ββββ help: Config is private but exposed by new_config, which is public. Add pub to the private type or remove it from the public API
π΄ Immutable argument passed to mut parameter ββ[sort.lis:5:13] 4 β let nums = [3, 1, 2] 5 β sort.Ints(nums) Β· βββ¬β Β· β°ββ expected mutable, found immutable 6 β } β°ββββ help: Bindings in Lisette are immutable by default. Use let mut nums = ... to allow mutation
π΄ Struct Server is missing fields ββ[server.lis:10:11] 9 β fn start(mux: http.Handler) { 10 β let s = Server { handler: mux } Β· ββββ¬ββ Β· β°ββ missing fields: db, logger 11 β http.ListenAndServe(":8080", s.handler) 12 β } β°ββββ help: Initialize all fields in this struct literal
LSP support for: VSCode Neovim Zed
Pipeline operator
import "go:strings"
fn slugify(input: string) -> string { input |> strings.TrimSpace |> strings.ToLower |> strings.ReplaceAll(" ", "-") }
slugify(" Hello World ") // => "hello-world"
Try blocks
fn load_config() -> Config { let result = try { let data = os.ReadFile("app.toml")? parse_toml(data)? } match result { Ok(config) => config, Err(_) => Config.default(), } }
Concurrency
fn fetch_primary() -> string { ... } fn fetch_replica() -> string { ... }
let ch1 = Channel.new<string>() let ch2 = Channel.new<string>()
task { ch1.send(fetch_primary()) } // goroutine task { ch2.send(fetch_replica()) } // goroutine
let quickest = select { match ch1.receive() { Some(v) => v, None => "closed", }, match ch2.receive() { Some(v) => v, None => "closed", }, }
Serialization attributes
#[json(camel_case)] struct UserProfile { user_name: string, // => `json:"userName"`
#[json("userID")] account_id: int, // => `json:"userID"`
#[json(omitempty)] bio: Option<string>, // => `json:"bio,omitempty"`
#[json(string)] score: int64, // => `json:"score,string"`
#[tag("validate", "required")] email: string, // => `validate:"required"`
#[json(skip)] internal_id: int, // => `json:"-"` }
Panic recovery
fn safe_divide(a: int, b: int) -> Result<int, string> { let result = recover { a / b } // => Result<int, PanicValue> result.map_err(|pv| pv.message()) }
match safe_divide(10, 0) { Ok(v) => fmt.Println(v), Err(e) => fmt.Println(e), }
Deferral
fn run(query: string) -> Result<(), error> { let db = connect()? defer db.Close()
let tx = db.Begin()? defer tx.Rollback()
tx.Exec(query)? tx.Commit() }
Lisette
fn classify(opt: Option<int>) -> string { match opt { Some(x) if x > 0 => "positive", Some(x) if x < 0 => "negative", Some(_) => "zero", None => "none", } }
Compiled Go
func classify(opt lisette.Option[int]) string { if opt.Tag == lisette.OptionSome { x := opt.SomeVal if x > 0 { return "positive" } if x < 0 { return "negative" } return "zero" } return "none" }
Lisette
fn combine() -> Result<int, string> { let x = first()? let y = second()? Ok(x + y) }
Compiled Go
func combine() lisette.Result[int, string] { check_1 := first() if check_1.Tag != lisette.ResultOk { return lisette.MakeResultErr[int, string]( check_1.ErrVal, ) } x := check_1.OkVal check_2 := second() if check_2.Tag != lisette.ResultOk { return lisette.MakeResultErr[int, string]( check_2.ErrVal, ) } y := check_2.OkVal return lisette.MakeResultOk[int, string](x + y) }