Open the source code of almost any mature compiler and you notice something strange: it is written in the language it compiles. The Rust compiler is written in Rust. Go’s compiler is written in Go. GCC, which compiles C, is written in C and C++.

To compile Rust code, you need a Rust compiler. But to build the Rust compiler, you also need a Rust compiler. It is the classic chicken-and-egg problem. And yet this is the normal state of affairs for any compiled language that has been around long enough.

Not every language does it. CPython, the standard Python, is written in C, so building Python needs no Python at all. Interpreters can get away with that. Compilers usually end up compiling themselves.

This is called self-hosting, and to understand how it can work at all, you have to face a paradox that most programmers never consider.

The Bootstrapping Problem

Self-hosting depends on a process called bootstrapping. You do not need the language to write itself from scratch. You just need to get it started, far enough that it can take over.

Rust’s origin story shows how this works. The first Rust compiler was written in OCaml. The team used the OCaml compiler to build a simple Rust compiler. Then they rewrote the compiler in Rust, used the OCaml-built version to compile it, and got a pure Rust binary. After that, the OCaml code was thrown away. Rust had pulled itself up by its own bootstraps.

Go did the same thing a few years into its life. Its compiler was written in C until Go 1.5, when the team translated it into Go and used Go 1.4 to build the result. From then on, Go built Go.

After that first leap, the process becomes self-sustaining.

  1. You have a compiler binary and the compiler’s own source code.
  2. You feed the source into the binary.
  3. You get a new binary with all the latest changes baked in.
  4. Repeat forever.

The compiler becomes its own ancestor. Each generation builds the next.

If you follow this chain back through history, before C, before FORTRAN, you end up with people sitting at machines, flipping switches, and punching programs onto cards by hand. Each generation built on the last, adding one layer of abstraction at a time.

Building a Self-Hosted Language From Scratch

It’s one thing to read about self-hosting. Building it yourself is another matter. Let’s make the smallest self-hosted language we can.

We’ll call it Q. It has one command: the letter q. When the compiler sees q, it prints its own source code.

The compiler will be written in C. The goal is simple: a C program that reads Q files and outputs C code that does the same thing.

The Paradox

Here’s where things get interesting. To output our own source code, we might try something like this:

C
if (*s == 'q') {
  printf("#include <stdio.h>\nint main() { ... printf(\"...wait, what goes here?\"); }");
}

But what do you put inside the printf? The printf itself. Which would contain another printf, and so on, forever.

This is the quine paradox. How do you write a program that prints itself, without getting stuck in an endless loop of self-reference?

The Anchor Trick

The solution is to separate data from logic. You need an anchor.

We introduce a string variable called self that holds the entire source code of our program:

C
const char *self = "...the entire program, escaped as a string...";

Now, when we see a q, we don’t reproduce logic, we just print data:

C
if (*s == 'q') {
  printf("%s", self);
}

But there is still a catch. If you just print self, you print the string’s contents. You do not print the const char *self = "..."; declaration that wraps it. To reproduce itself completely, the program has to print self twice: once as raw code, and once as an escaped string literal.

We handle this with an anchor character, a question mark, placed exactly where the string literal belongs:

C
const char *self = "?";

When outputting itself, the compiler walks through self character by character. Most characters get printed as-is. When it hits the ?, it prints the entire contents of self with quotes and backslashes properly escaped.

C
if (self[j] == 63) { // 63 = ASCII for '?'
  for (int i = 0; i < n; ++i) {
    switch (self[i]) {
      case '\n': printf("\\n"); break;
      case '"': printf("\\\""); break;
      case '\\': printf("\\\\"); break;
      default: printf("%c", self[i]);
    }
  }
} else {
  printf("%c", self[j]);
}

Why use 63 instead of just writing a question mark? If we wrote a literal question mark, it would trigger the anchor logic when we don’t want it to. So we use its ASCII value instead. This is a classic trick for hiding a character from your own parser.

The final step: paste the escaped source code into self where the ? is. Compile. Feed the binary a file containing a single q. Capture the output as q1.c. Then run:

Bash
diff q.c q1.c

No differences. The compiler has reproduced itself exactly. That is self-hosting at its smallest: a program whose output is its own input.

The Thompson Hack

Now, here’s an interesting consequence of a self-hosted compiler.

In 1984, Ken Thompson, co-creator of Unix, gave a Turing Award lecture titled Reflections on Trusting Trust. In it, he described a theoretical backdoor so elegant and so invisible that it’s been tormenting security researchers ever since.

Imagine you want to backdoor Unix’s login program, letting you bypass password checks. You could modify login.c, but a code reviewer would find it. So instead, you modify the C compiler. You add a rule: if the file being compiled is login.c, silently inject the backdoor into the output binary.

Now login.c looks clean. But every compiled login binary is compromised.

There’s still a problem: your malicious rule is sitting in the compiler’s source code, where a reviewer could find it. So Thompson proposed a second modification: if the file being compiled is the compiler itself, inject both rules, the login backdoor, and this self-replication rule, into the new binary.

Compile this poisoned compiler once. Then delete the malicious source code.

What you’re left with:

  • login.c - completely clean.
  • The compiler’s source code - completely clean.
  • Every binary compiled by this compiler - compromised.

The backdoor now lives only in the binary. It’s completely separated from the source code. No code review will ever find it. You would have to take apart the compiler binary itself.

What This Actually Means

There is a real lesson hiding in all of this, and it is not about any one language.

A compiler is not a fact about its source code. It is a chain of binaries, each built by the one before it, reaching back to something a person keyed in by hand. The source is the story we tell about what the binary does. Usually the story is true. The Thompson Hack shows that it does not have to be, and that no amount of reading source can prove it is.

That is why serious projects keep their bootstrap chain, why reproducible builds matter, and why the standard answer to Thompson, a technique called diverse double-compiling, comes down to building the same compiler with two unrelated compilers and checking that the results agree. Trust has to come from somewhere outside the source.

Self-hosting is the moment a language stops needing its parents. Once you see how that works, and how much trust is folded into every binary that came before, the stack you are standing on looks a little different.