Compiler Design and ConstructionUnit 211 min read
Lexical Analysis: Scanners, Regular Languages, DFAs, NFAs & Tokenization
Unit 2 of Compiler Design and Construction covers the lexical analyzer’s role in converting source code into tokens, its input buffering schemes, regular expressions, finite automata (DFA/NFA), and token recognition. This note explains how lexical analyzers work, their efficiency, and real-world applications in compile
TAKEAWAYS:
- Lexical analysis scans source code to identify tokens (keywords, identifiers, operators) using regular expressions and finite automata.
- Input buffering (e.g., buffer-pair scheme) improves efficiency by minimizing disk I/O and handling lookahead.
- DFAs and NFAs model lexical patterns: DFAs are deterministic and efficient, while NFAs allow nondeterminism and are easier to construct from regex.
- Token recognition involves matching input streams to predefined patterns (e.g.,
if,while,+) and rejecting invalid sequences. - Real-world compilers (e.g., GCC,
javac) use lexical analyzers to preprocess source code before parsing. - Lexical errors (e.g., misspelled keywords, invalid characters) are caught early to improve debugging.
1. Role of the Lexical Analyzer in Compiler Design
The lexical analyzer (or scanner) is the first phase of a compiler. Its primary tasks are:
- Tokenization: Breaking source code into meaningful units called tokens (e.g., keywords, identifiers, literals, operators).
- Lexical error detection: Identifying invalid characters or sequences (e.g.,
@in a variable name). - Removing whitespace/comments: Ignoring irrelevant characters (spaces, tabs,
//comments). - Efficiency: Minimizing I/O operations by buffering input.
Why is Lexical Analysis Important?
- Preprocessing: Simplifies parsing by converting raw text into structured tokens.
- Error handling: Catches syntax errors early (e.g.,
x@yis invalid in most languages). - Optimization: Reduces the workload for later phases (e.g., syntax analysis).
2. Input Buffering: How Lexical Analyzers Read Input Efficiently
Lexical analyzers read source code character by character, but disk I/O is slow. To optimize, they use buffering techniques:
Buffer-Pair Scheme with Sentinels
A common method is the two-buffer scheme:
- Buffer A holds the current chunk of input (e.g., 100 characters).
- Buffer B is loaded in advance while Buffer A is being processed.
- A sentinel (e.g.,
EOFor a special character) marks the end of valid data. - When Buffer A is exhausted, the analyzer swaps buffers and continues.
Why is this efficient?
- Reduces disk reads: Only two buffers are in memory at once.
- Overlapping I/O and processing: While one buffer is being read, the other is being scanned.
- Handles large files: Avoids loading the entire file into memory.
flowchart LR
A["Source File"] --> B["Buffer A (Current)"]
A --> C["Buffer B (Next)"]
B --> D["Scanner Processes Buffer A"]
C --> E["Loader Fills Buffer B"]
D --> F["Buffer A Empty?\nIf Yes, Swap A & B"]
F -->|"No"| D
F -->|"Yes"| G["Buffer B becomes A,\nNew Buffer B loaded"]
G --> D3. Regular Expressions and Finite Automata
Lexical analyzers recognize tokens using regular expressions (regex) and finite automata (DFA/NFA).
Regular Expressions (Regex)
A regex defines a pattern for tokens. Example:
if→ Matches the keywordif.[a-zA-Z][a-zA-Z0-9]*→ Matches identifiers (e.g.,x,totalPrice).[0-9]+→ Matches integers (e.g.,123).
Finite Automata
- DFA (Deterministic Finite Automaton): Each input symbol leads to one next state. Efficient but harder to construct.
- NFA (Nondeterministic Finite Automaton): Each input symbol can lead to multiple next states. Easier to build from regex but requires conversion to DFA for execution.
Example: Regex to DFA Conversion
Convert the regex a(a+b)(b+c)*a# to a DFA.
stateDiagram-v2
[*] --> q0
q0 --> q1 : a
q1 --> q2 : a
q1 --> q3 : b
q2 --> q4 : b
q2 --> q5 : c
q3 --> q4 : b
q3 --> q5 : c
q4 --> q4 : b
q4 --> q5 : c
q5 --> q5 : b
q5 --> q5 : c
q5 --> q6 : a
q6 --> [*] : #Steps to Build the DFA:
- Construct NFA from the regex using Thompson’s construction.
- Convert NFA to DFA using the subset construction method.
- Minimize the DFA (optional but improves efficiency).
4. How Lexical Analyzers Recognize Tokens
Lexical analyzers use finite automata to match input against token patterns. Here’s how it works:
Example: Token Recognition for if and while
Suppose the input is:
if (x > 5) while (y < 10)
The lexical analyzer processes it as:
if→ Matches the keywordif(token:IF).(→ Matches the operator((token:LPAREN).x→ Matches the identifierx(token:ID(x)).>→ Matches the operator>(token:GT).5→ Matches the integer5(token:INT(5)).)→ Matches the operator)(token:RPAREN).while→ Matches the keywordwhile(token:WHILE).- And so on...
flowchart LR
A["Source Code:\nif (x > 5)"] --> B["Lexical Analyzer"]
B --> C["Token 1: IF"]
B --> D["Token 2: LPAREN"]
B --> E["Token 3: ID(x)"]
B --> F["Token 4: GT"]
B --> G["Token 5: INT(5)"]
B --> H["Token 6: RPAREN"]5. NFA vs. DFA: A Comparison
| Feature | NFA (Nondeterministic) | DFA (Deterministic) |
|---|---|---|
| Definition | Multiple transitions per input | Exactly one transition per input |
| Construction | Easier from regex (Thompson’s) | Requires subset construction |
| Execution | Slower (backtracking) | Faster (no backtracking) |
| Memory | More states (due to ε-transitions) | Fewer states (minimized) |
| Use Case | Intermediate step (regex → NFA → DFA) | Final implementation in scanners |
6. Worked Example: Converting NFA to DFA
Given NFA:
stateDiagram-v2
[*] --> q0
q0 --> q1 : a
q1 --> q2 : b
q1 --> q3 : b
q2 --> q4 : a
q3 --> q4 : a
q4 --> [*] : #Steps to Convert to DFA:
- Initial State:
{q0}(start with the NFA’s initial state). - Process Input
a:- From
q0,aleads toq1→ New DFA state:{q1}.
- From
- Process Input
b:- From
q1,bleads to{q2, q3}→ New DFA state:{q2, q3}.
- From
- Process Input
a:- From
q2,aleads toq4. - From
q3,aleads toq4. - New DFA state:
{q4}.
- From
- Accepting State:
{q4}(sinceq4has a transition toEOF).
Final DFA:
stateDiagram-v2
[*] --> S0
S0 --> S1 : a
S1 --> S2 : b
S2 --> S3 : a
S3 --> [*] : #7. Real-World Applications of Lexical Analysis
Lexical analyzers are used in every compiler and interpreter, including:
1. eSewa (Nepal) – Token Validation in API Requests
- How it uses lexical analysis:
- When you submit a payment request, eSewa’s backend validates the input token-by-token.
- Example: The request
pay?amount=1000&user=abc123is split into tokens:pay(keyword),amount=1000(identifier + integer),user=abc123(identifier + string).
- The lexical analyzer checks for invalid characters (e.g.,
@inuser=abc@123) before processing.
2. GCC (GNU Compiler Collection) – C/C++ Lexical Scanning
- How it uses lexical analysis:
- GCC’s
cpp(C preprocessor) andgccfront-end use a lexical analyzer to:- Identify
#include,ifdef,return, etc. - Remove comments (
//,/* */). - Convert
main()into tokens for parsing.
- Identify
- Example: The line
int x = 5 + y;is tokenized as:int,ID(x),=,INT(5),+,ID(y),;.
- GCC’s
3. WhatsApp (Global) – Message Tokenization for Spam Detection
- How it uses lexical analysis:
- WhatsApp’s spam filter uses regex-based lexical analysis to:
- Detect phishing keywords (e.g.,
click here,urgent). - Flag invalid phone numbers (e.g.,
+977-123@456).
- Detect phishing keywords (e.g.,
- Example: The message
"Your account is locked! Click here to verify: http://fake.com"is scanned for:account,locked,click,verify,http://(tokens marked as suspicious).
- WhatsApp’s spam filter uses regex-based lexical analysis to:
8. Worked Example: Lexical Analysis in a Bank Loan Calculator
Scenario: A bank’s loan processing system reads input like:
loan_amount = 500000
interest_rate = 8.5%
term_years = 5
Lexical Analysis Steps:
- Tokenize the input:
loan_amount,=,500000,;interest_rate,=,8.5%,;term_years,=,5,;
- Validate tokens:
- Check if
loan_amountis a valid identifier. - Ensure
500000is a valid integer. - Reject
8.5%if the system expects only numbers (unless%is allowed).
- Check if
- Pass tokens to the parser:
- The parser then computes EMI using the formula: where , , .
Output:
Processing loan request...
Valid tokens detected.
Calculating EMI...
Monthly EMI: Rs. 9,980.55
9. Common Lexical Errors and How to Detect Them
| Error Type | Example | Detection Method |
|---|---|---|
| Invalid character | x@y |
Check if @ is allowed in identifiers. |
| Misspelled keyword | swich (instead of switch) |
Compare against reserved keywords. |
| Unterminated string | "hello (missing ") |
Track string delimiters in the automaton. |
| Number format error | 12.3.4 (invalid float) |
Regex: [0-9]+\.[0-9]+ (exactly one .). |
10. Exam Tip: How to Score Full Marks
For DFA/NFA questions:
- Always show all states and transitions.
- Label start and accept states clearly.
- If converting regex to DFA, explain each step (e.g., "From state X, input Y leads to state Z").
For buffer-pair scheme:
- Draw a diagram showing two buffers swapping.
- Mention sentinels and why they are used.
For token recognition:
- Give a real code example (e.g.,
int x = 5;→ tokens). - Explain how keywords vs. identifiers are distinguished.
- Give a real code example (e.g.,
For regex to automata:
- If asked to convert
(a+b)*a, first draw the NFA, then convert to DFA. - Minimize the DFA if possible (show steps).
- If asked to convert
Real-world applications:
- Link to eSewa, GCC, or WhatsApp in your answer.
- Example: "Like eSewa validates API tokens, a lexical analyzer checks for invalid characters in user input."
11. Summary Table: Key Concepts
| Concept | Description | Example |
|---|---|---|
| Lexical Analyzer | Converts source code → tokens. | if(x>5) → IF, ID(x), GT, INT(5) |
| Buffer-Pair Scheme | Two buffers for efficient I/O. | Swapping buffers in GCC. |
| Regex | Defines token patterns. | [a-z]+ → matches hello. |
| NFA | Nondeterministic automaton (easier to build from regex). | NFA for a*b. |
| DFA | Deterministic automaton (used in scanners). | DFA for (a+b)*. |
| Token | Meaningful unit (keyword, identifier, operator). | +, while, 42. |
12. Practice Questions (Exam-Style)
- Design a DFA for the regex
a*b*. - Explain how WhatsApp uses lexical analysis to detect spam messages.
- Convert the following NFA to DFA:
stateDiagram-v2 [*] --> q0 q0 --> q1 : a q1 --> q2 : b q1 --> q3 : b q2 --> q4 : a q3 --> q4 : a q4 --> [*] : # - What is the role of sentinels in the buffer-pair scheme? Give an example.
- Tokenize the following C code:
for (int i = 0; i < 10; i++) printf("Hello");
Based on the TU BSc CSIT syllabus for Compiler Design and Construction (CSC365), unit 2.
Discussion
Loading…