Compiler Design

The pipeline that turns source text into an executable, phase by phase — and what each phase can and cannot catch.

If you remember nothing else

  • Six phases: lexical, syntax, semantic, intermediate code generation, optimisation, code generation — with the symbol table and error handler spanning all of them.
  • Lexical analysis uses regular expressions and finite automata; syntax analysis uses context-free grammars and pushdown automata. The Chomsky hierarchy is why.
  • A type error is caught in semantic analysis, not syntax analysis — int x = "hello"; is grammatically perfectly well-formed.
  • LL parsers are top-down and build a leftmost derivation; LR parsers are bottom-up, build a rightmost derivation in reverse, and handle strictly more grammars.
  • Left recursion breaks recursive-descent parsing and must be eliminated; left factoring removes the need to look ahead further.
  • Compilation proper is only one stage: preprocessing, compiling, assembling and linking are four distinct programs.

The phases of a compiler

PhaseInputOutputCatches
1. Lexical analysisCharacter streamToken streamIllegal characters, malformed literals
2. Syntax analysisToken streamParse tree / ASTMissing semicolons, unbalanced brackets
3. Semantic analysisParse treeAnnotated ASTType errors, undeclared variables, wrong argument counts
4. Intermediate code genAnnotated ASTThree-address code / IR
5. OptimisationIRBetter IR
6. Code generationIRTarget assembly or machine code

The symbol table and the error handler interact with every phase, which is why they are drawn alongside rather than within the pipeline.

Lexical analysis

The scanner reads characters and groups them into tokens, discarding whitespace and comments. It is specified with regular expressions and implemented as a finite automaton — which is exactly why regular languages sit at the bottom of the Chomsky hierarchy and are enough for this job.

TermMeaningExample
TokenA category name plus optional attribute<id, ptr-to-symtab>
PatternThe rule describing what the token looks likeletter(letter|digit)*
LexemeThe actual character sequence matchedcount, 42, while
Plain textTokenising one line
Source: position = initial + rate * 60; Tokens: <id, "position"> <assign_op> <id, "initial"> <plus_op> <id, "rate"> <mult_op> <num, 60> <semicolon> Whitespace and comments are discarded and never reach the parser.

Syntax analysis

The parser checks that the token stream conforms to the language's context-free grammar and builds a parse tree. Regular expressions are insufficient here — nested constructs such as balanced parentheses and block structure require a stack, hence a pushdown automaton.

Top-down (LL)Bottom-up (LR)
Builds the treeFrom the root downFrom the leaves up
DerivationLeftmostRightmost, in reverse
Core actionsPredict and matchShift and reduce
Cannot handleLeft recursion; needs left factoringHandles both
Grammar coverageSmaller classStrictly larger class
ImplementationRecursive descent — easy to hand-writeTable-driven; usually generated
ToolsANTLR, hand-written parsersyacc, bison
Plain textLeft recursion and left factoring
LEFT RECURSION - a recursive-descent parser for this loops forever, because parsing E immediately calls E again with no input consumed: E -> E + T | T Eliminate by introducing a new non-terminal: E -> T E' E' -> + T E' | ε LEFT FACTORING - the parser cannot choose between these two with one token of lookahead, since both start with "if": S -> if E then S else S | if E then S Factor the common prefix out: S -> if E then S S' S' -> else S | ε

Semantic analysis and the symbol table

Syntax analysis proves the program is well-formed; semantic analysis proves it is well-meaning. These are the checks a context-free grammar cannot express.

  • Type checking — operands are compatible; implicit conversions are inserted where the language permits.
  • Declaration checking — every identifier is declared before use, and not declared twice in one scope.
  • Scope resolution — each use is bound to the correct declaration under the language's scoping rules.
  • Function signature checking — argument count and types match the declaration; the return type matches what is returned.
  • Flow checksbreak appears inside a loop, every path returns a value.

Intermediate code and optimisation

An intermediate representation is machine-independent but simpler than the source, which is what lets one optimiser serve every front end and every back end. Three-address code is the standard teaching form: at most one operator per instruction, with explicit temporaries.

Plain textThree-address code and common optimisations
Source: a = b * c + b * c * 2 Three-address code (naive): t1 = b * c t2 = b * c t3 = t2 * 2 t4 = t1 + t3 a = t4 After COMMON SUBEXPRESSION ELIMINATION (b*c computed once): t1 = b * c t3 = t1 * 2 t4 = t1 + t3 a = t4 Other representations you should be able to name: Postfix: a b c * b c * 2 * + = Syntax tree / DAG: shares the repeated b*c subtree Quadruples: (op, arg1, arg2, result) Triples: (op, arg1, arg2) - referenced by position
OptimisationDoesExample
Constant foldingEvaluates constant expressions at compile timex = 3 * 4x = 12
Constant propagationReplaces a variable with its known constant valuex=5; y=x+2y=7
Common subexpression eliminationComputes a repeated expression onceAs above
Dead code eliminationRemoves code whose result is never usedAn assignment to a variable never read again
Strength reductionReplaces an expensive operation with a cheaper onex*2x<<1; multiplication in a loop → addition
Loop-invariant code motionHoists a computation that does not change out of the loopfor(...) { y = a*b; ... }
Copy propagationReplaces a copied variable with its sourcey=x; z=y+1z=x+1
InliningReplaces a call with the function bodyRemoves call overhead, enables further optimisation

Code generation and the toolchain

The code generator maps IR to target instructions, and must solve three problems: instruction selection (which machine instructions implement this IR operation), register allocation (which values live in the limited register set), and instruction ordering (which schedule best fits the pipeline).

ShellWhat actually runs when you compile a C file
gcc hello.c -o hello # four separate programs run in sequence # 1. PREPROCESSOR (cpp) - expands #include, #define, strips comments gcc -E hello.c -o hello.i # 2. COMPILER (cc1) - the six phases; produces assembly gcc -S hello.i -o hello.s # 3. ASSEMBLER (as) - assembly to relocatable object code gcc -c hello.s -o hello.o # 4. LINKER (ld) - resolves symbols across object files and libraries, # assigns final addresses, produces the executable gcc hello.o -o hello # "undefined reference to printf" is a LINKER error, not a compiler one: # the compiler was satisfied by the declaration in stdio.h; only the # linker discovers that no definition was supplied.

Self-check

10 questions on Compiler Design · nothing is stored

0/10 answered
Question 1

1.int x = "hello"; is rejected during which phase?

Question 2

2.Lexical analysis is implemented with finite automata because:

Question 3

3.Left recursion must be eliminated for which parsing technique?

Question 4

4.An LR parser constructs:

Question 5

5.A basic block is:

Question 6

6."Undefined reference to printf" is produced by:

Question 7

7.Register allocation is typically modelled as:

Question 8

8.Replacing x * 2 with x << 1 is an example of:

Question 9

9.Which parser generator output is used by most real compilers, balancing power against table size?

Question 10

10.The compiler front end is distinguished from the back end in that the front end: