Effect System
Status: in progress
This article covers effect system foundations. The formal row-polymorphism rules and effect lattice will be expanded from spec source 04_EFFECTS.csl.
What effects are
Section titled “What effects are”An effect in Sigil is a capability a function uses that goes beyond returning a value. Calling println! is an effect (I/O). Allocating heap memory is an effect. Running on the GPU is an effect. Mutating global state is an effect.
In most languages, effects are implicit: a function can print to stdout whether or not its type says so. In Sigil, every effect is explicit in the function’s type. This is not optional — it is F2.
Effect annotations
Section titled “Effect annotations”Effects appear after the parameter list, prefixed with !:
fn pure_fn(x: f32) -> f32 { x * x } // no annotation = purefn io_fn() !io { println!("hello"); } // I/O effectfn gpu_fn() -> Vec4<Color> !gpu { /* ... */ } // GPU executionfn alloc_fn() -> Vec<f32> !alloc { vec![1.0] } // heap allocationMultiple effects can be combined:
fn io_alloc_fn() -> Vec<String> !io !alloc { let mut v = Vec::new(); v.push(read_line()?); v}Effect inference
Section titled “Effect inference”You rarely need to write effect annotations explicitly. The compiler infers the effect row from the function body:
// Effect is inferred as !io because println! has !iofn greet(name: &str) { println!("Hello, {name}!");}You write explicit annotations when you want to enforce a constraint:
// This must remain pure. If a future edit adds !io, the compiler// will reject the change — not silently allow it.fn magnitude(v: Vec3) -> f32 !pure { (v.x*v.x + v.y*v.y + v.z*v.z).sqrt()}Effect propagation
Section titled “Effect propagation”Effects propagate up the call graph. If you call an !io function from inside a function, your function also has the !io effect:
fn log_result(x: f32) !io { // io because it calls println! println!("{}", x * x);}
fn compute_and_log(v: Vec3) !io { // io because it calls log_result log_result(v.x);}The GPU/CPU boundary — the most important effect
Section titled “The GPU/CPU boundary — the most important effect”The !gpu effect is special. It is incompatible with all CPU-side effects:
// This is a compile error:#[shader(fragment)]fn bad_shader(uv: Vec2) -> Vec4<Color> !gpu { println!("uv: {uv}"); // ERROR: !io not allowed in !gpu context Vec4::BLACK}GPU shaders cannot do I/O, cannot allocate CPU memory, cannot call CPU functions. The type system enforces this statically. This is how Sigil prevents an entire class of bugs that appear in GPU programming: accidentally calling a CPU function from shader code (which either fails silently at runtime or requires a driver roundtrip).
Crossing the boundary
Section titled “Crossing the boundary”The CPU-to-GPU boundary is crossed through pipeline objects, not function calls. A Pipeline<VS, FS> connects a vertex shader to a fragment shader and submits work to the GPU. The CPU side has !io; the GPU side has !gpu; the pipeline manages the transfer.
// CPU side (!io): creates pipeline, submits draw calllet pipeline = Pipeline::new(vs_main, fs_main); // type-checks VS/FS interfacepipeline.draw(&vertices, &mut frame); // submits to GPU queue
// GPU side (!gpu): runs in parallel, no CPU access#[shader(vertex)]fn vs_main(v: Vertex) -> Vec4<Clip> !gpu { /* ... */ }Row polymorphism
Section titled “Row polymorphism”Effect rows are polymorphic — a function can be parameterized over the effects it is called with:
// This function works regardless of the effect context:fn apply<T, E>(f: fn(T) -> T | E, x: T) -> T | E { f(x)}
// Works with pure functions, io functions, etc.apply(|x: f32| x * x, 3.0); // pure callapply(|x: f32| { println!("{x}"); x }, 3.0); // !io callThe | E syntax is the effect variable. This is the formal row-polymorphism that appears in the 04_EFFECTS.csl spec.
Common effects reference
Section titled “Common effects reference”| Effect | Meaning | Typical sources |
|---|---|---|
| (none) | Pure — no observable effects | Math functions, pure transformations |
!io | Touches I/O | println!, file ops, network, window |
!gpu | GPU-resident execution | Shaders, GPU compute |
!alloc | Heap allocation | Vec::new(), Box::new(), String |
!mut | Mutates shared/global state | static mut, global registries |
!panic | May panic | Array indexing, unwrap(), arithmetic overflow |
!async | Suspends execution | async fn, .await |
See also
Section titled “See also”- §B.2 Type System — how effects appear in function types
- §B.4 Capability System — capabilities as access-controlled effects
- §C.3 Staged Computation — effects across staging phases
- Spec source:
04_EFFECTS.csl