An expression computes a value, identifies a place, or transfers control. Atoll
is expression-oriented: blocks, if, and match are expressions, so a value
can come straight out of a branch instead of through a mutable accumulator.
fn calculate(): int { return 42 }
fn f(ready: bool): int {
result := if ready {
calculate()
} else {
0
}
return result
}The families
| Family | Examples |
|---|---|
| Atomic | 42, name, None, Color.Red |
| Aggregate | (a, b), [1, 2], { x: 1 }, Point { x: 1, y: 2 } |
| Postfix | .field, .0, f(x), xs[i], ?., ?, catch |
| Prefix | -value, !ready, ~mask, &place |
| Infix | arithmetic, comparison, logic, ranges, ?? |
| Control | block, if, match, for |
| Function | v => v + 1, trailing closures |
| Concurrency | spawn, race, await |
| Transfer | return, error, break, continue |
| Domain | integrated SQL queries and transactions |
Bindings, const, and defer are statements. They contain expressions but
never become a block’s tail value.
Primary expressions
A primary expression is a complete expression that a postfix or infix operator can extend.
struct Point { x: int, y: int }
fn prepare(): void {}
fn finish(): int { return 1 }
fn f(condition: bool, value: int?): void {
a := 42
b := (1, 2)
c := [1, 2, 3]
d := Point { x: 1, y: 2 }
e := { prepare(); finish() }
g := if condition { 1 } else { 2 }
h := match value { Some(v) => v None => 0 }
println("${a} ${b.0} ${c.len()} ${d.x} ${e} ${g} ${h}")
}Parentheses group and override precedence. A comma inside them makes a tuple instead. Braces mean one of three things depending on position: a block, a nominal value after a type or variant name, or an anonymous record when the contents have a field shape.
Postfix chains
Postfix operations extend a completed expression left to right; each receiver is the result of the previous step.
struct Profile { name: string }
struct Account { profile: Profile? }
fn f(account: Account?, values: []int): void {
a := account?.profile?.name
b := values.filter(v => v > 0).map(v => v * 2).len()
c := (1, "two").1
println("${a ?? "none"} ${b} ${c}")
}A method call is member selection plus a call. A field access never runs a
getter — point.x reads storage, full stop.
The fresh-line rule
A ( or [ starting a fresh line does not continue the previous
expression. This is the one place where a newline is significant.
fn handler(x: int): int { return x }
fn f(): void {
request := 1
handler
(request)
}Dots and ? markers do continue across lines, so format long chains by
leading each continuation with its marker:
struct Profile { name: string }
struct Client { }
fn Client.load(self, id: int): Profile? { return None }
fn f(client: Client): string? {
return client
.load(3)
?.name
}Calls
A callee can be a named function, a method, a closure, or any function-valued expression. Arguments are positional and evaluate left to right.
fn double(x: int): int { return x * 2 }
fn choose_handler(): fn(int) -> int { return double }
fn f(text: string): int {
a := double(21)
b := text.trim().len()
c := (choose_handler())(21)
return a + b + c
}Construction looks like a call but is a distinct operation: Point { ... }, a
tuple variant, and a static function on a type stay separate in the AST and in
type checking even though they all produce values.
struct Point { x: int, y: int }
enum Value { Num(int), Text(string) }
fn Point.origin(): Point => Point { x: 0, y: 0 }
fn f(): void {
a := Point { x: 1, y: 2 } // struct construction
b := Point.origin() // static function
c := Value.Num(3) // tuple variant
println("${a.x} ${b.x} ${c == Value.Num(3)}")
}Values and places
Most expressions produce a value. A subset also names a place — storage that can be read, assigned, or referenced.
struct Point { x: int, y: int }
fn f(): int {
mut point := Point { x: 1, y: 2 }
mut grid := [10, 20, 30]
read := point.x // read a place
point.x = 3 // assign a place
grid[0] = 99 // assign an indexed place
r := &point.y // reference a place
return read + point.x + (grid[0] ?? 0) + r
}Assignment needs the whole access path to be mutable. A temporary is a value, not a durable place, so it cannot be assigned or outlive the expression that built it. Reference lifetimes are covered in References.
& also appears in signatures, where it lets a function borrow instead of
taking ownership:
struct Point { x: int, y: int }
fn manhattan(p: &Point): int {
return p.x + p.y
}
fn count(values: &[]int): int {
return values.len()
}
fn f(): int {
p := Point { x: 3, y: 4 }
xs := [1, 2, 3]
return manhattan(&p) + count(&xs)
}Indexing returns an Option
Indexing a list or an array yields T?, not T. There is no out-of-range trap
to think about, but there is always an Option to discharge.
fn f(values: []int): int {
a := values[0] ?? 0
b := values.get(1).unwrap_or(0)
c := match values[2] {
Some(v) => v
None => -1
}
return a + b + c
}Forgetting the discharge is a type error, not a silent unwrap:
fn f(values: []int): int {
return values[0]
}Expected types
An expression is checked with information from where it sits.
| Position | Expected type |
|---|---|
| binding annotation | the declared type |
| function argument | the parameter type |
return / function tail |
the function’s success type |
| struct or record field | the declared field type |
| collection element | the element type |
| branch or match arm | the join of the other arms, or the surrounding expectation |
| statement position | none — the value is discarded |
The expectation is what lets an incomplete literal finish:
fn f(): void {
ports: []u16 = [80, 443] // literal width from the annotation
fallback: string? = None // Option payload from the annotation
handler: fn(int) -> bool = value => value > 0 // closure parameter from the annotation
println("${ports.len()} ${fallback ?? "none"} ${handler(1)}")
}Expectations select literal widths, infer closure parameters, and lift a value into a carrier. They never enable an arbitrary conversion and never postpone a decision to runtime.
Branches as values
if and match produce values when every arm produces a compatible one.
fn classify(n: int): string {
return match n {
0 => "zero"
1 => "one"
_ => "many"
}
}
fn sign(value: int): int {
if value < 0 {
-1
} else if value > 0 {
1
} else {
0
}
}A block’s tail is its value; a semicolon discards it and makes the block void.
fn compute(): int { return 7 }
fn f(): void {
value := { compute() } // int
{ compute(); } // void
println("${value}")
}Statement position
An expression used as a statement is evaluated and its value dropped. Dropping the value drops nothing else:
fn send(message: string): int { return message.len() }
fn f(): void {
mut counter := 0
send("hello") // the returned int is discarded; the call still happens
counter += 1
println("${counter}")
}Mutation, host operations, task spawns, destructors, failures, and suspension all still occur. A fallible call in statement position still has to be handled.
Divergence
return, error, break, and continue are expressions with the never type:
they produce no value because control leaves. That lets them sit anywhere an
expression is expected, including on the right of ??.
fn fallback(): int { return -1 }
fn f(maybe_value: int?): int {
value := maybe_value ?? return fallback()
return value * 2
}
fn total(values: []int?): int {
mut sum := 0
for value in values {
v := value ?? continue
sum += v
}
return sum
}The same rule is what makes a diverging catch arm yield the plain success
type — see Catch.
Error and concurrency expressions
? propagates the error channel; catch converts or discharges it.
error LoadError { NotFound, Corrupt }
fn read(id: int): int ! LoadError {
if id < 0 { error NotFound }
return id * 2
}
fn propagate(id: int): int ! LoadError {
value := read(id)?
return value + 1
}
fn recover(id: int): int {
value := read(id) catch {
NotFound => return 0
Corrupt => return -1
}
return value
}spawn yields a Task[T], race picks the first arm to finish, and select is
a statement that waits on several handles.
fn slow(): int { return 1 }
fn fast(): int { return 2 }
fn concurrent(): int {
task := spawn { slow() }
winner := race { slow() fast() }
return task.await() + winner
}Note the arms of race are separated by whitespace only, and select evaluates
to void:
fn f(): void {
task := spawn { 1 }
select {
value := task => println("got ${value}")
default => println("nothing ready")
}
}What the spelling does not tell you
Parsing decides grouping; name resolution picks declarations; type checking resolves generics, overloads, and branch joins; effect checking records capabilities. No spelling alone proves purity:
- a member access is a field read, but an operator or method may dispatch user code through a trait;
- a query is an expression, but it crosses a host boundary;
- a closure literal is inert until called, yet it retains whatever it captured.
Composed example
error ParseError {
Empty
BadNumber { field: string }
}
struct Record {
id: int
score: float
tags: []string
}
fn parse_field(row: []string, index: int, name: string): string ! ParseError {
value := row[index] ?? error BadNumber { field: name }
if value.is_empty() { error Empty }
return value
}
fn parse_row(row: []string): Record ! ParseError {
id_text := parse_field(row, 0, "id")?
score_text := parse_field(row, 1, "score")?
id := id_text.to_int() ?? error BadNumber { field: "id" }
score := score_text.to_float() ?? error BadNumber { field: "score" }
tags := (row[2] ?? "").split(",").filter(t => t.is_not_empty())
return Record { id, score, tags }
}
fn summarize(rows: [][]string): string {
mut kept := 0
mut skipped := 0
mut best := 0.0
for row in rows {
record := parse_row(row) catch {
Empty => { skipped += 1 continue }
BadNumber { field: _ } => { skipped += 1 continue }
}
kept += 1
if record.score > best {
best = record.score
}
}
status := if skipped == 0 { "clean" } else { "partial" }
return "${status}: kept ${kept}, skipped ${skipped}, best ${best}"
}
fn main(): void {
rows := [
["1", "9.5", "a,b"],
["2", "oops", ""],
["3", "7.25", "c"],
]
println(summarize(rows))
}