· 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 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.
| Axe | EDL | Tig | |
|---|---|---|---|
| The opinion | parallelism belongs in loop syntax | resources belong to compilation | output should stay readable |
| Where it lives | a self-hosted parser and the OpenMP pragma the C backend emits | comptime contexts, the only place resources may be acquired | the emitted C11, open in a debugger |
| The admitted price | scheduling, races, and grain size still decide whether it pays | the program recompiles for every execution | no optimizer, no intermediate representation, no memory-safety claim |
| Pinned commit | 94921b9 | 126f498 | 2c63328 |
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.
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.
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.
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.