· 4 min read

Small compilers with sharp opinions

Axe, EDL, and Tig each commit to one opinion — parallel syntax, compile-time resources, readable C output — and stay small enough to read the commitment.

A shared electronics workshop with desks, monitors, tools, drawers, and parts bins arranged around the room.
James Bastow, CC BY-SA 2.0

A compiler small enough to read in an afternoon offers a particular pleasure: whatever it does, it does on purpose. Axe, EDL, and Tig are three of these. Each has committed, in working code at a pinned commit, to one opinion that a general-purpose language would still be debating.

AxeEDLTig
The opinionparallelism belongs in loop syntaxresources belong to compilationoutput should stay readable
Where it livesa self-hosted parser and the OpenMP pragma the C backend emitscomptime contexts, the only place resources may be acquiredthe emitted C11, open in a debugger
The admitted pricescheduling, races, and grain size still decide whether it paysthe program recompiles for every executionno optimizer, no intermediate representation, no memory-safety claim
Pinned commit94921b9126f4982c63328
Every repository link in this essay points into the commit in the last row.

The examples below come from the repositories at those commits, and they establish behavior and stated scope. Nobody here is claiming production readiness, least of all me.

Let syntax own parallel intent#

In Axe, parallel for sits in the ordinary loop vocabulary, where the parser can see the programmer's intent instead of excavating it from a library call or a build flag.

parallel.axe
def main() {  parallel for mut i = 0 to 10 {    println i;  }}

That spelling is implemented, not aspirational. Axe's self-hosted parser recognizes the construct, the test suite exercises simple and reduction forms, and the C backend lowers it to an OpenMP pragma. The grammar's contribution is a region the compiler can check and lower — whether ten tiny prints actually beat a serial loop is a different question, same as ever.

Let compilation own resource shape#

EDL configures performance-sensitive Rust applications, and its founding move sounds almost illegal the first time: the program is recompiled for every execution. A mesh file decides how many cells exist, and the pressure vector is allocated at exactly that size before the numerical work begins.

solver.eq
const NUM_CELLS: usize =    load_num_cells("config/mesh.msh");let pressure_field =    allocate_vector::<f64>(NUM_CELLS);

The language specification makes global initialization a comptime context and confines resource acquisition to such contexts, with heavy operations arriving as Rust callbacks whose generic signatures must match EDL's types exactly. Feel the shape of that trade. You give up the idea that one binary should accept every runtime shape, and in exchange the compiled work starts with its resources already chosen — a stark deal, priced in visible recompilation, which is what makes it honest enough to challenge with a real workload.

Let the output remain an escape hatch#

Tig keeps the shortest semantic distance of the three: parse a small systems language, check it, emit C11 a person can open in a debugger. Allocation and scope-exit cleanup arrive without hiding the memory model.

buffer.tc
use "stdlib/io.tc"fn i32 main: {  ->i32 values = alloc(i32, 100)  defer free(values)  ret 0}

The pinned README is disarmingly plain about what is missing. Fat pointers, slices, and defer are guardrails around manual memory management, with everything past them left explicitly unclaimed. The README's actual promise is the emitted file.

That promise doubles as Tig's refusal mechanism, because any feature that cannot lower transparently threatens the generated C's usefulness under a debugger. The same README now lists async functions and select. As far as I can tell those are on a collision course with the readable-output promise, and when they meet, the place to check will be an emitted .c file rather than the keyword count.