Compiling is not one translation. Each compiler has several stages, and each one throws away something the last one needed. Type C below, play any stage to watch it do its work one step at a time, then run what comes out.
Source
A subset of C: int and char, locals, arithmetic, if, while, for, functions that can call each other, and pointers and arrays —&x, *p, a[i]. Edit it and all seven stages recompile.
Examples
Preprocess
Text in, different text out. The compiler proper never sees what you wrote.
inthe characters you typedoutpreprocessed text
STEP 1 / 1
Where this step points in your source
What it produces
Scan
Characters become tokens. Spelling stops mattering; categories start.
inpreprocessed textouta token list
STEP 1 / 1
Where this step points in your source
What it produces
Parse
The flat list becomes a tree. Precedence is a shape, not an order.
ina token listouta syntax tree
STEP 1 / 1
Where this step points in your source
What it produces
Analyse
Names get homes and types get checked. None of this is in your source.
ina syntax treeouta symbol table and types
STEP 1 / 1
Where this step points in your source
What it produces
Lower to IR
The tree becomes a list. Control flow becomes jumps.
ina syntax tree + a symbol table and typesoutthree-address IR
STEP 1 / 1
Where this step points in your source
What it produces
Allocate registers
More values than registers. Which of them can share one is a graph-colouring problem.
inthree-address IRouta register assignment
STEP 1 / 1
Where this step points in your source
What it produces
Emit assembly
Instructions a processor could run — and a lot of moving data around.
inthree-address IR + a register assignment + a symbol table and typesoutx86-64 assembly
STEP 1 / 1
Where this step points in your source
What it produces
Run it
Not a rewrite. This executes the IR from stage five, one instruction at a time.
inthree-address IRouta return value, and whatever it wrote to memory
STEP 1 / 1
Where this step points in your source
What it produces
What is left out
A real compiler, but a small one. Being clear about the edges matters more than pretending otherwise.
- No structs, floats, strings or bitwise operators. Declarations stay simple —
int x;,int *p;,int a[10];— so no function pointers, noint (*p)[10], and one array dimension only. - No bounds checking, because C has none.
a[5]on a three-element array compiles quietly, and every stage below will show you it doing so. - No pointer subtraction and no
&array. Both are real C; both need type rules this explainer stops short of. - No standard library and no
#include. Every function has to be defined here. - Register allocation, but a plain one. Twelve colours, and it does coalescing — merging a destination with an operand when x86's own two-address arithmetic would rather compute there directly than copy afterwards — but no rematerialisation, so a spilled constant is reloaded rather than recomputed. Named locals are never in registers at all, because
&xhas to have an answer and only memory has an address. - No instruction scheduling and no peephole pass. What comes out is in the order lowering produced it, which is why the assembly still reads one IR instruction at a time.
- No assembler and no linker. The x86-64 is text, and nothing on this page executes it — there is no processor here to. It does assemble, though:
gccaccepts it, and a test builds and runs it on every check. - What runs below is the IR, interpreted — stage five, not stage seven. Same program, one level up, on the same frame layout. Memory is a flat array, so a pointer is an index into it and the addresses are not the ones a real process would see.