silt
A delightful language for programmers who’ve suffered enough. Types without annotations. Threads without locks. Errors without surprises.
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 playground1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz
Install
curl -fsSL https://silt-lang.com/install.sh | sh 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 playgroundErrors 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 playgroundStdlib 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 playgroundType 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 playgroundReference
| Feature | Details |
|---|---|
| keywords | as else fn import let loop match mod pub return trait type when where |
| types | inferred, with ADTs, records, and traits |
| literals | 42, 0xFF, 0b1010, 1e5, 1_000, "hi {x}" |
| branching | match only |
| mutability | none |
| errors | Result / Option / ? |
| concurrency | CSP with real parallelism |
| collections | [1, 2, 3] list, #{"k": "v"} map, #[1, 2] set |
| stdlib | small but exhaustive |
| tools | REPL, formatter, test runner, LSP |