Lisette — Rust syntax, Go runtime
Lisette
a little language inspired by Rust that compiles to Go
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
fn main() { match load_config("app.yaml") { Ok(config) => start(config), Err(e) => fmt.Println("error:", e), } }
Familiar
Syntactically similar to Rust
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(dxdx + dydy) } }
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
fn max(metrics: Slice
if let & let else
type Headers = Map
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) }
Safe
Go runtime issues, caught at compile time
🔴 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
🟡 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: sort.Ints() may mutate nums, so declare it mutable using let mut nums = ...
🔴 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 Helix GoLand
Ergonomic
Designed for interoperability with Go
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
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: Optionjson:"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
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() }
Transparent
Compiles to understandable Go
Lisette
fn classify(opt: Option
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
Compiled Go
func combine() lisette.Result[int, string] { check_1 := first() if check_1.Tag != lisette.ResultOk { return lisette.MakeResultErrint, string } x := check_1.OkVal check_2 := second() if check_2.Tag != lisette.ResultOk { return lisette.MakeResultErrint, string } y := check_2.OkVal return lisette.MakeResultOkint, string }
Learn more
By visiting the repository: github.com/ivov/lisette
By visiting the WASM playground: lisette.run/play