Getting Started

Silt is a statically-typed, expression-based language with a small, fixed keyword set, full immutability, and CSP-style concurrency. Pattern matching is the only way to branch. Types are inferred. Errors are values.

This guide walks you through the essentials. For the complete reference, see the Language Guide.

Install

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

Or build from source:

git clone https://github.com/rendro/silt.git
cd silt && cargo build --release
cp target/release/silt ~/.local/bin/

Your first program

silt init
silt run main.silt

silt init creates a starter main.silt. silt run executes it. That’s the whole loop.

See examples/ in the repository for runnable sample programs — start with examples/hello.silt, examples/fizzbuzz.silt, and examples/records.silt.

1. Bindings

Everything is immutable. let binds a name to a value. You can shadow, but you can’t reassign.

let x = 42
let x = x + 1   -- shadows, x is now 43

2. Functions

fn add(a, b) {
  a + b
}

The last expression is the return value. No return keyword needed (though it exists for early exits).

Anonymous functions:

let double = fn(x) { x * 2 }

3. Pattern matching

The only branching construct. Match on constructors, tuples, lists, records, guards, ranges, or-patterns.

fn describe(n) {
  match n {
    0 -> "zero"
    1 | 2 | 3 -> "small"
    _ when n < 0 -> "negative"
    _ -> "big"
  }
}

Match destructures:

let (x, y) = (1, 2)

match items {
  [] -> "empty"
  [head, ..tail] -> "non-empty"
}

4. Types

Types are inferred. You only write them for declarations:

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

type Shape {
  Circle(Float),
  Rect(Float, Float),
}

fn area(shape) {
  match shape {
    Circle(r) -> 3.14 * r * r
    Rect(w, h) -> w * h
  }
}

5. Errors as values

No exceptions. Fallible functions return Result. The ? operator propagates errors:

import io
import json

type Config { name: String }

fn read_config(path) {
  let content = io.read_file(path)?
  let config = json.parse(Config, content)?
  Ok(config)
}

Use match to handle the result:

match read_config("app.json") {
  Ok(cfg) -> println("loaded: {cfg.name}")
  Err(e) -> println("error: {e}")
}

6. Pipes and trailing closures

The |> operator passes the left value as the first argument of the right:

import list

[1, 2, 3, 4, 5]
|> list.filter { n -> n > 2 }
|> list.map { n -> n * n }
|> list.fold(0) { acc, n -> acc + n }

7. Concurrency

Spawn lightweight tasks that run in parallel. Communicate through channels. I/O inside tasks transparently yields — no async/await.

import channel
import task

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

  let worker = task.spawn(fn() {
    channel.each(ch) { msg ->
      println("got: {msg}")
    }
  })

  channel.send(ch, "hello")
  channel.send(ch, "world")
  channel.close(ch)
  task.join(worker)
}

Tooling

silt run <file.silt>       -- run a program
silt run -w <file.silt>    -- run and re-run on file changes
silt check <file.silt>     -- type-check without running
silt check --format json <file.silt>   -- type-check with JSON output (for CI/editors)
silt test [path]           -- run test functions
silt fmt [files...]        -- format source code
silt fmt --check           -- check formatting without modifying files
silt repl                  -- interactive REPL
silt init                  -- create a new main.silt
silt lsp                   -- start the language server
silt disasm <file.silt>    -- show bytecode disassembly (same as `silt run --disassemble`)
silt update                -- update silt to the latest release

The --watch / -w flag works with run, check, and test to automatically re-run on .silt file changes.

Staying up to date

Run silt update to replace the installed binary with the latest GitHub release. It detects your platform, fetches the prebuilt archive, verifies it against the release’s SHA-256 checksum, and atomically swaps the binary in place — no need to re-run the install script. Verification is fail-closed: a mismatch or missing SHA256SUMS file aborts the update without touching the installed binary. Pass --dry-run to preview the version that would be installed, or --force to reinstall when already current.

What’s next