Effect System

Algebraic effects with handlers: checking, inference, and runtime dispatch.

Effect System

Sounio tracks side effects explicitly at the type level using an algebraic effect system with handlers.

At a high level:

  1. Functions declare effects with with ... (e.g., with IO, Async).
  2. The compiler checks that effectful operations are only used when the corresponding effect is declared.
  3. At runtime, a handler stack (and continuation machinery) dispatches effect operations.

Where It Lives

  • crates/souc/src/effects/ — effect inference, continuations, and handler implementations
  • crates/souc/src/types/effects.rs — effect set representation used by the checker

Key submodules under crates/souc/src/effects/ include:

  • inference.rs — effect inference/checking pass
  • handlers/ — concrete handler implementations (IO, Async, GPU, …)
  • continuation.rs — CPS/continuation machinery for suspend/resume
  • linearity.rs — linearity checks around effects (when enabled/used)
  • resilience.rs — resilient patterns (retry, circuit breaker)

Built-In Effects (Examples)

The compiler/runtime has handler implementations for effects like:

EffectPurposeTypical Ops
IOconsole/filesystem I/Oprint, read_file, write_file
Panicrecoverable failurespanic, assert, unwrap
Asynctask schedulingspawn, await, join, select
GPUGPU executionlaunch, sync, alloc_device

Spec vs implementation: some effect operations may exist as stubs depending on enabled features and available external tooling (CUDA, LLVM, etc.). See docs/compiler/KNOWN_LIMITATIONS.md.

Effect Checking & Inference

Effect inference tracks where effects come from so diagnostics can be precise (direct ops, method calls, higher-order functions, pattern-match panics, …).

Common error classes:

  • Undeclared effect: using an effectful operation inside a function that didn’t declare the effect.
  • Unhandled effect: effect is declared but no handler is available for execution in that mode.
  • Effect in pure context: calling an effectful closure where a pure one is required.

Checker Shape (Selected Fields)

pub struct EffectChecker<'a> {
    symbols: &'a SymbolTable,
    type_info: Option<&'a TypeInfo>,
    fn_effects: HashMap<DefId, EffectSet>,
    method_effects: HashMap<(String, String), EffectSet>,
    declared: EffectSet,
    inferred: EffectSet,
    errors: Vec<EffectError>,
    effect_sources: HashMap<String, Span>,
}

Error Classification (Examples)

pub enum EffectErrorKind {
    UndeclaredEffect { effect: String, source: EffectSource },
    UnhandledEffect { effect: String },
    EffectInPureContext { effect: String },
    EffectfulClosureArg { effect: String, hof_name: String },
    RefutablePatternPanic { pattern_desc: String },
}

Handlers and Continuations

Handlers implement a capability interface (effect name + supported operations + handle(...)), and execution can suspend/resume via a continuation representation.

This design enables:

  • swapping handler implementations (e.g., testing IO)
  • resilient handlers (retry/circuit breaker style patterns)
  • integration points for async scheduling and GPU dispatch

Handler Capability Trait (Shape)

pub trait HandlerCapability {
    fn effect_name(&self) -> &str;
    fn operations(&self) -> &[OperationSpec];

    fn handle(
        &self,
        operation: &str,
        args: &[Value],
        cont: Continuation,
        state: &mut HandlerState,
    ) -> HandlerResult;
}

Continuation Resume Points (Shape)

pub enum ResumePoint {
    InterpreterClosure { resume_fn: OneShotResumeFn },
    InterpreterMultiShot { resume_fn: MultiShotResumeFn },
    Jit { return_address: usize, saved_registers: Vec<u64> },
    Stub,
}

Example (Language-Level)

fn read_config(path: string) -> string with IO {
    return perform IO.read_file(path)
}

fn pure_add(a: i32, b: i32) -> i32 {
    // Error (effect not declared): calling IO inside a pure function
    // print(a + b)
    return a + b
}

Next