This site is version 0.0.1. Most of the code on this site is drafted by an agent and needs review. I apologize for the AI-speak, we are working on rewriting it!
viv-lang.org

The kit underneath

knotkit

knotkit is a Rust library for building compilers. Its goal is to make a new language cheap enough to build that, for a problem with clean algebraic structure, writing a DSL costs less than working around an existing language's abstractions. Viv is the language it was built for; three other languages use it too.

The kit is 2,242 lines of Rust. It is small: a library you call rather than a framework you build inside, and each piece can be used on its own.

What a language gets from it

A C backend. The C99 backend emits readable C with no dependencies, so clang -O3 applies the LLVM optimizer without the kit containing any LLVM code, and the output can call any C library. A direct LLVM backend would be far more code to maintain for most of the same benefit.

A compiler that runs in a browser. The compiler does all file and output access through an interface the host supplies, so it compiles to wasm and runs in a browser unchanged. The compiler on this website is the same Rust crate.

The architectural commitments

The kit's design rests on a written list of commitments, recorded because they are expensive to change later. The main ones:

The IR is the source of truth
Source text is what people edit, the AST exists only inside the parser, and the IR is what every later phase works on. No code after lowering sees the AST.
Every node carries identity, location and lineage
A NodeId (its structural hash), a Span (where it came from), and a lowering chain (what transformed it). All three are mandatory, because retrofitting any of them would mean touching every IR-aware component.
NodeId is a structural hash
BLAKE3 over the variant tag, the literal payload, and the children’s hashes. References between nodes are by hash, not by pointer or index — so structurally identical nodes have one identity, and changing a child changes the parent’s address. The IR is immutable; transformations produce new IR.
The IR is stratified
When a construct is lowered, both the construct and what it becomes stay in the IR, linked by the lowering chain. sum(xs) and the accumulator loop it becomes are both kept, so a tool can show or debug either one.
Lowering rules are stored as data
A lowering rule is a pattern, a replacement and some metadata, stored in a table and content-addressed like the IR, instead of a hardcoded compiler pass. A better lowering for an operator product can then be shared as data rather than as a compiler patch.
Compiled programs contain no kit code
No kit code appears at runtime in a compiled program. No kit allocator, no GC, no imposed calling convention. Racket programs carry Racket’s runtime; programs compiled with the kit carry nothing from it.
Errors and warnings are structured values
Errors and warnings are values with severity, spans and suggestions. Renderers consume them: ANSI for a terminal, JSON for an editor, HTML for a page, plain text for tests. Adding a renderer touches nothing that produces diagnostics.
The host supplies all I/O
The compiler core never reads a file or touches the network directly; everything goes through an environment the host passes in. The native CLI supplies one, the wasm build supplies one that bridges to JS, tests supply an in-memory one, so the same compiler runs in a browser.

There are structural commitments too: a language is described to the kit rather than written as a subclass of it, ownership is an opt-in the kit supports with a borrow checker at the IR level, and backends stay faithful to IR semantics so debug information survives code generation. There is also a list of things the kit deliberately does not do. It is not general compiler infrastructure: it assumes structured control flow and statically determinable types, so heavily dynamic languages, first-class continuations and reflection are out of scope. It is not LLVM and not MLIR. And it provides no standard library, because a standard library would add a runtime to every compiled program.

One rule keeps the kit small: a feature is added only when a language built on it cannot proceed without it.

Parts of the kit

PieceFileLines
content-addressed IR store crates/knotkit/src/ir.rs 184
C backend + C AST + DebugMap crates/knotkit/src/backend.rs 1,494
diagnostics as data crates/knotkit/src/diag.rs 161
spans + source map crates/knotkit/src/span.rs 313
injected I/O crates/knotkit/src/io.rs 90

The C backend is the biggest piece and the most reused: it is generic over the source IR, so any language that implements the kit's code-generation interface gets a C99 backend without writing one. Viv and skein both use it unmodified. See it working on a nine-line program in the internals walkthrough.

Languages built on the kit

Building a first language shows what the kit has to support. Building a second one shows which parts work the same way for any language and which were specific to the first.

viv

the language the kit was built for

A general-purpose numerical language with floats, arrays, records and complex numbers, functions that cross-check several methods against each other, and three backends.

in use

weft

compiles to Viv

A language for the weak form of a PDE, in the style of FEniCS's UFL. Write the variational form, such as bilinear: dot(grad(u), grad(v)), and its compiler generates the finite-element solver. For nonlinear problems it differentiates the residual symbolically to build the Newton Jacobian.

v1 + nonlinear

skein

a second language, written to test the kit

A teaching Scheme that compiles to C through the kit: tagged values, lambda-lifted closures with capture, and proper tail calls through a trampoline, so (sum-to 1000000 0) runs in constant stack space.

v1, GC pending

oa

an experiment

Operator algebra for many-body problems. An early skeleton, written to find out what the kit needs from a language whose values are operators rather than numbers.

skeletal

weft compiles to Viv, not to C. A weak form compiles to a Viv program, which compiles to C, which compiles to native code or wasm. The element kernel it generates is bit-identical to the hand-written one in fem_poisson.viv.

What is not done

The commitments document separates what is built from what the design only allows for. The stratified IR and lineage tracking exist, but zoom-level abstraction — rendering one program at several levels at once and cycling through them interactively — is not built. Content addressing is in from day one, but the content-addressed store (definitions kept by hash, names as a mutable index) is at v0: viv hash and viv diff work, the store and snapshot/log do not. The borrow checker is a supported opt-in in the design and no language opts in yet. And cross-language interop at the IR level — compiling two kit languages into one program — is possible in the design but not built.