Topic 1 · Lecture notes · Week 1

The Structure of a Compiler, and the Theory of Lexical Analysis

Why a compiler is built as a sequence of phases, what each one is responsible for, and the automata theory that makes the first phase a solved problem.

Read alongside this

0Where this topic sits

1. Lexicalchars → tokensscanner.l2. Syntaxtokens → ASTparser.y3. SemanticAST → checked ASTsemantic.c4. IRAST → TACtac.c5. OptimizeTAC → better TACtac.c6. CodeGenTAC → MIPScodegen.c

Topic 1 covers two things that look unrelated and are not. The first is the architecture of a compiler — why it is built as a sequence of phases at all, and what each phase is responsible for. The second is the theory of the first phase: regular expressions, finite automata, and the equivalence between them that turns lexical analysis from a programming problem into a solved one.

The connection is this. Lexical analysis can be split off as a separate phase because the things it must recognize — keywords, identifiers, numbers, operators — form exactly the class of regular languages, and regular languages have an efficient, automatic recognition procedure. The moment you need to recognize something that is not regular, such as balanced parentheses, you need a different machine, and that machine is the parser. The phase boundary is not an engineering convenience. It is drawn where the mathematics changes.

1What a compiler is, and what surrounds it

A compiler reads a program in one language and produces an equivalent program in another, reporting any errors it finds along the way (Dragon §1.1). That is the whole definition, and notice what it does not say: nothing about optimization, nothing about machine code specifically, nothing about a particular architecture. A source-to-source translator is a compiler. So is a program that emits Java bytecode, or LLVM IR, or MIPS assembly — which is what yours will do.

The compiler is one component of a larger pipeline, and confusing its job with its neighbors' is a common early mistake:

ComponentConsumesProducesIn this course
Preprocessorsource with directivespure sourcenone — our language has no #include
Compilersourceassemblythe whole course
Assemblerassemblyrelocatable machine codeSPIM does it for you
Linker / loaderobject files + librariesan executable imageSPIM does this too

Because SPIM assembles and loads, you can stop at assembly text and still watch your programs run. That is the only reason a one-semester compiler course can reach executable output at all.

2The phases, and the running example

Dragon §1.2 decomposes compilation into six phases. Learn them with the book's own running example, because you will meet it in the reading and in every compiler text afterward:

position = initial + rate * 60
character streamposition = initial + rate * 60Lexical Analyzer<id,1> <=> <id,2> <+> <id,3> <*> <60>Syntax Analyzersyntax tree, rooted at =Semantic Analyzersame tree, plus inttofloat(60)Intermediate Code Gent1 = inttofloat(60) ; t2 = id3 * t1 …Code Optimizert1 = id3 * 60.0 ; id1 = id2 + t1Code GeneratorLDF R2,id3 / MULF R2,R2,#60.0 …SymbolTableread and writtenby every phase
The phases of a compiler. After Aho, Lam, Sethi & Ullman, Compilers: Principles, Techniques and Tools (2nd ed.), Fig. 1.7. Each phase consumes the representation the previous one produced. The symbol table is not a phase — it is a data structure every phase consults, which is why it is drawn to one side.

Read that figure as a sequence of representations, not a sequence of actions. The representation is the real content of each phase boundary:

PhaseInputOutputThe question it settles
1 · Lexical analysischaracter streamtoken streamAre these words in the language?
2 · Syntax analysistoken streamsyntax treeDo those words form a sentence?
3 · Semantic analysissyntax treeannotated treeDoes the sentence mean anything?
4 · Intermediate codeannotated treethree-address codeWhat steps compute it?
5 · Optimizationthree-address codebetter three-address codeWhich steps are unnecessary?
6 · Code generationthree-address codetarget assemblyHow does this machine do it?

The symbol table is not a phase

It is drawn beside the phases because every phase reads or writes it (Dragon §1.2.7, §2.7). Phase 2 or 3 inserts a name when it meets a declaration; phase 3 looks names up to check they were declared; phase 6 looks them up again to find their storage. Treating the symbol table as belonging to one phase is a design error that becomes expensive in Topic 3, when a second kind of symbol table appears and the distinction between them starts to matter.

Phases and passes are different things

A phase is a logical stage. A pass is one complete traversal of the program being compiled (Dragon §1.2.8). Several phases may be grouped into one pass: a classic one-pass compiler lexes, parses, checks and generates code during a single left-to-right walk, emitting code from inside the parser's semantic actions. Ours is multi-pass — the parser builds a complete tree and later phases traverse it again.

Multi-pass costs memory and buys two things you will want. Optimization cannot act on code it has not seen yet. Forward references — calling a function defined later in the file — need the whole program in hand, which is why Topic 3's semantic analyzer runs twice. Backpatching (Dragon §6.7) is the classic technique for recovering some of that ability within a one-pass design; worth knowing it exists even though we will not need it.

3The front end / back end split

FRONT ENDanalysis · phases 1–4depends on the SOURCE languageIntermediateRepresentationBACK ENDsynthesis · phases 5–6depends on the TARGET machineC-minusPascalFortranMIPSARMx86-64
Why the IR boundary exists. With m source languages and n target machines, separate compilers cost m×n pieces of work; routing everything through one intermediate representation costs m+n (Dragon §1.2.8, §2.8.1). This is the most consequential structural decision in the architecture, and it is why Topic 5 can ask you to retarget to ARM by rewriting one file.

Phases 1–4 form the front end: they depend on the source language and know nothing about the target machine. Phases 5–6 form the back end: they depend on the target and know nothing about the source. The intermediate representation is the interface between them, and the whole value of the arrangement follows from that (Dragon §1.2.8).

This is not an abstract benefit. Topic 5 asks you to compare MIPS and ARM code generation, and the honest answer is a list of which files would change — a list that is short precisely because this boundary exists.

The design principle to carry all semester

Each phase should assume as little as possible about its neighbors. When you are tempted to have the parser peek at the symbol table, or the code generator consult the syntax tree, stop and ask what representation the interface is supposed to be carrying. Most structural bugs in a student compiler are a phase reaching across a boundary it should not.

4The role of the lexical analyzer

Dragon §3.1 gives the scanner three jobs: group characters into lexemes and produce tokens; strip whitespace and comments; and correlate tokens with positions in the source so that later phases can report errors usefully.

Token, pattern, lexeme — three different things

These words are used interchangeably in casual speech and must not be here (Dragon §3.1.3):

TermWhat it isExample
Tokena pair: a name, and an optional attribute value<ID, pointer to symbol-table entry>
Patterna description of the form a lexeme may takeletter (letter | digit)*
Lexemethe actual characters matched, in this programposition

The distinction earns its keep immediately. The parser decides on the token name alone — it needs to know it has an ID, not which identifier. The attribute matters later: the semantic analyzer needs the name to look it up, the code generator needs its address. And nobody ever needs reminding that a SEMI was spelled ;, which is why most tokens carry no attribute at all.

Why separate the scanner from the parser?

Dragon §3.1.1 gives three reasons, and they are worth being able to state out loud:

  1. Simplicity of design. The parser's grammar does not have to describe whitespace and comments, which would otherwise be permitted between every pair of grammar symbols and would bloat every single production.
  2. Efficiency. Specialised buffering and matching techniques apply to the character-level task and not to parsing (Dragon §3.2, on input buffering and sentinels).
  3. Portability. Input-device and character-set peculiarities — line endings, encodings — are confined to one module.

5Specifying tokens: regular expressions

Dragon §3.3 builds the formal apparatus: an alphabet is a finite set of symbols; a string is a finite sequence of them; a language is any countable set of strings over an alphabet. Regular expressions denote languages, and are defined inductively — the whole definition is six lines:

BASIS       ε           denotes  { ε }
            a           denotes  { a }          for each symbol a of the alphabet

INDUCTION   (r) | (s)   denotes  L(r) ∪ L(s)          union
            (r)(s)      denotes  L(r) L(s)            concatenation
            (r)*        denotes  ( L(r) )*            Kleene closure
            (r)         denotes  L(r)                 grouping

Precedence — * highest, then concatenation, then |, all left-associative — lets most parentheses be dropped, exactly as it does for arithmetic. A language that some regular expression denotes is a regular set.

Regular definitions

Naming subexpressions makes a specification readable, provided each name is used only after it is defined, so that no recursion can creep in (Dragon §3.3.4). Our language's token specification written this way:

letter    ->  A | B | … | Z | a | b | … | z | _
digit     ->  0 | 1 | … | 9
id        ->  letter ( letter | digit )*
number    ->  digit digit*
ws        ->  ( blank | tab | newline )+
Recursion is exactly what is missing

Look hard at that list: no rule refers to itself, directly or indirectly. That restriction is not stylistic — it is the definition of a regular definition, and it is precisely what a context-free grammar relaxes. When you meet expr → expr '+' expr in Topic 2, you are watching the boundary between §5 of this page and §7 of it being crossed.

6Recognizing tokens: finite automata

A specification is not yet a recognizer. Dragon §3.4 moves from regular expressions to transition diagrams: states, edges labeled with characters, one start state, and accepting states that report a token.

01234start<=>otherreturn LEreturn NEreturn LTafter RETRACTING one char
A transition diagram for the operators beginning with < (Dragon §3.4.1, Fig. 3.13). State 4 is reached on a character that is not =, so the scanner must push that character back before returning LT. That retraction is the machine-level form of the longest-match rule, and it is why < and <= cannot be recognized independently of one another.

Note the retraction on the edge out of state 1 labeled other. Having read < and then a character that is not =, the scanner must return LT and put that character back, because it belongs to the next token. Every scanner has to do this somewhere; in a generated scanner it is hidden inside the simulation loop.

NFA and DFA

A nondeterministic finite automaton may have several edges leaving a state on the same symbol, and may have ε-edges that consume no input. A deterministic one has exactly one edge per state per symbol and no ε-edges (Dragon §3.6). NFAs are easy to build from regular expressions; DFAs are easy to run — linear in the input, no backtracking. The two are equally expressive, and the two constructions establishing that are the theoretical core of this topic.

Regular expression → NFA: Thompson's construction

alternation r | sifεεrsεεconcatenation r sifrsclosure r*ifεrεεε
Thompson's construction (Dragon §3.7.4). Every regular expression is built from these three inductive cases plus a base case for a single symbol, so every regular expression has a corresponding NFA — and that NFA has at most twice as many states as the expression has symbols. This is the constructive half of the equivalence between regular expressions and finite automata.

NFA → DFA: the subset construction

NFA fragment for (a|b)*abb0a1b2b3a, bthe DFA the subset construction yieldsAaBbCbD{0}{0,1}{0,2}{0,3}
The subset construction (Dragon §3.7.1). Each DFA state is a set of NFA states — the set the NFA could be in after reading the input so far. The DFA is deterministic and needs no backtracking, at the cost of possibly exponentially many states. flex performs exactly this conversion on your rules, which is why a generated scanner runs in time linear in the length of the input.

Together these give a mechanical route from a token specification to an efficient recognizer. DFA minimization (Dragon §3.9.6) then reduces the state count to the unique minimum. You will implement none of it — but you should understand that it is what flex does to your rules, because it explains both the performance and, more usefully, the conflict-resolution rules in §8.

Why can a DFA-based scanner recognize a token in time proportional to the length of the lexeme, with no backtracking?

7The limits of regular languages, and why phase 2 exists

This is the most important idea in Topic 1, and the one most often skipped. Consider the language of balanced parentheses:

    { (ⁿ )ⁿ  |  n ≥ 1 }        (  )   ( (  ) )   ( ( (  ) ) )   …

No regular expression denotes it and no finite automaton recognizes it. The argument is short (Dragon §3.3.5). A finite automaton has a fixed, finite number of states, so on a long enough input it must revisit one. Feed it ( repeatedly; after enough of them, two different counts i ≠ j must leave it in the same state. From that state the machine cannot tell them apart, so it accepts (ⁱ )ʲ — which is unbalanced. A machine with finite memory cannot count without bound.

This is why the compiler has a second phase

Nesting — parentheses, blocks, nested if statements, expressions inside expressions — is not regular. Recognizing it needs a machine with a stack, which is a pushdown automaton, which is what a parser is. The boundary between phase 1 and phase 2 is not a matter of taste or file organization. It is the boundary between the regular and the context-free languages, and everything in Topic 2 follows from being on the far side of it.

This also settles a question that recurs all term: which phase should catch this error? If detecting it requires remembering an unbounded amount, the scanner cannot. int int int; is three perfectly good tokens — nothing about the vocabulary is wrong — and it is the parser that must object.

8How a scanner generator actually works

Dragon §3.5 and §3.8 describe what lex does with your specification, and it is exactly the theory above, applied:

  1. Each pattern pi is converted to an NFA Ni by Thompson's construction.
  2. A single new start state is created, with an ε-edge to every Ni — one combined NFA for the whole specification.
  3. The subset construction converts that to a DFA, and each accepting DFA state records which of the original patterns it accepts.
  4. The DFA is minimized and emitted as tables inside the generated C.

Two rules then resolve the cases where several patterns match, and both are direct consequences of this construction (Dragon §3.5.3):

RuleStatementWhere it comes from
Longest matchPrefer the longest lexeme any pattern matches.The simulator keeps running past an accepting state, remembering the last one it saw, and retracts to it only when it gets stuck. Position in the file is irrelevant to this.
First matchAmong patterns matching the same longest lexeme, prefer the one written first.An accepting DFA state may record several patterns; the generator breaks the tie by rule order. This — and only this — is why keyword rules must precede the identifier rule.

Being able to say which rule explains a behavior is worth more than memorising either. <= beating < is longest match, and rule order cannot change it. int coming back as a keyword rather than an identifier is first match, and rule order is the only thing deciding it.

An alternative design worth knowing about

Dragon §3.3.4 notes that some compilers do not give keywords their own patterns at all. They match every word as an identifier, then look the lexeme up in a table of reserved words and change the token name if it is found. This keeps the automaton smaller and makes the reserved-word list a run-time table rather than part of the grammar — handy if a language lets you disable keywords. It is a legitimate alternative, not a fix for rule ordering, and being able to compare the two is a good interview answer.

9Errors, and what a compiler owes the programmer

Dragon §1.2 and §3.1.4 treat error handling as a first-class concern rather than an afterthought, and the reason is worth internalising: diagnostics are the part of a compiler that programmers interact with most. Very little can go wrong at the lexical level — essentially only a character that begins no legal lexeme — but how it is handled sets the pattern for every later phase.

StrategyWhat it doesCost
Panic modedelete characters until a well-formed token is foundsimple; can skip past real errors
Delete / insert / replace one characterattempt a minimal local repaircan cascade, or loop
Report and continuecount the error, advance one character, keep scanningreports every error in one run — what ours does

A compiler that stops at the first error makes the programmer recompile once per typo. One that recovers reports everything at once, at the risk of cascading — a real trade-off, and one you meet again in Topic 2, where the cascade risk is much higher because the parser has far more context to lose.

The other half of this is location. A token's position is not decoration: it is the only thing connecting a message to the source text. Note that flex maintains line numbers for you but not columns, and that a phase which silently loses position information — by failing to count the newlines inside a comment, say — makes every later diagnostic wrong in a way that is very hard to trace back.

10The approach for this milestone

Theory settled, here is the shape of the implementation and the reasoning behind each decision. The code itself is documented; what follows is why it is arranged that way.

Specify the whole language's vocabulary now

Your grammar will grow for four more topics; your token set barely will. Adding switch to the specification in week 1 costs one line. Discovering in week 12 that you must reopen and re-verify the scanner costs an afternoon. This also mirrors real practice: a language's lexical structure is normally settled long before its grammar is.

Choose the token names carefully

They become the parser generator's %token declarations in Topic 2 and are then used throughout the front end. They are the vocabulary of the interface between phases 1 and 2, and renaming them later touches every file.

Decide where the attribute lives

Dragon §2.7 and §3.1.3 observe that the natural attribute for an identifier is a pointer to a symbol-table entry rather than the string itself. We use the string at this milestone because there is no symbol table yet — but be aware this is a simplification and know what it costs: every identifier is copied, and comparing two identifiers is a string comparison rather than a pointer comparison.

Report all errors, with line and column

Both halves matter. Recovery gives one message per mistake instead of one per compile; position gives a message the programmer can act on. Columns are the part people skip, and they are exactly what is needed on a line holding three statements.

Draw the phase boundary deliberately

When you are unsure whether a check belongs in the scanner, apply the test from §7: could a machine with finite memory decide it by looking at one lexeme? If yes, it may belong here. If it needs to remember an unbounded amount, or to know what came before, it belongs to a later phase. That single question resolves most of the 'which phase?' arguments you will have this semester.

11Self-check

Which phase should reject int int int;?

Why is the set of balanced parenthesis strings not a regular language?

A compiler is described as having six phases and two passes. What does that mean?

Your scanner returns ID(while) for the input while. Which rule explains it, and what is the fix?

Which of these is a pattern, as Dragon §3.1.3 uses the term?

12References