Canonical LR(1) and LALR

SLR failed on a perfectly unambiguous grammar because it decided reductions with a global \text{FOLLOW} set — the tokens that can follow a nonterminal anywhere — when what it needed was the tokens that can follow this reduction, in this state. The fix is to stop guessing and carry the exact lookahead inside each item. That single upgrade produces the most powerful class of parser that still uses one token of lookahead: canonical LR(1), Knuth's original 1965 construction. Its only sin is size — the tables can be enormous — so in practice we shrink them back down with a clever compromise called LALR, the method that powers yacc and bison.

The LR(1) item: an item with a lookahead

An LR(1) item is an LR(0) item paired with a single terminal called its lookahead:

[\,A \to \alpha\,{\cdot}\,\beta,\; a\,].

The lookahead only matters for complete items. The item [A \to \alpha{\cdot},\, a] says: "reduce by A \to \alphabut only if the next input token is exactly a." Where SLR would reduce on all of \text{FOLLOW}(A), an LR(1) state reduces on just the lookaheads that are genuinely valid for the path that reached it. Different routes to the same core of items can now carry different lookaheads, and that context-sensitivity is exactly the extra power.

CLOSURE gains a lookahead-propagation rule. For an item [A \to \alpha{\cdot}B\beta,\, a], when we add the fresh items B \to {\cdot}\gamma, their lookaheads are everything in \text{FIRST}(\beta a) — the symbols that can appear right after this B. GOTO is unchanged except that it carries the lookahead along with the dot.

A worked LR(1) closure

Take the compact grammar (augmented with S' \to S):

S \to C\,C, \qquad C \to c\,C \;\mid\; d.

(It generates c^{*}d\,c^{*}d.) The start state is \text{CLOSURE}\big(\{[S' \to {\cdot}S,\ \$]\}\big). Work the propagation through:

Item addedLookaheadWhy
S' \to {\cdot}S\$the seed (end of input)
S \to {\cdot}CC\$from [S'\to{\cdot}S,\$]: \text{FIRST}(\$)=\{\$\}
C \to {\cdot}cCc,\ dfrom [S\to{\cdot}CC,\$]: \text{FIRST}(C\,\$)=\{c,d\}
C \to {\cdot}dc,\ dsame reason

So the first C in S\to CC may be followed by the start of the second C (a c or a d), giving the lookahead set \{c,d\}. Follow the automaton to the end and you find the state reached after the second C carries the lookahead \$ instead — the very distinction SLR could not make, because it lumped both into \text{FOLLOW}(C)=\{c,d,\$\}.

type Prod = { lhs: string; rhs: string[] }; // 0: S'->S 1: S->C C 2: C->c C 3: C->d const G: Prod[] = [ { lhs: "S'", rhs: ["S"] }, { lhs: "S", rhs: ["C", "C"] }, { lhs: "C", rhs: ["c", "C"] }, { lhs: "C", rhs: ["d"] }, ]; const NONTERM = new Set(["S'", "S", "C"]); // FIRST of a sequence (this grammar is ε-free). function firstSeq(seq: string[], fallback: string): string[] { if (seq.length === 0) return [fallback]; const x = seq[0]; if (!NONTERM.has(x)) return [x]; const out = new Set<string>(); for (const prod of G) if (prod.lhs === x) for (const t of firstSeq(prod.rhs, fallback)) out.add(t); return [...out]; } type Item = { p: number; dot: number; look: string }; const key = (it: Item) => `${it.p}:${it.dot}:${it.look}`; function closure(seed: Item[]): Item[] { const out = [...seed]; const seen = new Set(out.map(key)); for (let i = 0; i < out.length; i++) { const { p, dot, look } = out[i]; const B = G[p].rhs[dot]; if (B !== undefined && NONTERM.has(B)) { const beta = G[p].rhs.slice(dot + 1); const looks = firstSeq(beta, look); // FIRST(β a) G.forEach((prod, q) => { if (prod.lhs === B) for (const b of looks) { const ni = { p: q, dot: 0, look: b }; if (!seen.has(key(ni))) { seen.add(key(ni)); out.push(ni); } } }); } } return out; } const show = (it: Item) => { const body = [...G[it.p].rhs]; body.splice(it.dot, 0, "."); return `[${G[it.p].lhs} -> ${body.join(" ")}, ${it.look}]`; }; console.log("I0 = CLOSURE({ [S' -> .S, $] }):"); for (const it of closure([{ p: 0, dot: 0, look: "$" }])) console.log(" " + show(it));

LALR: merge states with the same core

Canonical LR(1) is precise but wasteful. Across the automaton you find many pairs of states whose cores are identical — the same set of LR(0) items (dot positions) — differing only in their lookahead sets. For our grammar S\to CC,\ C\to cC\mid d, the two states built around C \to c{\cdot}C are twins: one arose while expecting \{c,d\} ahead, the other while expecting \$.

LALR ("LookAhead LR") does the obvious thing: merge any two states with the same core, taking the union of their lookahead sets. The merged automaton has exactly as many states as the LR(0)/SLR automaton — for this grammar, the ten canonical LR(1) states collapse back to seven — yet it keeps far more of LR(1)'s lookahead precision than SLR's blunt FOLLOW sets. This is the sweet spot the whole industry settled on.

Four parsers, one automaton skeleton

All four LR variants share the same driver and, except for canonical LR(1), the same number of states. They differ only in where the reduce-lookaheads come from — which is the entire story of their relative power and cost.

MethodReduce lookahead# statesPower
LR(0)none — reduce on every tokenbase (call it N)weakest; most grammars conflict
SLR(1)global \text{FOLLOW}(A)Nmodest; coarse lookahead
LALR(1)per-state, merged from LR(1)Nstrong; yacc/bison default
LR(1)exact per-item lookaheadup to many× Nstrongest 1-token parser

The middle two columns are the punchline: LALR gets LR(1)-grade lookahead at SLR-grade table size. For a real programming language, canonical LR(1) might need tens of thousands of states where LALR needs a few hundred — the difference between a table you can ship and one you cannot.

Purely table size, and it is not close. In the 1970s, when yacc was written, a canonical-LR(1) table for a language like C could easily need ten to twenty times the states of the LALR table — thousands versus hundreds — and memory was measured in kilobytes. LALR delivers almost all of LR(1)'s expressive power for the cost of SLR, so it was an easy call, and it stuck: bison, byacc, and the ML/OCaml yacc derivatives are all LALR by default. The grammars that are LR(1) but not LALR are rare and usually a symptom of an awkwardly written grammar that is easily massaged. In the handful of cases where it truly matters, modern bison offers an explicit %glr-parser or an IELR(1) mode that recovers full LR(1) power — but the everyday default remains LALR precisely because its tables are small.

Merging is not free. When you union the lookahead sets of two same-core states, two reduce items that had disjoint lookaheads in their separate states can suddenly overlap, producing a reduce-reduce conflict that neither original state had. A grammar can therefore be canonical-LR(1) yet fail to be LALR(1) — this is the one place where the merge loses power.

The reassuring half of the theorem: LALR merging can never introduce a shift-reduce conflict. Here is the argument. Whether a state shifts on token a depends only on its core (is there an item with the dot before a?), and merged states share a core by definition — so the shift actions are identical before and after the merge. A shift-reduce conflict needs a shift on a to collide with a reduce on a; if the merged state had that collision, then one of the pre-merge states — having the same shift on a and already containing that reduce item with a in its lookahead — would already have had the conflict, contradicting that it was LR(1). So only reduce-reduce conflicts can be born from a merge. In practice these are rare, and bison flags them loudly.