MINIX, the birthing room of Linux. But what compiled MINIX itself?
gcc — it wedges an intermediate code called EM right in the middle. That single move transformed porting and bootstrapping.
In Episode 2 we saw that Linux was born in the birthing room called MINIX. So then — what compiled MINIX itself? The answer is ACK (the Amsterdam Compiler Kit), a compiler built by Tanenbaum and colleagues that belongs to a different lineage from gcc. This time we'll pause our journey back in time a little and savor the great invention that ACK embodies: "wedging an intermediate code into the middle." It made porting dramatically easier, and it runs like a spine through the history of compilers — through BCPL's O-code in Episode 6, and on to modern LLVM IR and WebAssembly.
MINIX is an educational Unix-like OS that Tanenbaum built in 1987 as an appendix to his textbook Operating Systems: Design and Implementation. Kernel, memory management, and file system came to about 12,000 lines. It was kept small enough for a student to read in full, and its design was a microkernel (the star of the debate in Episode 1). In the language of Episode 1, it was a leaf on the reimplementation tree — not a single line of AT&T code, just the feel of Unix written from a blank page.
What actually compiled MINIX was ACK. Built by Tanenbaum and Ceriel Jacobs, it was, for the early 1980s, an advanced set of retargetable (the target machine can be swapped out) compilers. A hallmark of ACK is that it handled more than one language — it had front ends for C, Pascal, Modula-2, Occam, and BASIC.
Front end (per language) → EM (intermediate bytecode) → general optimizer → back end (per machine) → machine code.
The heart of it is the EM in the middle. Every language's front end first lowers to EM, a common intermediate code. From there, a per-machine back end translates it into machine code. This resembles gcc's internal intermediate representation, but EM has its own character: it was designed as the language of a real abstract machine that could even be implemented in hardware.
The value of intermediate code becomes clear all at once when you count. Suppose you want to run M languages on N kinds of machines. Done naively, you need a compiler for every combination of language and machine — M×N of them. But if you place a common intermediate code in the middle, all you need is M front ends + N back ends = M+N of them. In the figure below, raise M and N and see how much the number of lines differs.
Intermediate code is the language-world version of Episode 0's "ignite only once." Knowledge of a language goes into the front end just once; knowledge of a machine goes into the back end just once. The two converse only through EM, so neither needs to know the other. By separating concerns in the middle, effort that used to multiply becomes effort that merely adds.
The benefit of M+N is maximized when you move to a new machine. Without intermediate code, you rewrite all M compilers for the new machine. With it — you write just one back end (or a small core that runs EM), and every existing language starts running on that machine. In the figure below, try adding a single new machine.
When you bring up a brand-new machine (the homework from Episode 3), the naive approach gets stuck on "how do we get a compiler onto it?" With intermediate code, port a single small core that runs EM, and the whole compiler asset base distributed in EM runs. A tall tower supported by one small foundation. It's a trick that makes Episode 0's "lift the ring once, from outside" a great deal lighter to lift.
The idea of "placing a common intermediate code in the middle" is not ACK's alone. It is, rather, a great river that runs through the history of compilers. BCPL's O-code (Episode 6), which we'll trace in future episodes, is a much older ancestor of this idea. And modern LLVM IR (clang's intermediate representation), JVM bytecode, and WebAssembly are all descendants that carry the same blood. ACK's EM was a beautiful, easy-to-grasp milestone somewhere along that great river.
clang. This is a real example of what Episode 1 foreshadowed: "the reimplementation tree borrowing leaves from the lineage tree, up in the canopy." ACK itself was open-sourced under a BSD license in 2003, and remains a classic you can still read today.
(1) ACK is itself a program, and running it requires a compiler — intermediate code does not eliminate the chicken and egg. It only makes porting and reuse dramatically easier; the "first seed" still has to be brought in from somewhere (the homework from Episode 3 remains alive). (2) "M+N" is a conceptual way of counting; in practice there are also other parts, such as the general optimizer and runtime libraries. The number of lines is an idealized rule of thumb.
(3) The relationship between MINIX and ACK also differs in detail by version and distribution (early on a limited C compiler; in later years other compilers could be ported and used too). Here we tell the skeleton: "the native core toolchain was ACK." (4) "EM can be implemented in hardware" is a claim about design philosophy, not a statement that it was in fact widely implemented.
What compiled MINIX — Linux's birthing room (1987, Tanenbaum, ~12,000 lines, microkernel, a leaf on the reimplementation tree) — was ACK. A retargetable compiler handling C, Pascal, Modula-2… its heart was the intermediate code EM in the middle. By splitting into three stages — front end (per language) → EM → back end (per machine) — the M×N compilers a naive approach would need shrink to just M+N.
Its true worth shows in porting. Add just one EM core for a new machine and every language runs — a lightening of bootstrapping, a tall tower supported by a small foundation. Yet intermediate code only makes the chicken and egg easier; it does not make it vanish (the seed still has to be brought in). This idea of "placing an intermediate code in the middle" was a great river, running from its ancestor O-code in Episode 6 all the way to LLVM IR and WebAssembly. — And that's the end of our detour. Next time, at last, we descend to the birth of the first C compiler, the ancestor of the ignition device cc.
Print / save as PDF: ⌘+P (Ctrl+P on Windows). On screen, use Figure 1's sliders to explore the gap between M×N and M+N, and Figure 2's button to try porting to a new machine. "Show the answer" reveals each solution.