A History of UNIX That ClicksEpisode 4 / The Invention of Intermediate Code

MINIX, the birthing room of Linux. But what compiled MINIX itself?

MINIX and ACK — The Invention of Intermediate Code In Episode 2, MINIX was the birthing room of Linux. What built MINIX was a compiler from Tanenbaum called ACK. Its philosophy differs from gccit wedges an intermediate code called EM right in the middle. That single move transformed porting and bootstrapping.

Tools you'll need: Episode 1 (the reimplementation tree), Episode 3 (gcc) The reveal: wedge something into the middle, and M×N becomes M+N

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.

01MINIX — a Unix born as a textbook appendix

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.

02What built MINIX was ACK

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.

Inside ACK (a three-stage structure)

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.

03Why wedge something into the middle — the M×N problem

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.

Figure 1: The M×N problem. Top = naive (M×N compilers, one per language×machine). Bottom = wedging in an intermediate code (only M+N needed). The larger M and N grow, the wider the gap.
This episode's crux — "translate once, then reuse"

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.

◇ ◇ ◇

04The reveal — intermediate code is the escape route for porting and bootstrapping

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.

Figure 2: Porting to a new machine. Prepare just one EM core (a back end / interpreter), and the whole asset base of every existing front end suddenly becomes usable on that machine.
Nothing runs yet on the new machine (gray). Port a single "EM core" and C, Pascal, Modula-2… all start running at once.
Why it works for bootstrapping

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.

05A family of intermediate codes — from O-code to WASM

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.

Bridging voice — swapping leaves up in the canopy (continued from Episode 1) ACK was long the native compiler of MINIX, but in later years MINIX 3 took in its userland from NetBSD on the lineage tree side, and switched its system compiler to 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.
The honest line — intermediate code "makes it easier," it doesn't "make it vanish"

(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.

Practice problems (solvable with just this episode)
  1. If you naively build every compiler for 5 languages and 4 machines, how many do you need? And how many if you wedge in an intermediate code?
    Show the answer
    Naive: 5×4 = 20. With intermediate code: 5 + 4 = 9 (5 front ends + 4 back ends).
  2. With the intermediate-code approach, when you add one new machine, what extra thing do you build?
    Show the answer
    Just one back end for that machine (a core that runs EM). Every existing front end — every language — then supports the new machine with only that.
  3. Does intermediate code solve the "chicken and egg"?
    Show the answer
    No. It only makes porting and reuse much easier; you still have to bring in a first compiler (a seed) from somewhere. The ignition problem of Episode 3 doesn't vanish.
  4. Name one "descendant" that is related by blood to ACK's EM.
    Show the answer
    Examples: BCPL's O-code (an ancestor), LLVM IR, JVM bytecode, WebAssembly. All share the same idea of "placing a common intermediate code in the middle."

SummaryWedge in the middle, and multiplication becomes addition

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.

This document is Episode 4 of the series "A History of UNIX That Clicks." Historical notes: MINIX was an educational Unix-like OS created by Andrew S. Tanenbaum (with the cooperation of Albert S. Woodhull) for the textbook Operating Systems: Design and Implementation (Prentice Hall, 1987); its roughly 12,000 lines of condensed C source for the kernel, memory management, and file system were included in the book. It was a microkernel design and an independent implementation that inherited no AT&T UNIX code. ACK (the Amsterdam Compiler Kit) was a set of retargetable compilers by Andrew Tanenbaum and Ceriel Jacobs, with front ends for C, Pascal, Modula-2, Occam, and BASIC, separating front and back ends via an intermediate bytecode called EM. It is known as an early portable compilation system, from the early 1980s, aimed at multiple languages and multiple machines. In MINIX 3 (3.2.0) the userland was replaced with one derived from NetBSD, and the system compiler became Clang. ACK was open-sourced under a BSD license in 2003. The design of passing through an intermediate code also connects to BCPL's O-code (Episode 6), LLVM IR, JVM bytecode, WebAssembly, and more. The "M+N" way of counting is an idealized, conceptual one; in practice there are additional elements such as the optimizer and runtime components. — To print, use your browser's "Print" and "Save as PDF" (in the printed version, the figure controls and answers are frozen / hidden).

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.