Handbooks  /  Compilers Basics
Handbook~16 min readSystemsworked math + runnable code
The Compilers Basics Handbook

Text becomes
meaning.

You type 2 + 3 * 4 and a machine somehow knows the answer is 14, not 20 — that multiplication happens first. How does a flat string of characters become a meaning, with rules like precedence baked in? A compiler does it in stages: chop the text into tokens, reorder them by precedence into a form that's trivial to run, then run it. That's the whole shape of every compiler and interpreter, from a four-function calculator to a language that powers the world. This handbook builds the tiny version end to end, and it actually runs.

01

A pipeline of stages

A compiler turns source text into something runnable, and the secret is that it never does it all at once — it's a pipeline of stages, each with one clear job. First, tokenizing (lexing) chops the raw characters into meaningful atoms: numbers, operators, identifiers, keywords. Then parsing arranges those tokens into a structure that captures the grammar — crucially, which operations bind to which (precedence). Then evaluation or code generation turns that structure into a result or into machine code.

An interpreter shares the same front end — tokenize, parse — but evaluates the structure directly instead of emitting code; a compiler emits code to run later. Either way, the shape is identical: text → tokens → structure → meaning, one well-defined transformation at a time. That staging is what makes something as daunting as "understand a programming language" tractable: no single step is hard, and each hands a cleaner representation to the next. We'll build the smallest real instance — an arithmetic calculator that respects precedence — because it contains the whole idea in miniature.

The one-sentence version

A compiler is a pipeline — tokenize the text, parse tokens by precedence into an easy-to-run form (Reverse Polish Notation via shunting-yard), then evaluate on a stack — and that same three-stage shape scales from a calculator to a full language.

02

Tokenizing

The first stage, lexical analysis, scans the character stream and groups characters into tokens — the words of the language. Given "12 + 3", the lexer emits [12, +, 3]: it recognizes that the adjacent digits 1 and 2 form one number token, treats + as an operator token, and throws away the whitespace. It turns a flat, characterwise string into a clean sequence of typed units.

This matters because it frees every later stage from dealing with raw characters. The parser doesn't want to worry about whether a number is one digit or seven, where spaces go, or how to skip a comment — the lexer has already resolved all of that, handing up a tidy token stream. In a real language the lexer also recognizes string literals, identifiers, keywords versus names, and multi-character operators like == or ->. But the essence is exactly the calculator's: read left to right, accumulate characters into the token they belong to, and emit clean atoms. Get the atoms right and the hard part — structure — has something orderly to work on.

03

Parsing precedence

Now the crux: 2 + 3 * 4 must mean 2 + (3 * 4), not (2 + 3) * 4. Encoding that is the parser's job, and the classic compact way is Dijkstra's shunting-yard algorithm, which converts our normal infix notation into Reverse Polish Notation (RPN, postfix) — a form with no parentheses and no ambiguity, trivial to evaluate.

Shunting-yard: 3 + 4 * 23 4 2 * +
numberGoes straight to output.
operatorPop stack ops of higher/equal precedence to output first, then push this one.
"("Push it; on ")" flush output back to the matching "(".
endPop any remaining operators to output → the RPN.

The precedence lives in one comparison: when * is on the stack and + arrives, * (higher precedence) is popped to output first, so it will be applied before +. That single rule is where "multiplication binds tighter" is encoded. Parentheses override it by forcing everything inside to flush before the closing ). The output, RPN, is then evaluated with a plain stack: push numbers, and on each operator pop the top two, apply, push the result. All the precedence complexity moved into the parse; evaluation is dead simple. (Real compilers usually build an explicit tree — an AST — but RPN captures the same precedence resolution in the clearest possible form.)

04

The pipeline math

The whole calculator is the composition of three functions — the classic compiler pipeline in miniature:

calc(src)  =  eval_rpn( to_rpn( tokenize(src) ) )

tokenize: string → tokens. to_rpn: infix tokens → postfix (shunting-yard). eval_rpn: postfix → value (stack).

Shunting-yard's precedence rule is the single comparison that makes * bind before +, and stack evaluation folds the RPN into a number:

on operator t:  pop while  prec(top) ≥ prec(t)  ⟹  2 + 3 * 4 → RPN  2 3 4 * + → 14  (not 20)

Because 3*4 is computed before the +, the answer is 14; parentheses (2+3)*4 flush the + first and give 20. The runnable version below is the whole tokenize → RPN → eval pipeline.

RUN IT YOURSELF

A calculator, in three stages

This is a whole (tiny) compiler front-to-back. tokenize chops the string into number and operator tokens, keeping multi-digit numbers whole. to_rpn is the shunting-yard algorithm: it reorders the infix tokens into Reverse Polish Notation, and the one precedence comparison is where "*" is made to bind tighter than "+". eval_rpn runs the RPN on a stack — push numbers, pop two and apply on each operator. So 2 + 3 * 4 is 14, not 20, and parentheses override precedence. Change the expression and watch the tokens, the RPN, and the result.

CPython · WebAssembly
05

Scaling to real languages

The calculator is small, but the skeleton is exactly what a production compiler uses — just with more stages bolted on:

StageCalculator · real compiler
LexDigits/operators → tokens · plus identifiers, keywords, strings, comments, positions for error messages.
ParseShunting-yard → RPN · a full grammar → an abstract syntax tree (AST) for statements, functions, types.
Analyze(none) · semantic analysis: type checking, scope/name resolution, catching errors before running.
Optimize(none) · transform the IR — constant folding, dead-code elimination, inlining — for faster output.
GenerateEvaluate on a stack · emit machine code or bytecode (or interpret the AST directly).

The shape never changes: text becomes tokens becomes a tree becomes lower-level code. A modern compiler adds an intermediate representation (IR) in the middle so optimization and code generation can target many source languages and many CPUs through a shared core (that's the whole idea behind LLVM). And an interpreter simply stops after building the tree and walks it. Even the thing turning your prompts into tokens for a language model is a lexer at heart. Once you see the pipeline in the calculator, you see it everywhere — the difference between a toy and a real compiler is the number of stages and the richness of each, not the shape.

06

Pitfalls

The first classic mistake is parsing with regexes or string splitting. It works for the simplest inputs and then collapses the moment you hit nesting — parentheses inside parentheses, precedence, function calls. Regular expressions genuinely cannot parse a nested grammar (it's a formal-language fact, not a skill issue); you need a real parser with a stack or recursion. Reaching for str.split to "parse" structured input is a trap that always ends in a rewrite.

Two more. Conflating the stages: trying to tokenize and evaluate in one pass tangles concerns and makes precedence and errors nearly impossible to get right — keep lex, parse, and evaluate as separate, testable steps, exactly as the pipeline prescribes. And ignoring error handling: real input is malformed — unmatched parentheses, stray operators, unexpected end — and a parser must report where and why, not crash or silently produce garbage (this is why lexers track line and column positions). The payoff for respecting the pipeline is huge: parsing and interpreting stop being intimidating and become a sequence of small, well-understood transformations you can build and test one at a time. The whole handbook is that realization — a compiler is not magic, it's a pipeline: chop into tokens, reorder by precedence, evaluate on a stack, and repeat the pattern all the way up to a real language.

Worth knowing

Don't parse nested grammars with regexes — you need a stack-based parser. Keep lex, parse, and evaluate as separate testable stages, and track positions so you can report where an error is. The calculator's tokenize→RPN→eval pipeline is the same skeleton a real compiler scales up.

Frequently asked

Quick answers

What does a compiler do?

Turns source text into runnable output in stages: tokenize → parse into structure → analyze/optimize → generate code. An interpreter evaluates the structure directly.

What is tokenizing?

Scanning characters into meaningful tokens — "12 + 3" becomes [12, +, 3] — so later stages work on clean atoms, not raw characters.

What is shunting-yard?

Dijkstra's algorithm converting infix to postfix (RPN) using an operator stack; its precedence comparison is where "*" is made to bind tighter than "+".

How is RPN evaluated?

With one stack: push numbers, and on each operator pop the top two, apply, push the result. No precedence rules needed — the order encodes them.

Finished this one? 0 / 99 Handbooks done

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.