Syntax_Analysis_Notes
Syntax_Analysis_Notes
2. Context-Free Grammars 5
3. Writing a Grammar 7
4. Top-Down Parsing 10
6. Bottom-Up Parsing 16
8. LR Parsers 22
8.1 LR(0) Items 8.2 SLR Parsing 8.3 CLR / LALR Parsing
A parser (syntax analyzer) obtains a string of tokens from the lexical analyzer, and verifies that the string can be generated by the grammar for the source
language. It reports any syntax errors, and constructs, at least conceptually, a parse tree representing the syntactic structure of the token stream, which
is passed on (often implicitly, via syntax-directed translation) to the rest of the compiler.
Top-Down Build the parse tree from the root downward to the leaves; can be viewed as attempting to find a leftmost Recursive Descent, LL(1) Predictive
Parsers derivation for the input string Parsing
Bottom-Up Build the parse tree from the leaves upward to the root; can be viewed as reducing the input string down to the Shift-Reduce, Operator-Precedence, LR
Parsers start symbol (a reverse rightmost derivation) (SLR, CLR, LALR)
Exam Tip: Both classes only efficiently handle restricted subsets of grammars — top-down methods generally require the grammar to be free of left
recursion and left-factored (ideally LL(1)); bottom-up LR methods can handle a much larger class of grammars, which is why virtually all parser generators
(like YACC) use LR-based techniques.
2. Context-Free Grammars
A context-free grammar (CFG) consists of terminals, nonterminals, a start symbol, and productions. It gives a precise, easy-to-understand syntactic
specification of a programming language, and its structure can often expose ambiguities that might otherwise go unnoticed in an initial design.
Component Meaning
T A finite set of terminals — the basic symbols (tokens) from which strings are formed; T ∩ V = ∅
P A finite set of productions, each of the form A → α, where A is a nonterminal and α is a string of terminals and nonterminals (including ε)
S A designated nonterminal called the start symbol, from which derivations begin
E → E + T | E - T | T
T → T * F | T / F | F
F → ( E ) | id
2.3 Derivations
A derivation is a sequence of production applications starting from the start symbol and ending at a string of terminals (a sentence). At each step, a
nonterminal in the current string is replaced by the right side of one of its productions.
Type Rule
Leftmost Derivation At each step, the leftmost nonterminal in the current string is replaced
Rightmost Derivation At each step, the rightmost nonterminal in the current string is replaced
E ⇒ E + T ⇒ T + T ⇒ F + T ⇒ id + T ⇒ id + T * F
⇒ id + F * F ⇒ id + id * F ⇒ id + id * id
A parse tree can be viewed as a graphical representation of a derivation that filters out the order in which productions are applied to replace
nonterminals. Each interior node represents the application of a production; its label is the nonterminal A on the left of the production, and its children are
labelled by the symbols on the right side of that production, left to right.
E
/ | \
E + T
| /|\
T T * F
| | |
F F id
| |
id id
2.5 Ambiguity
A grammar that produces more than one parse tree for some sentence is said to be ambiguous. Equivalently, an ambiguous grammar is one that
produces more than one leftmost (or more than one rightmost) derivation for the same sentence.
The grammar E → E + E | E * E | ( E ) | id is ambiguous — the string id + id * id has two distinct parse trees, one where + is applied last
(giving id + (id*id) ) and one where * is applied last (giving (id+id)*id ), so the grammar does not by itself fix operator precedence.
Exam Tip: "Show that the grammar E → E+E | E*E | (E) | id is ambiguous" is a classic question — always show two distinct parse trees (or two distinct
leftmost derivations) for the same string, such as id+id*id, to prove ambiguity.
3. Writing a Grammar
Even though the syntax of programming-language constructs can typically be specified by a context-free grammar, not every such grammar is suitable for
automatic parsing. Certain transformations are needed to prepare a grammar for use with a particular parsing method.
This grammar is ambiguous — for if E1 then if E2 then S1 else S2 , it is unclear whether "else S2" matches the first or second "if". The standard
convention (and rewritten unambiguous grammar) matches each else with the closest previous unmatched then, by distinguishing "matched" and
"unmatched" statements:
A grammar is left recursive if it has a nonterminal A such that there is a derivation A ⇒+ Aα for some string α. Top-down parsing methods cannot handle
left-recursive grammars, so such recursion must be eliminated before top-down parsing.
E → T E'
E' → + T E' | ε
This handles indirect left recursion (e.g., A → Bα, B → Aβ), which can be less obvious than the immediate case.
Left factoring is a grammar transformation useful when it is not clear which of two or more alternative productions to use to expand a nonterminal,
because they share a common prefix. In such cases, we can rewrite the productions to defer the decision until enough of the input has been seen to make
the right choice.
Example: Left-factor stmt → if expr then stmt else stmt | if expr then stmt :
Exam Tip: "Eliminate left recursion and perform left factoring" on a given grammar is one of the most frequently repeated numerical questions. Always
apply left-recursion elimination first, then check for any remaining common prefixes to left-factor.
4. Top-Down Parsing
Top-down parsing can be viewed as an attempt to find a leftmost derivation for an input string, or equivalently, an attempt to construct a parse tree for
the input starting from the root and creating the nodes in preorder.
A recursive-descent parser consists of a set of procedures, one for each nonterminal. Execution begins with the procedure for the start symbol, which
halts and announces success if its procedure body scans the entire input string. A general recursive-descent parsing method may require backtracking,
i.e., it may need to repeatedly scan the input.
void A() {
choose a production A → α_i by looking at input & lookahead;
for each symbol X on the right side of α_i (left to right):
if X is a nonterminal: call procedure X();
else if X matches current input token: advance input;
else: report a parsing error (or backtrack, if supported);
}
Backtracking
When a general recursive-descent parser tries a production and fails, it must backtrack and retry with a different alternative — this requires the parser to be
able to "reset" the input to a previous position, which can be costly and is avoided in efficient predictive parsers by choosing productions deterministically using
lookahead.
Definition of FIRST
FIRST(α) is the set of terminals that begin the strings derivable from α. If α can derive ε, then ε is also in FIRST(α).
1. If X is a terminal, FIRST(X) = { X }.
2. If X → ε is a production, add ε to FIRST(X).
3. If X is a nonterminal and X → Y1 Y2 ... Yk is a production:
add FIRST(Y1) - {ε} to FIRST(X);
if ε is in FIRST(Y1), also add FIRST(Y2) - {ε}; and so on...
if ε is in FIRST(Y1)...FIRST(Yk) (all of them), add ε to FIRST(X).
Definition of FOLLOW
FOLLOW(A), for a nonterminal A, is the set of terminals a that can appear immediately to the right of A in some sentential form derived from the start
symbol, i.e. S ⇒* αAaβ. If A can be the rightmost symbol in some sentential form, then $ (the input end-marker) is also in FOLLOW(A).
E → T E'
E' → + T E' | ε
T → F T'
T' → * F T' | ε
F → ( E ) | id
E' { +, ε } { $, ) }
T { (, id } { +, $, ) }
T' { *, ε } { +, $, ) }
F { (, id } { +, *, $, ) }
5. Non-Recursive Predictive Parsing (LL Parsing)
A non-recursive (table-driven) predictive parser can be built by maintaining a stack explicitly, rather than implicitly via recursive calls. The parser
mimics a leftmost derivation, using an explicit stack containing a sequence of grammar symbols, and a predictive parsing table M that tells it which
production to apply, based on the nonterminal on top of the stack and the current input symbol.
┌────────────────┐
Input a + b $ ─▶│ │
│ Predictive │──▶ Output (sequence of
Stack X │ Parser │ productions applied)
Y ◀────────────── │ (uses table M) │
Z │ │
$ └────────┬─────────┘
│
┌───────▼────────┐
│ Parsing Table M │
└────────────────┘
A grammar is said to be LL(1) if the parsing table constructed as above has at most one production in each table entry M[A, a] — i.e., no cell of the table
is multiply defined. ("LL(1)" = scan input Left to right, produce a Leftmost derivation, using 1 symbol of lookahead.)
Nonterminal id + * ( ) $
E E→TE' E→TE'
T T→FT' T→FT'
F F→id F→(E)
Exam Tip: "Check whether the given grammar is LL(1)" — always compute FIRST and FOLLOW carefully first, build the table, and explicitly point out any
cell with two or more productions as the reason the grammar fails to be LL(1).
6. Bottom-Up Parsing
Bottom-up parsing corresponds to the construction of a parse tree for an input string beginning at the leaves and working up towards the root. It can be
described as a reduction of the input string to the start symbol of the grammar, and corresponds to producing a rightmost derivation in reverse.
Shift-reduce parsing is a general style of bottom-up parsing in which a stack holds grammar symbols, and an input buffer holds the rest of the string to
be parsed. At each step, the parser either shifts the next input symbol onto the stack, or reduces a string of symbols at the top of the stack (matching the
right side of some production) to the corresponding left-side nonterminal.
Action Meaning
Shift Push the next input symbol onto the top of the stack
Reduce Replace a string β at the top of the stack (matching the right-hand side of a production A → β) with the nonterminal A
Accept Announce successful completion of parsing (stack contains just the start symbol, and input is exhausted)
$ id * id $ shift
$ id * id $ reduce by F→id
$F * id $ reduce by T→F
$T * id $ shift
$T* id $ shift
$ T * id $ reduce by F→id
$T $ reduce by E→T
$E $ accept
6.2 Handles
A handle of a right-sentential form γ is a production A → β together with a position in γ where the string β may be found, such that replacing β at that
position by A yields the previous right-sentential form in a rightmost derivation of γ. That is, if S ⇒*rm αAw ⇒rm αβw, then A → β in the position following α
is a handle of αβw.
The string w to the right of the handle contains only terminal symbols.
Informally, a handle is a substring that matches the right side of a production, and whose reduction represents one step along the reverse of a rightmost
derivation.
Because the string to the right of a handle contains only terminals, the string left of and including a handle in a right-sentential form is called a viable
prefix.
A viable prefix of a right-sentential form is any prefix of that sentential form that does not extend past the right end of the rightmost handle. Equivalently,
viable prefixes are exactly the set of prefixes of right-sentential forms that can appear on the stack of a shift-reduce parser at any point, i.e., they never
overshoot the handle so that the parser can always still find a way to reduce back to the start symbol.
Illustration: for the rightmost sentential form E + T * F with handle F (about to be reduced by F→id if F derives from id, or otherwise), any prefix of
the stack contents that stops at or before the end of the handle — e.g. E , E+ , E+T , E+T* , E+T*F — is a viable prefix. A string like E+T*F) (extending
past the handle onto the unscanned input) would not be viable.
Exam Tip: "Define handle and viable prefix" is a very common short-answer question. Remember: the handle is "what gets reduced next"; the viable
prefix is "everything on the stack up to and including the handle" — the set of strings that can legitimately appear on a shift-reduce parser's stack without
forcing an error.
7. Operator Precedence Parsing
Operator precedence parsing is a simple, easy-to-implement bottom-up parsing technique applicable to a class of grammars called operator grammars,
in which no production's right side is empty (ε) or has two adjacent nonterminals.
Equal precedence a≡b a and b have equal precedence (used mainly for matched delimiters like ( and ))
+ * ( ) id $
+ ⋗ ⋖ ⋖ ⋗ ⋖ ⋗
* ⋗ ⋗ ⋖ ⋗ ⋖ ⋗
( ⋖ ⋖ ⋖ ≡ ⋖ —
) ⋗ ⋗ — ⋗ — ⋗
id ⋗ ⋗ — ⋗ — ⋗
$ ⋖ ⋖ ⋖ — ⋖ —
Advantages Disadvantages
Simple and easy to implement by hand Only a small class of grammars (operator grammars) can be parsed this way
The precedence table is typically small Difficult to handle constructs like the unary minus, which has different precedence behavior than binary minus
Fast — parsing decisions made from a single table lookup Errors are detected quite late, and it is hard to give good diagnostics
Not all context-free grammars can be adapted into operator grammars, limiting general applicability
Exam Tip: Because of its limitations, operator precedence parsing has largely been superseded by LR parsing in modern parser generators. It is
nonetheless important academically as a simple, hand-implementable illustration of precedence-driven bottom-up parsing.
8. LR Parsers
An LR(k) parser reads input Left to right, constructs a Rightmost derivation in reverse, using at most k symbols of lookahead. LR parsing is attractive
because: (i) it can be used to parse virtually all programming-language constructs expressible by a context-free grammar, (ii) it is the most general non-
backtracking shift-reduce method known, yet can be implemented as efficiently as other shift-reduce methods, (iii) an LR parser can detect a syntax error
as soon as it is possible to do so on a left-to-right scan.
SLR (Simple Uses LR(0) items; resolves shift/reduce and reduce/reduce Least powerful; smallest tables; easiest to construct
LR) conflicts using FOLLOW sets
CLR (Canonical Uses LR(1) items, which carry an explicit lookahead symbol with Most powerful; largest number of states/tables
LR) each item
LALR (Look- Merges LR(1) states that have the same "core" (set of LR(0) Same number of states as SLR, almost as powerful as CLR; the method of choice for
Ahead LR) items), keeping the lookaheads most parser generators (e.g. YACC)
An LR(0) item of a grammar G is a production of G with a dot (•) at some position of the right side, indicating how much of a production we have seen at a
given point in the parsing process.
A → • X Y Z
A → X • Y Z
A → X Y • Z
A → X Y Z •
closure(I):
add every item in I to closure(I)
repeat until no more items can be added:
if A → α • B β is in closure(I), and B → γ is a production,
add the item B → • γ to closure(I) (if not already there)
goto(I, X):
let J = { items A → αX • β such that A → α • X β is in I }
return closure(J)
C = { closure({ S' → •S }) }
repeat until no more sets can be added to C:
for each set of items I in C, and each grammar symbol X:
if goto(I, X) is not empty and not already in C:
add goto(I, X) to C
Augmented grammar:
(0) S' → S
(1) S → S ; S
(2) S → id
State id ; $ GOTO(S)
0 shift 2 1
1 shift 3 accept
3 shift 2 4
Note state 4 shows a shift/reduce conflict on ";" — this simple grammar for a list of statements separated by ";" is ambiguous, illustrating a classic pitfall
when writing grammars for SLR parsing.
An LR(1) item is a pair [A → α • β, a] consisting of an LR(0) item together with a terminal a (or $) which is a lookahead symbol valid in the context
represented by that item. The extra lookahead component lets the parser reduce A → α only when the actual next input symbol is a, rather than for every
symbol in FOLLOW(A) — this eliminates many of the conflicts that arise in SLR construction.
CLR parsing tables are built the same way as SLR (closure, goto, canonical collection), except items now carry explicit lookaheads, and reductions are only
placed for the specific lookahead symbols attached to each completed item — this produces the most powerful and most conflict-free tables of the three
methods, but typically results in a much larger number of states than SLR or LALR.
The LALR (Look-Ahead LR) method is constructed by taking the canonical LR(1) collection of item sets, and merging together all sets that have the
same "core" (i.e., the same set of first components / LR(0) items, ignoring lookaheads). The resulting parser has exactly as many states as the SLR parser
for the same grammar, but with a more refined, context-sensitive lookahead — able to handle a strictly larger class of grammars than SLR, while remaining
much smaller than the corresponding CLR table.
LALR(1) Merged LR(1) items (by core) Same as SLR Larger than SLR; sufficient for almost all programming-language grammars
CLR(1) (Canonical LR) Full LR(1) items Many more than SLR/LALR Largest class; most powerful
Exam Tip: "Why is LALR preferred over CLR in practical parser generators such as YACC?" — Answer: LALR tables have the same number of states as
SLR (making them compact enough for practical use) while still being powerful enough to handle essentially all standard programming-language grammar
constructs, unlike the much larger and more memory-intensive CLR tables.
9. Parser Generators — YACC
YACC ("Yet Another Compiler-Compiler") is a widely used parser generator that takes a context-free grammar specification (with embedded actions) as
input, and automatically produces a bottom-up LALR(1) parser (as C code) for that grammar.
translate.y ┌──────────────┐
(YACC source with ────▶│ YACC Compiler │────▶ [Link].c
grammar + actions) └──────────────┘ (C source of
the parser)
│
lex.l ──▶ Lex Compiler ──▶ [Link].c ───┤
▼
┌──────────────┐
│ C Compiler │
└──────┬───────┘
▼
[Link]
(executable parser)
│
input stream ─────────────────────────▶ │ ──▶ output
{ declarations }
%%
{ translation rules (grammar productions with actions) }
%%
{ supporting C routines }
Section Contents
Declarations Token declarations ( %token ), precedence/associativity declarations ( %left , %right , %nonassoc ), and any C code copied verbatim
Translation Rules A series of grammar productions of the form head : body { semantic action } ; , where semantic actions are C code fragments executed on a
corresponding reduction
Supporting Auxiliary C functions, and typically a main() that calls the generated function yyparse()
Routines
%{
#include <stdio.h>
%}
%token NUMBER
%left '+' '-'
%left '*' '/'
%%
expr : expr '+' expr { $$ = $1 + $3; }
| expr '-' expr { $$ = $1 - $3; }
| expr '*' expr { $$ = $1 * $3; }
| expr '/' expr { $$ = $1 / $3; }
| NUMBER { $$ = $1; }
;
%%
Here $$ refers to the semantic value of the left-side nonterminal, and $1, $2, $3, ... refer to the semantic values of the corresponding right-side
symbols — this is the standard YACC convention for attaching syntax-directed actions to productions.
Declaration Meaning
%left Declares a group of tokens as left-associative, at increasing levels of precedence for successive %left / %right lines
%nonassoc Declares tokens as non-associative (e.g. relational operators, where a < b < c is disallowed)
Tokens declared later have higher precedence. On a shift/reduce conflict, YACC by default resolves it in favor of shift unless overridden by these precedence
rules — this default is exactly why the classic dangling-else ambiguity is automatically resolved correctly (matching each else with the nearest unmatched then)
without any grammar rewriting.
Exam Tip: "Explain the structure and working of YACC as a parser generator" — mention: (1) three-section input format similar to Lex, (2) automatic
LALR(1) table construction from the grammar, (3) semantic actions tied to productions using $$ / $1 / $2 notation, (4) precedence declarations to resolve
ambiguity without grammar rewriting, and (5) integration with a Lex-generated scanner via yylex() .
10. Error Recovery Strategies
A parser should be able to detect and report the presence of errors in the source program clearly and accurately, and should recover from each error quickly
enough to be able to detect subsequent, unrelated errors — ideally without an excessive number of cascading, spurious errors caused by an earlier one.
Strategy Description
Panic-Mode On discovering an error, the parser discards input symbols one at a time until one of a designated set of synchronizing tokens (e.g. ; , } ) is found. It is
Recovery simple, guaranteed not to loop, and widely used, but may skip a considerable amount of input without checking it for other errors.
Phrase- On discovering an error, the parser performs local correction on the remaining input — it may replace a prefix of the remaining input with some string that
Level allows parsing to continue (e.g. inserting a missing semicolon, or replacing a comma with a semicolon).
Recovery
Error If common errors are anticipated, the grammar writer can augment the grammar with productions that explicitly generate erroneous constructs; the parser
Productions then uses these to detect the anticipated errors and generate appropriate diagnostics.
Global Given the incorrect input string x and grammar G, algorithms exist to find a parse tree for a related string y "close to" x (minimizing
Correction insertions/deletions/changes) — this is theoretically interesting but too costly to implement in practice.
In predictive (table-driven LL) parsing, error handling is aided by the fact that an error is detected as soon as the current input symbol does not match
what the parser expects, i.e. an empty entry in the predictive parsing table.
An LR parser will detect an error when it consults the ACTION table and finds an "error" entry — this happens as soon as the prefix of the input read so far
is not a viable prefix of the grammar, which is the earliest possible point at which an error can be detected in a left-to-right scan.
Lexical Analysis No pattern matches remaining input Skip/insert/replace characters; panic-mode skip
LL (Predictive) Empty entry in M[A,a], or terminal mismatch Panic-mode using FOLLOW-based synchronizing sets; pop-on-synch for nonterminals
Operator No precedence relation defined between two Panic-mode skipping / localized fix-up rules
Precedence terminals
LR Empty entry in ACTION table (earliest possible Panic-mode stack unwinding to a state with a GOTO on a chosen nonterminal; or phrase-level
(SLR/CLR/LALR) detection point) per-entry fix-up routines
Exam Tip: "Compare error recovery in LL and LR parsing" — key point to mention: LR parsers can detect an error as soon as it is theoretically
possible to do so on a left-to-right scan (before an erroneous handle could ever be reduced), whereas predictive LL parsers detect the error as soon as an
unexpected token doesn't match the predicted production — both are considered "early detecting" compared to weaker methods, but LR is provably at
least as prompt as any other shift-reduce technique.
11. Quick Revision Summary
Parser Verifies the token stream against the grammar and builds a parse tree, reporting syntax errors.
Ambiguity A grammar producing more than one parse tree/derivation for some string.
Left Recursion Elimination A → Aα|β rewritten as A → βA', A' → αA'|ε; required for top-down parsing.
Left Factoring Defers the parser's choice between alternatives sharing a common prefix.
Recursive Descent One procedure per nonterminal; may need backtracking for a general grammar.
FIRST / FOLLOW Sets used to build predictive parsing tables and guide parser decisions.
LL(1) Parsing Table-driven, non-recursive, top-down parsing using 1 lookahead symbol; needs a conflict-free table.
Shift-Reduce Parsing General bottom-up strategy: shift input, reduce handles, until start symbol remains.
Handle The substring to be reduced next, matching a production's right side at the correct position.
Viable Prefix Any prefix of a right-sentential form that does not go past the rightmost handle.
Operator Precedence Parsing Simple bottom-up method using just 3 relations (⋖, ≐, ⋗) between terminals.
CLR Full LR(1) items with per-item lookahead; most powerful, largest tables.
LALR LR(1) states merged by core; same size as SLR, almost as powerful as CLR — used in YACC.
YACC Parser generator that builds an LALR(1) parser from a grammar + actions specification.
Error Recovery Panic-mode, phrase-level, error-productions, and global-correction strategies, adapted per parsing method.
12. Important Exam Question Bank
Prepared as detailed study notes for MAKAUT [Link] Compiler Design — Module 3: Syntax Analysis [9L].
Content structured for conceptual clarity and exam preparation.