Parsingimplementation

LR Parsing

Bottom-up: never choose a production until the whole right-hand side is on the stack. It handles left recursion natively, accepts a strictly larger class of grammars than LL — and reports its problems as "conflict in state 143".

The question

What makes LR parsing more powerful than LL, and what is a shift/reduce conflict actually telling me?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

A stack of (state, symbol) pairs and a position in the token stream. The symbols on the stack are a *viable prefix* — a sequence that could still be extended into a complete right-hand side of some production. The states are positions in a finite automaton over grammar *items*, and each state records every production the parser might currently be in the middle of, and how far into each one it has got. That automaton is the whole of the technique: the stack is data, the state is the parser's knowledge of what remains possible.

What this phase may assume or do

A grammar is LR(k) when, for every viable prefix and every k tokens of lookahead, the automaton's state uniquely determines whether to shift or which single production to reduce by. The parser is entitled to defer every decision until an entire right-hand side is on the stack, which is why it may assume nothing about a production until it has the evidence for all of it — and why left recursion, fatal to a top-down parser, costs it nothing. When the state does not determine the action, the generator reports a conflict, and that conflict is a property of the grammar: no implementation setting resolves it, only a change to the grammar or an explicit, deliberate override.

Key points

  • LR is left-to-right with a rightmost derivation discovered in reverse: it reduces rather than derives, and never commits until a full right-hand side is on the stack.
  • The stack holds a viable prefix; the states are sets of items, each item a production with a dot showing progress.
  • Because it commits late, left recursion is natural and preferred — the opposite of the top-down families.
  • The family differs only in lookahead precision: LR(0), SLR(1), LALR(1), LR(1), and GLR which forks instead of choosing.
  • LALR(1) merges LR(1) states with identical cores, keeping small tables at the cost of possible reduce/reduce conflicts — and it is what Yacc and Bison actually generate.
  • A shift/reduce conflict means a state can both complete a production and continue another; it reports a real ambiguity, and the default resolution being usually-correct is what makes ignoring them dangerous.
  • Precedence declarations resolve expression conflicts outside the grammar, which is compact and means the productions alone no longer specify the language.

The other letter

LR is Left-to-right scan, Rightmost derivation — in reverse. That last clause is the whole idea and it is worth taking slowly.

A rightmost derivation of 1 + 2 * 3 expands the rightmost nonterminal at each step. Run it backwards — from the tokens to the start symbol — and each step becomes a *reduction*: a substring on the stack matching some production's right-hand side is replaced by its left-hand side. The parser never derives; it un-derives. And because it works from the input inward, it never has to guess which production it is in. It accumulates tokens until the answer is unambiguous, and only then commits.

That is the source of every advantage LR has. An LL parser must pick a production for expr before it has seen anything of the expression; an LR parser can shift 1, +, 2, *, 3 and decide afterwards. Left recursion, which prevents a top-down parser from making progress at all, is for an LR parser simply the most natural way to write a repeated construct — it keeps the stack shallow, because each reduction consumes the accumulated left side immediately.

A left-recursive grammar — the form LR prefers and LL cannot use — with the rightmost derivation LR discovers in reverse
S'::=EThe augmented start production. Reducing by it is what "accept" means.
E::=E '+' T | TLeft-recursive on purpose. An LR parser reduces `E + T` to `E` as soon as it has all three.
T::=T '*' F | F
F::=NUMBER | '(' E ')'
Deriving 1 + 2 * 3
  1. 1.Eapplying rightmost derivation, step 1 — read this column DOWNWARD to derive
  2. 2.E '+' Tapplying E -> E '+' T
  3. 3.E '+' T '*' Fapplying T -> T '*' F (rightmost nonterminal is T)
  4. 4.E '+' T '*' 3applying F -> NUMBER
  5. 5.E '+' F '*' 3applying T -> F
  6. 6.E '+' 2 '*' 3applying F -> NUMBER
  7. 7.T '+' 2 '*' 3applying E -> T
  8. 8.F '+' 2 '*' 3applying T -> F
  9. 9.1 '+' 2 '*' 3applying F -> NUMBER — and the parser performs these steps in REVERSE, bottom-up

Items, states, and what the parser knows

simplifiedThe full automaton for this four-production grammar has twelve states; three are shown. A grammar for a real language runs to several hundred or several thousand states, which is exactly why conflicts are reported by state number and why reading them requires the generator to print the item sets — Bison does this with --report=all, and without that output a conflict report is close to unactionable.

An item is a production with a dot marking how far the parser has got: T -> T • '*' F means "I have seen a T, and if the next token is * I can continue in this production". A state of the LR automaton is a *set* of items — all the productions the parser might simultaneously be in the middle of, given everything on the stack so far.

Two operations build the automaton. Closure: if a state contains E -> E '+' • T, then the parser is about to start a T, so every production of T is added with the dot at the front — T -> • T '*' F and T -> • F — and transitively their own. Goto: from a state, moving the dot past a symbol X gives the state you reach after that symbol is on the stack. Do this from the start item until no new sets appear and you have the automaton. The parse table is a direct transcription of it.

The consequence worth holding onto: a state is a statement of *ambient possibility*. When the parser is in a state containing four items, it is genuinely tracking four hypotheses at once, and it has not committed to any of them. That is why it can handle grammars LL cannot, and also why its error messages are poor — when something goes wrong, the parser can say which state it was in and which tokens that state permits, but it cannot say "you were in the middle of an argument list", because it was in the middle of four things.

Two states of the LR(0) automaton for the grammar above
StateItem setWhat it meansAction on `*`
I₀ (start)S' -> • E; E -> • E '+' T; E -> • T; T -> • T '*' F; T -> • F; F -> • NUMBER; F -> • '(' E ')'Nothing consumed. Every production that could begin the input is live at once.error — no item has a dot before *
I₂ (after a T)E -> T •; T -> T • '*' FA T is on the stack. It is either a complete E, or the start of a multiplication.shift — the second item permits it
I₂, on + or $E -> T •; T -> T • '*' FSame state, different lookahead.reduce by E -> T — the first item is complete
I₉ (after T * F)T -> T '*' F •A complete right-hand side is on top of the stack. One hypothesis remains.reduce by T -> T '*' F

The LR family

LR is a family, not an algorithm, and the members differ only in how much lookahead information the states carry — which trades table size against the grammars accepted.

LR(0) uses no lookahead: it reduces whenever a state contains a completed item, which conflicts almost immediately on real grammars. SLR(1) reduces by A -> α only when the lookahead is in FOLLOW(A), which is cheap and resolves many conflicts. LR(1) carries an explicit lookahead set on every item, which is maximally precise and produces enormous tables. LALR(1) merges LR(1) states that have the same item cores, keeping LR(0)-sized tables with most of LR(1)'s power — this is what Yacc and Bison generate by default, and it is the reason LALR is the LR variant that actually shipped. The merge is not free: it can introduce reduce/reduce conflicts that pure LR(1) would not have, though never shift/reduce conflicts.

GLR abandons determinism entirely: when a state has a conflict, it forks and pursues every interpretation in parallel, discarding branches that die. That accepts *any* context-free grammar, including ambiguous ones — at the cost of potentially returning several parses, and of worst-case cubic time. Bison has a GLR mode; tree-sitter is built on a GLR-family algorithm precisely because real source code under edit is frequently ambiguous or broken.

The LR family, and who ships whichtypical
VariantLookaheadTable sizeAcceptsShipped in
LR(0)nonesmallestvery few real grammarsteaching only
SLR(1)FOLLOW(A) at reduce timeLR(0)-sizedmany practical grammarsolder generators
LALR(1)merged per-state lookaheadsLR(0)-sizedmost practical grammarsYacc, Bison (default), LALRPOP, Menhir
LR(1)exact per-item lookaheadoften 10× largerall deterministic CFLsMenhir, LALRPOP (opt-in)
GLRforks on conflictLALR table plus a graph-structured stackevery context-free grammar, ambiguous includedBison --glr, tree-sitter, Elkhound

What a shift/reduce conflict actually is

implementationBison's default of preferring shift over reduce, and preferring the earlier-listed production on a reduce/reduce conflict, is documented behaviour and stable across versions. Other tools differ: Menhir refuses to resolve conflicts silently at all by default and requires explicit precedence declarations, and LALRPOP reports conflicts as build errors. Do not carry "the generator will just pick shift" to a tool you have not checked.

A state contains a completed item A -> α • (so the parser *could* reduce) and also an item B -> β • t γ with the lookahead t after the dot (so the parser *could* shift t). Both are consistent with everything seen so far. The automaton has no basis to choose. That is a shift/reduce conflict, and it means the grammar is genuinely ambiguous at this point, or requires more lookahead than this variant carries.

The canonical instance is the dangling else. With stmt -> 'if' expr 'then' stmt | 'if' expr 'then' stmt 'else' stmt, parsing if a then if b then c else d reaches a state holding a stack of if expr then stmt with else as the lookahead. Reducing gives the else to the *outer* if; shifting gives it to the inner one. Both are valid derivations of the same input under this grammar — the grammar is ambiguous, and the conflict is the tool correctly reporting that.

Generators resolve it by default rather than refusing: Yacc and Bison prefer shift, which attaches the else to the nearest if — which happens to be what C, Java and almost every language specify, so the default is right and the warning is noise. That is a trap worth naming: because the default is usually correct, teams learn to ignore conflict warnings, and then a genuine ambiguity in a new construct is resolved arbitrarily and nobody notices. The disciplined practice is %expect N — declare the number of conflicts you have accepted, and let the build fail when the count changes.

A reduce/reduce conflict is worse: two completed items in the same state, so the parser has two different productions it could reduce by and no way to choose. There is no sensible default (Bison picks the production listed first), and it almost always means the grammar is genuinely ambiguous or that LALR state merging has lost information that LR(1) would have kept.

The dangling else, and the two ways to resolve it
1/* Ambiguous: 'if a then if b then c else d' has two parses. */
2stmt : 'if' expr 'then' stmt
3 | 'if' expr 'then' stmt 'else' stmt
4 | other
5 ;
6/* bison: conflicts: 1 shift/reduce (defaults to shift = nearest 'if') */
7
8
9/* Resolution 1 — declare the intent and let the build enforce the count. */
10%expect 1
11
12
13/* Resolution 2 — rewrite the grammar so it is unambiguous.
14 A 'matched' statement has every 'if' paired with an 'else';
15 only an 'unmatched' one may dangle, and it can never be the
16 'then' arm of a statement that also has an 'else'. */
17stmt : matched | unmatched ;
18matched : 'if' expr 'then' matched 'else' matched
19 | other
20 ;
21unmatched : 'if' expr 'then' stmt
22 | 'if' expr 'then' matched 'else' unmatched
23 ;
24/* bison: 0 conflicts — and a grammar that is now much harder to read. */

Both resolutions ship in real languages. The rewrite is honest — the grammar now says exactly what the language means — and it roughly doubles the statement rules and makes them substantially harder to read or to extend. That is the tradeoff in miniature: an unambiguous grammar is a better specification and a worse document.

Precedence declarations: conflicts resolved outside the grammar

There is a third route, and it is the one most real Yacc grammars take. Write the expression grammar in its ambiguous one-line form — E -> E '+' E | E '*' E | NUMBER — which conflicts everywhere, and then declare precedence and associativity *outside* the productions with %left '+' and %left '*'. The generator uses those declarations to resolve every shift/reduce conflict in the expression states: on a conflict, compare the precedence of the production being reduced against the precedence of the lookahead token, and shift if the token binds tighter.

This is the same computation [[pratt-parsing]] performs with binding powers, done at table-construction time rather than at parse time. It is genuinely good engineering — a fifteen-level C expression grammar becomes a handful of productions plus fifteen declaration lines instead of fifteen stratified nonterminals — and it has one specific cost: the grammar in the file no longer describes the language on its own. Read the productions alone and 1 + 2 * 3 is ambiguous. The declarations are load-bearing, and a reader who skips them draws the wrong tree.

How it works

The steps, in the order the compiler takes them.

  • Augment the grammar with a new start production so that reducing by it is unambiguously "accept".
  • Build item sets: start from the closure of the initial item, and repeatedly compute goto over every symbol until no new sets appear.
  • Transcribe the automaton into two tables — ACTION (state × terminal → shift, reduce, accept, or error) and GOTO (state × nonterminal → state).
  • Report a conflict wherever an ACTION cell would receive two entries; resolve it by grammar change, by precedence declaration, or by an accepted default.
  • At parse time, push the start state. Look at the top state and the lookahead token.
  • On shift: push the token and the state the ACTION table names, and advance the input.
  • On reduce by A -> α: pop |α| symbol/state pairs, then push A and the state GOTO names from the newly exposed top state. The input does not move.
  • On accept: the augmented start production has been reduced and the lookahead is end of input.
  • On error: the ACTION cell is empty; the non-error entries in that row are exactly the tokens that would have been legal, which is where the "expected one of" list comes from.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A conflict warning is ignored because the default resolution has always been right, and one release later a new construct is silently parsed the wrong way — the language reference and the implementation now disagree with no test failing.
  • A grammar change adds two conflicts and removes one; the total is unchanged, %expect still passes, and one construct now parses differently.
  • The parser reports "syntax error, unexpected '{', expecting ')' or ',' or ';'" — machine-derived, correct, and giving the user no idea which construct they were inside.
  • LALR state merging introduces a reduce/reduce conflict that does not exist in LR(1); the grammar author, reading the productions, cannot see why, because the cause is in the automaton and not in the grammar.
  • An error occurs deep in a nested construct and the recovery pops states until it finds one with an error production, discarding an entire function body along with the rest of that file's real errors.
  • Table sizes grow past what the build tolerates when someone switches from LALR(1) to full LR(1) to fix a conflict, and compile times for the compiler itself become the new problem.

When it helps

  • Grammars that are naturally left-recursive — expression and list constructs — where LR keeps the stack shallow and needs no rewriting.
  • Languages whose grammar is the specification and changes on someone else's schedule: a declarative grammar file regenerated on each revision beats re-deriving a hand-written parser.
  • Establishing that a grammar is unambiguous. A conflict-free LALR(1) build is a mechanical proof of a property that no amount of testing a hand-written parser can give you.
  • Data and query languages — SQL dialects, protocol grammars, configuration formats — where the grammar is large, changes often, and the diagnostics matter less than conformance.

When it hurts

  • Anywhere the error messages face users. The state-derived "expecting one of" list is the technique's structural weakness, and improving it means writing error productions per construct — which is most of the hand-written parser you were avoiding.
  • Frontends that must reparse on every keystroke and tolerate broken input. A deterministic LR parser has no tree to offer after an error, which is why IDE-grade tools use GLR or hand-written descent instead.
  • Languages with genuine context sensitivity. C's T * x needs the symbol table during parsing, and threading that into a generated parser is a lexer hack bolted onto a tool that was designed to avoid exactly this.

What it costs

Every one of these is paid by something.

  • Deferring every decision buys a strictly larger grammar class and native left recursion, and pays with a parser that does not know what construct it is inside — which is the direct cause of the family's poor diagnostics and weak error recovery.
  • LALR state merging buys tables an order of magnitude smaller than LR(1), and pays with reduce/reduce conflicts that are invisible in the grammar text and can only be diagnosed by reading the generated automaton.
  • A declarative grammar file buys a mechanical ambiguity check and cheap regeneration when the language changes, and pays a build step, a tool dependency, and debugging that happens against generated code and a state number rather than your own functions.
  • Precedence declarations buy a compact expression grammar — a handful of productions instead of fifteen stratified levels — and pay by moving part of the language definition out of the productions, so the grammar alone is ambiguous and a reader who skips the declarations infers the wrong tree.
  • GLR buys acceptance of every context-free grammar including ambiguous ones, and pays worst-case cubic time plus the obligation to decide what to do when it returns more than one parse.

What else you could do

What a different compiler or language does instead, and when that is better.

  • [[ll-parsing]] and [[recursive-descent]] commit early, which loses grammar power and gains exactly the thing LR lacks: knowledge of the construct in progress, and therefore diagnostics and recovery — see [[ll-vs-lr]].
  • [[pratt-parsing]] handles the expression grammar that LR precedence declarations exist to compact, with the same precedence computation done at parse time in ordinary code.
  • GLR or an Earley parser accepts any context-free grammar without conflict reports at all, which is the right choice for natural language, for ambiguous legacy formats, and for editors parsing broken code.
  • PEG makes conflicts unrepresentable via ordered choice, which is either the fix or the criticism depending on whether you wanted to be told about the ambiguity.

See it for yourself

The flag, dump or tool that shows you this directly.

  • bison --report=all -o parser.c grammar.y writes parser.output, containing every state, its item set, and every conflict with the actions it chose. This file is the only practical way to read a conflict report.
  • bison -Wcounterexamples (Bison 3.8+) prints an actual input string that exhibits the conflict — the single most useful conflict-debugging feature in any generator.
  • Compare table sizes directly: build the same grammar with Menhir in --table LR(1) mode and in LALR mode and look at the state counts, which makes the merging tradeoff concrete rather than theoretical.
  • tree-sitter: tree-sitter generate reports conflicts and requires you to declare them in the grammar's conflicts array, so GLR forking is opt-in per construct rather than global.
  • Our shift/reduce stepper at /compilers/parsing shows the stack, the remaining input and the chosen action for each step, with the item set of the current state alongside.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "LR is more powerful, so it is the better choice." It accepts more grammars. It also does not know what construct it is parsing, which is why the compilers you use every day are not LR — see [[ll-vs-lr]].
  • "A shift/reduce conflict is a bug in the generator." It is the generator correctly reporting that the grammar does not determine the parse. The bug, if there is one, is in the grammar.
  • "Bison defaults to shift, so conflicts are harmless." The default is usually what you wanted for dangling else and usually irrelevant elsewhere — and "usually" is doing all the work. A conflict count you do not track is a language definition you do not control.
  • "LALR is a weaker approximation of LR(1), so it accepts fewer languages." It accepts fewer *grammars*. Every LALR(1) language is an LR(1) language and vice versa; the merge costs you particular grammars, not expressive power.
  • "The parse stack holds the tree." It holds a viable prefix of grammar symbols. The tree is built by semantic actions attached to reductions, and if you attach none, an LR parser happily recognises the input and produces nothing at all.

Misconceptions

The claim, and what is actually true.

LR parsers build the tree as they go, top-down.
They build it bottom-up, at reductions, and only if semantic actions say to. The root node is created last, when the start production is reduced.
Conflicts mean the grammar is wrong.
Conflicts mean the grammar is not deterministic for this variant. Dangling else conflicts in a grammar that specifies exactly the language you want; the fix may legitimately be a declaration rather than a change.
LR handles left recursion because it is more powerful.
It handles left recursion because it commits late. The power difference and the left-recursion difference come from the same property, but the second is not a consequence of the first — it is the same fact stated twice.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

An LR parser works from the tokens up rather than the grammar down. It pushes tokens onto a stack until the top of the stack matches the right-hand side of a rule, then replaces them with the rule's left-hand side. Because it waits until it has the whole rule before deciding, it never has to guess — which is why it copes with grammars that a top-down parser cannot use, including left-recursive ones.

practical

If you are using Bison or a relative, three habits matter. Turn on --report=all and read parser.output when a conflict appears; a state number alone is unactionable. Use -Wcounterexamples if your version has it — it hands you an input that exhibits the conflict, which usually makes the cause obvious in seconds. And put %expect N in the file so the build fails when the conflict *count* changes, because the failure mode is never a new warning appearing, it is a warning you already tolerate quietly changing meaning.

advanced

The deep asymmetry with top-down parsing is about what the parser knows at the moment of failure. An LR state is a set of live hypotheses, so a syntax error says "none of these four productions can continue with this token" — which the tool can only render as a list of legal tokens. A recursive-descent parser is, at the same moment, three frames deep inside parseArgumentList and can say so. Every downstream property follows from that: message quality, recovery strategy, and whether an IDE can get a usable tree from a broken file. GLR partly escapes it by keeping the failed branches alive as a graph-structured stack, which is why tree-sitter can produce a tree for source that does not parse — but it escapes it by spending memory on hypotheses rather than by knowing which one is real.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

implementationBison generates LALR(1) by default and offers canonical LR(1) via %define lr.type canonical-lr, IELR(1) via lr.type ielr, and GLR via %glr-parser. Menhir generates LR(1) by default with automatic state merging. LALRPOP defaults to LR(1) and offers LALR opt-in. "LR" without a variant named is not a specification of behaviour, and the variant decides whether a given grammar builds.
typicalThat precedence declarations resolve expression conflicts cleanly holds for ordinary infix arithmetic. It degrades for constructs where the conflict is not really about operator precedence — casts, generics with <, and lambda syntax are the recurring cases — and there the declarations resolve the conflict by picking an arbitrary side, which is worse than a rewrite because it looks deliberate.
simplifiedOur automaton is presented as sets of items with a dot. Real generators represent it as compressed action and goto tables with default reductions and error entries elided, which is why the state number in a conflict message does not correspond to anything you can find in the grammar file without the generated report.

If you were asked this in an interview

  • Why can an LR parser handle left recursion when a recursive-descent parser cannot?
  • Bison reports one shift/reduce conflict in my statement grammar. Walk me through what that means and what my three options are.
  • What does LALR merge, and what does the merge cost?

Connections

Domains that do not exist yet
  • Testing & Reliability Engineering — Treating a warning count as a build gate
    %expect N is the general practice of pinning a known-defect count so that a change in it fails the build. The practice belongs there; here it is the only thing standing between a tolerated conflict and a silent language change.