The Compiler Grows a Trellis
Every accelerator target ships with an unwritten contract about shape. A GPU kernel mustn’t allocate. A SIMD loop mustn’t branch on data. A compile-time transform mustn’t do IO. Every toolchain enforces these contracts the same way: you find out when the lowering fails — an error pointing at generated code, three layers below anything you wrote.
Mojo earns real credit for compiling Python-flavored kernels to PTX through MLIR’s progressive lowering. But ask it what a kernel is and the honest answer is: whatever survives lowering. The contract is real; it just isn’t anywhere you can read it.
The contract is a value
In Koru, the contract is now spelled in Koru:
~std/trellis:define("kernel-shape")
| `std/kernel:init/kernel/.*` _ => ok
| `.*std/kernel:pairwise` _ => error "std/kernel:pairwise found outside std/kernel:init"
| `.*std/kernel:self` _ => error "std/kernel:self found outside std/kernel:init"
~std/trellis:enforce("kernel-shape") A trellis describes valid shapes of the AST. Every flow in your program is a tree; every node in that tree has an ancestry path — event names and branch labels, root to node:
std/kernel:init/kernel/std/kernel:self The trellis arms are regex over those paths, first match wins. If that
surface looks familiar, it should: it’s the same surface as std/regex:match, because in
Koru the branch name is the pattern, byte for byte. An arm that constructs => ok accepts the path. An arm that constructs => error "..." rejects it,
and its payload is the message you chose. Paths matching no arm are
unconstrained — the trellis constrains what it names, and closedness is
spelled the way it always was: a catch-all arm.
enforce makes the trellis law. Break it — invoke std/kernel:self outside
a kernel scope — and compilation stops at the checker, in your source, in
your vocabulary:
error: Check "kernel-shape" failed: std/kernel:self found outside std/kernel:init at input.kz:34:0 Not a Zig error. Not an IR dump. The trellis’s own message, at the location of the violating node, naming the rule that a human wrote for humans.
A law is also a question
The same judgment, asked instead of imposed:
~std/trellis:check("kernel-shape")
| ok |> std/io:print.ln("categorized: kernel-shape")
| error msg |> std/io:print.ln(msg) check runs the trellis and hands you the verdict as ordinary branch
dispatch, resolved at compile time — the losing branch is dead code by
construction. There is no runtime component to any of this: the matching
happens inside a compile-time transform, running the same linear-time,
no-backtracking engine that powers match, and then the whole apparatus
dissolves.
Which means shape-membership becomes something programs reason about. This flow is a valid kernel. That flow is a valid kernel that can target the GPU — same mechanism, stricter trellis. Categories compose by declaration.
Where this goes: capability floating
The reason to make membership first-class is what consumes it: variant
selection. Koru events already carry implementation variants — blur|naive, blur|gpu — and today, selecting one is an assertion of hope. The trellis
turns it into a checked request: ask for |gpu on a flow whose shape can’t
go there, and the compiler refuses at the request site, naming the exact node
that broke the contract — instead of handing you PTX wreckage.
And once the request is checked, you can stop requesting: let the compiler float the best implementation the shape admits. GPU if the flow satisfies the gpu trellis, fused SIMD if it satisfies the fusion trellis, scalar always. The trellis is the early-warning wall and the capability ladder at once — and “no silent performance degradation” gets its reason attached: the compiler can tell you which node cost you the faster category.
Zero compiler changes
The part that makes this a Koru story rather than a feature announcement: std/trellis touches no compiler source. define is inert metadata sitting
in the AST. enforce and check are ordinary compile-time transforms that
walk the program they’re handed, serialize paths, and run the regex engine on
them. The parser needed nothing — quoted branch names already carry any
pattern, and => ok was already a branch constructor.
That’s the thesis doing its job. When flows are trees the compiler hands you whole, and the surface grammar refuses to value-judge names, “what belongs where” stops being folklore in a spec PDF and becomes four lines you can grep. The stdlib’s own kernel scope-checking — previously a hand-rolled walker with a hardcoded denylist — becomes a trellis like any other. Libraries, not keywords; laws, not lore.