These chapters cover the forms that appear in nearly every Atoll program. Here is a complete file that uses most of them:
module inventory
const LOW_STOCK = 5
struct Item {
sku: string
on_hand: int
price: float
}
error StockError {
Unknown { sku: string }
Insufficient { sku: string, available: int }
}
fn find(items: []Item, sku: string): Item ! StockError {
for item in items {
if item.sku == sku {
return item
}
}
error Unknown { sku }
}
fn reserve(items: []Item, sku: string, count: int): float ! StockError {
item := find(items, sku)?
if item.on_hand < count {
error Insufficient { sku, available: item.on_hand }
}
return item.price * (count as float)
}
fn low_stock(items: []Item): []string {
return items.filter(i => i.on_hand < LOW_STOCK).map(i => i.sku)
}
fn main(): void {
items := [
Item { sku: "A-1", on_hand: 3, price: 9.99 },
Item { sku: "B-2", on_hand: 40, price: 1.50 },
]
cost := reserve(items, "B-2", 4) catch {
Unknown { sku } => return println("no such sku: ${sku}")
Insufficient { sku: _, available } => return println("only ${available} left")
}
mut low := ""
for sku in low_stock(items) {
if low.len() > 0 { low = low + "," }
low = low + sku
}
println("cost=${cost} low=${low}")
}Everything in that file is described in this section: a module header and
declarations (Source), a const
(Constants), struct and list literals
(Literals), := bindings
(Bindings), postfix chains and ?
(Expressions), fallible signatures
(Functions), and lambdas passed to filter
and map (Closures).
The chapters
| Chapter | Main question |
|---|---|
| Source | How are files, tokens, comments, identifiers, and statement boundaries written? |
| Literals | How are scalar, text, byte, list, map, set, tuple, record, and struct values constructed? |
| Expressions | Which forms produce values, identify places, call code, or transfer control? |
| Bindings | How do immutable, mutable, annotated, and destructured locals work? |
| Constants | Which values can be named and folded at compile time? |
| Declarations | Which module-level items exist, and how are signatures and bodies collected? |
| Functions | How are parameters, returns, methods, generics, effects, and fallibility declared? |
| Calls | How are arguments matched, methods resolved, and results propagated? |
| Closures | How do anonymous functions infer types and capture their environment? |
| Operators | What are the precedence, typing, assignment, range, and overload rules? |
| Evaluation | In what order do expressions run, and which forms are lazy? |
| Decorators | How does metadata attach to declarations, fields, and parameters? |
Four things that shape the rest
Statements live inside functions. A .at file holds declarations only. A
bare statement at column zero is ATOLL1019: Invalid statement.
total := 0Bindings are immutable unless marked mut, and := (infer) is spelled
differently from : T = (annotate).
fn f(): int {
name := "Ada" // inferred, immutable
limit: int = 10 // annotated, immutable
mut total := 0 // inferred, mutable
total += limit
println(name)
return total
}Branches are expressions. A value can come out of if, match, or a block
rather than through a mutable temporary.
fn tier(score: int): string {
return if score >= 90 {
"gold"
} else if score >= 50 {
"silver"
} else {
"bronze"
}
}
fn describe(value: int?): string {
return match value {
Some(0) => "zero"
Some(v) => "value ${v}"
None => "missing"
}
}Failure is in the signature. A function that can fail says so with ! E,
and callers cannot ignore it. Effects — suspending, allocating, spawning,
cancelling — are tracked too, but the compiler infers them rather than asking
you to declare them.
error MyError { Bad }
fn plain(x: int): int {
return x + 1
}
fn fallible(x: int): int ! MyError {
if x < 0 { error Bad }
return x
}
fn suspending(): int {
return 1
}Expected types
An expression is checked against the type its position expects. That is what lets an incomplete literal finish itself:
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 params from the annotation
println("${ports.len()} ${fallback ?? "none"} ${handler(1)}")
}Annotations constrain; they do not make typing dynamic. Every type is resolved before lowering. When nothing supplies the expectation, annotate the binding or pass explicit generic arguments.
Statement position discards a value but not its effects — a call made only for what it does still evaluates its arguments, may mutate state, may fail, and may suspend.
Naming at a glance
const MAX_CONNECTIONS = 64 // SCREAMING_SNAKE_CASE
struct ConnectionPool { // PascalCase type
in_use: int // snake_case field
}
fn available(pool: ConnectionPool): int { // snake_case function
spare := MAX_CONNECTIONS - pool.in_use // snake_case local
return spare
}Scalar types are lowercase (int, float, bool, char, byte, string,
i8…u128); composite types are PascalCase (List, Map, Set, Option,
Result, Stream, Task).
Reading order
Read Source, Literals, Expressions, Bindings, Constants, Declarations, Functions, and Calls in order. Closures, Operators, and Evaluation land better once function values and inference are familiar. Decorators can wait until a feature such as models, host calls, or SQL metadata requires one.
Then continue to Control Flow for branching and looping, and Types for the full static model.