Soppo

Go, with the features it's missing.

Type safety and ergonomics, with full interop.

Take the tour

Familiar syntax

Soppo uses Go syntax wherever possible. If you know Go, you already know most of Soppo. Use any Go library directly, keep your existing tooling, and introduce Soppo gradually into existing projects.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", handleHome)
    http.HandleFunc("/hello", handleHello)

    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

Enums and pattern matching

Tagged unions with struct variants, and pattern matching that ensures you handle every case. The compiler catches missing branches before your code ever runs, so you can refactor with confidence.

type Shape enum {
    Circle struct { radius float64 }
    Rectangle struct { width, height float64 }
}

func area(s Shape) float64 {
    match s {
    case Shape.Circle{radius: r}:
        return 3.14159 * r * r
    case Shape.Rectangle{width: w, height: h}:
        return w * h
    }
}

Error handling with ?

No more repetitive if err != nil blocks. The ? operator propagates errors concisely while keeping error handling explicit and visible. Add custom handling when you need it.

func processFile(path string) error {
    // Propagate error directly
    data := os.ReadFile(path) ?

    // Custom handling with named error
    result := parse(data) ? err {
        return fmt.Errorf("parse failed: {err}")
    }

    fmt.Println("Processed {len(result)} items")
    return nil
}

Nil safety

Nil pointer dereferences are a common source of runtime panics in Go. Soppo tracks nilability at compile time, so you catch these bugs before they reach production. After a nil check, the type narrows automatically.

func findUser(id int) ?*User {  // ? marks nilable
    if id == 0 {
        return nil
    }
    return &User{name: "Alice"}
}

func greet(id int) {
    user := findUser(id)

    if user != nil {
        // user is *User here, not ?*User
        fmt.Println("Hello {user.name}")
    }
}

Clear error messages

When something goes wrong, Soppo tells you exactly what happened and where. Error messages show the relevant code, highlight the problem, and suggest how to fix it. Debugging should be straightforward, not a guessing game.

× Non-exhaustive match
    ╭─[main.sop:11:5]
 10 │         var message string
 11 │ ╭─▶     match colour {
 12 │ │       case Colour.Red:
 13 │ │           message = "Stop"
 14 │ │       case Colour.Yellow:
 15 │ │           message = "Wait"
 16 │ ├─▶     }
    · ╰──── missing variants: Green
 17 │     }
    ╰────
  help: Ensure all enum variants are handled, or add a `default` case

Get started

The recommended way to install Soppo is through SOPMOD, a version manager that handles installing, updating, and switching between versions. It also manages Go, which Soppo uses internally for compilation.

curl -fsSL https://soppolang.dev/install.sh | sh

Then add to your PATH and install:

export PATH="$HOME/.sopmod/bin:$PATH"
sopmod install sop latest

Or install just the sop binary using Cargo, without SOPMOD (requires Rust and Go):

cargo install soppo