Yasir Explains/Compiler Design/Parsing/LR Parsers
Parsing

LR Parsers

On this page

What is an LR Parser?How LR Parsing Works at RuntimeLR Items — Tracking How Far We've ParsedWorked Example — Step 1: The GrammarWorked Example — Step 2: Build the DFA, State by StateWorked Example — Step 3: Fill the LR(0) Parse TableWorked Example — Step 4: Trace the Parse of `id + id`The LR Variants — A Quick Map
Parsing

LR Parsers

Learn what LR parsers are, how the shift-reduce engine works at runtime, and follow a complete end-to-end example with detailed step-by-step explanations — building a pure LR(0) parse table for a small expression grammar and tracing every action of a real parse.

What is an LR Parser?

An LR parser is the most popular kind of bottom-up parser. The two letters mean:

Example
L = read input Left-to-right
R = build a Rightmost derivation (in reverse)

Real-world parser generators — yacc, bison, GNU Bison, Java CUP, Menhir — all generate LR parsers. If you have ever used a .y file, you have used an LR parser.


Why LR Is Powerful

FeatureLL parserLR parser
Left recursionMust be eliminated✓ Handled naturally
Common prefixesMust be left-factored✓ Handled naturally
Decision based onWhat we expect nextWhat we have already seen

The last row is the key. LR waits until it has seen the full right-hand side of a production on the stack before reducing — so it commits only when the evidence is overwhelming.


The LR Family

There are four common variants of LR, in increasing order of power:

Example
LR(0) ⊂ SLR(1) ⊂ LALR(1) ⊂ CLR(1)

They all share the same runtime engine and the same DFA of states. They differ only in how reductions are decided. We will cover the variants in the next three topics — for now, just know they exist.

How LR Parsing Works at Runtime

Before we worry about how to build an LR parser, let's see what the runtime engine looks like. It is surprisingly simple.


The Three Ingredients

Example
1. A stack → holds states (and symbols, for clarity)
2. Two tables:
ACTION[ state ][ terminal ] → shift / reduce / accept / error
GOTO [ state ][ non-terminal ] → next state after a reduction
3. One lookahead token → the next input symbol

The stack starts with the initial state. The parser reads the next input token, looks up ACTION, and performs one of three actions.


The Three Actions

Example
SHIFT t push the current token, push state t, advance input
REDUCE A → β pop |β| items off the stack
let s = state now on top
push A, push GOTO[s][A]
ACCEPT done — the input is a valid program ✓

(An empty ACTION cell means syntax error.)

That's it. Whether the parser is SLR, LALR, or CLR, the engine is exactly the same — only the tables change.


A Picture

Example
┌─────────────────────┐
Input → │ id + id $ │
└─────────────────────┘
│ lookahead
▼
┌──────────────────────┐
│ ACTION[s][a] │
│ GOTO[s][A] │ ← precomputed tables
└──────────────────────┘
│ decision
▼
┌──────────────────────┐
│ Stack: 0 E 1 + 4 │ ← top = state 4
└──────────────────────┘

If we can build those two tables, we have an LR parser. Building them is what the rest of this topic is about.

LR Items — Tracking How Far We've Parsed

To build the tables, we need a way to talk about "how much of a production have we matched so far?" That's what an LR item is.


What is an Item?

An item is a production with a dot · showing the current position.

For a production A → B C D, there are four items:

Example
A → · B C D ← haven't matched anything yet
A → B · C D ← matched B
A → B C · D ← matched B and C
A → B C D · ← matched everything — ready to REDUCE

A dot at the end is the signal that a complete handle is on top of the stack.


What is a State?

A state of the parser is a set of items — every production we might currently be in the middle of.

For example, a state might contain:

Example
{ E → E · + T,
E → E · , ← could reduce E → E if we don't shift +
… }

The parser is always in exactly one state. The state plus the lookahead token uniquely determines the action.


Two Operations Build States

We compute states using CLOSURE and GOTO.

Example
CLOSURE(I)
If an item in I has a dot before a non-terminal X,
then add every production of X (with the dot at the start)
to I. Repeat until nothing new is added.
Example
GOTO(I, X)
Take every item A → α · X β in I,
shift the dot past X to get A → α X · β,
then apply CLOSURE.

Starting from the augmented start state and applying GOTO on every symbol, we get a DFA whose states are sets of items. That DFA is the backbone of the parse table.


Canonical Items

The complete set of states produced by this process — every reachable set of items — is called the canonical collection of LR(0) items, and each individual state is a canonical item set (often just "canonical items"). When textbooks say "build the canonical collection", they mean exactly this: keep applying CLOSURE and GOTO from the start state until no new states appear.

The same idea, but with lookaheads attached to each item, gives the canonical collection of LR(1) items — which is where the C in CLR (Canonical LR) comes from. We will see that in the CLR topic.

The next section makes all of this concrete with one small grammar — start to finish.

Worked Example — Step 1: The Grammar

Let's build a pure LR(0) parser end-to-end on a small grammar, explaining every step in detail. LR(0) is the simplest LR variant — it uses no lookahead at all when deciding to reduce.


The Grammar

Example
(1) E → E + T
(2) E → T
(3) T → id

It generates simple additions of identifiers: id, id + id, id + id + id, and so on.

Small, but interesting:

  • E → E + T is left-recursive — something an LL parser cannot handle, but LR can.
  • It has multi-symbol reductions (E → E + T has three symbols on the right).
  • It has chained single-symbol reductions (id → T → E).
  • It is genuinely LR(0): every reduce state in its DFA has no outgoing transitions, so the parser can decide to reduce without any lookahead at all.

Augment the Grammar

Add one extra production at the very top:

Example
(0) E' → E ← new (augmented start)
(1) E → E + T
(2) E → T
(3) T → id

Why augment?

The original grammar's start symbol is E. But E appears inside another production (E → E + T), so "we just finished an E" is ambiguous — it could mean "we're done" or "we're about to read a +."

Adding E' → E gives the parser a unique signal for done: we accept only when we have reduced all the way to E'. The dot at the end of this rule (E' → E ·) cannot occur anywhere else, so there's no confusion.


Conventions

  • Non-terminals: E', E, T
  • Terminals: id, +, $ ($ = end of input)
  • Production numbers: r1 = E → E + T, r2 = E → T, r3 = T → id.

Worked Example — Step 2: Build the DFA, State by State

We build the DFA by starting with the augmented item E' → · E and applying CLOSURE and GOTO repeatedly until no new states appear.

The grammar has 3 productions, so the DFA is small — only 6 states. Let's build them one at a time.


State I₀ — the start

We always begin with the kernel item E' → · E. The dot at the start means "we haven't parsed anything yet, but we expect to parse an E."

Now apply CLOSURE step by step:

Example
Start with:
E' → · E ← dot before E, so add E productions
Add (E productions with the dot at the start):
E → · E + T
E → · T
Look at the new items:
E → · E + T ← dot before E again — E productions already in set
E → · T ← dot before T, so add T productions
Add (T productions):
T → · id
Look at T → · id ← dot before id (a terminal) — nothing to close.
Done.

I₀ = { E' → · E, E → · E + T, E → · T, T → · id }

This state means: "we are ready to start parsing an E, in any of its three forms."


Transitions out of I₀

Look at every item in I₀ and note the symbol right after each dot. That tells us which transitions the state has.

Example
E' → · E → transition on E
E → · E + T → transition on E
E → · T → transition on T
T → · id → transition on id

So I₀ has three outgoing transitions: on E, on T, and on id. We compute each next.


State I₁ — GOTO(I₀, E)

"What state do we land in if we read an E from I₀?"

Step 1 — take every item in I₀ with the dot before E:

Example
E' → · E
E → · E + T

Step 2 — move the dot past E:

Example
E' → E ·
E → E · + T

Step 3 — apply CLOSURE. In E' → E · the dot is at the end. In E → E · + T the dot is before a terminal (+). Neither needs closure.

I₁ = { E' → E ·, E → E · + T }

Notice: E' → E · is complete — this is the future ACCEPT state.


State I₂ — GOTO(I₀, T)

"What if we read a T from I₀?"

Items with dot before T: E → · T. Move the dot:

Example
E → T ·

CLOSURE: dot at end, nothing to do.

I₂ = { E → T · }

Complete item — reduce-only state.


State I₃ — GOTO(I₀, id)

"What if we read an id from I₀?"

Items with dot before id: T → · id. Move the dot:

Example
T → id ·

I₃ = { T → id · }

Another reduce-only state.


State I₄ — GOTO(I₁, +)

I₁ has E → E · + T, which has the dot before +. So I₁ has one transition: on +.

Take the item, move the dot past +:

Example
E → E + · T

Now CLOSURE: dot before T, so add T productions.

Example
T → · id

T → · id has the dot before terminal id — nothing more to close.

I₄ = { E → E + · T, T → · id }


State I₅ — GOTO(I₄, T)

I₄ has items with dots before T and id. Two outgoing transitions.

Take items with dot before T: E → E + · T. Move:

Example
E → E + T ·

I₅ = { E → E + T · }

Reduce-only state.


GOTO(I₄, id) — already exists!

Take items with dot before id: T → · id. Move:

Example
T → id ·

This is exactly the same item set as I₃. So we reuse I₃ — no new state is created.

This is important: two transitions can lead to the same state whenever they would produce identical item sets. The DFA is a graph, not a tree.


The Complete DFA — 6 States

Example
State Items Outgoing transitions
─────────────────────────────────────────────────────────────────
I₀ E' → · E, E → · E + T, E → I₁, T → I₂, id → I₃
E → · T, T → · id
I₁ E' → E ·, E → E · + T + → I₄
I₂ E → T · (none — reduce only)
I₃ T → id · (none — reduce only)
I₄ E → E + · T, T → · id T → I₅, id → I₃
I₅ E → E + T · (none — reduce only)

Visual Diagram of the DFA

Each box is a state with its items inside. Each arrow is a transition labelled by the symbol that triggers it. Note that I₃ is a single state — both I₀ (on id) and I₄ (on id) point to it.

How to read it:

  • Blue — the start state (I₀).
  • Cyan — the accept state (I₁, on lookahead $).
  • Amber — reduce-only states (I₂, I₃, I₅).
  • Arrow labels — the grammar symbol that triggers the transition.

Notice how I₃ has two incoming arrows (from I₀ and from I₄) but is only one node. That is the DFA's signature: identical item sets share a single state.

That's the entire canonical collection. Six states.

Worked Example — Step 3: Fill the LR(0) Parse Table

Now we turn the DFA into a parse table. Because we are building a pure LR(0) parser, the rule for reductions is very simple:

Whenever a state contains a complete item A → α ·, reduce by A → α on every terminal — no lookahead is consulted.

That is the entire difference between LR(0) and SLR/LALR/CLR. LR(0) reduces "blindly"; the more advanced variants add lookahead to be more selective. No FOLLOW sets are needed here at all.


The Three Table-Filling Rules

Example
Rule 1 — Shifts:
If state Iₛ has a transition on terminal a to state Iₜ,
then ACTION[s][a] = shift t.
Rule 2 — Gotos:
If state Iₛ has a transition on non-terminal A to state Iₜ,
then GOTO[s][A] = t.
Rule 3 — Reductions and Accept:
For every complete item A → α · in state Iₛ:
- If A = E' (the augmented start) : ACTION[s][$] = accept
- Otherwise (LR(0) rule) : ACTION[s][a] = reduce A → α
for every terminal a (including $)

Now we apply these rules state by state.


State I₀ — { E' → · E, E → · E + T, E → · T, T → · id }

No complete items, so no reductions. Just shifts and gotos:

Example
ACTION[0][id] = s3 (transition on terminal 'id')
GOTO[0][E] = 1 (transition on non-terminal E)
GOTO[0][T] = 2 (transition on non-terminal T)

State I₁ — { E' → E ·, E → E · + T }

Two items deserve attention here:

  • E' → E · is complete and uses the augmented start E'. By Rule 3 we set the accept entry:
    Example
    ACTION[1][$] = accept
  • E → E · + T has a transition on + to state 4:
    Example
    ACTION[1][+] = s4

No conflicts: the accept entry is on $, the shift is on + — different columns.


State I₂ — { E → T · }

Complete item E → T ·. This is production 2 (E → T). By the LR(0) rule we reduce on every terminal:

Example
ACTION[2][id] = r2
ACTION[2][+] = r2
ACTION[2][$] = r2

There are no outgoing transitions from I₂, so reducing blindly is safe.


State I₃ — { T → id · }

Complete item T → id · — production 3 (T → id). Reduce on every terminal:

Example
ACTION[3][id] = r3
ACTION[3][+] = r3
ACTION[3][$] = r3

State I₄ — { E → E + · T, T → · id }

No complete items. Just shifts and gotos:

Example
ACTION[4][id] = s3
GOTO[4][T] = 5

State I₅ — { E → E + T · }

Complete item E → E + T · — production 1. Reduce on every terminal:

Example
ACTION[5][id] = r1
ACTION[5][+] = r1
ACTION[5][$] = r1

The Complete Parse Table

Example
ACTION GOTO
State | id + $ | E T
──────┼──────────────────────────────────────────────
0 | s3 | 1 2
1 | s4 acc |
2 | r2 r2 r2 |
3 | r3 r3 r3 |
4 | s3 | 5
5 | r1 r1 r1 |

Reading the table:

  • s3 — shift, go to state 3.
  • r2 — reduce by production 2 (E → T).
  • acc — accept.
  • Empty cells — syntax error.

Every cell has at most one entry → no shift/reduce or reduce/reduce conflicts → the grammar is genuinely LR(0).


Why This Worked Without Lookahead

Notice that every reduce state (I₂, I₃, I₅) has no outgoing transitions. The state itself unambiguously says "reduce" — there is no shift to compete with the reduction, so the parser can decide blindly. That is exactly the property that makes a grammar LR(0).

Many real-world grammars are not LR(0). They have states with both a complete item and outgoing terminal transitions — a shift/reduce conflict in LR(0). To resolve those, the parser needs to peek at the next token. That is the lookahead added by SLR(1), LALR(1), and CLR(1), covered in the next three topics.

Worked Example — Step 4: Trace the Parse of `id + id`

Now we use the table to parse id + id. The stack starts with just state 0, and the input ends with $.

At each step:

  1. Look at the top state of the stack and the next input token.
  2. Look up ACTION[state][token] in the table.
  3. Do what it says — shift, reduce, or accept.

Mechanics:

  • shift t → push the token, then push state t.
  • reduce A → β → pop 2 × |β| items, then push A and GOTO[top][A].

The Trace

Example
Step Stack Input Action
──────────────────────────────────────────────────────────────────────
1 0 id + id $ ACTION[0][id] = s3 → shift, goto 3
2 0 id 3 + id $ ACTION[3][+] = r3 → reduce T → id
3 0 T 2 + id $ ACTION[2][+] = r2 → reduce E → T
4 0 E 1 + id $ ACTION[1][+] = s4 → shift, goto 4
5 0 E 1 + 4 id $ ACTION[4][id] = s3 → shift, goto 3
6 0 E 1 + 4 id 3 $ ACTION[3][$] = r3 → reduce T → id
7 0 E 1 + 4 T 5 $ ACTION[5][$] = r1 → reduce E → E + T
8 0 E 1 $ ACTION[1][$] = acc → ACCEPT ✓

What Each Step Does

  • Step 1 — Shift the first id; the parser enters state 3.
  • Step 2 — In state 3 the only item is T → id ·, so reduce T → id. The stack becomes 0 T 2 (because GOTO[0][T] = 2).
  • Step 3 — In state 2 we have E → T ·, so reduce E → T. Stack becomes 0 E 1.
  • Step 4 — Shift the +. The parser now waits for the right operand.
  • Step 5 — Shift the second id, entering state 3 again.
  • Step 6 — Reduce T → id again. This time GOTO[4][T] = 5, so the top state becomes 5.
  • Step 7 — Multi-symbol reduction: E → E + T pops 6 stack items (3 symbols × 2). The whole expression collapses to a single E.
  • Step 8 — Stack holds 0 E 1, input is empty. ACTION[1][$] = acc → accept.

Rightmost Derivation

Reading the reductions in order — T → id, E → T, T → id, E → E + T — and reversing them gives the rightmost derivation of id + id:

Example
E ⟹ E + T ⟹ E + id ⟹ T + id ⟹ id + id

Every bottom-up parser produces exactly this: a rightmost derivation, played back in reverse.

The LR Variants — A Quick Map

We built the table using the SLR(1) rule (reduce on FOLLOW). The other LR variants differ only in how the reduce entries are decided:

VariantReduce on lookahead…PowerTable size
LR(0)every terminal (no lookahead)weakestsmall
SLR(1)every terminal in FOLLOW(A)bettersmall
LALR(1)per-state lookaheads from merged LR(1) statesnearly bestsmall
CLR(1)per-state lookaheads from full LR(1) itemsbestlarge

Everything else — the DFA construction, the shift entries, the GOTO table, the runtime engine — is identical across all four.


Which One Should You Use?

Example
Teaching / small grammars → SLR(1)
Real compilers (yacc, bison) → LALR(1) ← industry standard
Hardest grammars → CLR(1) ← rarely worth its bulk

The next three topics build full parse tables for each variant. SLR(1) continues with this same expression grammar, so you can see it refine the LR(0) table directly. CLR(1) and LALR(1) then switch to a grammar that actually triggers lookahead splitting, since this one is already LR(0) and would hide their differences.