Bootstrapping Puffin
This document is for contributors: how puffincc builds itself from a
C toolchain alone โ the committed bytecode seed, the stage chain in
bin/bootstrap and its byte-identical fixpoint, the seed-refresh
discipline (bin/refresh-boot), and where the golden-corpus
authority lives. The history of the port from the Racket compiler is
in docs/DELTA.md.
The self-hosted compiler
puffincc lives in puffincc-src/ (~8,200 lines of Puffin), a module
DAG rooted at main.puf (docs/MODULES.md): the s-expression reader
(reader.puf), module resolution, desugar, the typechecker
(types.puf, docs/TYPES.md), the optimizer (optimize.puf =
contraction/inlining, aam.puf = the -O2 flow analysis;
docs/OPTIMIZER.md), the middle passes, register allocation, the
arm64 and x86-64 backends, the bytecode backend (docs/BYTECODE.md),
and the CLI driver.
build/puffincc prog.puf -o prog compiles a program (resolving its
require/provide modules from disk), writes the assembly, and shells
out to clang through the runtime's system primitive โ no wrapper
script. -o out.s stops at assembly; with no -o it prints the
assembly to stdout, so the classic pipe mode puffincc < prog.puf > prog.s also works for single-file programs. -t bytecode emits a
.pbc unit for the bytecode VM instead.
Two components stay in C by design: the runtime (src/runtime,
linked into every native binary) and the bytecode VM (src/vm,
which also compiles to wasm for the browser โ docs/WASM-VM.md).
The seed
boot/puffincc.pbc โ puffincc compiled to bytecode โ is the
committed seed. A bytecode seed was chosen over a native one because
it is portable across both native targets, deterministic, and runs
on the VM we build from C anyway.
The bootstrap chain (bin/bootstrap)
bin/bootstrap builds everything with a C toolchain only (cc/clang,
make, ar/libtool/lipo) โ no Racket anywhere on the machine:
make -C src/runtimeโlibpuffin.afromccplus the committed vendored Boehm collector (src/runtime/vendor/gc).make -C src/vmโbin/puffin-vmfromccplus the committedsrc/vm/vm-prims.inc. (The Makefile regenerates that prim table only when a generator is available โbuild/puffinccrunningtools/gen-vm-prims.puf, cross-checked against the Racket generator whenracketis also present; a fresh machine has neither and uses the committed copy, with a loud note.)- Stage 1: the seed runs on the VM and compiles
puffincc-src/main.pufโbuild/stage1.pbc. The script notes whetherstage1.pbcmatches the seed byte-for-byte (see "Seed freshness" below). - Native:
stage1.pbcruns on the VM and compiles the same source tobuild/puffincc(at-O0) โ puffincc shells out to/usr/bin/clangthrough the VM'ssystemprimitive.systemis native-only; the wasm VM stubs it, and the browser's-t bytecodepath never needs it. - Stage 2:
build/puffinccself-compiles โbuild/stage2.pbc. - Stage 3:
stage2.pbcruns on the VM and self-compiles โbuild/stage3.pbc. The script fails unless stage 2 and stage 3 are byte-identical.
The fixpoint is the correctness proof of the chain: the same compiler source, compiled by itself twice โ once hosted natively, once hosted on the VM โ must agree byte-for-byte. Determinism comes free because the runtime's gensym counter starts fresh per process.
The whole chain runs end-to-end with Racket stripped from PATH,
and from a fresh clone. It takes ~6 minutes on an M-series mac (each
self-compile is ~85 s; the VM-hosted compiler and the -O0 native
one run at about the same speed). A full native-vs-Racket benchmark
suite with plots lives in bench/report.html (regenerate:
python3 bench/run-benchmarks.py && python3 bench/build-report.py).
Seed freshness (bin/refresh-boot)
Classic self-hosting seed rules apply:
- The seed does not need to match current
puffincc-srcbyte-for-byte; it only needs to be able to compile it. A stale but compatible seed shiftsstage1.pbc(bin/bootstrapnotes this), never the stage-2/stage-3 fixpoint. - Refresh at release points with
bin/refresh-boot, which rebuilds a candidate seed from the currentbuild/puffinccand refuses to install one that does not reach a self-compile fixpoint on the VM (the candidate, run on the VM overpuffincc-src, must reproduce itself byte-identically). Commit the refreshed seed. - If
puffincc-srcstarts using a language feature the committed seed cannot compile, refresh the seed first from a commit that can compile the tree (check out that commit,bin/bootstrap,bin/refresh-boot, carryboot/puffincc.pbcforward) โ or use the hostedbin/build-puffinccas the escape hatch while the Racket implementation is around.
The golden authority: tools/test-corpus.sh
tools/test-corpus.sh is the Racket-free corpus harness and the
author of record for the goldens: every program in
src/test-programs (plain files and module directories) ร every
src/input-files/*.in, compiled with build/puffincc -t bytecode,
run on bin/puffin-vm, and compared (whitespace-trimmed) against
src/goldens โ 309 checks. Its gen mode writes the goldens
the same way; setting GOLDENS_DIR regenerates into a fresh
directory so it can be diffed against src/goldens.
The Racket oracle
The Racket implementation in src/ is the optional consistency
oracle, not the source of truth. racket src/test.rkt -m interp
(and -m gen plus a diff) re-derives the goldens independently โ
regenerating with the reference interpreter and diffing against the
puffincc-authored goldens is the differential test between the two
implementations, and it is byte-identical, 309/309. src/diff-ir.rkt
cross-checks the two compilers per pass, via puffincc's
(dump-after pass) directive. bin/build-puffincc is the hosted
alternative to stage 1: it builds build/puffincc with the Racket
compiler instead of the seed.
What still needs Racket
Only the generators. After a stdlib-manifest (src/stdlib.rkt) or
prelude change, src/gen-puffincc-tables.rkt,
tools/gen-prim-names.rkt, and the STDLIB-doc generator
(src/gen-stdlib-docs.rkt) re-derive the committed tables and
docs/STDLIB.md. The VM prim table is already self-hosted:
tools/gen-vm-prims.puf regenerates src/vm/vm-prims.inc, with
src/gen-vm-prims.rkt kept as a lockstep cross-check. Building,
testing, and shipping Puffin โ bin/bootstrap,
tools/test-corpus.sh, tools/gen-web-vm.sh โ need no Racket.