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.