Evaluation order matters whenever an expression can mutate state, suspend, perform I/O, fail, or call user code. Atoll specifies order at those observable boundaries; an optimizer may rearrange only computations whose reordering cannot change observable behavior.
Core order
The following rules are part of the language contract:
- statements in a block run in source order;
- the left operand of an ordinary binary operator is evaluated before the right operand;
- call arguments are evaluated from left to right;
- tuple, list, array, and struct-literal initializers are evaluated in source order;
- the receiver of a field, index, or method operation is evaluated once for that operation;
- a
matchsubject is evaluated once, then arms are tested in source order; - interpolation expressions run in their textual order;
- deferred actions run in last-in, first-out order when their scope exits.
These are runtime rules, not optimization suggestions. For example, this call
prints first before second:
fn observed(label: string, value: int): int {
println(label)
return value
}
sum := observed("first", 10) + observed("second", 20)Avoid using order merely as a terse substitute for clearer statements. When two operations mutate the same object, naming their results usually makes the dependency easier to see and debug.
Lazy forms
Some expressions deliberately do not evaluate every syntactic child.
| Form | Evaluation rule |
|---|---|
left and right |
Evaluate right only when left is true |
left or right |
Evaluate right only when left is false |
value ?? fallback |
Evaluate fallback only when the left value has no successful payload |
target ??= fallback |
Evaluate and store fallback only when the target lacks a successful payload |
if |
Evaluate the selected branch only |
match |
Evaluate the selected arm body only |
postfix ? |
Stop the enclosing expression and return on propagation |
catch |
Run an arm only for the error case |
token := cached_token ?? request_token()
if user != None and user.unwrap().is_active {
show_dashboard()
}The type checker still checks every branch. “Not evaluated” means absent from a particular runtime path, not exempt from name, type, effect, or exhaustiveness checking.
Matching
A match separates three ordered actions:
- evaluate the subject once;
- try arm patterns from top to bottom;
- after a pattern matches, evaluate its guard, if any.
A false guard resumes with the next arm. Bindings made by a pattern are available to its guard and body, and they do not escape that arm.
message := match next_event() {
Event.Data(value) if value.is_valid() => render(value)
Event.Data(_) => "invalid"
Event.Closed => "closed"
}Here next_event() runs once. is_valid() runs only for a Data value, and
render runs only when that guard succeeds.
Assignment
An assignment resolves its destination and evaluates its source exactly once. Compound assignment behaves as one read-modify-write operation on the same place; it is not a license to duplicate a receiver or index expression.
items[next_index()] += amount()next_index() identifies one element. The implementation must not invoke it
again while writing the result. For code where the relative ordering of
destination lookup and source effects matters, use explicit bindings:
index := next_index()
delta := amount()
items[index] += deltaThe explicit form also remains clearer if either operation can suspend or fail.
Suspension
Argument and operand evaluation follows the same order even when one step can suspend. Values computed before the suspension remain part of the suspended function state; evaluation resumes at the continuation rather than restarting the entire expression.
result := combine(read_cached(), fetch_remote().await())read_cached() runs before fetch_remote(). If the await suspends, the cached
value must survive in the continuation frame and must not be recomputed after
resume.
Unspecified cases
Atoll does not promise observable ordering where the source does not define a sequence. In particular:
- declaration discovery across files is compile-time work, not runtime order;
- map and set traversal order is not source evaluation order;
- task completion order is independent of the order in which task handles were created;
- the order between evaluating a dynamically computed callee value and its argument list is not yet a stable language guarantee.
If a dynamic callee expression has an effect, bind it before the call:
handler := choose_handler()
result := handler(load_input())This limitation does not affect ordinary calls to a named function or method, where resolving the callable itself performs no user-visible work. It is listed explicitly so compiler optimizations and future syntax work do not accidentally turn an implementation detail into a hidden promise.
See Operators for precedence and operator typing, Blocks for scope exit, and Match for pattern selection.