Effect System

Aria tracks side effects in function signatures. This tells the compiler — and the AI generating code — exactly what a function can and cannot do. Effect enforcement is implemented: the compiler warns when impure built-in functions are called from pure functions, and user-defined effects are enforced.

Declaring Effects

Effects are declared with with [...] after the return type:

// Pure function — no effects
fn calculateTotal(items: [Item]) -> f64 =
    items.map(.price).sum()

// I/O and filesystem effects
fn readConfig(path: str) -> Config ! IoError with [Io, Fs] {
    content := io.readFile(path)?
    json.decode[Config](content)?
}

// Network effects
fn fetchData(url: str) -> [byte] ! HttpError with [Net] {
    net.get(url)?.body
}

// Async effects (concurrency)
fn processAll(urls: [str]) -> [Result] with [Async] {
    scope {
        urls.map(fn(url) => spawn fetch(url))
            .map(fn(t) => t.await())
    }
}

// FFI effects (foreign function calls)
fn callCLib(data: [byte]) -> i32 with [Ffi] {
    ...
}

Built-in Effects

EffectCovers
IoI/O operations (print, stdin/stdout)
FsFilesystem access (read, write, delete)
NetNetwork operations (HTTP, TCP, DNS)
AsyncConcurrency primitives (spawn, channels)
FfiForeign function interface calls

Why Effects Matter

For the Compiler

The compiler enforces effect obligations. A pure function cannot call a function with effects without declaring them:

// Compile error: calculateTotal is pure but calls readFile which has [Io, Fs]
fn calculateTotal() -> f64 {
    data := io.readFile("prices.json")?  // ERROR
    ...
}

// Fix: declare the effects
fn calculateTotal() -> f64 ! IoError with [Io, Fs] {
    data := io.readFile("prices.json")?  // OK
    ...
}

For AI Code Generation

  • Pure functions are safe to cache, parallelize, reorder, and memoize
  • Effect declarations make dependencies explicit — the AI knows what context is needed
  • No hidden I/O — you can never accidentally call a network function from a "pure" computation

For Testing

Pure functions are trivially testable — no mocking needed:

// This function is pure, so tests are simple
fn calculateDiscount(price: f64, rate: f64) -> f64 =
    price * (1.0 - rate)

test calculateDiscount {
    assert calculateDiscount(100.0, 0.1) == 90.0
    assert calculateDiscount(50.0, 0.25) == 37.5
}

Effect Propagation

Effects propagate through the call chain. If function A calls function B which has [Io], then A must also declare [Io]:

fn helper() -> str ! IoError with [Io, Fs] {
    io.readFile("data.txt")?
}

// Must declare Io and Fs because it calls helper()
fn main() -> str ! IoError with [Io, Fs] {
    helper()?
}

Next Steps