silt

A delightful language for programmers who’ve suffered enough. Types without annotations. Threads without locks. Errors without surprises.

fizzbuzz.silt
import list

fn fizzbuzz(n) {
  match (n % 3, n % 5) {
    (0, 0) -> "FizzBuzz"
    (0, _) -> "Fizz"
    (_, 0) -> "Buzz"
    _ -> "{n}"
  }
}

fn main() {
  1..21 |> list.each { n -> println(fizzbuzz(n)) }
}
open in playground
$ silt run fizzbuzz.silt
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
Fizz

Install

Linux and macOS. On Windows, take the .zip of the latest release.

curl -fsSL https://silt-lang.com/install.sh | sh
copied

Pattern matching

The only way to branch. Define your types, then match on their shape. The compiler verifies every case is handled.

type Shape {
  Circle(Int),
  Square(Int),
  Triangle(Int, Int),
}

fn area(s: Shape) -> Int {
  match s {
    Circle(r) -> 3 * r * r
    Square(w) -> w * w
    Triangle(b, h) -> b * h / 2
  }
}

Parallelism

Spawn lightweight tasks that run in parallel on a fixed thread pool. Communicate through channels. Every value is immutable, so there are no data races to debug. I/O operations transparently yield to the scheduler — no async/await needed.

import channel
import list
import task

fn main() {
  let ch = channel.new(10)

  let w1 = task.spawn { -> channel.each(ch) { msg -> println("w1: {msg}") } }
  let w2 = task.spawn { -> channel.each(ch) { msg -> println("w2: {msg}") } }

  list.each(1..100) { n -> channel.send(ch, n) }
  channel.close(ch)

  task.join(w1)
  task.join(w2)
}
open in playground

Errors as values

Every function that can fail returns a Result. The ? operator propagates errors without nesting. Nothing is thrown or caught.

import io
import json

type Config {
  name: String,
}

fn main() {
  match io.read_file("settings.json") {
    Ok(content) -> match json.parse(content, Config) {
      Ok(cfg) -> println("loaded: {cfg.name}")
      Err(e) -> println("parse error: {e.message()}")
    }
    Err(io.NotFound(path)) -> println("no config at {path} — run `silt init` first")
    Err(e) -> println("read error: {e.message()}")
  }
}
open in playground

Stdlib errors are typed enums — IoError, JsonError, HttpError, and so on — so match can fork on specific failures like NotFound(path) and still fall back to .message() for the long tail.

HTTP

Built-in HTTP client and server. Pattern matching replaces routing frameworks. Requests are handled concurrently.

import bytes
import http
import json
import tcp

type Todo {
  id: Int,
  title: String,
  done: Bool,
}

-- A body is bytes: this server answers with JSON, and says so.
fn reply(status, data) {
  http.Response {
    status: status,
    body: bytes.from_string(json.stringify(data)),
    headers: #{ "Content-Type": "application/json" },
  }
}

fn main() {
  when let Ok(listener) = tcp.listen("127.0.0.1:8080") else {
    panic("cannot listen on port 8080")
  }
  http.serve(listener) { req ->
    match (req.method, http.segments(req.path)) {
      (http.GET, ["todos"]) -> {
        let todos = [
          Todo { id: 1, title: "Learn silt", done: true },
          Todo { id: 2, title: "Build an API", done: false },
        ]
        reply(200, todos)
      }
      (http.POST, ["todos"]) -> match bytes.to_string(req.body) {
        Ok(text) -> match json.parse(text, Todo) {
          Ok(todo) -> reply(201, todo)
          Err(e) -> reply(400, #{ "error": e.message() })
        }
        Err(e) -> reply(400, #{ "error": e.message() })
      }
      _ -> reply(404, #{ "error": "Not found" })
    }
  }
}
open in playground

Type inference

The type checker infers everything. You get static type safety without writing annotations. Define records, enums, and traits when you need structure.

type User {
  name: String,
  age: Int,
}

fn greet(user) {
  "hello, {user.name} ({user.age})"
}

fn main() {
  let u = User { name: "alice", age: 30 }
  println(greet(u))
  println(greet(u.{ age: 31 })) -- record update
}
open in playground

Reference

FeatureDetails
keywordsas else fn import let loop match mod pub return trait type when where
typesinferred, with ADTs, records, and traits
literals42, 0xFF, 0b1010, 1e5, 1_000, "hi {x}"
branchingmatch only
mutabilitynone
errorsResult / Option / ?
concurrencyCSP with real parallelism
collections[1, 2, 3] list, #{"k": "v"} map, #[1, 2] set
stdlibsmall but exhaustive
toolsREPL, formatter, test runner, LSP
read the full guide