Skip to main content

Effect system (@io)

Status: the @io effect, propagation through call sites, and removal of the each-allowlist landed in v0.13. This page covers what shipped and what is still ahead.

What shipped in v0.13

esque is a pure-by-default expression language. Function signatures carry an EffectSet (internal/types/types.go); today the only bit is EffIO. The empty set means "pure".

The surface form is an @-attribute placed before fn — the same syntax planned for @kernel/@grad/@inline:

@io fn print_line(s: string) -> i32 = print_str(s)
@io fn caller() -> i32 = print_line("hi") # @io propagates
fn pure() -> i32 = print_line("hi") # type error

Effects compose: if a function calls any @io function its signature must also be @io. The runtime intrinsics (print_i32, print_f32, print_str) are seeded with EffIO in registerRuntimeBuiltins. Both *ast.Call and *ast.Each verify callee.Effects ⊆ caller.Effects; mismatches produce a diagnostic that names both sides and suggests adding the annotation.

The pre-v0.13 hardcoded {print_i32, print_f32} allowlist on each is gone — any function whose effects fit the caller is accepted, including pure user functions and user-defined @io wrappers around the print intrinsics.

What is still planned

  • Effect-polymorphic core combinators. tabulate, scan, and iterate_until callbacks are pure today. The surface for declaring effect-polymorphic combinator inputs is reserved for a follow-up milestone.

  • More effect bits. Other effects on the eventual list:

    EffectMeaning
    @ioSide effects on the world (stdout, files, ...) — shipped
    @allocHeap allocation
    @panicMay abort
    @nondetReads non-deterministic state (clock, RNG)

    These ship one at a time, after @io.

Before vs after

Before (v0.12): each allowlist. Two intrinsics callable from plain functions only via each. No general print discipline.

After (v0.13): print_i32 is a library function with signature @io fn print_i32(x: i32) -> i32 (it returns its argument so calls fit in expression position). each accepts any f whose effect is at most the caller's. New intrinsics (file I/O, env access) can appear in the stdlib without a language change.