Syntax Basics
Functions
Section titled “Functions”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 inferablefn double(x) { x * 2}
// Single-expression functions (block with implicit return)fn square(x: Int) -> Int = x * xLet Bindings
Section titled “Let Bindings”let introduces immutable bindings. The type is inferred unless annotated:
let x = 42let y: Int = x + 8let pair = (x, y) // Tuple: (Int, Int)Control Flow
Section titled “Control Flow”If Expressions
Section titled “If Expressions”if is an expression — it returns a value:
let x = 42let status = if x > 0 then "positive" else "non-positive"Match Expressions
Section titled “Match Expressions”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:
let value = 7match value { n if n > 0 => "positive", n if n < 0 => "negative", _ => "zero"}Records
Section titled “Records”Records are row-polymorphic — a function can accept any record with the fields it needs. When the parameter type is inferred (no annotation), the row stays open and extra fields are fine:
fn full_name(r) { r.first + " " + r.last}
let person = { first: "Alice", last: "Smith", age: 30 }perform IO.print(full_name(person)) // "Alice Smith" — extra fields are fineAn explicit annotation like fn f(r: { first: String, last: String }) creates a closed record that rejects extra fields. Use inferred parameters for row-polymorphic functions.
Records are structurally typed and created with { field: value, ... } syntax.
Variants
Section titled “Variants”Algebraic data types defined with type ... = ... | ...:
type Result[T, E] = Ok(T) | Err(E)
type Tree[T] = Leaf | Node((Tree[T], T, Tree[T]))Construct and match:
let ok = Ok(42)
match ok { Ok(v) => "Got " + perform Int.to_string(v), Error(e) => "Error: " + e}Arrays
Section titled “Arrays”let nums = [1, 2, 3, 4, 5]let first = nums[0] // Index access (out-of-bounds returns nil)Array length is available via the built-in Array.length effect:
let nums = [1, 2, 3, 4, 5]let n = perform Array.length(nums) // 5for loops also iterate over the full array without an explicit length call.
Pipe Operator
Section titled “Pipe Operator”The |> operator pipes a value left-to-right into a function:
let inc = fn(x) { x + 1 } inlet dbl = fn(x) { x * 2 } in1 |> inc |> dbl // 4x |> f is equivalent to f(x). Chaining a |> f |> g |> h applies f, then g, then h in order.
Send Operators
Section titled “Send Operators”There are two syntaxes for sending a message to an actor:
actor Counter { behavior inc() { 0 }}let counter = spawn Counter {}
// Keyword formsend 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:
actor Counter { state count: Int = 0 behavior inc_by(n: Int) { self.count = self.count + n } behavior watch(v: Int) { 0 } behavior get() { self.count }}let counter = spawn Counter {}let w = spawn Counter {}
w ! watch(7)send counter inc_by(5)send counter get()Ask Operator
Section titled “Ask Operator”ask is a synchronous request/reply call to an agent or actor behavior. The caller blocks until the target responds:
actor Assistant { behavior ask(q: String) -> String { "answer to " + q }}
let a = spawn Assistant {} inask a ask("What is an actor model?")See AI Agents for agent declarations and tool binding.
Ternary Expressions
Section titled “Ternary Expressions”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.
Effect Annotations
Section titled “Effect Annotations”Function signatures declare their effects with ! or throws followed by an effect row:
// Pure function — no effectsfn add(x: Int, y: Int) -> Int = x + y
// Effectful — performs IO (effect row after return type)fn greet() -> Unit ! {IO} { perform IO.print("Hello")}
// throws is an alias for !fn log(msg: String) -> Unit throws {IO} { perform IO.print(msg)}See Algebraic Effects for the full effect system.
Comments
Section titled “Comments”// Single-line comment
//// Regular comment (not a doc comment)
/// Doc comment — attaches to the next declaration
//! Module-level doc commentUse exactly /// for declaration docs — the lexer recognizes it as a doc
comment, and consecutive /// lines attach to the declaration that
immediately follows them. //! lines form the module overview in generated
docs (nulang --doc).