SLR Parsing

The LR(0) automaton tells us, for any stack, which state we are in and which items are live. That is almost a parser — but not quite. When a state contains a complete item A \to \beta{\cdot}, the automaton says "you could reduce here", and when the state also has a shift available, or a second complete item, LR(0) alone cannot choose. It has no lookahead: it would reduce on every input symbol, causing conflicts.

SLR parsing — "Simple LR", due to Frank DeRemer — is the smallest sensible fix. Keep the exact same LR(0) automaton, but reduce by A \to \beta only when the next input token is in \text{FOLLOW}(A). If a token could never legally follow an A, then reducing to A now would be pointless, so we don't. That one guard — a FOLLOW set on each reduction — turns the LR(0) automaton into a genuine, deterministic parser for a large and useful class of grammars, at essentially no extra cost in table size.

From automaton to table

An LR parser is table-driven. Two tables, indexed by the current state:

If following these rules ever asks you to write two different actions in one cell, the grammar is not SLR(1) — a conflict. Otherwise the table is a complete, deterministic parser.

The SLR table for S \to AA,\ A \to aA \mid b

Reuse the seven-state automaton from the previous page (augmented grammar S' \to S, and productions (1) S \to AA, (2) A \to aA, (3) A \to b). We need the FOLLOW sets:

\text{FOLLOW}(S) = \{\$\}, \qquad \text{FOLLOW}(A) = \{a,\, b,\, \$\}.

\text{FOLLOW}(A) is everything, because an A can be followed by the start of another A (an a or a b) or by end-of-input. Now fill the table. State I_4 reduces by A\to b on all of \{a,b,\$\}; I_6 reduces by A\to aA on the same set; but I_5 (S\to AA{\cdot}) reduces by S\to AA only on \$ — because that is all of \text{FOLLOW}(S).

StateACTIONGOTO
ab\$SA
0s3s412
1acc
2s3s45
3s3s46
4r3r3r3
5r1
6r2r2r2

Read s3 as "shift and go to state 3", r2 as "reduce by production 2 (A\to aA)", and acc as accept. No cell holds two actions, so this grammar is SLR(1).

Running the parser: a full stack trace of abab

The LR driver keeps a stack of states (we show the grammar symbols beside them for readability). On a shift j it pushes the token and state j. On a reduce A\to\beta it pops |\beta| state-symbol pairs, then pushes A and the state \text{GOTO}[\,\text{top},A\,]. Here is the entire parse of abab — read the string as two As, each an ab:

StepState stackSymbolsInputAction
10abab\$shift 3
20 3abab\$shift 4
30 3 4a\,bab\$reduce A\to b
40 3 6a\,Aab\$reduce A\to aA
50 2Aab\$shift 3
60 2 3A\,ab\$shift 4
70 2 3 4A\,a\,b\$reduce A\to b
80 2 3 6A\,a\,A\$reduce A\to aA
90 2 5A\,A\$reduce S\to AA
100 1S\$accept

Look at step 3: the state on top is I_4 and the lookahead is a. Because a \in \text{FOLLOW}(A), the SLR rule permits the reduce A\to b — and it is correct. That FOLLOW check is the only thing SLR adds beyond LR(0), and it is exactly what lets the parser reduce confidently instead of blindly.

One driver runs any LR table

The beautiful part of table-driven parsing: the driver never changes. Swap in an SLR, LALR, or canonical-LR(1) table and the same twelve-line loop parses the language. Below is the generic LR driver running the SLR table above on abab; it prints the same trace.

type Act = string; // "sN" shift, "rN" reduce, "acc", or "" = error // ACTION[state][terminal] const ACTION: Record<number, Record<string, Act>> = { 0: { a: "s3", b: "s4" }, 1: { $: "acc" }, 2: { a: "s3", b: "s4" }, 3: { a: "s3", b: "s4" }, 4: { a: "r3", b: "r3", $: "r3" }, 5: { $: "r1" }, 6: { a: "r2", b: "r2", $: "r2" }, }; // GOTO[state][nonterminal] const GOTO: Record<number, Record<string, number>> = { 0: { S: 1, A: 2 }, 2: { A: 5 }, 3: { A: 6 }, }; // Productions: 1..3 (0 = S'->S handled by "acc"). const PROD: Record<number, { lhs: string; len: number; name: string }> = { 1: { lhs: "S", len: 2, name: "S->AA" }, 2: { lhs: "A", len: 2, name: "A->aA" }, 3: { lhs: "A", len: 1, name: "A->b" }, }; function parse(input: string[]): boolean { const tokens = [...input, "$"]; const states: number[] = [0]; const syms: string[] = []; let ip = 0; for (let guard = 0; guard < 1000; guard++) { const s = states[states.length - 1]; const a = tokens[ip]; const act = ACTION[s]?.[a] ?? ""; const view = `[${states.join(" ")}] ${syms.join("")}`.padEnd(22); if (act.startsWith("s")) { const j = Number(act.slice(1)); console.log(`${view} shift ${a}`); syms.push(a); states.push(j); ip++; } else if (act.startsWith("r")) { const p = PROD[Number(act.slice(1))]; for (let k = 0; k < p.len; k++) { states.pop(); syms.pop(); } const t = states[states.length - 1]; syms.push(p.lhs); states.push(GOTO[t][p.lhs]); console.log(`${view} reduce ${p.name}`); } else if (act === "acc") { console.log(`${view} ACCEPT`); return true; } else { console.log(`${view} ERROR on '${a}'`); return false; } } return false; } parse(["a", "b", "a", "b"]); // try ["b","b"], or ["a","b"] (should error: only one A)

Try editing the input. ["b","b"] parses (two As, each a lone b); ["a","b"] fails, because S \to AA demands two As and only one is present — the driver reports an error exactly where the second A should begin.

A subtlety worth pinning down: the LR stack holds states, not just symbols, and the symbols are really only there for our eyes. After a reduce, the parser has popped back to some older state t and pushed a nonterminal A; the new state is \text{GOTO}[t, A], read from the table. In other words the stack of states is a running record of "where the LR(0) automaton would be if you replayed the current stack of symbols from the start" — but maintained incrementally, without any replay. That is the whole efficiency trick: each parsing action is O(1), so the parser runs in time linear in the input plus the number of reductions.

SLR's one idea — "reduce by A\to\beta whenever the lookahead is in \text{FOLLOW}(A)" — is also its weakness. \text{FOLLOW}(A) pools together every token that can follow any A anywhere in the grammar. But in a particular state, the tokens that can legitimately follow this reduction may be a strict subset. When SLR reduces on a FOLLOW token that is not valid in this context, it can collide with a shift and manufacture a conflict that a smarter parser would avoid.

The classic witness is the grammar S \to L=R \mid R, L \to *R \mid \texttt{id}, R \to L (pointers and assignment). One LR(0) state contains both R \to L{\cdot} (reduce) and S \to L{\cdot}=R (shift on =). Since = \in \text{FOLLOW}(R), SLR writes both "reduce R\to L" and "shift =" into the same cell — a shift-reduce conflict — even though the grammar is perfectly unambiguous. The trouble is that = follows R globally but not right here. The cure is to attach a precise, per-item lookahead instead of a global FOLLOW — which is exactly what canonical LR(1) and LALR do.