Lexical and syntax analysis: lexer, recursive descent, LL and LR
The lexer producing lexemes/tokens, state diagrams, tracing a recursive-descent parser, removing left recursion, the pairwise disjointness test and the idea of bottom-up (shift-reduce) parsing.
The first two phases of a compiler are lexical analysis and syntax analysis (parsing). The question that comes up most on the midterm: tracing the parse of an expression step by step with given recursive-descent functions. Also, removing left recursion and the pairwise disjointness test are short, clear points.
Golden rules:
- The lexer produces tokens, the parser builds the tree.
- In recursive descent every nonterminal is a function.
{ }is a while loop,[ ]is an if. lex()is called once per token and puts the next token innextToken.- Left recursion kills a top-down parser, it must be removed.
- LL = top-down, LR = bottom-up.
Why two separate phases?
| Reason | Explanation |
|---|---|
| Simplicity | The lexer is simple and recognizes regular patterns. It frees the parser from this detail. |
| Efficiency | Most of the compile time is spent on character-by-character reading; the lexer is optimized separately. |
| Portability | Platform-dependent jobs such as reading files and buffers are collected in the lexer. The parser stays portable. |
Lexical analysis
The lexer reads the source code character by character, gathers the characters into lexemes and gives each lexeme a token. It skips whitespace and comments, adds user-defined names to the symbol table, and reports errors (invalid characters).
result = rate * (count + 15);
IDENT(result) ASSIGN_OP(=) IDENT(rate) MULT_OP(*) LEFT_PAREN(()
IDENT(count) ADD_OP(+) INT_LIT(15) RIGHT_PAREN()) SEMICOLON(;)
10 lexemes. The lexer works like a finite automaton; the way to draw it is a state diagram:
letter/digit
┌────┐
letter ▼ │
Start ─────► id ──┘ ── other character ──► return IDENT (or reserved word)
│
│ digit digit
│ ┌────┐
└───────► int ◄┘ ── other character ──► return INT_LIT
Instead of drawing a separate arrow for every letter and digit, character classes are used: LETTER and DIGIT. Reserved words (if, while) do not get a separate state; they are recognized as identifiers and then separated by looking them up in a table.
Parsing: top-down and bottom-up
A parser does two jobs: finding syntax errors and producing the parse tree (or its information).
| Top-down | Bottom-up | |
|---|---|---|
| Tree | From the root to the leaves, preorder | From the leaves to the root |
| Derivation | Leftmost | Rightmost, in reverse |
| Main decision | Which RHS to choose? | Which substring to reduce (handle)? |
| Family | LL (recursive descent, LL table) | LR (shift-reduce) |
LL: Left-to-right scan, Leftmost derivation. LR: Left-to-right scan, Rightmost derivation (in reverse). General parsing algorithms for any unambiguous grammar are ; the LL and LR parsers used in compilers work with a subclass of grammars and are .
Recursive descent
A function is written for each nonterminal. It maps very naturally to EBNF:
/* <expr> -> <term> {(+ | -) <term>} */
void expr() {
term();
while (nextToken == ADD_OP || nextToken == SUB_OP) {
lex();
term();
}
}
/* <term> -> <factor> {(* | /) <factor>} */
void term() {
factor();
while (nextToken == MULT_OP || nextToken == DIV_OP) {
lex();
factor();
}
}
/* <factor> -> id | int_constant | ( <expr> ) */
void factor() {
if (nextToken == IDENT || nextToken == INT_LIT)
lex();
else if (nextToken == LEFT_PAREN) {
lex();
expr();
if (nextToken == RIGHT_PAREN) lex();
else error();
}
else error();
}
Rule: when a function is called, nextToken already holds the first token of that function. The program starts with lex(), then expr() is called.
Trace: (a + b) * c
Enter <expr>
Enter <term>
Enter <factor> next = ( → lex, next = a
Enter <expr>
Enter <term>
Enter <factor> next = a → lex, next = +
Exit <factor>
Exit <term> + is not multiplication, term returns
lex, next = b expr's while saw +
Enter <term>
Enter <factor> next = b → lex, next = )
Exit <factor>
Exit <term>
Exit <expr> ) is not addition
lex, next = * factor was expecting ), found it
Exit <factor>
lex, next = c term's while saw *
Enter <factor> next = c → lex, next = EOF
Exit <factor>
Exit <term>
Exit <expr>
Count: factor entered 4 times, term 3 times, expr 2 times, lex called 7 times. When tracing on an exam, ask on every line “what is nextToken right now?” The loop condition always looks at the current nextToken.
PITFALL · which loop catches it? For
x - y / 2, the-sign is caught by the loop of expr, not term (term looks only for*and/). The/is handled inside the secondterm()call. So the tree becomesx - (y / 2)and precedence comes out right by itself.
The same logic holds for <list> -> id { , id }: with the input id , id ,, after the last comma an id is expected but EOF arrives, an error.
Two problems of LL grammars
1. Left recursion
<expr> -> <expr> + <term> | <term>
expr() calls expr() as its first action, without consuming any token. Infinite recursion. The pattern for removing direct left recursion:
<T> -> <T> * <F> | <T> / <F> | <F>
⇓
<T> -> <F> <T'>
<T'> -> * <F> <T'> | / <F> <T'> | ε
The new grammar produces the same language (id, id * id, id / id * id, …). Only the structure is different.
2. Pairwise disjointness
The parser must know which RHS to choose by looking at the next token. For this, the FIRST sets of the alternatives of a nonterminal must not intersect. FIRST(α) is the set of first terminals of the strings derived from α.
<A> -> a <B> | <B> b FIRST(a<B>) = {a}
<B> -> a <C> | d FIRST(<B>b) = FIRST(<B>) = {a, d}
{a} ∩ {a, d} = {a} → TEST FAILS
The fix is left factoring: take the common prefix out.
<stmt> -> if <c> then <s> | if <c> then <s> else <s>
⇓
<stmt> -> if <c> then <s> <rest>
<rest> -> else <s> | ε
Bottom-up parsing
A bottom-up parser reads the input from the left, pushes tokens onto a stack (shift), and when the sequence on top of the stack matches an RHS, reduces it to the LHS (reduce). The correct substring to reduce is called the handle. The result is the rightmost derivation written in reverse:
id + id * id
<factor> + id * id handle: the first id
<term> + id * id
<expr> + id * id
<expr> + <factor> * id
<expr> + <term> * id here <expr> + <term> is not reduced, * is waiting
<expr> + <term> * <factor>
<expr> + <term>
<expr>
8 reductions. The hard part is choosing the handle correctly: <expr> + <term> is an RHS, but since * comes after it, reducing it now would bind the multiplication wrongly. LR parsers make this decision with a table (ACTION/GOTO); building the table is outside the midterm scope of this course, knowing the idea is enough.
Common mistakes
| Wrong | Correct |
|---|---|
| Counting whitespace as a lexeme | The lexer skips whitespace and comments |
Thinking lex() is called at function entry | Only called when a token is consumed |
Looking for - in term’s loop | + - in expr, * / in term |
| Turning left recursion into right recursion and forgetting ε | A' -> α A' | ε |
FIRST(<B> b) = {b} | The first symbol is <B>, look at its FIRST |
| LR = Left-to-right, Rightmost (plain) | Rightmost derivation in reverse |
| Bottom-up = preorder | Preorder is top-down |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
How many lexemes are in the statement below? Give the token of 15 and of (.
result = rate * (count + 15);Show answer
result, =, rate, *, (, count, +, 15, ), ; → 10 lexemes. Whitespace is not a lexeme; the lexer skips it. Three different names (result, rate, count) get the same token: IDENT.
A lexer recognizes identifiers with the pattern letter (letter | digit)*. Which are identifiers: x1, 1x, abc9, a_b, Z?
Show answer
The first character must be a letter: 1x sees a digit in the start state while expecting a letter, and is rejected. a_b: the underscore is not in the pattern, so the lexer stops after a; a_b is not recognized as a single identifier. Z is a single letter, and (letter | digit)* can be zero repetitions.
Using the recursive-descent functions for <expr> -> <term> {(+|-) <term>}, <term> -> <factor> {(*|/) <factor>}, <factor> -> id | int_constant | ( <expr> ), how many times is factor() entered and how many times is lex() called while parsing (a + b) * c? (The first token is already read before expr() is called.)
Show answer
factor calls: the outer ( ... ), the inner a, the inner b, then c → 4. lex is called once per token (it brings the new nextToken): (, a, +, b, ), *, c are consumed → 7. expr is entered 2 times (outer and inside the parentheses), term 3 times.
Give the first six lines of the Enter/Exit trace when parsing x - y / 2 with the same functions.
Show answer
Enter <expr>
Enter <term>
Enter <factor> next = x (IDENT) → lex, next = -
Exit <factor>
Exit <term> - is not multiplication/division, term returns
expr sees - → lex, next = y
Enter <term> ... y / 2 is here (factor, lex /, factor)- is caught in the loop of expr, not of term; / is inside the second term. Total 5 lex, 3 factor.What happens when the recursive-descent parser above is given (a + b (missing right parenthesis)?
Show answer
factor sees (, calls lex, and calls expr. expr processes a + b and returns; nextToken is EOF. factor expects ), cannot find it → error(). Recursive descent catches the error exactly in the function where the expectation is broken.
Perform the pairwise disjointness test on <A>. Does the grammar pass?
<A> -> a <B> | <B> b
<B> -> a <C> | d
<C> -> cShow answer
The FIRST set of the alternative <B> b depends on what <B> starts with: a <C> → a, d → d. Since both alternatives can start with a, the parser cannot know which to choose by looking at a single token. The test fails. The <B> rule ({a} and {d}) passes, however.
Does <S> pass the pairwise disjointness test?
<S> -> x <S> | <T> y | z
<T> -> w <T> | vShow answer
FIRST(x<S>) = {x}. FIRST(<T>y) = FIRST(<T>) = {w, v}. FIRST(z) = {z}. The intersections of every pair are empty → the test passes, one token of lookahead is enough.
Remove the left recursion from: <T> -> <T> * <F> | <T> / <F> | <F>
Show answer
Pattern: ⇒ , . Here β = <F>, α1 = * <F>, α2 = / <F>. The new grammar produces the same language, but in recursive descent T() no longer calls itself without consuming a token.
Left-factor the rule: <stmt> -> if <c> then <s> | if <c> then <s> else <s>
Show answer
The two alternatives start with the same prefix (if <c> then <s>), and their FIRST sets overlap. Take the common prefix out and put the differing remainder in a new nonterminal. With EBNF: <stmt> -> if <c> then <s> [else <s>], and in recursive descent the [ ] becomes an if.
In a bottom-up (shift-reduce) parse of id + id * id with the expr/term/factor grammar, what is the first handle reduced, and how many reductions are performed in total?
Show answer
A bottom-up parser builds the rightmost derivation in reverse. The reduction order:
id + id * id
<factor> + id * id
<term> + id * id
<expr> + id * id
<expr> + <factor> * id
<expr> + <term> * id
<expr> + <term> * <factor>
<expr> + <term>
<expr>8 reductions = the number of interior nodes in the tree. Note: at the stage <expr> + <term> * id, <expr> + <term> is not reduced; the handle is id because * must bind first.What do the letters in LL and LR stand for, and which one builds the parse tree from the leaves up?
Show answer
In both types the input is read left to right. LL is top-down, starts at the root and builds the tree in preorder (recursive descent is an LL parser). LR is bottom-up, finds handles from the leaves toward the root and reduces (shift-reduce).
Give the three reasons for separating lexical analysis from syntax analysis.
Show answer
Simplicity: the lexer is a simple pattern matcher and makes the parser smaller. Efficiency: most of the compile time is spent in the lexer, which can be optimized separately. Portability: the lexer does platform-dependent jobs such as reading files, so the parser stays portable.
Verification: Lexeme lists were checked with a state-diagram lexer, recursive-descent traces (Enter/Exit, lex counts) by running the same EBNF functions in node, FIRST sets and that removing left recursion does not change the language (all strings up to length 5) with node; the bottom-up reduction order was taken from the reverse of the rightmost derivation (verify/SEN2001/03b.mjs).