An effect is something a function may do beyond returning a value: suspend, fail, allocate, spawn, or cancel. The compiler tracks all five for you.
You almost never write an effect down. Effects are inferred from what a function calls, and ordinary Atoll code carries no effect annotations at all — there is not a single one in the compiler’s sample projects, its test fixtures, or its prelude. Read this page to understand the diagnostics you will see, not because you are expected to annotate anything.
fn checksum(data: []byte): int {
mut sum := 0
for b in data {
sum = sum + b.to_int()
}
return sum
}
fn log_checksum(data: []byte): int {
total := checksum(data)
println("checksum ${total}")
return total
}checksum only computes. log_checksum calls println, which is a @suspend
host op, so the compiler infers Suspend for it — and for everything that calls
it. That inference is what drives the CPS transform in the backend; it is not a
style rule you are being asked to follow.
The explicit using [...] row exists, and the rest of this page shows it,
because it is how you read the diagnostics and how you would pin a contract on a
published API boundary. Treat it as a specialist tool.
The five effects
There are exactly five. Any other name in a using clause is an error.
| Effect | A function carries it when it may… |
|---|---|
Suspend |
pause and resume later — a @suspend host call, a task join, a blocking select, a backpressured send_await |
Error |
leave through the error channel — error V, or ? on a Result |
Alloc |
allocate in managed arena storage — build a List, Map, Set, or interpolated string |
Spawn |
start a child task — spawn, or race (which spawns each arm) |
Cancel |
request cancellation — Task.cancel(), or race (which cancels losers) |
Each one, in isolation:
error DecodeError { Truncated }
fn may_suspend(): void using [Suspend] {
println("waiting")
}
fn may_error(input: string): int ! DecodeError using [Error] {
if input.is_empty() { error Truncated }
return input.len()
}
fn may_allocate(n: int): []string using [Alloc] {
mut out: []string = []
mut i := 0
for i < n {
out.add("row ${i}")
i = i + 1
}
return out
}
fn may_spawn(n: int): int using [Spawn, Suspend] {
child := spawn { n * 2 }
return child.await()
}
fn may_cancel(n: int): void using [Spawn, Suspend, Cancel] {
child := spawn { n * 2 }
child.cancel()
}spawn always brings Suspend along with Spawn, because the join that
follows it can pause. race brings all three of Spawn, Suspend, and
Cancel.
An effect is a conservative may fact. Suspend does not mean every call will
yield — awaiting an already-completed task takes a synchronous fast path. It
means the caller must be compiled to permit a yield on some reachable path.
Rows are sets. Repeating a name adds nothing, and the row records neither how many times an operation happens nor in what order.
Inference
Most functions should not declare a row at all. Omit the clause and the compiler computes one, unioning the rows of everything the body calls until the whole call graph reaches a fixed point.
fn fetch(id: int): int {
println("fetching ${id}")
return id
}
fn fetch_pair(a: int, b: int): int {
return fetch(a) + fetch(b)
}
fn report(a: int, b: int): int {
return fetch_pair(a, b)
}None of these declare anything, and all three end up carrying Suspend:
println seeds it, fetch inherits it, fetch_pair inherits it from fetch,
and report from fetch_pair. Recursion converges the same way — to the union
of everything reachable in the recursive component.
ATOLL2031 — the inheritance hint
ATOLL2031 is a hint. It fires when a function’s row picks up an effect from
a called function rather than from an operation in its own body, and it fires
whether or not you declared that effect. It is a trace, not a complaint.
fn tick(): int {
println("tick")
return 1
}
fn twice(): int using [Suspend] {
return tick() + tick()
}Checking that unit prints:
hint[ATOLL2031]: fn `twice` inherits effect `suspend` from a transitively-called
calleeThe declaration is correct and complete; the hint simply tells you where
Suspend came from, so you can follow the chain when a row surprises you.
Diagnostics render normalized lowercase names (suspend, alloc); source uses
the PascalCase spellings.
There is nothing to fix. Hints never fail a build.
ATOLL2032 — the row-violation warning
ATOLL2032 is a warning. It fires when a function inherits an effect through
a call edge that its declared row does not list. A function with no using
clause is treated as declaring nothing, so it triggers the warning too as soon as
it calls something effectful.
fn poll_backend(): int using [Suspend] {
println("polling")
return 1
}
fn refresh(): int using [] {
return poll_backend()
}That unit still compiles, but reports:
warning[ATOLL2032]: fn `refresh` inherits effect `suspend` from a
transitively-called callee but its declared `using [..]` row does not list
`suspend`Because a function with no using clause declares nothing, this warning fires
on almost every effectful function in an ordinary program. That is the normal
state of Atoll code, not a defect in it.
The responses, in order of how often they are the right one:
- Ignore it. This is the usual answer. The program is correct, the compiler has inferred the effect it needs, and nothing downstream is weaker for it. The warning is telling you the inferred row is wider than the declared one — and you never declared one.
- Stop calling the effectful thing — if you wanted this function to stay pure, move the work to a caller and take the result as a parameter. Use the warning as the signal that purity slipped, not as a prompt to annotate.
- Widen the row — only when you have deliberately written a
usingclause because this function is a contract boundary you want checked. - Make the boundary polymorphic — if the effect arrives through a callback, thread it with a row variable (below) rather than hard-coding it.
Widening is the fix here:
fn poll_backend(): int using [Suspend] {
println("polling")
return 1
}
fn refresh(): int using [Suspend] {
return poll_backend()
}Response 2 is worth seeing too, because it is the one that keeps a row narrow:
fn poll_backend(): int using [Suspend] {
println("polling")
return 1
}
fn interpret(sample: int): string using [] {
return if sample > 0 { "up" } else { "flat" }
}
fn refresh(): string using [Suspend] {
return interpret(poll_backend())
}interpret does the decision-making and stays provably effect-free; only the
thin outer function carries Suspend.
Do not reflexively add all five effects to silence the warning. using [] is
worth writing precisely because it turns “this stayed pure” into a checked
statement.
Note the boundary of the check: ATOLL2032 is about inherited effects. An
operation written directly in the body of a using [] function is not currently
reported, so the row is an assertion about your call graph, not a sandbox.
Unknown effect names are errors
Unlike the two diagnostics above, a bad name in a using clause fails the build.
fn f(): int using [Blocking] {
return 1
}That is ATOLL2021: unknown effect name 'Blocking' in using clause. The
lowercase spellings that appear in diagnostics are not accepted in source either:
fn f(): int using [suspend] {
return 1
}Row variables
A higher-order function should not have to claim its callback’s effects. Bind a generic parameter and use it as a row:
fn apply[T, R, E](value: T, op: fn(T) using [E] -> R): R using [E] {
return op(value)
}
fn shout(s: string): string {
println(s)
return s
}
fn pure_twice(n: int): int {
return n * 2
}
fn use_pure(): int using [] {
return apply(21, pure_twice)
}
fn use_suspending(): string using [Suspend] {
return apply("hi", shout)
}One apply, two specializations. With pure_twice the row variable resolves to
the empty row, so use_pure stays using []. With shout it resolves to
[Suspend], and the caller must permit it.
A row variable may sit behind a fixed prefix. using [Suspend, E] means “always
suspends, plus whatever the callback contributes”:
fn instrumented[T, E](op: fn() using [E] -> T): T using [Suspend, E] {
println("begin")
result := op()
println("end")
return result
}
fn make_answer(): int {
return 42
}
fn run(): int using [Suspend] {
return instrumented(make_answer)
}The identifier must be a generic parameter of the enclosing declaration. A bare capital letter that binds nothing is just an unknown effect name:
fn f(): int using [E] {
return 1
}Effects in function types
Effect rows are part of a function type, so they participate in assignment compatibility.
type PureTransform = fn(string) using [] -> string
type WaitingTransform = fn(string) using [Suspend] -> string
fn upper(s: string): string using [] {
return s.to_upper_ascii()
}
fn announce(s: string): string using [Suspend] {
println(s)
return s
}
fn run(): string using [Suspend] {
quiet: PureTransform = upper
loud: WaitingTransform = announce
return quiet("a") + loud("b")
}The using clauses on upper and announce are load-bearing here. Matching a
function type that names a non-empty row requires a declared row on the
function value — an inferred row does not unify with it:
type WaitingTransform = fn(string) using [Suspend] -> string
fn announce(s: string): string {
println(s)
return s
}
fn run(): string using [Suspend] {
loud: WaitingTransform = announce
return loud("b")
}announce inherits Suspend, but because it declares nothing the assignment is
rejected with ATOLL2002. Add using [Suspend] to announce and it compiles.
This is one of the places where declaring a row is not optional.
Effects are not error types
Error and the T ! E signature answer different questions. The signature says
which error values can escape; the effect says that an error exit exists.
error StoreError { Missing, Conflict }
fn read(key: string): string ! StoreError using [Error] {
if key.is_empty() { error Missing }
return "value"
}
fn read_or_default(key: string): string using [Error] {
return read(key).unwrap_or("fallback")
}read_or_default discharges the Result with unwrap_or, so no error value can
escape it — yet it still declares Error, because the effect travels along the
call edge to read regardless of how the result is consumed. Dropping the
declaration there yields an ATOLL2032 warning, not a narrower contract.
Likewise, Suspend does not say why a function waits, and Alloc does not
expose an allocator. Effects are static contracts used for checking, ABI
selection, and lowering. They are not runtime handlers, they do not authorize
unsafe memory access, and they are not a security boundary.
When to declare a row
Write one explicitly for:
- public library signatures whose capability set is part of the contract;
- callback parameters and generic adapters (use a row variable);
- host and compiler boundaries;
- functions where “this is effect-free” is the point.
Leave it off for ordinary internal functions. Inference is shorter and it stays correct when the body changes.
Current precision
Two places over-approximate, and the diagnostics reflect the approximation rather than runtime behaviour:
selectrecordsSpawn,Suspend, andCancel, even though a source-level selection spawns nothing and cancels nothing. Aselectwith adefaultarm uses a non-blocking poll and still carries the full row.- Spawn bodies are checked inside the enclosing function, so an effect used only inside a child can widen the parent’s inferred row.
fn work(): int { return 1 }
fn dispatch(): int using [Spawn, Suspend, Cancel] {
a := spawn { work() }
b := spawn { work() }
mut chosen := 0
select {
v := a => chosen = v
v := b => chosen = v
}
return chosen
}dispatch never calls cancel, but the declared row needs Cancel to stay
warning-free. Treat the explicit row as the contract, and let the diagnostics
tell you which capability the current analysis wants.