Lexical Analysis: Formalism
Phase 1 of the Compiler Construction pipeline, slides 19–51 (Univ. Duisburg-Essen, Janis Voigtländer, winter term 2026/27, slides version 28.10.2026).
The lecture’s triad for every phase: formalisms → algorithms → tools. For lexing:
| Formalisms | regular expressions, finite automata |
| Algorithms | automata constructions (NFA from regex, DFA from NFA, minimization) |
| Tools | lexer generators (lex/flex, Alex, …) |
The practical side (token streams, longest match, generators) is on practical-lexers. This page is only the mathematical machinery.
Exam relevance. The written exam is planned for 11 March 2027. “Build the NFA for this regex, then decide it by powerset construction, then minimize it” is the single most probable constructive task of the whole course. The exercises on slides 35 and 51 are the canonical drill.
0. Where lexing sits
Slide 13’s compiler structure, phase for phase, with the lecture’s annotations:
| Phase | Input | Output | Note from the slides |
|---|---|---|---|
| lexical analysis | program text (characters) | sequence of tokens | ”use simple means for simple tasks” |
| syntactical analysis | tokens | parse tree | works on tokens, never on characters |
| semantical analysis | parse tree | annotated tree | frontend ends here |
| intermediate code | annotated tree | IR | ”often transformations on intermediate code” |
| register allocation | IR | IR + machine registers | not covered in this course |
| machine code | IR | assembly | not covered in this course |
| assembling & linking | assembly | executable | not covered |
Two framing ideas from the slides worth keeping:
- well-defined interfaces, establishing invariants — each phase may assume what the previous one guarantees. The lexer guarantees “the token stream is well-formed w.r.t. the token classes”, so the parser never has to think about whitespace, comments, or characters.
- frontend vs. backend, and written by hand vs. generated. Lexers are the phase where “generated” wins most easily, because the theory section below gives a fully mechanical construction.
Why a separate phase at all (slide 24) — the five justifications:
- efficiency — simple means for simple tasks; the DFA-driven scanner is a tight loop over a transition table.
- simplification of subsequent phases — the parser sees ~40 token classes instead of 256 characters.
- reduced programming effort through decoupling — lexer and parser evolve independently.
- conceptual clarity — also in the language definition (the language spec has a token layer and a grammar layer).
- modularity, reusability.
Naive hand-rolled lexing (“check whether a prefix is a number, a keyword, …”) fails on scalability (efficiency and maintainability) and on token boundaries (start/end of tokens resolved ad hoc). Instead: declarative description of lexeme classes + generation of the lexer from that description.
1. Vocabulary of formal languages
The slide-27 definitions, in standard notation:
- the empty word is the sequence of length ;
- = set of all words over , and recursively:
- if and , then
- no further words are in (i.e. is the smallest such set — the same “smallest set closed under …” pattern that returns for and for below)
- concatenation joins two words: , with
- a language is a subset
Example (slide 28) with :
| word | |
| concatenation | |
| language |
Meta language vs. object language — the distinction that makes the whole chapter work:
- object language: the language being described — here the lexeme classes of the source language, over the character alphabet.
- meta language: the language used to describe it — here regular expressions, over an alphabet of characters plus the operator symbols
( ) | * ε.
Which is exactly why lexer tools need escaping/quoting (\., "if", [|]): one symbol can be both object and meta symbol. See practical-lexers §3.
Note the teaser on slide 28: — the meta language slot is deliberately left as ”???” and the language is not regular (a finite automaton cannot count). That is the boundary of this chapter, and the reason for the syntactic analysis chapter.
2. Regular expressions
2.1 Syntax (slide 30)
For a given alphabet , the regular expressions are defined recursively:
- is a regular expression.
- every is a regular expression.
- if and are regular expressions, then is a regular expression.
- if and are regular expressions, then — usually written — is a regular expression.
- if is a regular expression, then is a regular expression.
Round brackets clarify grouping. Otherwise the precedence is
so means and not . This is the one thing that silently produces wrong hand-built NFAs in the exam.
2.2 Semantics, definition A: the language
The recursive set definition (slide 31):
Equivalently with and .
2.3 Semantics, definition B: the derivation relation
The lecture also gives the semantics operationally, as a relation between regular expressions extended by words. Full rule set (slide 32):
| # | Rule | Reading |
|---|---|---|
| 1 | blank on the slide — the empty regex derives the empty word | |
| 2 | pick the left branch | |
| 3 | pick the right branch | |
| 4 | if and | congruence of concatenation |
| 5 | blank on the slide — a star may derive nothing | |
| 6 | unfold the star once | |
| 7 | reflexivity | |
| 8 | if and | transitivity |
On the two blanks. In the PDF, the right-hand sides of rules 1 and 5 are visibly empty (everything else on those two lines renders fine, so it is a deliberate in-lecture fill-in, not a font bug). Both are .
Semantics: “continue deriving until only symbols from the alphabet of the object language remain” — and
Worked derivation for slide-35 exercise 1(a), , word :
The final step legitimately applies rule 7 on the atomic symbols (, ): “only symbols remain”, so nothing further happens.
Why two definitions at all? Definition A is the one you compute with (it is a set recursion, so it feeds directly into the automata constructions); definition B is the one that makes “acceptance of a word” a derivation — which is precisely what the NFA run of §3.2 formalizes. Both yield the same language; that they coincide is the informal statement of Kleene’s theorem (regex ⇔ finite automaton), which is what §4 and §5 prove constructively.
2.4 Abbreviations (slide 33)
The lecture’s abbreviation layer, in LaTeX:
| Abbreviation | Meaning | Note |
|---|---|---|
| character class, | ||
| range, requires a known order on | ||
| optional | ||
| one or more | ||
| for ; = exactly , = at least | ||
| any symbol | tool notation (flex, Alex, …), not in the lecture’s core syntax | |
| the empty language | not in the lecture’s syntax either — see the note below |
Detail worth knowing. The lecture’s syntax has no constructor, only the atoms and . The standard textbook syntax adds because it is the neutral element of (, see §2.6) and the zero of concatenation (). In the exam, use it freely in the algebra part, but never inside an automaton construction — there is no slide-40 template for it.
Lexeme-class examples (slide 33):
- identifiers from lower case letters:
- decimal whole numbers: — note there is no leading-zero blob, so
007is not a decimal number in this class; contrast slide 35 exercise 4 where leading zeros are allowed - hexadecimal numbers, in tool notation:
0[xX][0-9a-fA-F]+ - a C₀ floating-point literal (not on the slides, but the slide-26 example of a “nontrivial” characterisation):
[0-9]+ "." [0-9]* ([eE] [+-]? [0-9]+)?
2.5 What a regular expression cannot do
Slide 28 already named the boundary: needs unbounded counting, i.e. memory, i.e. a stack — not a finite automaton. Consequences you meet in real lexers:
- nested comments (Haskell
{- {- -} -}, Rust/* /* */ */) are not regular; C-style/* … */without nesting is regular,/\*([^*]|\*+[^*/])*\*+/style - balanced brackets, indentation-sensitive layout (Python), string interpolation with nesting — all beyond regex
- that is exactly where the lexer stops and the parser starts; the practical workarounds live in practical-lexers §5
2.6 Algebraic laws (the answer to slide-35 exercise 3)
The question: which of , concatenation, , , are associative, commutative, idempotent, have a neutral element, satisfy a distributivity law?
| Operation | Associative | Commutative | Idempotent | Neutral element | Distributive |
|---|---|---|---|---|---|
| yes: | yes | yes: | — | ||
| (concatenation) | yes | no: | no | : | yes over , both sides |
| vacuous (unary) | vacuous | yes: | — | see below | |
| vacuous (unary) | vacuous | yes: | — | — | |
| vacuous (unary) | vacuous | yes: | — | — |
Distributivity, written out:
Careful: does not distribute over concatenation in general — .
Further identities that are handy in the exam when you must state “an equivalent, simpler regex”:
and are the two that get marked wrong most often.
2.7 Slide-35 exercises, worked
1. . (a) derivation in §2.3. (b) — six words, i.e. .
2. Unfold the abbreviations in . With :
so
( is not an abbreviation — it is core syntax — so it stays.)
3. table in §2.6.
4. number words from , leading zeros allowed:
| regex | |
|---|---|
| (a) value | |
| (b) value | |
| (c) value |
(b) case analysis: one digit; ending in a digit ; ending in with ; ending in but with a nonzero digit somewhere before. (c): more than two significant digits, or two significant digits , or .
3. Finite automata
3.1 The three illustrative NFAs (slide 37)
Left (nondeterministic because state has both a - and an -edge, and is reachable by two different routes):
stateDiagram-v2 direction LR [*] --> A A --> B : a B --> B : b B --> C : a C --> B : b C --> D : c D --> [*]
Right — nondeterminism via an -edge and via the race on :
stateDiagram-v2 direction LR [*] --> 1 1 --> 2 : ε 2 --> 2 : b 1 --> 3 : a 2 --> 3 : c 3 --> [*]
3.2 NFAs, formally (slide 38)
An NFA is a 4-tuple :
- — finite set of states
- — distinguished initial state
- — set of final states
- — the transition relation, written
The lecture’s running example — note the arrow into is the initial marker, the double ring is :
stateDiagram-v2 direction LR [*] --> q0 q0 --> q1 : a q0 --> q2 : a q1 --> q3 : b q3 --> q2 : c q2 --> q2 : d q2 --> [*]
Formally, as on the slide:
Nondeterminism has two independent sources, both visible here: two -transitions out of , and in general -transitions (none in this particular example).
3.3 When does an NFA accept a word?
Not spelled out on the slide, but it is what the powerset construction later simulates, so it is worth having exactly:
i.e. there exists at least one accepting path. A word is rejected only if all paths die or end outside . That existential quantifier is precisely what is expensive about NFAs (exponential search) and what the DFA removes.
3.4 DFAs, formally (slide 43)
A DFA is a 4-tuple — same first three components, but the transition component is a (possibly partial) function
(lecture notation , “partial function”). Intuitively: an NFA without -transitions in which “never and for ” holds — at most one successor per symbol.
Acceptance degenerates to a unique run: iff the (unique) path labelled exists and ends in .
DFAs are obviously a special case of NFAs. The converse is the content of §5: for every NFA there is an equivalent DFA. Together with §4 that closes the loop
with no loss of expressive power at any step (Kleene).
Notation drift warning. The lecture uses and (DFA) / (NFA). Mogensen’s book and most literature write . In the exam use the lecture’s letters; mentally translate when reading the book.
4. Regular expression → NFA (Thompson construction)
The lecture calls it only “constructing an NFA from a regular expression”; in the literature it is the McNaughton–Yamada–Thompson construction (Thompson, CACM 1968).
4.1 The invariant: fragments with a single entry and a single exit
The construction produces, for each subexpression , an NFA fragment with
- some states and transitions,
- an ingoing, unlabelled transition (the entry hook),
- an outgoing transition labelled with an element of (the exit hook).
That interface is exactly what makes the construction compositional: fragments are glued only through their hooks, never touching their internals. The classical statement of the invariant:
exactly one start state with no incoming arc, exactly one final state with no outgoing arc, and at most states.
(The lecture’s presentation hides the final state until §4.4: “ultimately, completion to a whole NFA: the ingoing unlabelled transition marks the initial state; the outgoing labelled transition gets connected with a newly added final state.”)
4.2 Base cases
The slide draws each base case as two hook-arrows around a label — nothing more:
──> ε ──> ──> a ──>Read literally: a fragment for an atom is one transition, labelled or , whose source is the entry hook (the ingoing unlabelled transition) and whose target is the exit hook (the outgoing labelled transition). So the smallest fragment has exactly two states. Concatenation then glues exit to entry; and add states around existing fragments — that is the whole algorithm.
flowchart LR subgraph eps["ε-fragment"] direction LR i1(( )) -->|ε| e1(( )) e1 -.->|exit| o1(( )) end subgraph aa["a-fragment"] direction LR i2(( )) -->|a| a1(( )) a1 -.->|exit| o2(( )) end
4.3 Recursive cases — the three gluing templates (slide 40)
flowchart LR i(( )) -->|ε| r[r] i -->|ε| s[s] r --> o(( )) s --> o
flowchart LR i(( )) --> r[r] r --> s[s] s --> o(( ))
flowchart LR i(( )) -->|ε| r[r] i -->|ε| o(( )) r -->|ε| i
Read them as state-machine surgery:
| Case | New states | New -edges |
|---|---|---|
| 2 (a split and a join) | split → , split → , → join, → join | |
| 0 | becomes the connection (exit hook = entry hook) | |
| 1 (a split, which is also the join) | split → , split → exit, → split |
is the case that needs a loop back: the star is the only unbounded construct, hence the only one producing a cycle. This is also why every star adds exactly one -cycle, and why an NFA can loop forever on -moves if a construction is wrong (a good self-check).
4.4 Worked example: (slide 42)
The slides skip the intermediate steps and show the finished 8-state NFA (“outcome of the construction concretely”). Reconstructed exactly:
stateDiagram-v2 direction LR [*] --> 1 1 --> 5 : ε 1 --> 2 : ε 5 --> 6 : ε 5 --> 7 : ε 6 --> 8 : a 7 --> 8 : b 8 --> 1 : ε 2 --> 3 : a 3 --> 4 : c 4 --> [*]
Machine-readable form (this is the object the exam expects you to write down):
How each piece maps back to the syntax tree — this mapping is the thing to be able to recite:
| States | Fragment | Rule used |
|---|---|---|
| 5, 6, 7, 8 | alternative: split 5, join 8, branching to the -edge 6→8 and the -edge 7→8 | |
| 1, 2, 5–8 | star: split 1 with to the body-entry 5 and to the continuation 2; body-exit 8 recurses with back to 1 | |
| 2, 3 | (concatenation) | exit hook of the star is the entry of the -fragment |
| 3, 4 | concatenation again; 4 is the newly added final state |
Why 8 states and not 11? Textbook Thompson on gives more states (each atom two states, each operator two more). The lecture’s variant is the standard optimisation the slide alludes to: “potentially discussion of optimizations, in particular for some of the abbreviations” —
- the branch node of is merged with the star’s body entry (5), and the join is merged with the star’s body exit (8), so costs 4 states instead of 6;
- the star costs 1 new state (1) instead of 2, because the split and the join of the star are the same state — legal because both the entry- and the exit- leave from it;
- the final state 4 is created once, at the very end, as §4.1 prescribes.
You may always answer the exam with either the literal textbook construction or this optimised one, as long as the language is right and you can name the fragments.
5. NFA → DFA: ε-closure and the powerset construction
The plan: simulate all NFA paths in parallel. A DFA state is a set of NFA states. Two technicalities make it work — ignoring -transitions, and building only the reachable sets.
5.1 -closure (slide 44)
For :
computable as the smallest fixpoint of
Equivalently, the more usable form: = all states reachable from by a path of -moves (including the empty path, hence ; and — an idempotent, which is why the definition can say “smallest set with a) and b)” instead of “iterate until closed”).
Worklist implementation (what you actually code):
closure(M):
X = M; work = M
while work not empty:
s = pop(work)
for t in eps_successors(s):
if t not in X: X.add(t); push(work, t)
return X
Cost: per call, not “loop times until nothing changes” — same result, better constant.
The distributivity fact behind the identity we need later. above is distributive with respect to , because whenever . Hence
This is exactly the step handwritten on slide 48, and it is what lets you precompute once per state instead of once per subset. Spot-check on the example of §4.4:
which is precisely the slide-47 label.
5.2 Background: fixpoint iteration (slide 45)
Let be a finite set and monotone with respect to . Then:
- there is a smallest pre-fixpoint of , i.e. the (unique) smallest set with ;
- it is even a (smallest) fixpoint: for this , holds;
- it can be computed by evaluating until no changes occur;
- if is moreover distributive with respect to , then additionally
Why it holds (the intuition the slide leaves implicit): is a finite lattice, so the ascending chain
cannot grow more than times; monotonicity guarantees the chain is ascending, finiteness guarantees termination, and the last element is simultaneously the least pre-fixpoint and the least fixpoint. Point 4 is the “only look at what is new this round” optimisation — precisely the worklist/new states only trick used by both and the powerset construction.
The same pattern shows up three times in this chapter — worth noticing once: as the smallest set closed under appending a symbol, as the smallest set closed under -steps, and as the smallest set of reachable DFA states.
5.3 The construction (slide 46)
Given an NFA , define the DFA :
Three things to be able to justify:
- is closed under — otherwise the DFA would “forget” states the NFA could already be in before consuming anything.
- takes a second closure — after an -step, the NFA may move on with any number of -steps before the next real symbol.
- uses , not — reaching any final state in the simulated set counts as accepting, because NFA acceptance is existential (§3.3).
- is only defined when the move set is non-empty: the result is a partial DFA, exactly the of §3.4. Some textbooks add a sink state to totalise it; the lecture does not, and that choice is what makes the minimization example below interesting.
5.4 Worked example on (slide 47)
Order of discovery: start closure, then BFS over symbols .
| DFA state | NFA subset | on | on | on |
|---|---|---|---|---|
| A | B | C | – | |
| B | B | C | D | |
| C | B | C | – | |
| D | – | – | – |
Spot-checks that the table is right:
- : from the only -edges are and , giving , whose closure is B. ✔ (this is why A on jumps to B, not to C — the -edge out of 2 is easy to miss)
- : only , closure = D. ✔
- since . ✔
Rendered (DOT rather than Mermaid: the labels are sets and B/C need a self-loop each):
stateDiagram-v2 direction LR [*] --> A state "A = {1,2,5,6,7}" as A state "B = {1,2,3,5,6,7,8}" as B state "C = {1,2,5,6,7,8}" as C state "D = {4}" as D A --> B : a A --> C : b B --> B : a B --> C : b B --> D : c C --> B : a C --> C : b D --> [*]
The language check by eye: is “the last symbol was ”, is “…followed by ”, A/C are “no pending”.
The fixpoint trace (slide 48). The slide’s handwriting computes as the fixpoint iteration of :
so — three rounds, and it is -safe because the chain is monotone and is finite. The same slide writes and the concrete step function , then notes the distributivity used in §5.1.
5.5 Cost of the construction
Worst case: DFA states, and it is attained (classic family: needs states). For real lexers this is a non-issue:
- lexeme classes are “shallow” — no nested structure, so subsets stay small;
- the construction only visits reachable subsets (the fixpoint, not );
- if a lexer spec ever blows up, that is a diagnostic: the language is being over-specified (e.g. accidental backtracking-like patterns).
Also: the resulting DFA is correct by construction but not necessarily small — hence §6.
6. DFA minimization
6.1 When are two states the same?
Two states of a DFA are equivalent () iff for all words :
with the extended transition function (, , undefined if the partial function gets stuck). Equivalence is the Nerode congruence; the minimal DFA is unique up to isomorphism.
The practical formulation is contrapositive and is what the table-filling algorithm uses: iff and are distinguishable, i.e. there is a word such that exactly one of , is in .
6.2 Algorithm: partition refinement (“Moore”)
- Initial partition — final states vs. non-final states. (If the DFA is partial, add one dead state and complete it; is non-final.)
- Refine: split a block whenever two of its states go to different blocks for some symbol .
- Repeat until no block splits. Blocks = states of the minimal DFA; = blocks containing a final state.
Termination: each round only splits, the block count is bounded by . Complexity of this naive refinement: ; Hopcroft’s algorithm (1971) gets with a worklist of splitters.
The lecture introduces minimization on slide 46 only as “discussion of minimization” and then shows the result (slide 49) rather than the algorithm — so the algorithm is the main gap to fill from literature.
6.3 Worked minimization of the slide-47 DFA
Start from the 4-state DFA of §5.4, completed with :
| A | B | C | |
| B | B | C | D |
| C | B | C | |
| D | |||
Round 0: (final vs. non-final).
Round 1: look at the block and symbol :
| state | lands in block | |
|---|---|---|
| A | non-final | |
| B | D | final |
| C | non-final | |
| non-final |
→ separates: .
Round 2: block on symbol : A→B, C→B (block ) but → (still this block) → separates: .
Round 3: : →B/B ✔ same block, →C/C ✔ same, →/ ✔ same. No more splits. Stable.
Result: A ≡ C, and nothing else. This is the interesting part of the example — and look different (C = “just finished an -or- iteration”, its subset contains state 8) but they are behaviourally identical: from both, only and lead anywhere, cannot follow. The distinction is an artefact of the subset construction, which is exactly what minimization removes.
The slide’s handwritten solution writes the same thing, with its classes named , first and then , — i.e. the two refinement rounds above, and a table of the split criterion “on : vs. none”.
stateDiagram-v2 direction LR [*] --> AC state "{A,C}" as AC state "{B}" as B state "{D}" as DD AC --> B : a AC --> AC : b B --> B : a B --> AC : b B --> DD : c DD --> [*]
Partial vs. complete — the honest bookkeeping. If you keep the dead state (needed for the partition refinement to be well defined), the minimal complete DFA has 4 states: . If you drop again — legal, because a partial DFA may omit transitions — you get the 3-state partial automaton above, which is what the slide draws, and its transition table needs no -column:
| – | |||
| – | – | – |
Sanity check against the intuition for : the minimal DFA is exactly “no pending” / “last symbol was ” / “done”, which is obviously right and obviously irreducible.
6.4 Why minimize at all
- the transition table is the hot data structure of the scanner: smaller table = better cache behaviour;
- dead/duplicate states are the most common artefact of the subset construction (§5), so removing them is cheap correctness hygiene;
minimalgives a canonical form — two regexes with the same language yield the same minimal DFA, so minimization doubles as an equivalence test (used by tools that warn about duplicate/overlapping lexer rules).
7. Exam checklist
Constructive tasks, in the order they are likely to be asked:
- Regex → NFA. Draw the syntax tree first, attach the fragment table of §4.3 to each node, then merge. Check: single start state, single final state, number of states, and that only creates cycles.
- NFA → DFA. Write / / / as sets (not as a drawing) — the arithmetic is much harder to get wrong that way. Then BFS: of the start set, then for each new state and each symbol,
movethenclose. Fill the table before drawing. - Minimize. Complete with , round 0 = final vs. non-final, refine by symbol until stable, then drop if the exercise wants a partial DFA.
- Justify (short-answer): why needs a fixpoint (idempotence/least-closure); why uses (existential acceptance); why the powerset construction can blow up (); why -transitions are harmless (they can always be eliminated by the construction).
Self-check questions that are cheap to grade yourself on:
- Does your NFA accept ? (Only if the start closure hits a final state — check the closure again.)
- Did you apply the precedence correctly in ?
- Is every DFA state a closed subset? An unclosed subset in an exam answer is the classic deduction.
- Did you forget that of a set includes the set itself?
8. Sources
| Source | Used for |
|---|---|
slides 19–51 (raw/compilerbau/compilerbau-slides-55.md, version 28.10.2026) | all formal definitions, notation, the examples redrawn above |
| Torben Ægidius Mogensen, Introduction to Compiler Design, Springer 2017 — ch. 2 (lexical analysis) | the book the lecture aligns with; notation |
| Ken Thompson, “Programming Techniques: Regular expression search algorithm”, CACM 11(6), 1968 | the regex→NFA construction and its single-entry/single-final invariant, states |
| Hopcroft, “An algorithm for minimizing states in a finite automaton”, 1971 (PDF) | partition-refinement minimization and its complexity |
| DFA minimization (Wikipedia) | Moore vs. Hopcroft, table-filling formulation, Nerode congruence |
Related: practical-lexers (what a real lexer does with all this), Compiler Construction course page, concept map.