LR and LALR Parsing
Consider a compiler front end reading tokens left to right. It must decide exactly when a sequence like `E + T` should collapse into `E`. A recursive-descent parser needs a human to encode this decision as explicit function calls. It also chokes on left-recursive grammars, which are the natural way to write arithmetic expressions. LR parsing instead builds a deterministic finite automaton over grammar items. These are dotted productions that record how much of a rule has been matched. That automaton then drives a stack-based shift-reduce engine. The result is a parser that accepts the largest practical class of unambiguous context-free grammars. This class covers any linear-time bottom-up method. This matters because nearly every production-quality parser generator relies on exactly this theory. That includes Yacc, Bison, GNU Bison, ANTLR's LALR mode ancestors, and the parser tables inside many hand-tuned compilers. LR parsing is not just an algorithm; it is a table-construction discipline. You write a grammar, and a generator computes item sets and a transition automaton. The generator reports precisely which sentences are ambiguous via shift-reduce or reduce-reduce conflicts. That diagnostic power is rooted in a decidable and mechanically checkable construction. This is why LR methods displaced ad hoc bottom-up parsing in industrial practice. The big picture: LR(k) parsers use a deterministic pushdown automaton whose states are sets of items. These are augmented with k tokens of lookahead. At every step, the automaton decides whether to shift the next token onto a stack. Alternatively, it reduces the top of the stack using a grammar production. The naive construction, canonical LR or LR(1), produces very large tables. The practical fix, LALR(1), merges LR(1) states with identical cores. This shrinks the automaton to roughly the size of the simpler SLR(1) automaton. It retains almost all of LR(1)'s discriminating power. This article builds the full machinery: items, closures, gotos, the canonical collection, the shift-reduce parsing engine, the taxonomy of conflicts, and the LALR merge that makes the theory deployable. It assumes familiarity with Bottom-Up Parsing. It also sets up the machinery that later produces Abstract Syntax Trees as a side effect of each reduction.
- LR parsing builds a DFA over "items" (dotted productions) that tracks how much of each candidate production has been recognized.
- A shift-reduce engine driven by ACTION/GOTO tables uses this DFA plus a stack to parse any deterministic context-free language in linear time.
- LR(0), SLR(1), and canonical LR(1) differ only in how much lookahead informs the reduce decision, trading table size against grammar coverage.
- LALR(1) merges LR(1) states sharing the same core, giving near-LR(1) power with SLR(1)-sized tables — the industrial sweet spot.
- Conflicts (shift-reduce, reduce-reduce) are detected automatically at table-construction time, not at runtime, giving early, precise ambiguity diagnostics.
- Every reduction step corresponds to building one interior node, making LR parsing a natural engine for producing Abstract Syntax Trees.
LR parsing turns "what production am I in the middle of?" into a DFA state, so a stack of states plus one token of lookahead deterministically decides shift or reduce.
The idea
An LR parser is a deterministic, stack-based bottom-up parser that reads input left to right (L) and produces a rightmost derivation in reverse. At each step it is in some configuration — a stack of grammar symbols interleaved with automaton states — and it consults the next input token (the lookahead) to choose one of two actions: shift the token onto the stack, or reduce the symbols at the top of the stack using some production `A → β`, replacing β with A. Parsing succeeds when the stack reduces to the start symbol and all input is consumed; it fails the moment no legal action exists, which is exactly why LR parsers, unlike general CFG parsers, can report errors at the earliest possible point. The central technical device is the LR item: a production with a dot marking a position, e.g. `A → α · β`, meaning "if we are here, we have already seen a string derivable from α and are hoping next to see one derivable from β." An LR(1) item additionally carries a lookahead symbol, `[A → α · β, a]`, meaning this configuration is only relevant if the token immediately following the eventual A is `a`. Items are grouped into sets of items (also called states) via two closure-and-transition operations — `closure` and `goto` — that mirror subset construction in NFA-to-DFA conversion. The collection of all reachable item sets, connected by goto edges, is the LR automaton; it is built once, offline, by the parser generator. Different LR variants differ only in how precisely they use lookahead to decide reduces. LR(0) ignores lookahead entirely for reduce decisions (usable only for very restricted grammars). SLR(1) ("Simple LR") reduces on `A → α·` whenever the lookahead lies in `FOLLOW(A)` — cheap to compute but sometimes too permissive, causing spurious conflicts. Canonical LR(1) carries a lookahead per item, computed contextually, giving the most precise (and most expensive) automaton. LALR(1) takes the canonical LR(1) automaton and merges states with identical cores (item sets ignoring lookaheads), recovering most of LR(1)'s power in an automaton the size of SLR(1)'s. This hierarchy — LR(0) ⊊ SLR(1) ⊊ LALR(1) ⊆ LR(1) in grammar-class power — is the organizing fact of the whole theory.
How it works
Step 1 — Augment the grammar. Add a new start symbol `S'` with production `S' → S`, where `S` was the original start symbol. This guarantees a unique accepting state and gives the parser a clean "done" signal: when `S' → S ·` is reduced (conceptually — it is never actually reduced, it triggers `accept`).
Step 2 — Define closure. Given a set of items `I`, `closure(I)` adds, for every item `[A → α · B β, a]` in I and every production `B → γ`, the item `[B → · γ, b]` for every `b` in `FIRST(β a)` (for LR(1); for LR(0)/SLR(1), lookahead is simply dropped). This saturates the item set with everything that must also be "in progress" if `A → α · B β` is in progress, since parsing β starts by parsing some derivation of B.
Step 3 — Define goto. `goto(I, X)`, for grammar symbol X, is `closure` of the set of items `[A → α X · β, a]` for every `[A → α · X β, a]` in `I`. This is literally the subset-construction transition function: it moves the dot past X.
Step 4 — Build the canonical collection. Starting from `closure({[S' → · S, $]})`, repeatedly apply `goto` on every symbol to every existing state until no new states appear. This is exactly BFS/worklist subset construction and terminates because there are finitely many item sets over a finite grammar.
Step 5 — Fill ACTION and GOTO tables. For each state `I` and terminal `a`: if `[A → α · a β, b] ∈ I` and `goto(I, a) = J`, set `ACTION[I, a] = shift J`. If `[A → α ·, a] ∈ I` (A ≠ S'), set `ACTION[I, a] = reduce A → α`. If `[S' → S ·, ] = accept`. For nonterminal `X`, `GOTO[I, X] = goto(I, X)` when defined.
Step 6 — Run the shift-reduce engine. Maintain a stack of (state, symbol) pairs. Repeat: look at the top state and the current lookahead token; consult ACTION; shift, reduce, accept, or error accordingly. ``` function LR_PARSE(tokens): stack = [state0] # stack of states; symbols implied by transitions symbols = [] # parallel stack of grammar symbols (for AST building) i = 0 tokens.append('') # end marker loop: s = stack.top() a = tokens[i] action = ACTION[s][a] if action == "error": report_syntax_error(a); return FAIL else if action == shift(t): stack.push(t) symbols.push(a) i = i + 1 else if action == reduce(A -> beta): k = length(beta) pop k states from stack pop k symbols from symbols # these become children of the AST node for A s' = stack.top() stack.push(GOTO[s'][A]) symbols.push(A) # build AST node here, with popped children else if action == accept: return symbols.top() # the parse tree / AST root ``` ``` function CLOSURE(items): # LR(1) version result = items changed = true while changed: changed = false for [A -> alpha . B beta, a] in result: for each production B -> gamma: for b in FIRST(beta a): if [B -> . gamma, b] not in result: add it; changed = true return result function GOTO(items, X): moved = { [A -> alpha X . beta, a] : [A -> alpha . X beta, a] in items } return CLOSURE(moved) function BUILD_CANONICAL_COLLECTION(grammar): C = { CLOSURE({ [S' -> . S, ] }) } worklist = C while worklist not empty: I = worklist.pop() for each grammar symbol X: J = GOTO(I, X) if J not empty and J not in C: add J to C; add J to worklist return C ```
Each item set is a DFA state; each `goto` edge is a transition on a grammar symbol; states containing a "dot at the end" item generate reduce actions in ACTION. The stack in the parser always mirrors a path from the start state through this automaton, which is precisely why the algorithm is correct — see below.
Complexity & correctness
Table construction. Let `n` be the grammar size (number of symbols across all productions) and `G` the number of distinct items. The number of possible item sets is bounded by `2^G` in the worst case, but in practice grammars used in compilers yield automata with a number of states linear to low-polynomial in grammar size. Constructing the canonical LR(1) collection takes `O(n · 2^n)` in the theoretical worst case (each state's closure can enumerate many lookahead-augmented items), which is why LR(1) tables can explode — this is the textbook justification for LALR. LALR(1) construction merges states sharing a core; the number of LALR states equals the number of LR(0) states, which is empirically close to linear in grammar size for real programming-language grammars. SLR(1) construction is cheapest: it reuses the LR(0) automaton and only needs `FOLLOW` sets, computable in `O(n^2)` (fixed-point iteration over a bounded lattice) in the worst case, typically much less.
Parsing time. Once tables are built, parsing itself is `O(m)` in the length `m` of the input: each input token is shifted exactly once, and each reduction pops a bounded number of stack entries corresponding to a fixed production length, so the total number of stack operations is `O(m)` amortized (a standard potential-function argument: shifts add to a budget that reduces spend, and each token contributes at most a constant number of pushes across its lifetime). Table lookups are `O(1)` with array-indexed ACTION/GOTO tables (or `O(log |Σ|)` with hashed tables for large alphabets). This linear-time guarantee, independent of grammar ambiguity subtleties, is the entire commercial argument for LR parsing over general CFG parsers (Earley/CYK, which are `O(m^3)` worst case).
Correctness argument. The key invariant is: > At every point during parsing, the sequence of states on the stack corresponds to a valid path through the LR automaton from the start state, and the grammar symbols on the stack form a viable prefix — a prefix of some right-sentential form that can still be extended to a full derivation of the augmented start symbol. This is proved by induction on parsing steps. Initially the stack holds only the start state, trivially a viable prefix (the empty string). A shift step extends the viable prefix by one terminal and moves along a `goto` edge that exists precisely because that terminal was expected by some item with the dot immediately before it — so the invariant is preserved by construction of `goto`. A reduce step replaces `α` (matching `A → α·` in the current state) with `A`; since the state before `α` was pushed has a `goto` on `A` defined (this is guaranteed because `A → α` was itself introduced via `closure` from some earlier item expecting `A`), the new top state is well defined and the new stack remains a valid path. The parser accepts exactly when the stack spells out `S` and lookahead is `$`, matching the unique item `S' → S ·`, which by the invariant means the entire input is a sentence of the augmented grammar, i.e., of the original grammar. The lookahead refinement (SLR/LALR/canonical LR(1)) only affects when reduce actions are chosen among competing options; it never allows an invalid reduce, because a reduce action is only listed in ACTION at all when the corresponding complete item (with matching lookahead, for LR(1)/LALR(1)) exists in that state. The soundness of restricting lookahead to `FOLLOW(A)` in SLR follows because any valid rightmost derivation reducing `A → α` at this point must be followed by a token in `FOLLOW(A)`; the incompleteness of SLR arises because `FOLLOW(A)` is context-insensitive — it merges lookahead information across all contexts where A appears, which can incorrectly permit a reduce that is only valid in some contexts, producing a spurious conflict that canonical LR(1) (context-sensitive lookahead) avoids.
The LALR merge and its risk. Merging LR(1) states with identical cores can only ever lose precision, never introduce a shift-reduce conflict that wasn't structurally present (a theorem due to DeRemer): merging preserves the shift actions exactly (cores are identical, so the same gotos exist) and can only take the union of lookaheads on reduce items. The one danger is a new reduce-reduce conflict: if two originally distinct LR(1) states had, say, `[A → α·, a]` in one and `[B → α·, b]` in the other with disjoint lookaheads, merging can create a state containing both items with lookaheads now overlapping in the union. This is rare in practical grammars (it does not occur for any standard programming-language grammar construct), which is why LALR(1) is the workhorse in Yacc/Bison despite this theoretical caveat.
Worked example
Grammar (already augmented), a minimal expression grammar: ``` (0) S' -> E (1) E -> E + T (2) E -> T (3) T -> id ``` Input: `id + id `. Building the automaton (abbreviated, matching the figure): `I0 = closure({[S'\to \cdot E,]})` contains items for `E→·E+T`, `E→·T`, `T→·id`, all with lookahead `) = {+}` for the item `[E→·E+T, ]`'s own sub-items, so `T\to \cdot id` in fact gets lookaheads `{, +}`.
| Step | Stack (states) | Symbols | Lookahead | Action |
|---|---|---|---|---|
| 1 | I0 | — | `id` | shift → push I? via `goto(I0, id)` = I4 |
| 2 | I0 I4 | id | `+` | reduce `T → id` (item `[T→id·, $/+]` matches lookahead `+`); pop 1, push `goto(I0, T)` = I2 |
| 3 | I0 I2 | T | `+` | reduce `E → T` (`[E→T·, $/+]`); pop 1, push `goto(I0, E)` = I1 |
| 4 | I0 I1 | E | `+` | shift `+` → push `goto(I1, +)` = I3 |
| 5 | I0 I1 I3 | E + | `id` | shift `id` → push `goto(I3, id)` = I4 |
| 6 | I0 I1 I3 I4 | E + id | `$` | reduce `T → id`; pop 1, push `goto(I3, T)` = I5 |
| 7 | I0 I1 I3 I5 | E + T | `$` | reduce `E → E + T`; pop 3, push `goto(I0, E)` = I1 |
| 8 | I0 I1 | E | `$` | `[S'→E·, $]` present → accept |
Every reduction step in this trace corresponds to constructing one AST node — step 2 builds a leaf `id`, step 6 builds another leaf, step 7 builds the interior node `E+T` with the two previously built subtrees as children — which is exactly the mechanism used to hand a completed Abstract Syntax Trees structure to later compiler phases. Notice that the crucial disambiguating fact is in step 2: the parser reduces `T → id` under lookahead `+` (not shift, since `id` has no outgoing shift item at that point) — this is a decision only possible because the automaton correctly tracked that `id` is complete once no further extension (`.` at the end) exists in that state.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Treating SLR(1) failures as "the grammar is ambiguous" | SLR uses `FOLLOW(A)` context-insensitively, causing false conflicts on grammars that are genuinely LALR(1) or LR(1) | Recompute with LALR(1)/canonical LR(1) lookaheads before concluding ambiguity |
| Dangling-else style shift-reduce conflicts | Grammar for `if-then-else` is genuinely ambiguous; parser generator resolves by default (usually "prefer shift") | Explicitly declare precedence/associativity, or restructure grammar with a matched/unmatched nonterminal split |
| Forgetting the augmented start symbol `S'` | Without it, the accept condition is ambiguous with ordinary reduces of `S` | Always add `S' → S` with fresh `S'` and end marker `$` |
| Assuming LALR(1) is always safe | Rare grammars introduce new reduce-reduce conflicts purely from state merging | Test on the exact grammar; canonical LR(1) as fallback if conflicts appear only after merging |
| Off-by-one in item dot position when writing closures by hand | Miscounting symbols after the dot breaks `FIRST(β a)` lookahead computation | Mechanically apply `closure`/`goto` per the algorithm, never "eyeball" it for grammars beyond trivial size |
| Precedence declarations silently masking real ambiguity | Yacc-style `%left`/`%right` resolve conflicts without warning once declared | Always inspect the generated conflict report even when precedence is used |
Pioneers & history
The LR(k) concept originates with Donald Knuth's foundational 1965 analysis of deterministic bottom-up parsing, which characterized exactly which context-free languages admit a deterministic left-to-right, rightmost-derivation-in-reverse parser with bounded lookahead. Knuth's original construction was too large for practical compilers; the subsequent decade of work by Frank DeRemer produced SLR(1) and, crucially, the LALR(1) merge that made table sizes tractable enough for real machines, directly enabling tools like Yacc.
Origins · 1 minDonald Ervin Knuth and how this was discovered
Knuth's 1965 paper "On the Translation of Languages from Left to Right" introduced LR(k) grammars, proving the class captures exactly the deterministic context-free languages parsable with k tokens of lookahead. This single result underlies essentially every table-driven parser generator built since.
Where this matters
LALR(1) parser generators are the backbone of production compiler front ends: Yacc and Bison generate parsers for C, historical Unix tools, and countless domain-specific languages; many SQL and configuration-language parsers still emit LALR grammars by hand or via Bison. GCC's early C and C++ front ends used Bison-generated LALR parsers for years before hand-written recursive-descent replacements were adopted for better error messages. Java's `javacc`-adjacent and ANTLR ecosystems reference LR/LALR theory even when using LL(*) generation, because the conflict-detection vocabulary (shift-reduce, reduce-reduce) is universal. Database query parsers, protocol description languages, and many "little languages" embedded in build systems (Make-like DSLs, some templating engines) rely on LALR tooling because the theory gives a mechanically checkable guarantee: if the table builds with zero conflicts, the grammar is unambiguous under the parser's own resolution rules, a guarantee unavailable from ad hoc hand-rolled parsers. Once a program is reduced through this automaton, the popped children at each reduce step are exactly the raw material assembled into an Abstract Syntax Trees for the next compiler phase, connecting this article directly to Bottom-Up Parsing on one side and tree-based semantic analysis on the other.
Ask the tutor: Can you walk through building the full LALR(1) table for a grammar with a real dangling-else conflict, and show exactly how precedence declarations resolve it?
What is the practical difference between LR(0), SLR(1), LALR(1), and canonical LR(1)?
They differ in how much and how precisely they use lookahead to decide reduce actions. LR(0) uses none, so it can only handle grammars where every completed item in a state is unambiguous regardless of what follows — extremely restrictive. SLR(1) adds one token of lookahead computed from `FOLLOW` sets, which is cheap but context-insensitive: it merges lookahead information across all uses of a nonterminal. LALR(1) computes context-sensitive lookaheads per item but then merges states with identical cores, trading a small amount of precision for dramatically smaller tables. Canonical LR(1) keeps everything separate, giving maximum precision at the cost of potentially exponential table growth. In practice, essentially all real programming-language grammars are LALR(1), so the last tier of extra precision is rarely needed.
Why can't LR parsers handle every context-free grammar?
LR parsing requires that a single token of lookahead (or a fixed k) together with the current automaton state always determines a unique next action deterministically. Genuinely ambiguous grammars — like the classic dangling-else `if-then-else` grammar without disambiguation — have sentences with two distinct valid parses, so no amount of lookahead can pick one over the other without an external rule. Some unambiguous grammars are also not LR(k) for any fixed k, because they require unbounded lookahead to distinguish two productions that share arbitrarily long common prefixes. Such languages need either grammar rewriting, semantic disambiguation rules bolted onto the parser, or a more general algorithm like GLR or Earley parsing, both of which tolerate ambiguity at higher time complexity.
How does a shift-reduce conflict differ from a reduce-reduce conflict?
A shift-reduce conflict occurs when a state contains both a completed item `[A → α·, a]` (suggesting reduce) and another item `[B → β · a γ, b]` (suggesting shift) for the same lookahead token `a`. A reduce-reduce conflict occurs when two distinct completed items `[A → α·, a]` and `[B → γ·, a]` exist in the same state with the same lookahead, so the parser cannot decide which production to reduce by. Shift-reduce conflicts are common and often intentionally resolved via precedence declarations (e.g., operator precedence for `+`/`*`, or "prefer shift" for dangling-else). Reduce-reduce conflicts are rarer and almost always indicate a genuine grammar design flaw that should be fixed by restructuring rather than papered over with a default tie-break.
Why is the LALR(1) merge theorem (no new shift-reduce conflicts) true?
Merging states with identical cores never changes the set of shift actions in the merged state, because shift actions depend only on which items with the dot immediately before a terminal exist — and the core (item without lookahead) determines this set entirely. Since the cores are identical by definition of the merge, the merged state's shift actions are exactly the union of identical shift actions from each original state, i.e., unchanged. The only thing merging can alter is the lookahead sets attached to completed items, which affects only reduce actions. So any new conflict introduced by merging must be a reduce-reduce conflict (two completed items whose lookaheads now overlap after taking the union), never a shift-reduce conflict.
What role does the augmented start symbol `S'` play?
Adding `S' → S` with a fresh nonterminal `S'` guarantees the parser has one unambiguous "done" condition: the item `[S' → S ·, ]`. Without this, if `S` itself could be reduced in multiple contexts or the original grammar had recursive productions involving `S`, the parser could not cleanly distinguish "finished parsing the whole program" from "just finished reducing an inner instance of S." The augmentation also gives a canonical single start state (`closure({[S'\to \cdot S, ]})`) for the automaton construction, which is required for the closure/goto algorithm to have a well-defined single entry point.
How does LR parsing connect to building an abstract syntax tree?
Every reduce action in the LR engine corresponds exactly to recognizing a complete right-hand side `α` of some production `A → α`. At that moment the parser has, on an auxiliary stack, the already-built subtrees for each symbol in `α` (built by earlier shifts of terminals as leaves, or earlier reduces as internal nodes). The reduce action pops those `|α|` subtrees and attaches them as children of a freshly created node labeled `A`, pushing that node back. By the time the parser accepts, the single remaining subtree is the completed Abstract Syntax Trees for the entire input, built bottom-up in exact lockstep with the shift-reduce trace.
Why do modern compilers sometimes replace LALR-generated parsers with hand-written recursive-descent parsers?
LALR tables are compact and provably linear-time, but the generated automaton gives error messages phrased in terms of automaton states and expected token sets, which can be cryptic ("syntax error, unexpected token, expecting one of: ..."). Hand-written recursive-descent parsers can embed custom, context-aware error recovery and suggestions much more easily, and modern languages often need extensible or context-sensitive parsing hooks (macros, generics disambiguation) that are awkward to express as pure context-free grammars. The tradeoff is development effort: LALR generation is nearly free once the grammar is written, while hand-written parsers require manually implementing what the LR theory automates, in exchange for better diagnostics and flexibility.