Nesta página
Algebraic effects
Status: available on the VM and on the native Cranelift and LLVM backends for the core one-shot model. Read Backend status before relying on terminal
abortbehavior or wasm-aot.
Algebraic effects let a function name the capabilities it needs without choosing their implementation. The caller installs a handler at the boundary, so the same function can use a production service, an in-memory test double, or a recorder without receiving a dependency container.
A minimal effect¶
effect IO {
fn read(path: str) -> str
}
fn read_config() with IO -> str {
return perform IO::read("config.toml")
}
let config = handle read_config() with {
IO::read(path) => "default from {path}",
}
print(config)Output:
default from config.tomlThe four pieces are:
| Purpose | Syntax |
|---|---|
| Declare operations | effect IO { fn read(path: str) -> str } |
| Declare a function's effects | fn load() with IO -> str |
| Request an operation | perform IO::read("config.toml") |
| Interpret operations | handle load() with { IO::read(path) => value } |
The with clause comes after the parameter list and before the return arrow.
Combine effects with +, as in fn run() with Log + Clock -> int.
An inline handler arm receives the operation's arguments. Its resulting value
becomes the value of perform. A handler { ... } literal creates a reusable,
first-class handler value that can be installed with handle expression with handler_value.
resume and abort¶
Returning the arm's final expression resumes the suspended perform
implicitly. Use resume(value) when the arm needs to finish explicitly after
logging or branching.
abort(value) is terminal: it discards the pending continuation and makes
value the result of the enclosing handle.
effect Guard {
fn check(value: int) -> int
}
fn double_checked(value: int) with Guard -> int {
let checked = perform Guard::check(value)
return checked * 2
}
let result = handle double_checked(-4) with {
Guard::check(value) => {
if value < 0 {
abort(0)
}
resume(value)
},
}
print(result)Output:
0Both forms are one-shot today. Declaring a multi fn is accepted and checked,
but invoking resume repeatedly to explore multiple continuations is not a
portable runtime feature yet.
Static diagnostics¶
Run zolo check when effect safety matters. The public diagnostic family is
TE800 through TE814:
| Codes | What they report |
|---|---|
TE800, TE809 |
a performed effect is missing from the enclosing boundary |
TE801, TE802 |
an unknown effect or an uncovered effectful call |
TE803, TE807 |
incomplete inline or first-class handler coverage |
TE804–TE806 |
unknown operation, wrong arm arity, or duplicate arm |
TE808 |
operation argument type mismatch |
TE810 |
handler coverage that the body does not use (warning) |
TE811 |
a generic name used as both a type variable and an effect-row variable |
TE812 |
a multi-shot operation on the native backend |
TE813 |
an ambiguous generic-effect type argument |
TE814 |
a non-handler value after handle ... with |
TE809 is the boundary summary used for public functions and implementation
methods; TE800 remains the per-site form for top-level and other sites that
have no single function signature to annotate.
One CLI gap is intentional to document: the native zolo build path does not
currently run the TE812 multi-shot check even though the compiler API and its
tests do. Do not treat a successful native build of a multi fn program as
proof of multi-shot support.
Backend status¶
| Backend | Current status |
|---|---|
VM (zolo run) |
Reference behavior for the one-shot model and the full feature examples |
| Native / Cranelift | Core declaration, perform, inline and first-class handlers, handler combinators, resume, and abort are in the VM/native differential corpus |
| LLVM | Uses the shared native lowering and an unwind-capable transport; core one-shot effects are implemented |
| wasm-aot | No effects parity corpus is maintained; treat effects as unsupported until a wasm-aot e2e case exists |
Cranelift has one documented semantic exception. It emits no unwind metadata,
so terminal abort uses a pending-flag transport. Statements between the
aborting perform and the end of the handled body may still run with a
placeholder value before the handler's abort value is delivered. The
abort_side_effects.zolo differential fixture is skipped on native for this
reason. LLVM can unwind and matches the VM for that case.
Nested resume or abort inside another lambda in a handler arm has the same
Cranelift transport limitation. Keep terminal control directly in the arm when
portable side-effect ordering matters.
Practical guidance¶
- Use effects at capability boundaries: filesystem, clock, network, database, randomness, logging, and process execution.
- Keep pure transformations as ordinary functions.
- Install handlers near the application entry point or test boundary.
- Prefer
Resultfor a local operation that simply returns an expected error; effects are most useful when a capability crosses several call layers. - Use a first-class handler when the same interpretation is installed more
than once or composed with
std::handler.
Learn more¶
- Effects by example starts with runnable programs and continues through handlers, rows, and generic effects.
- Effect diagnostic reference
documents each
TE800–TE814code. - VM/native effects corpus is the source of truth for differential backend claims and skip reasons.