COMPILER DESIGN / 2. SYNTAX ANALYSIS — PARSING
Syntax Analysis — The Parser
Building Abstract Syntax Trees from token streams using CFGs
EXPLANATION
The parser takes the token stream from the lexer and checks that it conforms to the grammar of the language, building an Abstract Syntax Tree (AST) as output. Parse Tree vs AST: - Parse Tree (Concrete Syntax Tree): reflects every step of the grammar derivation, including all grammar symbols. Contains redundant nodes (parentheses, semicolons already encoded in structure). - AST (Abstract Syntax Tree): stripped-down version — only semantically meaningful nodes. "2 + 3 * x" doesn't need parenthesis nodes if the tree structure encodes precedence. Parsing strategies — top-down vs bottom-up: TOP-DOWN PARSING (LL parsers): Builds tree from root to leaves. Starts with start symbol S, predicts which production to apply based on current input token (lookahead). Recursive Descent Parser: - Each grammar variable becomes a function - Each function reads tokens and calls other variable functions - Easiest to write by hand — what most hand-written parsers use (GCC was recursive descent) - Requires grammar to be LL(k): no left recursion, must predict from k tokens of lookahead Left Recursion problem: E → E + T fails in recursive descent (infinite loop). Must eliminate: E → T E' where E' → + T E' | ε LL(1) parser: uses one token of lookahead. Parsing table drives decisions. FIRST(A) = set of terminals that can begin strings derived from A FOLLOW(A) = set of terminals that can follow A in some derivation BOTTOM-UP PARSING (LR parsers): Builds tree from leaves to root. Shifts tokens onto a stack, reduces when a production's RHS is on top. Shift-Reduce parsing: - SHIFT: push next input token onto stack - REDUCE: pop RHS of a production, push LHS - LR(0), SLR(1), LALR(1), LR(1): different amounts of lookahead and states LALR(1) — most widely used (yacc, bison, most production parsers): - More powerful than LL(1) — handles more grammars - One token lookahead - Compact parsing tables - Can handle most programming language grammars Parser generators: lex+yacc, flex+bison, ANTLR — you write the grammar, the tool generates the parser code. Python's parser is LALR(1) generated from Grammar/Grammar file. Parsing errors: "SyntaxError: invalid syntax" — the parser found a token it didn't expect given the grammar. The parser can't continue (or tries to recover to report more errors).
DIAGRAM
GRAMMAR (simplified expression):
E → E + T | T
T → T * F | F
F → ( E ) | id | num
After left-recursion elimination (for LL):
E → T E'
E' → + T E' | ε
T → F T'
T' → * F T' | ε
F → ( E ) | id | num
RECURSIVE DESCENT trace for "2 + 3 * x":
parse_E()
parse_T()
parse_F() → consume NUM(2) → return 2
parse_T'()
lookahead = PLUS ≠ STAR → return ε
→ return Num(2)
parse_E'()
consume PLUS
parse_T()
parse_F() → consume NUM(3) → return 3
parse_T'()
consume STAR
parse_F() → consume ID(x)
parse_T'() → ε
→ return BinOp(3, *, x)
→ return BinOp(3, *, x)
parse_E'() → ε
→ return BinOp(2, +, BinOp(3,*,x))
AST:
+
/ 2 *
/ 3 xCODE