Atoll is a statically typed language that compiles to WebAssembly. It has
nominal structs and enums, typed errors instead of exceptions, Option
instead of null, static trait dispatch, structured concurrency, and SQL
queries the compiler type-checks against your schema.
Here is a whole program:
struct LineItem {
sku: string
price: float
qty: int
}
error OrderError {
Empty
BadQuantity { sku: string }
}
fn total(items: []LineItem): float ! OrderError {
if items.is_empty() {
error Empty
}
mut sum := 0.0
for item in items {
if item.qty <= 0 {
error BadQuantity { sku: item.sku }
}
sum += item.price * (item.qty as float)
}
return sum
}
fn main(): void {
mut cart: []LineItem = []
cart.add(LineItem { sku: "AT-1", price: 12.5, qty: 2 })
cart.add(LineItem { sku: "AT-9", price: 4.0, qty: 3 })
match total(cart) {
Ok(amount) => println("order total ${amount}")
Err(Empty) => println("cart is empty")
Err(BadQuantity { sku }) => println("bad quantity for ${sku}")
}
}Four things in that program are worth naming before you read further.
float ! OrderError is sugar for Result[float, OrderError]: the failure
modes are in the signature, and match cannot forget one of them. error Empty leaves through the error channel — there is nothing to throw and
nothing to catch by accident.
[]LineItem is sugar for List[LineItem], an owned growable list. add
needs the mut binding because it takes a mutable receiver.
for is the only loop keyword. It iterates (for item in items), tests a
condition (for i < n), or runs forever (for { }).
${...} interpolates through the Display trait, so any type that implements
to_string can appear inside a string.
Absence, failure, and concurrency in one place
The three features that most change how you write code are Option,
declared errors, and spawn. They compose without ceremony:
error FetchError { Timeout { host: string } }
struct Page {
host: string
bytes: int
}
fn fetch(host: string): Page ! FetchError {
if host.is_empty() { error Timeout { host: host } }
return Page { host: host, bytes: host.len() * 100 }
}
fn largest(pages: []Page): Page? {
mut best: Page? = None
for page in pages {
if page.bytes > (best?.bytes ?? 0) {
best = Some(page)
}
}
return best
}
fn main(): void {
left := spawn { fetch("example.com") }
right := spawn { fetch("atoll.dev") }
mut pages: []Page = []
match left.await() {
Ok(page) => pages.add(page)
Err(Timeout { host }) => println("timed out: ${host}")
}
match right.await() {
Ok(page) => pages.add(page)
Err(Timeout { host }) => println("timed out: ${host}")
}
match largest(pages) {
Some(page) => println("largest: ${page.host} (${page.bytes} bytes)")
None => println("nothing fetched")
}
}Both spawn bodies start before the first await. Task[T] preserves the
body’s static type, so awaiting a fallible task hands back an ordinary
Result — concurrency changes scheduling, not the type or error model.
largest returns Page? because “no pages” is a legitimate answer, not an
error. best?.bytes ?? 0 reaches through the Option and supplies a fallback
in one expression. Spawning and awaiting also give the function the Spawn and
Suspend effects — which the compiler infers for you; effect annotations are
not something you write.
Reading path
Start with the Tour. It builds one small program from
println to modules and concurrency, and every block on it compiles.
Then work through the chapters in order:
| Chapter | What it covers |
|---|---|
| Fundamentals | Source text, literals, bindings, constants, declarations, functions, calls, closures, operators, decorators |
| Control flow | Blocks, conditions, patterns, match, the for forms, transfer expressions, defer |
| Types | Primitives, inference, conversions, generics, structs, enums, aliases, tuples, traits, references |
| Errors | Option, Result, declared errors, ?, catch, error unions |
| Modules | Module paths, imports, aliases, visibility, atoll.toml |
| Concurrency | Effects, suspension, tasks, spawn, select, race, streams, cancellation |
| SQL | Models, schemas, routing, queries, frames, writes, transactions |
| Standard Library | Scalars, text, collections, time, I/O, networking, traits, intrinsics |
Read Errors before I/O, concurrency, or queries: all
three expose typed fallibility, and the rest of the language assumes you are
comfortable with Option, Result, and ?.
Two reference pages sit outside that sequence. Diagnostics shows real compiler output next to the code that produced it. Feature Status records which prelude APIs actually lower to WebAssembly — a program can type-check and still fail to build, and that page tells you where the line is.
If you are coming from another language
| Habit | Atoll |
|---|---|
null / nil |
T? — an Option. xs[0] returns T?, not T |
| exceptions | T ! E — a Result with a declared error type |
while (cond) |
for cond { } |
let / var |
x := v and mut x := v |
List<T>, Foo<T> |
[]T, Foo[T] — brackets, not angle brackets |
Int, String, Boolean |
int, string, bool — scalars are lowercase |
| interfaces with runtime dispatch | traits with static dispatch; there is no dyn |
camelCaseMethods |
snake_case functions, fields, and locals |
throw / try / catch |
error V, ?, and catch — which converts errors rather than unwrapping them |
Names follow one rule set throughout: snake_case for functions, methods,
locals, parameters, and fields; PascalCase for nominal types and enum
variants; SCREAMING_SNAKE_CASE for constants; lowercase for scalar builtins.
Three syntax details cause most first-day errors, so they are worth having in advance:
- Struct declarations separate fields by newline; struct literals require commas.
erroris a keyword and can never be an identifier. Bind an error payload aserrore.?propagates and needs a fallible enclosing function;??supplies anOptionfallback;?.reaches through anOption. They are three different operators.
Where the normative text lives
This guide teaches the language through working programs. For normative grammar and invariants, use the Language Specification. For how source becomes WebAssembly, continue to the Compiler, IR, and Runtime sections.