Skip to content

Effects

Describe and infer the observable capabilities of Atoll functions.

Updated View as Markdown

An effect row records operations a function may perform beyond returning a value. Effects are part of semantic analysis and function types, but most application functions let the compiler infer them.

The current concrete effects are:

Effect Meaning
Suspend May yield and resume later
Error May exit through the error channel
Alloc May allocate in managed arena storage
Spawn May start a child task
Cancel May request task cancellation

Effects propagate through calls. If load suspends and render calls load, render inherits Suspend even though its own body contains no runtime primitive.

Effect rows are sets, not execution traces. Repeating an effect does not add a second entry, and the row does not record how many times an operation occurs or in which order.

Common sources

Source construct Effects introduced directly
Managed allocation Alloc
Typed error exit Error
spawn Spawn, Suspend
race Spawn, Suspend, Cancel
Task.cancel() Cancel
Suspended host call or task join Suspend

The body of a called function contributes its inferred or declared row through the call graph.

Declared rows

Place using after the result type:

fn poll(): int using [Suspend] {
    return read_counter()
}

A declared row is a contract. Calling something with an effect absent from the closed row is diagnosed:

fn pure_poll(): int using [] {
    return read_counter()
}

Use using [] deliberately; it opts out of implicit effect inference for that contract.

Closed rows are appropriate when callers depend on a capability guarantee:

fn normalize(value: string): string using [Alloc] {
    return value.trim().to_lower_ascii()
}

If implementation changes introduce a new effect, the declaration turns that change into a diagnostic instead of silently widening the public contract.

Row variables

Higher-order functions can preserve effects belonging to a callback:

fn apply[T, R, E](
    value: T,
    operation: fn(T) using [E] -> R
): R using [E] {
    return operation(value)
}

The identifier E is an open suffix row. A concrete prefix can be combined with it:

fn scheduled[T, E](
    operation: fn() using [E] -> T
): T using [Suspend, E] {
    yield_once()
    return operation()
}

At specialization time, the callback’s concrete effects replace the row variable.

An open row can also have a fixed prefix. using [Suspend, E] means the function always permits suspension and additionally preserves whatever effects the callback contributes. The row variable represents a row suffix, not an ordinary runtime type value.

When to write one

Explicit rows are most useful for:

  • stable public library contracts;
  • callback parameters and generic adapters;
  • compiler and host boundaries;
  • assertions that a function is effect-free.

For ordinary private functions, inference is less repetitive and stays accurate when the body changes.

Effects and errors

Error does not replace the typed T ! E signature. The signature says which error values can escape; the effect row says that an error exit exists. The two represent different layers of the same function contract.

Likewise, Suspend does not say why a function waits, and Alloc does not expose an allocator. Effects constrain and explain calls; they are not runtime handlers that application code can catch.

Current precision

Effect analysis is intentionally conservative in a few concurrency lowerings. The current select checker records Spawn, Suspend, and Cancel even though a source-level select does not itself spawn or cancel its losing sources. A default arm uses a non-blocking runtime poll but still carries that conservative static row.

Spawn-body effects are also synthesized while checking the enclosing function, so effects used only inside a child can widen the parent’s inferred row. Treat explicit public rows as the contract and use diagnostics to catch places where the current conservative analysis needs an allowed capability.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close