Skip to main content

Effects and I/O

esque tracks side effects in the type system. As of v0.13 the only effect is @io — a function that may write to stdout (or, in future, to files / the environment). Pure functions (no annotation) may not call @io functions; @io functions may call anything.

What esque has today​

  • print_i32(x: i32) -> i32 @io — write a decimal integer + \n, returning x.
  • print_f32(x: f32) -> f32 @io — write a decimal float + \n, returning x.
  • print_str(s: string) -> string @io — write a string verbatim, returning s.
  • each(v, f) where f is any named function whose effect set is a subset of the enclosing function's effect set.

Everything else is pure. There is no file I/O, no environment access, no clock, no randomness, no general stdout writer — but adding any of those is now a stdlib question, not a language one.

The rule​

callee.effects ⊆ caller.effects

A pure caller may call only pure callees. An @io caller may call either pure or @io callees. each(v, f) applies the same rule to its second argument.

@io fn shout(x: i32) -> i32 = print_i32(x)

@io fn main() -> i32 = {
each(0..3, shout); # ok: shout is @io, main is @io
0
}

fn pure_main() -> i32 = print_i32(1) # type error

The type error names both sides and suggests adding the annotation to the caller.

Why the design​

esque is built around the idea that the type system should tell you what code can do. A function annotated (i32) -> i32 cannot secretly produce side effects. each does not have to trust an arbitrary function; it just checks the effect set fits.

What you write today​

  • Mark main as @io fn main() whenever you want to print.
  • Wrap repeated print logic in your own @io fn. The pre-v0.13 hardcoded allowlist (print_i32, print_f32) is gone — any effect-correct function works.
  • Pure-by-default functions stay easy to read: if there is no annotation, there is no I/O.

Beyond @io​

Other effects on the long-term roadmap:

  • @alloc — allocates heap memory (relevant once dynamic-shape tensors arrive).
  • @panic — may abort.
  • @nondet — random / nondeterministic.

These are all ways for the type system to keep promising more about what a function does. The EffectSet representation is already a bitmask, so adding each new effect is a one-bit change plus its propagation rules.