Lisette
A love letter to Go
written in Rust

Lisette is a little language inspired by Rust that compiles to Go, combining compile-time safety, expressive syntax and Go's ecosystem.

  • algebraic data types
  • pattern matching
  • no nil
  • immutability
  • readable Go
  • extensive tooling
import "go:encoding/json"
import "go:fmt"
import "go:os"

fn load_config(path: string) -> Result<Cfg, error> {
  let data = os.ReadFile(path)?
  let mut config = Cfg { port: 8080, tls: true }
  json.Unmarshal(data, &config)?
  Ok(config)
}

fn main() {
  match load_config("app.json") {
    Ok(config) => start(config),
    Err(e) => fmt.Println("error:", e),
  }
}

Expressive

Syntactically similar to Rust, conceptually simpler

enum Message {
  Ready,
  Write(string),
  Move { x: int, y: int },
}

fn handle(msg: Message) -> string {
  match msg {
    Ready => "ready",
    Write(text) => f"wrote: {text}",
    Move { x, y } => f"move to ({x}, {y})",
  }
}
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)
  }
}

Safe

Go runtime failures, caught at compile time

  match is not exhaustive
   ╭─[example.lis:5:3]
 4 │ fn should_alert(s: Severity) -> bool {
 5match s {
   ·   ───┬───
   ·      ╰── not all patterns covered
 6 │     Severity.Low => false,
 7 │     Severity.High => true,
 8 │   }
 9 │ }
   ╰────
  help: Handle the missing case Severity.Critical,
        e.g. Severity.Critical => { ... }
nil is not supported
   ╭─[users.lis:5:12]
 3 │ fn find(name: string) -> Option<User> {
 4 │   if name.is_empty() {
 5 │     return nil
   ·            ─┬─
   ·             ╰── does not exist
 6 │   }
 7 │   lookup(name)
 8 │ }
   ╰────
  help: Absence is encoded with Option<T> in Lisette. Use
        None to represent absent values
Result is silently discarded
    ╭─[files.lis:9:3]
  8 │ fn shutdown() {
  9cleanup()
    ·   ────┬────
    ·       ╰── failure will go unnoticed
 10 │   fmt.Println("done")
 11 │ }
    ╰────
  help: Handle this Result with ? or match, or explicitly
        discard it with let _ = ...
Option is silently discarded
    ╭─[cache.lis:10:3]
  9 │ fn warm(c: Cache) {
 10hit_count(c, "home")
    ·   ──────────┬─────────
    ·             ╰── absence will go unnoticed
 11 │   fmt.Println("warmed")
 12 │ }
    ╰────
  help: Handle this Option with ? or match, or explicitly
        discard it with let _ = ...
LSP support for: VSCode Neovim Zed Helix GoLand

Interoperable

Import Go packages, stay on Go's runtime

import "go:os"

fn server_url() -> string {
  let scheme = os.LookupEnv("HTTPS")
    .filter(|s| s == "1")
    .map(|_| "https")
    .unwrap_or("http")

  let host = os.LookupEnv("HOST")
    .unwrap_or("localhost")

  let port = os.LookupEnv("PORT")
    .unwrap_or("8080")

  scheme + "://" + host + ":" + port
}
os.d.lis
// func Getenv(key string) string
pub fn Getenv(key: string) -> string

// func LookupEnv(key string) (string, bool)
pub fn LookupEnv(key: string) -> Option<string>

// func Open(name string) (*File, error)
pub fn Open(name: string) -> Result<Ref<File>, error>

// func Remove(name string) error
pub fn Remove(name: string) -> Result<(), error>

// func Hostname() (string, error)
pub fn Hostname() -> Result<string, error>

Get started

Little to learn, and the rest is Go

Quickstart
learn-lisette
src/main.lis
import "go:errors"
import "go:fmt"
import "go:os"

import "commands"
import "display"

fn main() {
  let Some(command) = os.Args.get(1) else {
    display.print_usage()
    return
  }

  let result = match command {
    "add" => commands.add(os.Args),
    "done" => commands.done(os.Args),
    "cancel" => commands.cancel(os.Args),
    "list" => commands.list(),
    "stats" => commands.stats(),
    "watch" => commands.watch(),
    other => Err(errors.New(f"unknown command: '{other}'")),
  }

  if let Err(err) = result {
    fmt.Println(f"Error: {err}")
    os.Exit(1)
  }
}