Compiler Mints: Pinning the Backend That Compiles You
Koru generates its backend per program. The comptime modules your imports declare, the transform handlers, the command dispatch — all of it is baked into backend_output_emitted.zig, and a zig build turns that into the binary that actually compiles you. The frontend’s own product, program.ast.json, is runtime data that binary reads at startup; the binary never needs your source, only your comptime closure — the set of compile-time machinery your program pulls in.
Which means the backend binary is already the right shape for a stronger artifact. So: mint it.
Mint, use, check
Three verbs. mint builds the backend once and stores it under a name:
koruc program.k mint my-compiler That writes ~/.koru/mints/my-compiler/ — the backend binary plus a manifest recording its sha256, the tree hashes of src/ and koru_std/, the closure hash, and the koruc version that produced it. Then the program names its own backend in source:
import std/io
std/compiler:use(my-compiler)
tor greet { name: string } -> string
greet -> "Hello, " ++ name ++ "!"
greet (name: "World"): msg |> std/io:print.ln(msg) use is a frontend directive — collected textually, the same seam compiler:requires rides, and stripped from the AST before it can reach the backend at all. With it, the compile is: parse, validate, write the runtime inputs, verify the mint, exec it. No backend.zig, no emitted file, no zig build. On this program that’s ~10.7 seconds of mint once against ~1.2 seconds per compile — but speed is not the point.
hash: pins binary identity under the name:
std/compiler:use(my-compiler, hash: "66c9c450d741c79cff859c32bb7feea0cf5df1011a026f1be25bb3764aaa0a03") A name is mutable — anyone can re-mint under it and the binary changes. The hash refuses that: a mismatch reports pinned-vs-actual in the diagnostic and nothing runs. The name follows the artifact; the hash is the artifact.
What a mint covers
A mint is not “a compiler” in the abstract — it is a compiler for a comptime/command closure. Commands are the sharpest case: koru_command_dispatch is generated into the backend at mint time, so a mint that was cut before your program declared mycmd does not contain mycmd’s handler. use recomputes the closure hash for the current program and refuses a mismatch rather than falling back to a mutable rebuild:
error: mint 'my-compiler' does not cover this program
The mint's comptime/command closure differs from what this program needs
(a transform or command import changed since the mint). Re-mint:
koruc <file.k> mint my-compiler The predicate is the emitted backend minus // comments — provenance markers carry filenames and absolute stdlib paths, which are not semantics. Normalizing them is what makes a mint serve the family a closure names rather than the file it happened to be minted from.
mint check: valid versus stale
use asks whether a mint is valid for this program. check asks the stronger question — whether the mint still represents the tree it came from. It recomputes the component hashes over the live src/, every library root the resolver would consult, and the freshly emitted closure, and diffs them against the manifest:
mint my-compiler (minted 1790689191, koruc 0.1.7, source post.k)
binary integrity sha256 match
ast/env protocol clean
core (src/) clean
program closure clean
lib /Users/larsde/src/koru-libs clean
lib /Users/larsde/src/koru/koru_std clean
lib /usr/local/lib/koru_std clean
verdict fresh — mint still represents this tree The library rows are the point worth pausing on. koru_std is not special to the compiler — it is only the default entry on the resolver’s path list. KORU_PATH, KORU_STDLIB, koru.json paths, and std/compiler:paths aliases are all first-class roots, and a manifest that hashed only koru_std would claim “clean” while a program’s actual imports drifted unmeasured in some aliased directory. So check reports every configured root on its own row — and hashes only the files the compilation actually loaded under each, so a rebuilt binary nobody imports cannot stale a mint, and a drift in a loaded file names the exact root that supplied it.
Valid-but-stale is a legal, visible state: a mint can keep correctly compiling every program it covers while the tree drifts out from under it. The pin says keep serving it; check is what tells you the world moved. That’s the go mod verify shape — the audit verb that makes a frozen artifact trustworthy instead of just frozen.
Why not the cache
The backend-binary cache already exists — content-addressed on src/ + koru_std/ + the emitted file, serving an equivalent binary on a warm key. A mint is often the same bytes the cache would serve. The differences are the feature:
- A mint has a name — source can pin it; the compiler version travels with the program, the
rust-toolchain.tomlshape. - A mint has a manifest — identity is inspectable, not implied by whoever populated the cache directory.
- A mint does not get evicted — the cache is opportunistic; the mint is a commitment.
- A mint’s coverage hash is normalized — comments carrying filenames and absolute paths are stripped before hashing. The cache hashes the raw emitted file, so renaming
program.ksilently busts it today; a mint survives the rename.
The cache makes a mutable compiler fast. The mint makes a compiler nameable — and nameable is what makes it pinnable, auditable, and shippable. A library could ship its compiler mint with its transforms baked in; consumers compile against the artifact without ever building it.
The safety frame: mint the shaped pipeline
That is the real argument, and it sharpens once you see where the malleability actually lives. std/compiler declares coordinate — an abstract comptime tor whose default impl is the six-pass pipeline:
coordinate = context-create(program_ast, allocator): c0 |> elaborate(ctx: c0): c1 |> analysis(ctx: c1)
| ctx c2 |> test-generation(ctx: c2): c3 |> std/optimizer:optimize(ctx: c3): c4 |> emission(ctx: c4): c5
|> metrics-format(c5.ctx.passes_completed, allocator): m |> std/inter:start(contexts: [
c0, c1, c2, c3, c4, c5.ctx
])
| launched => coordinated { c5.ctx.ast, c5.code, metrics: m }
| skipped => coordinated { c5.ctx.ast, c5.code, metrics: m }
| failed f => error f.message Abstract means any program — or any module it imports — can replace it wholesale:
std/compiler:coordinate = error "sanctioned pipeline only" That is the malleability skeptics object to: the program reshapes its own compiler. Minting is the other half of the same design. The override resolves at emission time — it lands inside the emitted backend — so the closure hash captures which pipeline was baked. A mint cut under an override refuses a program without it, and vice versa: the closure is the shape, and coverage is exact.
Which gives the regulated-industry shape directly. An aerospace profile is a module that overrides coordinate — drop the optimizer, insert audit passes, remove a feature the standard won’t certify. Mint it: koruc profile.k mint aerospace-koru. Consumers pin: std/compiler:use(aerospace-koru, hash: "…"). Three bindings, each enforced by a different mechanism — the override shapes the pipeline, the closure hash binds the artifact to that shape, the hash: pin binds the program to that artifact. “My program was compiled by the compiler we sanctioned” becomes a checkable fact rather than a process claim.
The honest limits, so the claim is exact: use pins the backend stages — everything after parsing. Stage A — parsing, shape-checking, collecting use itself — still runs in whatever koruc you invoked, so a fully pinned story wants koruc itself to be a versioned release. Mints are target-locked binaries. And use is a commitment the program makes, not a wall around programs that don’t — mandating it is a packaging question (the certified toolchain ships the mint and a thin stage-A driver, not the malleable pipeline). The manifest is sha256 plus filesystem trust — signing is the honest next step, not a shipped one.
What it dug out of the compiler
Building the coverage predicate surfaced two bugs about where identity lives:
- The directive self-referenced.
useis acomptimeevent, so the emitter swept theuseflow into the emitted backend — the mint’s own name joined its closure hash, anduse(A)versususe(B)read as different closures. The directive is now stripped from the AST the moment it’s collected: frontend-only means frontend-only. - Provenance comments pinned coverage to filenames.
// >>> PROC: mycmd [cmdprog.kz:18]embedded the source filename — and absolute stdlib paths — into the emitted bytes, so a mint minted fromcmdprog.kzrefusedcmduse.kzover a comment, and would have refused the same program on a different checkout. The hash now strips comments.
Neither is a mint bug. They are coverage-predicate bugs that only a coverage predicate could find — the same reason std/pump found its four: a new instrument trips over what nothing was wide enough to touch before.