A terse example of every construct. See lib/parser/parser.mly for authoritative grammar.
-- line comment
{- block comment
(nestable: {- inner -}) -}
mod MyApp do
...
end
Every file must start with a module declaration. Dotted names and nesting are supported:
mod A.B.C do ... end -- dotted module name
mod Outer do
mod Inner do ... end -- nested module
end
fn add(x: Int, y: Int): Int do
x + y
end
pfn helper(x) do -- pfn = private
x * 2
end
Multi-head pattern matching — consecutive clauses with the same name are merged:
fn len(Nil) do 0 end
fn len(Cons(_, t)) do 1 + len(t) end
Function clause guard:
fn abs(n) when n < 0 do -n end
fn abs(n) do n end
Return-type annotation is optional (fn f(x: Int): Bool do ... end).
Default argument values (Elixir-style \\):
fn greet(name, greeting \\ "Hello") do
greeting ++ ", " ++ name ++ "!"
end
greet("World") -- "Hello, World!"
greet("World", "Hi") -- "Hi, World!"
Multiple defaults — all must be trailing parameters:
fn make(x, y \\ 10, z \\ 20) do x + y + z end
make(1) -- 31 (uses y=10, z=20)
make(1, 5) -- 26 (uses z=20)
make(1, 5, 6) -- 12 (all explicit)
Local function inside a block:
fn outer() do
fn inner(x) do x + 1 end
inner(5)
end
fn x -> x + 1 -- single param
fn _ -> 42 -- wildcard (1-arg, NOT zero-arg)
fn (a, b) -> a + b -- multiple params
fn -> compute() -- zero-arg short form
fn () -> compute() -- zero-arg explicit form (identical)
Multi-statement lambda bodies are supported with leading let bindings followed by
a final expression — identical to match arm block bodies:
fn x ->
let y = x + 1
let z = y * 2
z
fn (a, b) ->
let sum = a + b
sum * 2
fn () ->
let x = compute()
let y = x + 1
y
The body is: zero or more let/linear let bindings, then a final expression.
Single-expression lambdas are unchanged — no let bindings means no EBlock wrapper.
Both fn -> expr and fn () -> expr are valid zero-arg lambdas — they are identical.
Block-level let — no in; subsequent exprs in the block see the binding:
fn main() do
let x = 42
let y = x + 1
y
end
With type annotation:
let count: Int = 0
Linear let (must be consumed exactly once):
linear let handle: Handle = open_file("foo.txt")
Module-level let:
let pi = 3.14159
let? p = e binds the Ok payload of a Result expression and short-circuits on Err:
fn parse_and_add(a: String, b: String): Result(Int, String) do
let? x = int_from_string(a) -- propagates Err(msg) immediately
let? y = int_from_string(b) -- only reached if previous succeeded
Ok(x + y)
end
Rules:
- The right-hand side must be
Result(T, E). - The pattern binds the
T(Ok payload). - The code after the
let?must also produceResult(R, E)— the error typeEmust match across alllet?bindings in the block. let?cannot be the last expression in a block (there must be something after it).
Works in function bodies, match arms, and lambda bodies:
fn process(items: List(String)): Result(List(Int), String) do
List.map(items, fn s ->
let? n = parse(s)
Ok(n * 2))
end
Variant (ADT) — no leading |:
type Color = Red | Green | Blue
type Shape = Circle(Float) | Rect(Float, Float)
Generic variant:
type Option(a) = None | Some(a)
type Result(a, b) = Ok(a) | Err(b)
Record:
type Point = { x: Float, y: Float }
type User = { name: String, age: Int }
Private type (type and constructors both private):
ptype Internal = Foo | Bar(Int)
Phantom label type (tag — zero-arg type used as a state or resource marker):
tag ConnTag -- equivalent to: type ConnTag = ConnTag
tag Open -- equivalent to: type Open = Open
tag Closed -- equivalent to: type Closed = Closed
Always-linear type (every binding is automatically tracked as linear — no per-site annotation needed):
always_linear type Handle(r, s) = Handle(Int)
-- Binding a Handle without consuming it is a compile-time error.
-- Double-use is also an error.
Combining tag + always_linear type for typestate handles:
mod Connection do
tag ConnTag
tag Open
tag Closed
always_linear type Conn(r, s) = Conn(Int)
fn connect(_host : String) : Conn(ConnTag, Closed) do Conn(0) end
fn open(h : Conn(ConnTag, Closed)) : Conn(ConnTag, Open) do
match h do Conn(raw) -> Conn(raw) end
end
fn close(h : Conn(ConnTag, Open)) : Unit do
match h do Conn(_) -> () end
end
end
-- Wrong-state calls are caught at compile time.
-- Dropped handles are caught at compile time.
Opaque type (type name public, constructors private):
opaque type Handle = Handle(Int)
-- Inside the module: can construct and pattern-match Handle
-- Outside the module: type name visible, constructors hidden
Use opaque types to hide implementation details while keeping the type name usable in signatures:
mod Token do
opaque type Token = Token(String)
fn make(s) do Token(s) end
fn value(Token(s)) do s end
end
-- Outside: can call Token.make/Token.value, cannot use Token(_) directly
Int -- concrete type
List(Int) -- generic application
(Int, String) -- tuple type
() -- unit type
Int -> Bool -- function type (right-associative)
a -> b -> c -- curried: a -> (b -> c)
Mod.Type -- qualified type
linear Handle -- linear type (must use exactly once)
affine Handle -- affine type (use at most once)
In nat-level arithmetic (for sized arrays):
type Arr(n) = Array(n * 2)
A base type plus a predicate, checked by an SMT solver (Z3) at compile time.
_ is the value being refined; the predicate is Int/Bool linear arithmetic.
{Int | _ >= 0} -- a non-negative Int
{Int | _ != 0} -- a non-zero Int
{v : Int | v >= 0 && v < 100} -- named binder form
Used as parameter (precondition) or return (postcondition) types:
fn at(xs : List(Int), i : {Int | _ >= 0 && _ < len(xs)}) : Int do ... end
fn count(xs : List(a)) : {Int | _ >= 0} do List.length(xs) end
A @[measure] function (total, terminating, pure) may be used in predicates:
@[measure]
fn size(t : Tree(a)) : Int do
match t do
Leaf -> 0
Node(l, x, r) -> 1 + size(l) + size(r)
end
end
Checking is definite-failure (flags only what can never hold — no false
positives) and runs only when z3 is on PATH. See the
Refinement Types guide for the full story and
limitations (Int/Bool only, no Float value-refinements, direct calls only).
_ -- wildcard
x -- variable binding
42 -- int literal
3.14 -- float literal
"hi" -- string literal
true / false -- bool literals
Nil -- nullary constructor
Some(x) -- constructor with args
Cons(h, t) -- nested constructor
(a, b) -- tuple
(a, b, c) -- triple
[a, b, c] -- list (sugar for Cons chains)
[] -- empty list (Nil)
:ok -- atom
:error(msg) -- atom with args
Mod.Con(x) -- qualified constructor (disambiguation)
-5 -- negative int literal
match expr do
Nil -> "empty"
Cons(h, _) -> h
end
Arms separated by newlines. Multi-statement arms need no wrapper:
match result do
Ok(v) ->
let s = to_string(v)
print(s)
Err(e) -> print(e)
end
Guard on a match arm:
match n do
x when x > 0 -> "positive"
x when x < 0 -> "negative"
_ -> "zero"
end
Cond (pattern-free multi-way if):
match do
x > 10 -> "big"
x > 0 -> "small"
_ -> "non-positive"
end
Elixir-style monadic chaining for Result/Option types:
with Ok(x) <- f(),
Ok(y) <- g(x) do
x + y
end
With an else handler for failed patterns:
with Ok(x) <- fetch_user(id),
Ok(y) <- fetch_data(x) do
process(x, y)
else
Err(e) -> handle_error(e)
end
Each pat <- expr binding: if expr matches pat, continue; otherwise fall through to else arms (or propagate the non-matching value if no else). Multiple bindings are separated by commas.
if x > 0 do
"positive"
end
With optional else block (both branches can be multi-statement):
if x > 0 do
let msg = "positive"
print(msg)
else
print("non-positive")
end
else is optional — if without else returns (). There is no then keyword.
Integer arithmetic: +, -, *, /, %
Float arithmetic: +., -., *., /.
String/list concat: ++
Comparison: ==, !=, <, >, <=, >=
Logic: &&, ||, ! (prefix not), unary - (negate)
[1, 2, 3]
|> List.map(fn x -> x * 2)
|> List.filter(fn x -> x > 2)
|> threads the left value as the first argument of the right expression.
42 -- Int
3.14 -- Float
"hello" -- String
true / false -- Bool
:ok -- Atom
:error("msg") -- Atom with args
Triple-quoted strings preserve newlines:
let s = """
multi
line
"""
String interpolation:
let greeting = "Hello, ${name}!"
let info = "x = ${to_string(x)}"
~H"<p>Hello</p>" -- HTML sigil (produces IOList)
~H"<p>${name}</p>" -- sigil with interpolation
~H"""
<div>multi-line</div>
""" -- triple-quoted sigil
Any uppercase letter can be a sigil prefix (~H, ~R, etc.).
(1, "two", true) -- 3-tuple
(x, y) -- 2-tuple (pair)
() -- unit
[] -- empty list (Nil)
[1, 2, 3] -- list literal (sugar for Cons chains)
Cons(1, Cons(2, Nil)) -- explicit cons
[expr for pat in list] -- map: apply expr to each element
[expr for pat in list, pred] -- filter-map: only elements where pred is true
-- Examples:
[x * 2 for x in [1, 2, 3]] -- [2, 4, 6]
[x for x in nums, x % 2 == 0] -- only even numbers
[x + 1 for x in [10, 20, 30]] -- [11, 21, 31]
Desugars to List.map / List.filter + List.map. Requires List in scope.
Literal:
let p = { x: 1.0, y: 2.0 }
Field access:
p.x
Functional update:
{ p with x: 3.0 }
{ state with count: state.count + 1, name: "new" }
List.map(xs, fn x -> x + 1) -- module-qualified call
String.length(s)
p.x -- field access
a.b.c -- chained field/module access
Constructor application:
Some(42)
Ok("result")
Cons(1, Nil)
do ... end is usable as an expression anywhere:
let result = do
let a = compute()
a + 1
end
? -- anonymous hole (compiler fills / reports type)
?name -- named hole
Useful for type-directed search.
fn pub_fn() do ... end -- public (default)
pfn priv_fn() do ... end -- private
doc "Returns the length of a list."
fn length(xs) do ... end
@[deprecated]
fn old_api() do ... end
interface Eq(a) do
fn eq: a -> a -> Bool
fn neq: a -> a -> Bool do -- default implementation
!eq(x, y)
end
end
interface Ord(a) requires Eq(a) do
fn cmp: a -> a -> Int
end
impl Eq(Int) do
fn eq(x, y) do x == y end
end
impl Eq(List(a)) when Eq(a) do
fn eq(xs, ys) do ... end
end
derive Json, Eq for MyType
derive Show for Color
use List.* -- import all from List
use List.{map, filter} -- import specific names
use List.map -- import single name
use A.B.C.* -- dotted path, all names
import String -- Elixir-style, all names
import String, only: [length, split]
import String, except: [dangerous_fn]
import String.{length, split} -- dot-brace form
alias Very.Long.Module as Short
alias Very.Long.Module, as: Short -- comma-colon form
alias Very.Long.Module -- alias to last segment
sig MyCollection do
type Elem
fn insert: Int -> List -> Int
end
extern "libc": Cap(LibC) do
fn malloc(n: Int): Int
fn free(ptr: Int): ()
end
needs IO.Network, IO.Clock
Declares capability requirements for the module.
actor Counter do
state { count: Int }
init { count: 0 }
on Increment() do
{ state with count: state.count + 1 }
end
on GetCount(reply_to) do
send(reply_to, state.count)
state
end
end
Spawn an actor and send messages:
let pid = spawn(Counter)
send(pid, Increment())
Supervision block inside an actor:
actor App do
state {}
init {}
supervise do
strategy one_for_one
max_restarts 3 within 60
Worker w
end
end
app MyApp do
on_start do
Logger.info("starting")
end
on_stop do
Logger.info("stopping")
end
Supervisor.spec(:one_for_one, [Worker])
end
Spawn a task and await its result:
let t = Task.async(fn () -> expensive_computation())
Task.await(t) -- Ok(result) or Err(reason)
Task.await_unwrap(t) -- unwrap, panic on Err
Parallel map:
let results = Task.async_stream([1, 2, 3], fn n -> n * n)
-- [Ok(1), Ok(4), Ok(9)]
Structured combinators:
-- First to finish wins; the rest are cancelled
Task.race([t1, t2, t3])
-- First Ok wins; all-Err returns Err(list_of_reasons)
Task.any([t1, t2, t3])
-- Collect every result; never short-circuits
Task.all_settled([t1, t2, t3]) -- [Ok(v1), Err(e2), Ok(v3)]
-- Cancel any tasks still running when the scope exits
Task.scope(fn () ->
let t = Task.async(fn () -> fetch_data())
Task.await_unwrap(t)
)
Cancellation tokens:
let tok = task_cancel_token_new()
task_is_cancelled(tok) -- false
let t = task_spawn_with_cancel(fn _ -> work(), tok)
task_cancel(tok) -- mark cancelled
Task.await(t) -- Err("cancelled")
-- Cancel a running task by its handle
task_cancel_by_id(t)
protocol Transfer do
Client -> Server : Request(String)
Server -> Client : Response(Int)
loop do
Client -> Server : More(String)
Server -> Client : Ack()
end
end
protocol Negotiation do
choose by Client:
| accept -> Client -> Server : Accept()
| reject -> Client -> Server : Reject()
end
end
fn consume(linear h: Handle): () do
close(h)
end
type Resource = { linear fd: FileDesc }
test "addition works" do
assert (1 + 1 == 2)
end
describe "arithmetic" do
setup do
-- runs before each test in this describe block
end
setup_all do
-- runs once before all tests
end
test "multiply" do
assert (2 * 3 == 6)
end
end
dbg() -- unconditional breakpoint
dbg(some_expr) -- trace / conditional
mod Main do
use List.*
type Shape = Circle(Float) | Rect(Float, Float)
fn area(Circle(r)) do
3.14159 *. r *. r
end
fn area(Rect(w, h)) do
w *. h
end
fn main() do
let shapes = [Circle(1.0), Rect(2.0, 3.0)]
let areas = shapes |> map(fn s -> area(s))
print(to_string(areas))
end
end