Candor compiles Candor
selfhost/candorc.cnd is a complete Candor compiler written in Candor: a lexer, a parser, a checker with every diagnostic, and a C backend. It builds itself, and the result builds itself again, identically.
The bootstrap
| Stage | What happens |
|---|---|
| 0 | The JavaScript reference compiler, the seed. |
| 1 | Stage 0 compiles candorc.cnd into a native candorc1. |
| 2 | candorc1 compiles candorc.cnd, producing stage2.c, which builds candorc2. |
| 3, 4 | candorc2 and then candorc3 each compile candorc.cnd again. |
The C from stages 2, 3 and 4 must be byte-identical. That is the fixed point: the compiler carries itself, and successive generations stop changing. A compiled compiler then builds itself in about 10 ms.
How it's checked
selfhost/test.sh # stage 1, comparisons, bootstrap
FULL=1 selfhost/test.sh # ...and every benchmark too
- Behavior: every program in
tests/run,examplesandbenchgives identical stdout, stderr and exit code when compiled bycandorcand when run by the reference compiler. - Diagnostics: every file in
tests/errorsgets word-for-word identical errors, hints and caret positions from both compilers. One file alone produces 46 different errors. - Fixed point: stages 2, 3 and 4 match byte for byte.
Every language change goes into both compilers, and the suite keeps them in agreement. So the language can now evolve using itself.
Field notes
The point of self-hosting was to learn whether the language holds up for a real program. The full notes are in selfhost/NOTES.md. The short version:
What worked
inoutplus borrowed arguments. The whole compiler threads its state throughinoutparameters. Every mutation is visible at the call site, and read-only helpers cost nothing.- The type checker as a refactoring tool. Moving the compiler's internals from strings and magic numbers to enums touched about 330 places, using a regex script. The checker reported every site the script got wrong. After those were fixed, the rewritten compiler passed everything and reached a new fixed point on the first run.
- Labeled
ends stayed readable landmarks across about 4,200 lines.
What hurt, and what changed because of it
| Pain point | Status |
|---|---|
| Enums couldn't be compared, so the AST used strings for its kinds | Fixed: == and show on payload-free enums. The AST now uses NodeKind. |
| No constants, so type ids were magic numbers | Fixed: top-level constants (T_INT, …). |
No array pop | Fixed: pop(xs). |
No modules: six files joined with cat | Fixed in v0.3: candorc is six real modules, and errors cite lexer.cnd:46. |
| No way to move a large value: loading each module copies the AST arena in and out | Measured: peak memory went from 36 to 54 MB. Addressed by v0.6 (copy-on-write) |
No error propagation: a failed flag threaded everywhere | Planned: generics with Result[T, E] |
Type ids are plain Ints, interchangeable with node ids | Planned: distinct types |
| No closures | Open question |
An honest caveat. candorc was a port of a design first written in JavaScript, which is easier than designing a compiler from scratch. The clean first runs say more about the porting than about the language. The pain points are the more trustworthy signal.