Lexical Analysis — Tokens, Regex, NFA→DFA, Flex
896 words
4 min read
Visual companion
Python
Type and operator map
Python Week 1: the first filter for runtime behavior
View
Revision summary
What this note is really saying
Short form
# Lexical Analysis — Tokens, Regex, NFA→DFA, Flex ## 🎯 Learning Objectives - Define tokens, lexemes, and patterns - Construct NFA from regular expressions (Thompson's construction) - Convert NFA to DFA (subset construction) - Write Flex lex specifications * * * ## 1. Introduction to Lexical Analysis ### 1.1 Intuiti...

Lexical Analysis — Tokens, Regex, NFA→DFA, Flex
🎯 Learning Objectives
- Define tokens, lexemes, and patterns
- Construct NFA from regular expressions (Thompson's construction)
- Convert NFA to DFA (subset construction)
- Write Flex lex specifications
1. Introduction to Lexical Analysis
1.1 Intuition
The lexer (scanner) is the first phase of a compiler. It reads the source character by character and groups them into tokens — meaningful units like keywords, identifiers, operators, and literals. Think of it as splitting an English sentence into words and punctuation.
1.2 Lexer Role in Compiler
(Diagram)
1.3 Token Types
| Token Type | Examples | Pattern |
|---|---|---|
| Keyword | int, if, while, return | Fixed string |
| Identifier | count, sum, x1 | [a-zA-Z_][a-zA-Z0-9_]* |
| Number | 42, 3.14, 0xFF | [0-9]+(\.[0-9]+)? |
| Operator | +, -, *, = | \+, -, =, etc. |
| Separator | ;, (, ), {, } | Fixed character |
| String | "hello", 'c' | "[^"]*" |
2. Regular Expressions to NFA
2.1 Thompson's Construction
Every regular expression can be transformed into an equivalent NFA:
(Diagram)
Example: Regex
a(b|c)* → NFA
(Diagram)3. NFA to DFA (Subset Construction)
3.1 Algorithm
pseudo1. Start state of DFA = ε-closure(start state of NFA) 2. For each DFA state (set of NFA states): a. For each input symbol a: - Let next = ε-closure(move(state_set, a)) - If next is non-empty and new → add to DFA states - Add transition state_set --a--> next 3. Accepting DFA states = any set containing an NFA accept state
3.2 Worked Example
NFA for regex
a(b|c)*:| State | a | b | c | ε |
|---|---|---|---|---|
| 0 | {1} | — | — | — |
| 1 | — | — | — | {2,7} |
| 2 | — | — | — | {3} |
| 3 | — | {4} | {5} | — |
| 4 | — | — | — | {6} |
| 5 | — | — | — | {6} |
| 6 | — | — | — | {2,7} |
| 7 | — | — | — | — |
Subset Construction:
| DFA State | NFA States | a-transition | b-transition | c-transition |
|---|---|---|---|---|
| A (start) | ε-closure(0) = {0} | {0,1,2,3,7} = B | — | — |
| B | ε-closure({1}) = {1,2,3,7} | — | ε-closure({4}) = {2,3,4,6,7} = C | ε-closure({5}) = {2,3,5,6,7} = D |
| C | ε-closure({4}) = {2,3,4,6,7} | — | ε-closure({4}) = C | ε-closure({5}) = D |
| D | ε-closure({5}) = {2,3,5,6,7} | — | ε-closure({4}) = C | ε-closure({5}) = D |
Final DFA:
(Diagram)
3.3 DFA Minimization
- Partition into accept and non-accept states
- Iteratively split groups that have different transitions for the same input
- Continue until no changes
4. Flex (Scanner Generator)
4.1 Flex Specification Structure
flex%{ /* C declarations — included in generated scanner */ #include "tokens.h" int line_count = 1; %} /* Regular expression definitions */ DIGIT [0-9] ID [a-zA-Z_][a-zA-Z0-9_]* NUMBER {DIGIT}+ WS [ \t\n]+ %% /* Rules: pattern → action */ {WS} { /* skip whitespace */ } "int" { return INT; } "if" { return IF; } "while" { return WHILE; } {ID} { yylval.id = strdup(yytext); return ID; } {NUMBER} { yylval.num = atoi(yytext); return NUM; } "+" { return PLUS; } "-" { return MINUS; } "=" { return ASSIGN; } ";" { return SEMI; } . { printf("Unknown token: %s (line %d)\n", yytext, line_count); } %% /* C code — user functions */ int main() { yylex(); // Run the scanner return 0; }
4.2 Generated Scanner
(Diagram)
5. Common Pitfalls
Pitfall 1: Regex too greedy (overlapping patterns)
Problem:
if matches both keyword if and identifier pattern.
Fix: Order rules so keywords appear before identifier pattern. Flex matches the longest token, and ties go to the first rule.Pitfall 2: Forgetting to handle whitespace and comments
Mistake: Scanner returns whitespace as tokens, cluttering the parser.
Fix: Match
[ \t\n]+ and //.* with empty action (no return).Pitfall 3: ε-closure includes the state itself
Mistake: Forgetting that the ε-closure of a state always includes that state.
Fix: Always add the starting state to its own closure before following ε-transitions.
6. 📐 Key Formulas / Concepts
| Concept | Description |
|---|---|
| Token | (token_type, attribute) pair |
| Pattern | Regular expression describing token structure |
| ε-closure | Set of states reachable without consuming input |
| Subset construction | NFA→DFA by tracking sets of NFA states |
| DFA minimization | Merge indistinguishable states |
7. 📝 Practice Questions
Q1: Write a regular expression for identifiers in C (letters, digits, underscores, must start with letter or underscore).Answer:[a-zA-Z_][a-zA-Z0-9_]*Q2: Build an NFA for regex(a|b)*abbusing Thompson's construction.Answer: The NFA has: (1) NFA fora|b(union of a and b), (2) Kleene star of that, (3) concatenation witha, thenb, thenb. The resulting NFA has 11 states (starting from ε-NFA for star and concatenation). Q3: What is the purpose of the ε-closure in subset construction?Answer: The ε-closure accounts for all states reachable via ε-transitions without consuming input. Since NFA can take ε-transitions "for free," the DFA state must track all possible NFA states reachable before processing the next input symbol. Q4: Why does Flex use the "longest match" rule?Answer: To correctly handle tokens where one is a prefix of another. For example,<=should be a single token (LESS_EQ), not<followed by=. Longest match ensures multi-character operators are recognized correctly. Q5: Convert the DFA from the worked example above into a minimal DFA.Answer: Start with partitions: Accepting = {B, C, D} (all contain NFA accepting state 7) vs Non-accepting = {A}. For {B,C,D}: B goes to C on b, D on c; C goes to C on b, D on c; D goes to C on b, D on c. Since B, C, D have identical transition patterns, they can be merged into one state. Final DFA: A--a-->BCD, BCD--b-->BCD, BCD--c-->BCD.
8. 🔗 Cross-References
- Week 3 - Syntax Analysis: Token stream feeds into parser
- BSCS3021 (ToC): Regular languages, NFA, DFA theory
- Week 2 - Compiler Overview: Where lexer fits in compiler phases Join Discord PreviousCompiler OverviewNextSyntax Analysis I