What are effects?
Every function in Loon declares what side effects it can perform. The declaration is in square brackets after the parameters:
fn name(params) [Effects] -> ReturnType
A function with [] is pure — guaranteed no side effects. The compiler enforces this. If a function says it's pure, it is pure.
Built-in effects
Loon defines four built-in effects:
IO— print, exit, any output to the outside worldCrypto— cryptographic operations (hash_password, etc.)Audit— audit logging, required forexpose()FileIO— reading files (read_file, get_arg)
Each effect is a capability. A function must declare the capability before it can use it.
The do keyword
Every call to an effectful function must be prefixed with do. This makes side effects visible at every call site, not just in signatures.
// Pure — no do needed
let sum: Int = add(1, 2);
// Effectful — do required
do print("hello");
You can never accidentally call an effectful function. The do keyword is a conscious acknowledgment: "I know this has side effects, and I intend them."
Effect propagation
If function A calls function B which has [IO], then A must also declare [IO]. Effects propagate up the call chain. The compiler verifies this — you cannot hide effects.
fn helper() [IO] -> Unit {
do print("helping");
}
fn caller() [] -> Unit {
do helper(); // COMPILE ERROR: undeclared effect
}
The compiler produces a structured error:
error: undeclared effect: function caller uses IO but declares []
The fix is to declare the effect honestly:
fn caller() [IO] -> Unit {
do helper(); // now valid
}
This propagation means you can trace any effect from main down to its origin. Every function in the call chain admits what it does.
Why this matters
Pure functions are safe to call anywhere. A function declared [] cannot print, cannot write files, cannot exit the process. It takes values in and returns values out. You can call it in tests, in parallel, in any context, with zero risk of hidden behavior.
You can tell from the signature alone what a function can do. No need to read the implementation. No need to trace through layers of abstraction. The effect list is the truth.
AI agents generating code get immediate feedback about unintended effects. When an LLM adds a print call for debugging and forgets to remove it, the effect checker catches it. The function signature says pure; the body says otherwise. That's a compile error, not a code review comment.
Security auditing becomes a search problem. Find every function with [Crypto] to audit all cryptographic code. Find every function with [Audit] to review all security boundaries. The effect declarations are a machine-readable index of every capability in the codebase.
Effect combinations
Functions can declare multiple effects:
fn register(user: String) [IO, Crypto, Audit] -> Unit
The order doesn't matter. The compiler checks each one independently. A function that hashes a password, logs the registration, and prints a confirmation needs all three effects declared.
Interaction with privacy types
The expose() builtin requires the [Audit] effect. This means any function that crosses a privacy boundary must declare it in its signature — visible in every call chain up to main.
fn log_user(email: Private<String>) [Audit] -> Unit {
let raw: String = do expose(email);
do audit_log(raw);
}
A function that tries to expose() without declaring [Audit] gets a compile error. Privacy boundaries and effect boundaries reinforce each other — you cannot access sensitive data without the compiler knowing about it.