Skip to content

Syntax Basics

Functions are defined with fn and use Hindley-Milner type inference:

fn add(x: Int, y: Int) -> Int {
x + y
}
// Type annotations are optional when inferable
fn double(x) {
x * 2
}
// Single-expression functions
fn square(x: Int) -> Int = x * x

let introduces immutable bindings. The type is inferred unless annotated:

let x = 42
let y: Int = x + 8
let pair = (x, y) // Tuple: (Int, Int)

if is an expression — it returns a value:

let status = if x > 0 { "positive" } else { "non-positive" }

Pattern matching on variants, records, and primitives:

type Option[T] = Some(T) | None
fn unwrap_or(o: Option[Int], default: Int) -> Int {
match o {
Some(v) => v,
None => default
}
}

Match arms can have guards:

match value {
n if n > 0 => "positive",
n if n < 0 => "negative",
_ => "zero"
}

Records are row-polymorphic — a function can accept any record with the fields it needs:

fn full_name(r: { first: String, last: String }) -> String {
r.first + " " + r.last
}
let person = { first: "Alice", last: "Smith", age: 30 }
full_name(person) // "Alice Smith" — extra fields are fine

Records are structurally typed and created with { field: value, ... } syntax.

Algebraic data types defined with type ... = ... | ...:

type Result[T, E] = Ok(T) | Err(E)
type Tree[T] = Leaf(T) | Node(Tree[T], Tree[T])

Construct and match:

let ok = Ok(42)
match ok {
Ok(v) => "Got " + Int.to_string(v),
Err(e) => "Error: " + e
}
let nums = [1, 2, 3, 4, 5]
let first = nums[0] // Index access
let len = length(nums) // Built-in length

The |> operator pipes a value left-to-right into a function:

let inc = fn(x) { x + 1 } in
let dbl = fn(x) { x * 2 } in
1 |> inc |> dbl // 4

x |> f is equivalent to f(x). Chaining a |> f |> g |> h applies f, then g, then h in order.

There are two syntaxes for sending a message to an actor:

// Keyword form
send counter inc()
// Operator form (equivalent)
counter ! inc()

Both parse to the same AST. The ! form is more concise; the send form is more readable for complex arguments:

w ! watch(v)
send counter inc_by(5)
send counter get(self)

ask is a synchronous request/reply call to an agent or actor behavior. The caller blocks until the target responds:

let a = spawn Assistant {} in
ask a ask("What is an actor model?")

See AI Agents for agent declarations and tool binding.

if can be used inline with the then keyword:

let fib = fn(n) {
if n <= 1 then n else fib(n - 1) + fib(n - 2)
} in fib(10)

if cond then a else b returns a when cond is truthy, b otherwise. The block form (if cond { a } else { b }) is equivalent.

Function signatures declare their effects with ! or throws followed by an effect row:

// Pure function — no effects
fn add(x: Int, y: Int) -> Int = x + y
// Effectful — performs IO
fn greet(): ! IO Unit {
perform IO.print("Hello")
}
// throws is an alias for !
fn log(msg: String): throws IO Unit {
perform IO.print(msg)
}

See Algebraic Effects for the full effect system.

// Single-line comment
//// Regular comment (not a doc comment)
/// Doc comment — attaches to the next declaration
//! Module-level doc comment