Bootstrapimplementation

Bootstrapping a Compiler

If the compiler for X is written in X, what compiled the first one? Write a minimal version in another language, use it to compile the real one, then use the result to compile itself — and throw the first one away.

The question

If the Rust compiler is written in Rust, what compiled the first Rust compiler?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

The compiler as a program that must itself be compiled, which makes the toolchain a chain of artifacts rather than a single tool. Each link is a binary produced by the previous one, and the representation that matters is the chain: stage 0 is a compiler you obtained from somewhere else, and every later stage is defined relative to it. The question "what is this compiler" only has an answer if you can say what produced it.

What this phase may assume or do

A bootstrap chain is sound only if each stage compiles the next correctly for the language subset that stage's source actually uses — stage 0 need not implement the whole language, only enough of it to compile the compiler's own source. That is the precondition the whole technique rests on, and it is why bootstrap compilers are permitted to be slow, incomplete and unoptimizing. What it may not do is accept a program stage 1 would reject or compile it to different behavior, because then stage 1 is not the compiler its source describes.

Key points

  • The first compiler for a language is written in a different language, and only has to support the subset the real compiler's source uses.
  • Once stage 1 exists, the original language is no longer needed and the bootstrap compiler is retired.
  • Stage 1 and stage 2 come from the same source and differ in quality, because stage 1 was compiled by an unoptimizing compiler.
  • Real chains: C from B by incremental growth, rustc from OCaml, Go translated from C at version 1.5, GCC three-staged every build.
  • Modern chains bootstrap from a previous binary release, which moves the trust root to an artifact nobody can rebuild — a real problem for source-based distributions.
  • Cross-compilation is the other bootstrap mechanism: build for a new platform from a working one, rather than from a simpler compiler.

The chicken and the egg, resolved

The question is genuinely puzzling the first time. A compiler for a language is often written in that language: rustc is Rust, the Go compiler is Go, Clang is C++, and a C compiler is very commonly C. So the source cannot be compiled without a compiler for the language it is written in, which does not exist yet.

The resolution is that the first one is written in something else. You implement a small compiler for a *subset* of the language, in whatever is available — assembly, C, another high-level language — and it only has to be good enough to compile the real compiler's source. Then you compile the real compiler with it, and from that point on you have a compiler for the language and never need the first one again.

The crucial relaxation is "subset". Stage 0 does not implement the language; it implements enough of it to compile one specific program. If the compiler's own source avoids generics, or exceptions, or the fancy pattern matching, stage 0 need not know about any of them. It also need not optimize, produce good errors, or be fast. It has one user and one input.

The classic bootstrap chaintypical
  1. Stage 0 sourceyou write it
    A minimal compiler for a subset of X, written in some other language Y.
    The ability to compile X-subset source at all.
  2. Stage 0 binarybuild time
    The minimal compiler, built by Y's existing toolchain.
    A working, unoptimizing X compiler.
    Nothing yet — but this binary is where all trust in the chain originates.
  3. Compiler source in Xyou write it
    The real compiler, written in X, using only the subset stage 0 supports.
    The full language implementation, expressed in the language itself.
  4. Stage 1 binarybuild time
    The real compiler, compiled by stage 0.
    Full language support. Poorly optimized, because stage 0 optimizes nothing.
    The dependency on language Y. From here X compiles X.
  5. Stage 2 binarybuild time
    The same source, compiled by stage 1.
    The compiler's own optimizations applied to itself — this is the artifact you ship.
  6. Stage 3 binarybuild time
    The same source again, compiled by stage 2.
    A verification: stage 2 and stage 3 should be byte-identical. See [[self-hosting]].

Read it asNotice that stage 1 and stage 2 are built from the *same source* and differ anyway: stage 1 was compiled by a poor compiler and stage 2 by a good one, so they behave identically and perform differently. Stage 2 and stage 3 were both compiled by good compilers from the same source, so they should be identical — and comparing them is the check that makes the whole chain testable.

Four real chains

implementationEvery row describes a specific project's history and current practice, both of which change. Rust's stage 0 has at various times been a downloaded beta compiler and, in some distribution builds, a chain from an older release; Go's minimum bootstrap version has been raised several times. Check the project's own bootstrap documentation rather than relying on a remembered fact.

The first C compiler is the canonical case and it is documented in Dennis Ritchie's own account. It began as a translator written in B, itself a descendant of BCPL, and grew incrementally: each version of the compiler was written in the subset of C that the previous version already supported. Adding a feature meant first teaching the compiler to compile it, then using it. Ritchie describes bootstrapping the character escape \n this way — the compiler had to be taught the escape before its own source could use it, so the first version encoded it numerically.

rustc bootstrapped from OCaml. The original Rust compiler was written in OCaml, and once it could compile enough Rust, the compiler was rewritten in Rust and the OCaml one retired. Today rustc is built by downloading a previous released rustc — the "beta" compiler — which is stage 0. That means the chain does not go back to OCaml at build time; it goes back to a binary Rust's infrastructure produced, which is a very different trust proposition and is exactly what [[trusting-trust]] is about.

Go was written in C and then translated. Through Go 1.4 the compiler and runtime were C programs; for Go 1.5 the team ran an automated translation of that C source into Go and then developed the result as Go, so the modern chain bootstraps from a previous Go release. The 1.4 C compiler remains the historical root, and Go maintains an explicit documented bootstrap path from it.

And GCC is built in three stages every time, as a matter of routine, precisely to run the identity check the previous section named. That is standard practice rather than a special event, which is what makes it a useful verification rather than a ceremony.

How four toolchains got startedimplementation
LanguageStage 0 wasToday's build starts from
CimplementationA translator written in B, grown incrementally in C subsetsAny existing C compiler; the language is now ubiquitous
RustimplementationA compiler written in OCamlA previously released rustc binary, downloaded by the build
GoimplementationA compiler and runtime written in C, through Go 1.4A previously released Go toolchain; Go 1.4 remains the documented root
GCCimplementationWritten in C from the start, buildable by any C compilerA host C++ compiler, then three self-compiled stages with an identity check

The stage-0 problem nobody has fully solved

There is a practical wrinkle in the modern chains that is easy to miss. Rust and Go both bootstrap from a *previous binary release* rather than from their historical stage 0. That is efficient and it means the trust chain at build time is: this compiler is trustworthy because the last one was, which was trustworthy because the one before it was, going back to a binary somebody produced years ago and which nobody can now rebuild from scratch on current hardware.

For a distribution that wants to build everything from source, this is a genuine problem: a binary you did not build is a dependency you cannot audit. It is why Debian, Guix and others care about bootstrappability as an explicit property, and why the Bootstrappable Builds project exists — an effort to reduce the unauditable seed to something a human could inspect, starting from a few hundred bytes of hex and building upward through progressively larger compilers.

The honest position is that this is unsolved for most modern toolchains and improving for a few. It is worth knowing about because it reframes what a bootstrap chain is for: not merely a historical curiosity about how the first compiler existed, but a live question about what you are actually trusting when you run a build today.

  • Stage 0 need only support the subset the compiler's own source uses, and need not optimize or produce good diagnostics.
  • Modern chains usually bootstrap from a previous binary release, which is fast and moves the trust to an artifact nobody rebuilt.
  • Distributions that build everything from source treat this as a real problem, not a theoretical one.
  • The Bootstrappable Builds effort reduces the unauditable seed toward something inspectable, building upward from a tiny hex seed.
  • A cross-compiler is the other escape hatch: build for a new platform from an existing one, which is [[cross-compilation]] doing bootstrap duty.

How it works

The steps, in the order the compiler takes them.

  • Write a compiler for a subset of the target language in an existing language, sufficient to compile the real compiler's source.
  • Build that with the existing language's toolchain to obtain the stage 0 binary.
  • Write the real compiler in the target language, restricting its own source to the subset stage 0 supports.
  • Compile that source with stage 0 to obtain stage 1: full language support, poor code quality.
  • Compile the same source with stage 1 to obtain stage 2: the artifact you ship, with the compiler's optimizations applied to itself.
  • Compile the same source with stage 2 to obtain stage 3, and compare it byte-for-byte with stage 2 as a correctness check.
  • Once stage 1 exists, retire stage 0; from then on new versions are compiled by the previous release, and the subset restriction relaxes to "whatever the previous release supports".

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A new language feature is used in the compiler's own source before the bootstrap compiler supports it, and the build fails at stage 0 with an error about the compiler's own code.
  • The bootstrap compiler has a bug that produces a subtly wrong stage 1, which then produces a wrong stage 2, and the defect appears in shipped output with no source change to blame.
  • A distribution cannot build the language from source at all because stage 0 is a downloaded binary for an architecture the distribution does not have.
  • A build succeeds on the maintainers' machines and fails everywhere else, because the documented minimum bootstrap version was raised without anyone noticing.
  • The chain works and nobody can say what the trust root is, because the current compiler was built by the previous release for so many generations that the original artifact is lost.

When it helps

  • Designing a new language: knowing that stage 0 only needs a subset makes "write the compiler in the language" a reachable goal rather than a paradox.
  • Porting a toolchain to a new architecture, where cross-compiling from a working host is the standard bootstrap and the only practical one.
  • Evaluating a toolchain's supply-chain posture, where "what is stage 0 and can I build it" is a concrete and answerable question.

When it hurts

  • Treating bootstrappability as free. Keeping the compiler's own source inside a bootstrappable subset is a real constraint on the language team, and most projects trade it away for a binary stage 0.
  • Confusing the historical bootstrap with the current build. Rust bootstrapped from OCaml once; your build today downloads a Rust binary, and the trust properties are entirely different.

What it costs

Every one of these is paid by something.

  • Bootstrapping from a previous binary release buys a fast, simple build and costs auditability: the trust root is an artifact nobody rebuilt and increasingly nobody can.
  • Bootstrapping from a minimal stage 0 written in another language buys an auditable root and costs maintaining a second compiler, in a second language, that must keep up with whatever subset the main compiler's source uses.
  • Writing the compiler in its own language buys dogfooding — every language weakness is felt first by the people who can fix it — and costs the bootstrap problem plus a build that is several times longer than a single compile.
  • Restricting the compiler's own source to a bootstrappable subset buys the ability to rebuild from a small seed and costs the language team the use of their own newest features in their own codebase.

What else you could do

What a different compiler or language does instead, and when that is better.

  • Write the compiler in a different language permanently. Many production compilers do — TypeScript's was TypeScript compiled by itself, but Sorbet is C++ for Ruby, and plenty of tooling is written in whatever was convenient. It removes the bootstrap problem entirely and gives up the dogfooding.
  • Transpile to an existing language: emit C and hand the rest to a C compiler. Nim and many DSLs do this, and it makes bootstrapping trivial because the target language's toolchain already exists — see [[dsl-implementation-strategies]].
  • Interpret first: write a tree-walking interpreter for the language in a host language, run the compiler under it, and use the output. Slow and entirely sufficient for a one-time bootstrap.
  • Cross-compile from a working platform, which is how every new architecture gets its first toolchain and requires no simplified compiler at all.

See it for yourself

The flag, dump or tool that shows you this directly.

  • Read a real bootstrap: Rust's src/bootstrap and its documentation describe the stage 0/1/2 process explicitly, including where stage 0 comes from.
  • Watch GCC do it: a default make in a GCC source tree runs a three-stage bootstrap and compares stage 2 with stage 3; make bootstrap-lean and the build logs show each stage.
  • Go's bootstrap requirements are documented per release, and GOROOT_BOOTSTRAP names the previous toolchain the build uses.
  • The Bootstrappable Builds project publishes chains that start from a few hundred bytes of hex and build up to a full toolchain — the clearest demonstration of what a small trust root would look like.
  • For any toolchain you depend on: find its stage 0 and ask whether you could produce it yourself. The answer is the actual supply-chain position.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "The first compiler must have been written in assembly." It must have been written in something that already had an implementation. Assembly is one option; another existing high-level language is more common and much easier.
  • "Once a language is self-hosting, there is no dependency on anything else." The build still starts from a binary somebody produced. The dependency moved rather than disappeared.
  • "Stage 0 has to implement the language." It has to compile one program. Every feature the compiler's own source avoids is a feature stage 0 can omit.
  • "Bootstrapping is a historical curiosity." It is the current build process for every self-hosted toolchain and it determines what you are trusting when you run it.

Misconceptions

The claim, and what is actually true.

A self-hosted compiler is compiled by itself.
It is compiled by a previous version of itself, or originally by a compiler in another language. Nothing compiles itself from nothing.
Bootstrapping only happens once, at the start of a language's life.
It happens on every build of a self-hosted toolchain. The current build of rustc is a bootstrap, using a downloaded compiler as stage 0.
Stage 1 and stage 2 differ because the source changed.
They come from identical source. They differ because stage 1 was compiled by a worse compiler, which affects performance and not behavior.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

A compiler written in its own language cannot compile itself the first time, so somebody writes a simpler one in a different language — good enough to handle the compiler's own source and nothing more. That produces a working compiler for the language, which then compiles the real source, which then compiles itself. After that the original is never needed again.

practical

When a toolchain build fails, find out which stage failed. A stage 0 failure means the compiler's source used something the bootstrap compiler does not support — usually a version-skew problem with the documented minimum bootstrap version. A stage 2 failure from source that stage 1 compiled fine is more interesting, and usually means the compiler miscompiled itself. And if you care about supply chain, the question to ask any toolchain is simply: what is stage 0, and could I build it?

advanced

The subtlety worth carrying is that bootstrapping quietly relocates the trust rather than establishing it. Reading the compiler's source tells you what stage 2 should do; it tells you nothing about what stage 0 did while producing stage 1. Modern chains make this worse in exchange for convenience: bootstrapping from the previous release means the chain has no inspectable root at all, only a regress of binaries. That is precisely the gap [[trusting-trust]] exploits, and it is why the Bootstrappable Builds effort spends real work reducing the seed to a few hundred bytes of hex. The interesting property of that work is that it does not try to make the seed trustworthy by argument — it makes it small enough that a human could actually read it, which is a different and much stronger claim.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

implementationEvery bootstrap chain described here is a property of a specific project at a specific time. Rust bootstraps from a downloaded beta compiler today and did not always; Go raised its minimum bootstrap version several times and now uses a chain of releases; GCC's three-stage build is its default but can be disabled. Consult the project's bootstrap documentation rather than a remembered chain.
typicalThat a bootstrap compiler need only support a subset is typical and is what makes the technique practical, but it puts a live constraint on the main compiler's source: every feature the team uses in their own code must already be supported by whatever stage 0 is. Projects that bootstrap from the previous release feel this as "we can only use features from N-1".
simplifiedOur AtlasLang implementation is not self-hosted: it is written in TypeScript and compiles AtlasLang, so it has no bootstrap problem at all. Everything in this lesson describes chains we deliberately do not have, which is why the examples here are drawn from real toolchains rather than from our own.

If you were asked this in an interview

  • The Rust compiler is written in Rust. What compiled the first one, and what compiles it today?
  • Why do stage 1 and stage 2 differ when they are built from identical source?
  • What does a bootstrap compiler need to support, and what does it get to ignore?

Connections

Domains that do not exist yet
  • DevOps / Production Engineering — Build inputs as artifacts with provenance
    A downloaded stage 0 compiler is a build input like any dependency, and the questions that apply to it — where did it come from, can it be verified, could it be rebuilt — are the same ones a supply-chain policy asks of every artifact. Answering them for the toolchain is where that policy is usually weakest.