Skip to content

§7 Effect System

Every function must declare the effects recognized directly in its body and inherited through statically resolved callees with ![...]:

fn pure(x: int) -> int { return x * 2; } // no recognized effects
fn read_file(path: string) ![io] { ... } // requires io capability
fn fetch(url: string) ![net] { ... } // requires net capability
fn analyze(text: string) ![io, model] { } // multiple effects

Canonical effects:

Effect Token Grants Enforced Today
IO io Filesystem, console, logging (sub-effects io.fs, io.console — see below) Yes
Network net HTTP, WebSocket, serve (sub-effects net.http, net.ws — see below) Yes
Clock clock sleep, wall-clock Yes
Model model AI library invocation via sfn/ai (post-1.0) Reserved (no detector yet)
GPU gpu Device dispatch via sfn/device (CPU reference backend today; no accelerator in tree) Yes, at the sfn/device boundary
Random rand Random generation Yes, at the sfn/crypto::random_bytes boundary

Enforcement rules:

  1. Any function calling an effectful operation directly must declare that effect — violations produce diagnostics with fix-it hints and are errors by default

  2. Enforcement runs on every build path (sfn dev bootstrap build, sfn build, sfn run, sfn test, sfn check); the SAILFIN_EFFECT_ENFORCE env var lets capsule authors opt into =warning (telemetry-only) or =off (build-path bypass; sfn check still validates)

  3. Tests follow the same rules as functions

  4. Cross-module call-graph propagation (Phase E, shipped): if A imports B and calls it, A must declare every effect B declares. Diagnostic code E0402. Aliased imports (import { foo as bar }) resolve under the local name. Member-callee resolution (mod.fn()) is a Phase E2 follow-up

  5. Capsule capability cross-check (Phase F, shipped): every function’s declared effects must be a subset of the capsule manifest’s [capabilities] required = [...] surface. Diagnostic code E0403. Whether the check runs is decided by whether a [capabilities] section was declared, never by whether the resulting surface is empty:

    Manifest state Behaviour
    No capsule.toml in scope (standalone .sfn) skipped — nothing declared a surface
    capsule.toml present, no [capabilities] section skipped, so pre-Phase-F projects keep building
    [capabilities] present, required = [] deny-all — every declared effect is E0403
    [capabilities] present, required missing or malformed deny-all — same path
    [capabilities] present, required = [...] non-empty subset check against the listed surface

    An explicit required = [] therefore asserts “this capsule needs no capability” and is enforced as a deny-all ceiling; it does not disable the check. Only an absent section is an exemption. A deny-all surface renders in the diagnostic as ![], and ![pure] standing alone still passes, since pure is the empty effect set rather than a capability. A capsule.toml that exists but has no readable [capsule] name is a hard error (E0407) — an unreadable manifest never degrades to “allow everything”.

    Scope of the cross-check. Unlike the in-module and cross-module gates in item 2, E0403 is enforced by sfn check and sfn test, which resolve the manifest surface, and not yet by sfn build / sfn run, whose module path validates effects without a capability surface. E0407 is raised by the resolver and so does apply on every path. Do not read “every build path” in item 2 as covering the capsule ceiling

    Test-time capabilities. [capabilities] dev-required = [...] widens the ceiling for test blocks only; shipped functions and struct methods are always measured against required alone. It exists so a capsule whose tests exercise an effect it never ships need not name that effect in its shipped surface — which SFEP-0016 turns into a syscall mask. Absent dev-required means empty, so a manifest without the key behaves exactly as one written before the key existed. This mirrors sfn add, which already excludes dev-dependencies’ capabilities from a consumer’s runtime surface:

    [capabilities]
    required = ["io", "rand"] # the shipped surface, and the derived mask
    dev-required = ["clock"] # timing assertions in `test` blocks only

    An effect that appears only in dev-required still fails E0403 on a shipped fn

Sub-effect refinements (SFEP-0017, shipped): sub-effects are dotted-name refinements within the locked six roots — io.fs ⊑ io — never a seventh canonical effect. The target-neutral intrinsic registry detects four families: fs.* calls require io.fs, print.*/console.* calls require io.console, http.* calls require net.http, and websocket.* calls require net.ws (the io/net rows in the table above cover these sub-effects). A bare-root grant (![io], ![net]) subsumes every requirement under that root, so existing annotations are unaffected. A narrow grant is also sufficient on its own: ![io.fs] satisfies a detected fs.* call but not a sibling console.* call (missing-effect diagnostic). At the capsule boundary, [capabilities] required = ["io.fs"] tightens a capsule to filesystem-only and rejects a sibling ![io.console] function with E0403; required = ["io"] continues to authorize every io.* sub-effect.

See Effect System Reference for the complete API surface per effect.

An absent effect annotation proves only that the 0.8 checker found no registered direct operation or effect inherited through a resolved call. Unresolved or dynamic callees yield no guessed effect, and FFI/native code is not confined by the emitted binary. Capsule and workspace checks are compile-time declaration contracts; the runtime syscall seal is a 1.0 target.