The Contract File
You wrote a Koru module. Something like this:
~pub event compute { x: u32 }
| done u32
~proc compute|zig {
return .{ .done = x * 2 };
} The |zig after the proc name says the body is in Zig. The same event can have implementations in other host languages — |gpu, |js, |c — so the resolver needs to know which one a given proc is. Pick the host language; the rest is just the body.
Bringing in a second variant
Suppose you have a GPU on the same machine, and compute is the kind of work that would run faster there. You’d like a second implementation — a GLSL kernel — without losing the Zig one.
You add it to the same file:
// compute.kz
~pub event compute { x: u32 }
| done u32
~proc compute|zig {
return .{ .done = x * 2 };
}
~proc compute|gpu {
// GLSL kernel body
} One file, one event, two implementations. A build flag picks which variant your call sites dispatch to at compile time. Same source, one knob switches between CPU and GPU paths. Neither implementation has to know the other exists.
The variant tag is what matters here, not the file extension. The .kz is a hint that the bodies are Zig; Koru’s parser doesn’t actually check, because proc bodies are opaque to it — they’re bytes addressed to a backend. A GLSL kernel sitting next to a Zig proc inside compute.kz is fine. The file extension becomes meaningful when you split, which is the next move.
When one file isn’t enough
Eventually a single file starts to chafe. Maybe compute now has three variants and a hundred lines of GLSL inside what is nominally a Zig file. You want the contract somewhere readable without scrolling past kernel code. You want your editor to syntax-highlight GLSL as GLSL and Zig as Zig, which it can’t do when both live in .kz. You reach for the split.
You take the event declaration out and put it in a sibling compute.k file:
// compute.k
~pub event compute { x: u32 }
| done u32 You leave the Zig implementation in compute.kz:
// compute.kz
~proc compute|zig {
return .{ .done = x * 2 };
} And you move the GPU implementation into its own file, compute.kgpu:
// compute.kgpu
~proc compute|gpu {
// GLSL kernel body
} Three files now share a module. Koru treats them as one.
Guardrails
The presence of a .k file is what activates the rules. Without it, you can keep declaring ~pub event anywhere in a .kz and the compiler doesn’t care — that’s the world your single-file module was already in. The moment a .k exists in the module, the contract has a home and the language enforces what belongs there.
The rule fires both ways. Public events must live in .k: if you write ~pub event compute in .kz while compute.k exists in the same module, the compiler stops you. And private events must live elsewhere: a non-pub event inside .k is also rejected — there are no procs in .k to call it, and nothing outside the file can see it, so the declaration is dead syntax. The contract file is for the public surface, exclusively. Implementation files are for everything else.
The split is an organizational tool. The language doesn’t ask you to use it. It scales as you decide it should scale. The day you reach for it, the rules show up to make sure the split actually means something — public surface in one place, implementations in their respective host-language files, drift becomes structurally impossible.
Incomplete by design
The contract doesn’t demand implementations everywhere it appears. You can declare ~pub event compute in compute.k and have no proc in any sibling file; as long as nothing in your program calls compute, the module compiles. The variant resolver only fires at call sites — a declared-but-uncalled event is partial, not broken.
The same goes for variants. A compute.kjs that implements three of the seven events declared in compute.k is a valid state of the program; the unported four stay only in compute.kz. Your test suite runs the ported three against both implementations; the unported four run only in Zig. Every intermediate state is a valid program. The error you’d hit is the one you’d want — a call site for an event you haven’t implemented for the variant you’re trying to ship.
Porting in place
The GPU case stays easy. Whether you keep both variants in one .kz or split them across .kz and .kgpu, both implementations land in the same AST after parsing, and the emitter ships whichever one your build flag selected. The architecture also permits a future build pipeline that ships both — the GPU is a co-processor, the CPU-side code already orchestrates it, there’s no runtime boundary in the way — but even without that, swapping CPU and GPU is a build-flag flip away. JavaScript is the harder case, because the runtime lives somewhere else entirely.
The harder case is when the target runtime is somewhere else entirely. You have a working Koru module. You want the same logic in a browser. In most languages, this is the moment the port begins. You stand up a parallel JavaScript implementation in another directory, copy the logic piece by piece, rebuild the tests, and stare at the two versions hoping they agree. The original stays in the repo until you trust the new one — which is usually forever, because you can never quite prove the two are equivalent on every input.
In Koru you don’t delete anything. You write compute.kjs next to compute.kz:
// compute.kjs
~proc compute|js {
// JavaScript body
return { done: x * 2 };
} Same module. Same contract from compute.k. Two implementations in two host languages. The variant resolver picks which one to emit by default; the other gets parsed into the AST cheaply and sits there as latent capability. The capability is the point: a property test that invokes both variants by name forces the emitter to keep both, and a generator produces inputs that run through each. Same inputs, two host languages, divergence shows up exactly where the port is wrong. When you trust the JavaScript version, you delete the .kz — not before, not on faith, not because a Slack thread said the rewrite was finished.
This is not a workflow you can build out of files-on-disk discipline alone. It needs the contract to be one declaration, not two — otherwise the inputs to the property test are different things in different host languages, and “same inputs” stops meaning anything before the procs run. It needs the parser to load every variant of every event into the same AST so the test framework can see them. It needs the compiler to refuse to emit duplicate public events from sibling files so the contract can’t quietly fork. All of those pieces sit underneath the moment when you write the second variant alongside the first.
What stays canonical
There is one asymmetry to be honest about. Variants are equal at the implementation layer — any host language can hold a proc body, and the language treats them as peers. But the type system has a canonical owner: Zig.
When compute.k declares ~pub event compute { x: u32 }, the u32 is Zig’s u32. When compute.kjs implements that event, the binding x it receives is JavaScript’s projection of a Zig u32 through whatever translation layer carries the contract across the boundary. The contract speaks Zig types; the implementation translates.
This is what makes cross-variant equivalence testing meaningful. “Same inputs to both implementations” is only a real sentence if there is one canonical type for the inputs. If the Zig variant saw a u32 and the JavaScript variant saw a JavaScript number that might silently lose precision past 2^53, “same inputs” would have stopped meaning anything before the test ran. With one canonical type system, the boundary is one-way and well-defined: any divergence in test results lives in proc logic, not in the type plumbing.
A future Koru will have its own type system, and a way to write proc bodies in pure Koru that emit to any host. When those land, the canonicity drops back into the language itself and the host languages become genuinely interchangeable at every layer. That’s the long shape. The current shape — Zig as canonical type system, host languages as equal at the implementation layer — is the middle ground that works today, and it is enough to make the porting workflow real.
Variants are equal
There is a principle in Koru that gets restated at every layer. Branches are equal: no outcome is privileged over another, no “success path,” no “happy case,” just named outcomes that the proc emits. This post is what happens when that principle extends one layer down.
Variants are equal. Zig is one host language of many. The proc body that lives inside braces is one of many ways to satisfy a contract, not the way. The contract file is where that equality becomes structural — a literal artifact that says “here is what this module promises, and the implementations are downstream.”
The module starts as one file. It stays one file as long as one file is enough. Variants pile up in that file until one file stops being enough. The day you reach for the split, the language meets you there with rules that make the split coherent — public events in the contract, implementations in their respective host-language files, drift impossible. The system scales as you decide it should scale.