Execution modes
A Kernel runs in one of six modes, named by its body. A reduction, a scan, a scatter and a prefix each walk an input, one body run per element, named after over; the order inputs are declared in means nothing.
| Mode | Body | One run per | Output | Fuses |
|---|---|---|---|---|
| Map | compute { … } |
element of the output domain | that domain, several outputs allowed | yes |
| Reduction | compute over x { … } + merge(a, b) { … } |
element of x |
Rows of exactly one element |
no |
| Scan | scan over x with y = init { … } |
element of x, in order |
as many as x |
no |
| Scatter | scatter over x into t with sum|min|max|last { … } |
element of x |
the target t’s shape |
no |
| Prefix | prefix over x with sum|min|max { … } |
element of x |
Rows as many as x |
— |
| Group | group N { … } |
elements N at a time | as a map | — |
Every backend runs every mode; crates/backends/conformance holds the JIT and the GPU to the interpreter over 36 language features × 7 execution forms.
Map (compute)
Section titled “Map (compute)”Once per element of the output domain. A map may write several outputs over one domain, given at once as a record:
kernel shade.surface@1 { requires { field.write; } output albedo: Image; output normal: Grid<Vec3>; output depth: Grid<Float>; compute { let hit = cast(eye, ray()); if hit.t < 0.0 { return { albedo: sky, normal: vec3(0.0, 0.0, 0.0), depth: 100.0 }; } { albedo: vec4(hit.albedo, 1.0), normal: hit.normal, depth: hit.t } }}A graph reads node.albedo. When one consumer is the only reader of every output and reads them all by sample, the two fuse and no output is written.
Reduction (compute + merge)
Section titled “Reduction (compute + merge)”kernel stats.luma@1 { requires { field.read; field.write; } input source: Image; output result: Rows<Light>; // exactly one element comes out struct Light { sum: Float; max: Float; count: Int; } compute over source { // once per element of `source` let l = luma(sample(source)); return Light { sum: l, max: l, count: 1 }; } merge(a, b) { // folds two results into one return Light { sum: a.sum + b.sum, max: max(a.max, b.max), count: a.count + b.count }; }}merge must be associative and commutative: the fold’s tree is the backend’s; integers and booleans are exact, floats agree to rounding. A reduce node takes no domain and never fuses.
Scan (scan): ordered work
Section titled “Scan (scan): ordered work”kernel audio.lowpass@1 { requires { field.read; field.write; } input samples: Rows<Float>; param k: Float = 0.1; output result: Rows<Float>; // one element per input element scan over samples with y = 0.0 { // the input it walks, the carried value and its start return y + (sample(samples) - y) * k; }}The body runs once per element, in order: y is the previous result (init first), the result is this output element and the next y, index() the element index. A scan runs on one thread and never fuses.
memory: a scan may own memory
Section titled “memory: a scan may own memory”kernel machine.step@1 { requires { field.read; field.write; } input ticks: Rows<Int>; // one step per element memory ram: Rows<Int>; // bound like an input, read like an output output result: Rows<Int>; scan over ticks with pc = 0 { let op = at(ram, pc); // what an earlier step wrote is there put(ram, pc + 100, op * 2); // a write, in step order return pc + 1; }}ram <~ machine.ram from zeros.result persists across evaluations; ram <- fresh.result starts each over. A put outside is dropped, never clamped; the bound Field is never touched, and a failed evaluation leaves it as it was. The CHIP-8 and ARM Thumb machines (examples/machines) are written this way.
Scatter (scatter): writes where it computes
Section titled “Scatter (scatter): writes where it computes”kernel particles.splat@1 { requires { field.read; field.write; } input particles: Rows<Particle>; input canvas: Grid<Vec3>; output result: Grid<Vec3>; // starts as `canvas` struct Splat { cell: Vec2; heat: Vec3; } scatter over particles into canvas with sum { let p = sample(particles); return Splat { cell: floor(p.position * size(canvas)), heat: tint(p) }; }}- The body yields a struct of two fields: an address into the target (
Intfor rows,Vec2grid,Vec3volume) and a value of its element type. - Combining:
sumadds fieldwise,min/maxtake the fieldwise extreme,lastreplaces; the result equals applying sources in element order. - An address outside the target, NaN included, is dropped. The output has the target’s shape, layout and extent.
emit(pair);gives more pairs (undermin,max, or asumof no floats).
Prefix (prefix)
Section titled “Prefix (prefix)”kernel compact.offsets@1 { requires { field.read; field.write; } input flags: Rows<Int>; output result: Rows<Int>; // one element per input element prefix over flags with sum { // the rank of each kept element return if sample(flags) != 0 { 1 } else { 0 }; }}The output at i holds the values at 0..=i combined by sum, min or max. A prefix may fold in any grouping, so sum over an element holding a float is refused (use scan), as is last.
Group (group N)
Section titled “Group (group N)”kernel fft.row@1 { requires { field.read; field.write; } input line: Rows<Vec2>; output result: Rows<Vec2>; group 64 { // one row of 64 per group let x = sample(line); let pair = exchange(x, lane() ^ 1); return if lane() % 2 == 0 { x + pair } else { pair - x }; }}Element e is lane e % N (lane()) of group e / N. exchange(v, k) is the v lane k holds at that same exchange. Every lane reaches each exchange alike, so one stands under no branch, match, loop count or until that turns on what a lane holds alone (params, literals and the domain may steer one). On a device a group is a workgroup and an exchange a pass through shared memory between barriers; a group exchanging more than a workgroup holds runs on the JIT. packages/signal (the FFT) is built on groups.
Node-level options
Section titled “Node-level options”Written on a scene’s node, not in the Kernel:
| Form | Effect |
|---|---|
async = true; |
the node runs off the frame on its own thread; readers take its last output until it lands |
times = N; + carry port <- output; |
the node placed N times, each reading the last one’s output (a Jacobi iteration) |
awake = value(source, row); |
the node sleeps, unrun, while that row is 0 |
See Graph & state.

