Atoll’s control-flow forms are expressions. A block, an if, a match, and a
value-bearing loop can each be assigned, returned, or nested wherever their
result type is accepted.
| Chapter | Main question |
|---|---|
| Blocks | How do statement sequences create scopes and values? |
| Conditionals | How do if, else if, and pattern conditions branch? |
| Loops | What do the four for forms do? |
| Patterns | Which shapes can bindings and arms destructure? |
| Match | How are arms selected, guarded, and checked for coverage? |
| Transfers | Where do return, error, ?, break, and continue land? |
| Defer | How is deterministic scope cleanup registered and ordered? |
Everything is an expression
enum Job { Queued, Running(int), Done { code: int } }
fn describe(job: Job, count: int): string {
state := match job {
Queued => "queued"
Running(percent) if percent > 90 => "almost done"
Running(percent) => "running at ${percent}%"
Done { code: 0 } => "succeeded"
Done { code } => "failed with ${code}"
}
label := if count == 0 {
"empty"
} else if count == 1 {
"single"
} else {
"batch of ${count}"
}
return "${label}: ${state}"
}Each branch contributes to the expression’s type. A branch that transfers
control with return, error, break, or continue has no normal value, so it
never forces the other branches to invent one:
error ConfigError { Missing }
fn read(key: string): string ! ConfigError {
if key.is_empty() { error Missing }
return key
}
fn width(key: string): int ! ConfigError {
value := if key.is_empty() {
return 0
} else {
read(key)?
}
return value.len()
}Loops
for is the only loop keyword — there is no while and no loop. Four forms
cover every shape:
fn shapes(values: []int, scores: Map[string, int]): int {
mut total := 0
// iterate a collection, a range, or a map's entries
for value in values { total += value }
for index in 0..10 { total += index }
for name, score in scores { total += score + name.len() }
// loop while a condition holds
mut remaining := 3
for remaining > 0 { remaining -= 1 }
// loop until the head stops matching
mut cursor := values.get(0)
for Some(value) := cursor {
total += value
cursor = None
}
// loop until the body transfers out
for { break }
return total
}A bare for { } can produce a value through break value; the other forms can
also finish through their head, so they type as void.
Transfer map
| Form | Target | Carries |
|---|---|---|
return value |
enclosing function or closure | success value |
error Variant |
enclosing fallible function or closure | error value |
postfix ? |
enclosing optional or fallible function | None or a compatible error |
break value |
nearest or labeled loop | optional loop result |
continue |
nearest or labeled loop | nothing |
| pattern or guard failure | next arm, or the loop-head decision | nothing user-visible |
Before reaching its target, a transfer leaves the intervening lexical scopes and runs their defers and managed destruction. A transfer is never a jump that bypasses ownership or cleanup.
Composition
A realistic path combines several of these at once.
error LoadError { NotFound { id: int }, Corrupt }
struct User { id: int, name: string, active: bool }
fn load_user(id: int): User ! LoadError {
if id < 0 { error NotFound { id: id } }
if id == 13 { error Corrupt }
return User { id: id, name: "user-${id}", active: id % 2 == 0 }
}
fn note(message: string): void { println(message) }
fn first_active(ids: []int): User? ! LoadError {
defer note("lookup finished")
for id in ids {
user := load_user(id)?
if !user.active {
continue
}
return Some(user)
}
return None
}Read each transfer by its target: the iterator controls repetition, ? can leave
the whole function with a LoadError, continue advances only the loop,
return produces the optional success, and the defer runs on all three exits.
Reading an unfamiliar block
Trace four things separately, in this order:
- scope — which names begin and end at each brace;
- value — which tail expression or
break valuereaches the enclosing expression; - transfer — which function, loop, or arm receives each early exit;
- cleanup — which defers and managed values leave scope on that path.
Then check that every value-producing path joins to one accepted type. Applied
to the example above: user is scoped to one iteration; the function’s value
comes from return Some(user) or the final return None; ? targets the
function boundary while continue targets the loop; and the defer belongs to
the function scope, so it runs once on every one of those exits.
This is also what catches the three common mistakes — assuming continue
returns from the function, forgetting that ? exits through the error boundary,
and registering cleanup after the operation that can fail.
For how branch types are joined, continue to Inference. For cleanup under cancellation, continue to Cancellation.