Skip to content
View source

Kernels & the .ent language

An .ent file is a Kernel library: kernel items and the declarations they share (struct, type, fn). .ent and .esn share one grammar, lexer and literal syntax: // comments, ;-terminated statements, { } blocks. Diagnostics are file:line:column: message. The exact language is defined by the front end (crates/frontend/language) and its tests.

struct Particle { position: Vec2; velocity: Vec2; } // a document struct: every Kernel below sees it
type Swarm = Rows<Particle>; // a port type alias, shared the same way
kernel image.grayscale@1 {
requires { field.read; field.write; } // capabilities
input source: Image; // an input port
param amount: Float = 1.0; // a param and its default
output result: Image; // an output port
struct Hit { t: Float; id: Int; } // this Kernel's own struct
fn luma(c: Vec4) -> Float { return c.r * 0.2126 + c.g * 0.7152 + c.b * 0.0722; }
compute { // the body (here, the map mode)
let c = sample(source);
let g = luma(c);
return mix(c, vec4(g, g, g, c.a), amount);
}
}
  • Identity: name@version is the Kernel’s identity. Graphs bind to it, never to a file.
  • Capabilities: requires { … } lists what it needs; without one, its builtins cannot be called. See Capabilities & security.
  • Ports: input, output, and a scan’s memory. Each carries a Field.
  • Body: one of compute, scan, scatter, prefix, group N — the execution mode.
  • Budget: budget N; declares the loop iterations one element may run (16777216 by default).
//! A preset graph's or a package manifest's doc: the lines a document opens with.
use "kernels/";
/// A Kernel's doc: the lines directly above it.
kernel tally.twice@1 {
requires { field.read; field.write; }
/// A port's or a param's doc, directly above it.
input counts: Rows<Int>;
output result: Rows<Int>;
compute { return sample(counts) * 2; }
}

/// lines directly above a kernel, input, param, output or memory are its doc; a blank or // line ends them. They are comments to the grammar, but travel with the Kernel’s source through publish, kernel and snapshot; changing them publishes it again.

A compute body is let bindings and a result.

Kind Forms and rules
Bindings let n = …;; let { x, y } = window(); by name; let { x: width, y: height } = window(); renames; _ binds nothing; expr; is let _ = expr;
Arithmetic + - * /, % (truncated remainder); Int and Float kept apart, but vec2/3/4 take Int components; a Float beside a vector broadcasts
Swizzles v.xy, v.zyx, c.rgb; v.x one component
Bits & | ^, << >> >>> (count modulo 32, >> arithmetic, >>> logical); 0xFF hex, more than eight digits or an l a Long
Logic &&, || short-circuit; ==, != compare scalars and whole vectors, < and the rest scalars
if if c { a } else { b } is an expression; if c { return a; } with no else guards what follows; a statements-only if hit { emit(p); } runs and goes on
match on an Int: match i { 0 => a, 1 | 2 => …, 8..16 => c, 16..=31 => d, _ => b }; ranges at most 256 values; arms of one type
Enums type Mode = enum { Idle, Walk, Attack = 10, Guard }; — named Ints, Mode.Guard is 11; a match naming every variant needs no _
loop loop n with x = init until done { next } is an expression; the count a literal (at most 65536) or any Int expression; if done { break x; } stops the innermost loop
Structs Name { a: …, b: … } builds; Name { ..base, a: … } copies with fields replaced
Nesting at most 256 deep; past that the document is refused, saying where

[T; N] holds N values of one type: [Int; 16], [Vec3; 4], [[Int; 4]; 4] — a struct field, an argument or result, a loop’s carried value, or a Field’s element.

let a = [1, 2, 3];
let zeros = [0.0; 8];
let w = [each 32 { unit(weights, x, index()) }]; // element by element
let b = [each 4 { if index() == k { v } else { a[index()] } }]; // element k replaced

A literal index is checked when compiled; a computed Int is clamped. 1 to 4096 elements; a value holds at most 65536 words; structs and arrays nest at most 64 deep.

A param is any value a Kernel holds:

param lens: Mat4 = mat4(1.0);
param cell: IVec2 = ivec2(3, 4);
param edges: [IVec2; 12] = [ … ];
param style: Style = Style { ground: vec3(0.1, 0.1, 0.1), width: 1.0 };

A default is a literal. A value matches by layout, never spelling: from outside (a scene, a replay’s param, the protocol) an array of values in order takes any shape — [[1, 0], [0, 1]] is a Mat2.

  • A top-level struct, type or fn is shared by every Kernel in the document and every document that uses it. An .ent may use other .ent files or directories.
  • A name reaches the Kernel’s own declaration, then the document’s own, else the one in the documents it uses. Where two used packages answer differently, qualify: cast_fx::clear(p), Rows<ui_panel::Mark>.
  • Functions call one another in any order but never recurse (a cycle is refused, named); repetition is a loop.
  • A function may take a Field: fn cast(voxels: Volume<Int>, eye: Vec3) -> Hit, called with an input port’s name, instantiated once per port.
  • Generics: fn twice<T>(v: T) -> T, one instance per set of types.
  • A function may take a math builtin’s name (step, mix) and stands for it in its own document; the Field, memory, context and construction builtins keep their names.
  • Integer failures: overflow of + - *, division or remainder by zero, the least integer negated, fail the element on every backend, naming the first element in row-major order; nothing the evaluation computed commits. add_wrap, sub_wrap, mul_wrap wrap.
  • Floats: a positive over zero is infinity, zero over zero NaN, and NaN equals and orders against nothing. A NaN made by arithmetic on the GPU may come out a number under a driver’s fast math.
  • Budget: an element’s iterations, all loops together, 16777216 unless budget N; (1 to 2^30). A run, all elements together, is bounded by the Host (four thousand million unless --work <M>, in millions). Past either, the run fails and the system keeps its last state.

Double, Long and their vectors and matrices (DVec2..4, LVec2/3, DMat2..4). double(x) and long(x) convert; float(x) and int(x) convert back. A Double never meets a Float, nor a Long an Int, without a conversion. A written number is exact until it meets a width; 0.1d and 8l name one. A GPU computes Long in maps, groups, reductions and some scatters but never a Double: a GPU session runs such a node on the JIT.

After as, a port stores scalars narrower: Grid<Vec4 as unorm8> is four bytes a pixel, Rows<Int as u8> a byte a row. The Kernel still computes in Float and Int; the Field widens on read and narrows on write. f16, unorm8, unorm16, snorm8, u8, i8, u16, i16. A narrow and a wide Field are different layouts, and a wire between them is refused; only a map or a scan’s steps write a narrow output; a 64-bit scalar is never narrow.