One step up from regular languages: add a stack, and you can count and nest.
This is the theory behind every parser, from gcc to json.loads.
Rewrite rules. One variable on the left.
A finite automaton plus one stack.
CYK, LL(1), LR: find the tree.
Pumping lemma. What a stack cannot do.
Formal definitions, proof sketches, and runnable Python for every algorithm. Every output shown was produced by running the code (Python 3.11+).
A DFA has a fixed number of states. So it cannot count without bound.
(()(())) are not regular.if blocks, JSON arrays, HTML tags: all nest to any depth.Give the machine a stack. Push on open, pop on close.
The machine is a pushdown automaton. The matching grammar is a context-free grammar. They describe the same class: the context-free languages (CFLs).
Each ring strictly contains the one inside. We climb one ring in this deck.
A context-free grammar is a 4-tuple G = (V, Σ, R, S):
The language is L(G) = { w ∈ Σ* : S ⇒* w }.
Context-free means the left side is a single variable. You may rewrite A no matter what surrounds it. Context-sensitive rules like aAb → acb are not allowed.
| Grammar | Language |
|---|---|
S → aSb | ε | anbn |
S → (S)S | ε | balanced parentheses |
S → aSa | bSb | a | b | ε | palindromes over {a,b} |
S → aSb | aS | ε | aibj, i ≥ j |
E → E+T | T | arithmetic expressions |
A → α | β is shorthand for two rules. This notation is BNF (Backus–Naur form), first used for ALGOL 60.
u A v ⇒ u α v if A → α is a rule. ⇒* is zero or more steps.
A string over V ∪ Σ reached from S is a sentential form.
Leftmost derivation: always rewrite the leftmost variable. Rightmost: always the rightmost.
(())() with S → (S)S | εS ⇒ (S)S ⇒ ((S)S)S ⇒ (()S)S
⇒ (())S ⇒ (())(S)S ⇒ (())()S ⇒ (())()
This is leftmost. Each step expands the first S.
from collections import deque
G = {"S": ["(S)S", ""]} # balanced parentheses
def generate(G, start="S", max_len=6):
"""BFS over leftmost derivations; return terminal strings."""
seen, out = {start}, set()
q = deque([start])
while q:
form = q.popleft()
i = next((k for k, c in enumerate(form) if c in G), None)
if i is None: # no variables left
out.add(form); continue
for rhs in G[form[i]]:
new = form[:i] + rhs + form[i+1:]
if sum(c not in G for c in new) <= max_len and new not in seen:
seen.add(new); q.append(new)
return sorted(out, key=lambda s: (len(s), s))
print(generate(G))
['', '()', '(())', '()()', '((()))', '(()())', '(())()', '()(())', '()()()']
There are 1, 1, 2, 5 strings of 0, 2, 4, 6 characters: the Catalan numbers again.
A parse tree for G:
Fact. Parse trees for w are in one-to-one match with leftmost derivations of w. The same holds for rightmost derivations.
A derivation fixes an order. A tree forgets the order and keeps the structure. Compilers care about structure: the tree tells them that * happens before +.
id + id * idGrammar: E → E+T | T, T → T*F | F, F → id. The * sits deeper, so it is evaluated first.
| Goal | Pattern |
|---|---|
| Either X or Y | S → X | Y |
| X then Y | S → X Y |
| Any number of X | R → X R | ε |
| Matched pairs, same count | S → a S b | ε |
| Pairs around a middle part | S → a S c | T |
Key idea: a rule like a S b adds one symbol on each side at the same time. That is how a grammar "counts".
S → aS | Sb | ε gives all of a*b*, not anbn.| ε (or a short string), no derivation ever ends.Each c matches either an a or a b. The a's are outermost, so pair them with the last c's first. Then pair the b's with the rest.
S → a S c | T # one a, one c T → b T c | ε # one b, one c
aabccc (i=2, j=1, k=3)S ⇒ aSc ⇒ aaScc ⇒ aaTcc ⇒ aabTccc ⇒ aabccc
Checked by brute force: this grammar makes exactly the strings with k = i + j, up to length 7.
S → a S b b | ε. Each step adds one a and two b's.
S → a S b S | b S a S | ε. The first symbol is matched with its partner. The two S parts are balanced on their own.
G is ambiguous if some w ∈ L(G) has two different parse trees. Equivalently: two leftmost derivations.
E → E+E | E*E | a on a+a*a(a+a)*a ✗
a+(a*a) ✓
from functools import lru_cache
# Ambiguous: E -> E + E | E * E | a
def count_trees(w):
@lru_cache(None)
def n(i, j): # number of trees for w[i:j]
total = 1 if w[i:j] == "a" else 0
for k in range(i + 1, j - 1):
if w[k] in "+*": # w[k] is the root operator
total += n(i, k) * n(k + 1, j)
return total
return n(0, len(w))
for w in ["a+a", "a+a*a", "a+a*a+a", "a+a+a+a+a"]:
print(w, count_trees(w))
a+a 1 a+a*a 2 a+a*a+a 5 a+a+a+a+a 14
With n operators there are Cn trees (Catalan: 1, 2, 5, 14, …). Each tree can give a different value.
Some CFLs are inherently ambiguous: every grammar for them is ambiguous. Example: {aibjck : i = j or j = k}. The strings anbncn always get two trees.
E → E + T | E - T | T # lowest: + - T → T * F | T / F | F # middle: * / F → ( E ) | - F | num # highest: atoms
T can never contain a bare +. So * binds tighter.E → E + T recurses on the left. So a-b-c parses as (a-b)-c.** or =): recurse on the right, P → F ** P | F.a - a - a has one treeThe language is unchanged. Only the trees change. Parser generators like yacc and bison offer a shortcut: keep the ambiguous grammar and declare %left '+' '-', %left '*' '/'.
S → if E then S | if E then S else S | other
The input if a then if b then x else y has two trees. Which if owns the else?
if a then if b then x else y
if a then if b then x else y
Every mainstream language picks the inner if: "an else matches the nearest unmatched then".
S → M | U M → if E then M else M | other U → if E then S | if E then M else U
M is a statement whose every then has an else. Only an M may appear between then and else. So an else can only close the nearest open if.
end if (Ada), braces required (Go, Rust, Swift), indentation (Python).Generating: A ⇒* w for some terminal string. Reachable: S ⇒* αAβ.
Drop non-generating symbols first, then unreachable ones. The order matters.
S → AB | a A → b # B generates nothing: # drop S → AB, then A S → a
Find the nullable variables (those with A ⇒* ε) by a fixed point. For each rule, add every copy with some nullable symbols deleted. Then drop A → ε.
S → aSb | ε
# becomes
S → aSb | ab
If ε ∈ L, keep one rule S0 → ε with a fresh start S0.
A unit rule is A → B. Compute all unit pairs A ⇒* B. Then give A every non-unit rule of B.
E → T | E+T
T → id
# becomes
E → id | E+T
T → id
Do the steps in order: ε, then unit, then useless. Each step keeps L(G) the same (except maybe ε).
A grammar is in CNF if every rule has one of these shapes:
A → B C (two variables)
A → a (one terminal)
S → ε (only if ε ∈ L, S not on any right side)
Theorem (Chomsky 1959). Every CFG has an equivalent CNF grammar.
In this order, DEL on a long rule with k nullable variables can make 2k copies. Doing BIN before DEL avoids that and keeps growth at O(n2).
S → aSb | ab# TERM: name the terminals S → A S B | A B A → a B → b # BIN: split the length-3 rule S → A C | A B C → S B A → a B → b
The last slide skipped the hard steps. This grammar has an ε-rule, so we need them all. It makes anbn with n ≥ 0.
S → a S b | ε
New start, so S never appears on a right side as the start.
S0 → S S → a S b | ε
S is nullable. For each rule that uses S, add a copy with that S left out. Keep S0 → ε because ε is in the language.
S0 → S | ε S → a S b | a b
S0 → SCopy the rules of S up to S0.
S0 → a S b | a b | ε S → a S b | a b
Name the terminals A → a, B → b. Then split A S B with C → S B.
S0 → A C | A B | ε S → A C | A B C → S B A → a B → b
S0 ⇒ AC ⇒ aC ⇒ aSB ⇒ aABB ⇒ aaBB ⇒ aabB ⇒ aabb. That is 7 steps, and 2n − 1 = 7 for n = 4.
Common mistake: in DEL, just deleting S → ε. Then ab is lost. With k nullable symbols in one rule, add a copy for every subset left out.
A grammar is in GNF if every rule has the shape
A → a B1 B2 … Bk (k ≥ 0)
One terminal first, then only variables. (Plus S → ε if needed.)
Theorem (Greibach 1965). Every CFL without ε has a GNF grammar.
A → A α | β # generates β α* # becomes A → β A' A' → α A' | ε
Applied to expressions:
E → T E' E' → + T E' | ε T → F T' T' → * F T' | ε F → ( E ) | id
This is the LL(1) grammar we use for FIRST sets later. Full GNF also substitutes leading variables until a terminal comes first.
Example in GNF: {anbn : n ≥ 1} is S → aSB | aB, B → b.
Cocke, Younger and Kasami (1960s). Input: a grammar in CNF and a string w = w1…wn.
Let T[i, ℓ] be the set of variables that derive the substring of length ℓ starting at i.
T[i, 1] = { A : A → wi }
T[i, ℓ] = { A : A → BC, B ∈ T[i,k], C ∈ T[i+k, ℓ−k] }
Accept iff S ∈ T[1, n].
Cost. O(n3 · |G|) time and O(n2) space. So membership for any CFL is in P.
Valiant (1975) reduced it to matrix multiplication, O(n2.37). Real parsers use linear-time LL/LR on restricted grammars.
aabb (grammar from the CNF slide)| ℓ \ i | 1: a | 2: a | 3: b | 4: b |
|---|---|---|---|---|
| 4 | S | |||
| 3 | ∅ | C | ||
| 2 | ∅ | S | ∅ | |
| 1 | A | A | B | B |
ab = A·B, and S → AB.abb = S·B, and C → SB.aabb = A·C, and S → AC. Accept.# CNF grammar for { a^n b^n : n >= 1 }
# S -> A B | A C C -> S B A -> a B -> b
RULES = [("S", "A", "B"), ("S", "A", "C"), ("C", "S", "B")]
TERM = {"a": {"A"}, "b": {"B"}}
def cyk(w, start="S"):
n = len(w)
if n == 0:
return False
# T[i][l] = variables deriving w[i:i+l]
T = [[set() for _ in range(n + 1)] for _ in range(n)]
for i, c in enumerate(w):
T[i][1] = set(TERM.get(c, ()))
for l in range(2, n + 1): # span length
for i in range(n - l + 1): # span start
for k in range(1, l): # split point
for A, B, C in RULES:
if B in T[i][k] and C in T[i + k][l - k]:
T[i][l].add(A)
return start in T[0][n]
tests = ["ab", "aabb", "aab", "abab", "aaabbb", "ba"]
print([w for w in tests if cyk(w)])
['ab', 'aabb', 'aaabbb']
Store a back-pointer (k, B, C) with each variable added. Walk the pointers from T[0][n] to rebuild a parse tree.
Store counts instead of sets and CYK counts parse trees. A count above 1 proves the string is ambiguous.
Replace sets by probabilities and take the max: that is the Viterbi parser for probabilistic CFGs, used in classic NLP.
A finite automaton forgets. To match nesting, remember what is still open. The most recent open thing closes first. That is a stack.
PAIRS = {")": "(", "]": "[", "}": "{"}
def balanced(s):
stack = []
for c in s:
if c in "([{":
stack.append(c) # push
elif c in PAIRS:
if not stack or stack.pop() != PAIRS[c]:
return False # pop must match
return not stack # accept on empty stack
print([balanced(s) for s in ["([]{})", "([)]", "((", ""]])
[True, False, False, True]
This program is a deterministic PDA with one state. Its finite control is trivial. All the power is in the stack.
Only the top is visible. The machine cannot peek deeper without popping, and popped data is gone. That limit is exactly what the pumping lemma exploits.
A PDA is a 7-tuple P = (Q, Σ, Γ, δ, q0, Z0, F):
(p, γ) ∈ δ(q, a, X): in state q, read a, pop X, push γ, go to p. Written a, X → γ on an edge.
By default a PDA is nondeterministic. That matters: unlike for finite automata, determinism loses power here.
Configuration (instantaneous description): (q, w, γ) = state, input left, stack with top on the left.
(q, aw, Xβ) ⊢ (p, w, γβ) if (p,γ) ∈ δ(q,a,X)
| Mode | Accept w iff |
|---|---|
| Final state | (q0, w, Z0) ⊢* (f, ε, γ), f ∈ F |
| Empty stack | (q0, w, Z0) ⊢* (q, ε, ε) |
For nondeterministic PDAs the two modes accept the same class. Convert one way by adding a bottom marker. Convert back by emptying the stack in an accept state.
A for each a.A for each b.$ means the counts match. Move to qf.aabb| State | Input left | Stack | Move |
|---|---|---|---|
| q0 | aabb | $ | read a, push A |
| q0 | abb | A$ | read a, push A |
| q0 | bb | AA$ | ε to q1 |
| q1 | bb | AA$ | read b, pop A |
| q1 | b | A$ | read b, pop A |
| q1 | ε | $ | ε, see $, to qf |
| qf | ε | $ | accept |
On aab the run ends in q1 with A$: stuck, reject. On abb the second b finds $, no move: reject.
from collections import deque
class PDA:
"""Nondeterministic PDA, accept by final state.
delta[(q, a, X)] = {(p, gamma), ...}; a or X may be ''.
gamma replaces X; gamma[0] becomes the new top."""
def __init__(self, delta, start, z0, accept):
self.delta, self.start = delta, start
self.z0, self.accept = z0, set(accept)
def accepts(self, w, limit=10_000):
todo = deque([(self.start, 0, self.z0)]) # (state, pos, stack)
seen = set()
while todo and limit:
limit -= 1
cfg = todo.popleft()
if cfg in seen:
continue
seen.add(cfg)
q, i, stack = cfg
if i == len(w) and q in self.accept:
return True
top, rest = stack[:1], stack[1:]
for a in ([w[i]] if i < len(w) else []) + [""]:
for X in {top, ""}:
for p, gamma in self.delta.get((q, a, X), ()):
new = gamma + (rest if X else stack)
todo.append((p, i + len(a), new))
return False
anbn = PDA({("q0", "a", ""): {("q0", "A")},
("q0", "", ""): {("q1", "")},
("q1", "b", "A"): {("q1", "")},
("q1", "", "$"): {("qf", "$")}},
start="q0", z0="$", accept={"qf"})
tests = ["", "ab", "aabb", "aab", "abab", "aaabbb"]
print([w for w in tests if anbn.accepts(w)])
['', 'ab', 'aabb', 'aaabbb']
(state, pos, stack), the stack as a string with the top first.seen skips repeats.limit caps the search. A real decider would convert to a grammar and run CYK.$ first. Seeing it again means the stack is empty.| State | Move | Why |
|---|---|---|
| q0 | a, ε → a and b, ε → b | save the first half |
| q0 → q1 | ε, ε → ε | guess the middle |
| q1 | a, a → ε and b, b → ε | match in reverse |
| q1 → qf | ε, $ → $ | all matched: accept |
abba (the good guess)| State | Input left | Stack | Move |
|---|---|---|---|
| q0 | abba | $ | read a, push a |
| q0 | bba | a$ | read b, push b |
| q0 | ba | ba$ | guess middle, to q1 |
| q1 | ba | ba$ | read b, pop b |
| q1 | a | a$ | read a, pop a |
| q1 | ε | $ | see $, to qf |
| qf | ε | $ | accept |
Guess the middle after one symbol: q1 has stack a$ and input bba. It must read b but the top is a. No move, so that branch dies. One good branch is enough to accept.
With a middle marker, { w#wR } needs no guess, so a DPDA can do it. Without the marker, no DPDA can.
pal = PDA({**{("q0", c, ""): {("q0", c)} for c in "ab"}, # push
("q0", "", ""): {("q1", "")}, # guess middle
**{("q1", c, c): {("q1", "")} for c in "ab"}, # match
("q1", "", "$"): {("qf", "$")}},
start="q0", z0="$", accept={"qf"})
tests = ["abba", "aa", "abab", "baab", "aba"]
print([w for w in tests if pal.accepts(w)])
['abba', 'aa', 'baab']
No deterministic PDA accepts wwR. The machine cannot know where the middle is. With a center marker, wcwR is deterministic.
def cfg_to_pda(G, start="S"):
"""Top of stack is a variable: expand it (guess a rule).
Top is a terminal: match it against the input."""
terms = {c for rhss in G.values() for r in rhss
for c in r if c not in G}
d = {("q0", "", "$"): {("loop", start + "$")}}
for A, rhss in G.items():
d[("loop", "", A)] = {("loop", r) for r in rhss}
for a in terms:
d[("loop", a, a)] = {("loop", "")}
d[("loop", "", "$")] = {("acc", "$")}
return PDA(d, start="q0", z0="$", accept={"acc"})
paren = cfg_to_pda({"S": ["(S)S", ""]})
tests = ["(())()", "(()", ")(", "", "((()))"]
print([w for w in tests if paren.accepts(w)])
['(())()', '', '((()))']
The stack holds the unfinished part of a leftmost sentential form. The PDA has 3 states, whatever the grammar.
Theorem. A language is context-free if and only if some PDA accepts it.
This is the construction on the last slide.
Invariant: consumed input + stack contents = a leftmost sentential form. So accepting runs match leftmost derivations one to one.
First normalize: one accept state, empty stack at the end, and each move either pushes one symbol or pops one.
Make a variable Apq for each pair of states. It generates every w that takes p with empty stack to q with empty stack.
Apq → a Ars b (push X on a, pop X on b)
Apq → Apr Arq (stack empties at r)
App → ε
Start variable Aq0 qacc. Proof: induction on the length of the run (Sipser, Lemma 2.27).
Lemma (Bar-Hillel, Perles, Shamir 1961). If L is context-free, there is a p ≥ 1 such that every s ∈ L with |s| ≥ p splits as s = uvxyz with:
Take a CNF grammar with k variables and set p = 2k. A binary tree with ≥ p leaves has a path longer than k. So some variable R repeats on it (pigeonhole).
The upper R yields vxy. The lower R yields x. Swap subtrees to pump up or down.
Replace the lower R-subtree by a copy of the upper one: uv2xy2z. Replace the upper by the lower: uxz. Choosing the lowest repeat bounds |vxy| ≤ p.
The lemma is a necessary condition only. Some non-CF languages pass it. For those, use Ogden's lemma (you mark which positions count).
If v or y mixes two letters, pumping up also breaks the a*b*c* order.
For a fixed p we can check every split by brute force:
def in_L(s): # { a^n b^n c^n }
n = len(s) // 3
return s == "a" * n + "b" * n + "c" * n
def no_split_survives(s, p):
"""True if EVERY split s = uvxyz with |vxy| <= p, |vy| >= 1
leaves L when pumped with i = 0 or i = 2."""
for i in range(len(s) + 1): # u = s[:i]
for j in range(i, min(i + p, len(s)) + 1): # vxy = s[i:j]
for a in range(i, j + 1):
for b in range(a, j + 1):
for c in range(b, j + 1):
u, v, x = s[:i], s[i:a], s[a:b]
y, z = s[b:c], s[c:]
if not v + y:
continue
if all(in_L(u + v*k + x + y*k + z)
for k in (0, 2)):
return False # split survives
return True
for p in (2, 3, 4, 5):
s = "a" * p + "b" * p + "c" * p
print(p, s, no_split_survives(s, p))
2 aabbcc True 3 aaabbbccc True 4 aaaabbbbcccc True 5 aaaaabbbbbccccc True
| Operation | CFL? | Why |
|---|---|---|
| Union L1 ∪ L2 | yes | S → S1 | S2 |
| Concatenation | yes | S → S1 S2 |
| Kleene star | yes | S → S S1 | ε |
| Reversal | yes | reverse every right side |
| Homomorphism | yes | substitute in the rules |
| ∩ regular | yes | product: PDA × DFA |
| Intersection | no | counterexample → |
| Complement | no | De Morgan → |
| Difference | no | Σ* − L is complement |
Rename variables apart first so the two grammars share none.
L1 = { anbncm } S → XC, X → aXb | ε, C → cC | ε
L2 = { ambncn } symmetric
L1 ∩ L2 = { anbncn } not CF
If it were, then L1 ∩ L2 = ¬(¬L1 ∪ ¬L2) would be CF, using union. Contradiction.
Concrete case: {ww : w ∈ {a,b}*} is not CF, but its complement is.
Is L = {w : #a = #b = #c} CF? Intersect with regular a*b*c* and you get anbncn. So L is not CF.
A PDA is deterministic if in every configuration at most one move applies. For each (q, a, X), at most one of δ(q,a,X) and δ(q,ε,X) is non-empty, with at most one choice.
The languages of DPDAs (accept by final state) are the deterministic CFLs (DCFL).
Strict inclusion. Regular ⊂ DCFL ⊂ CFL.
| Language | DCFL? |
|---|---|
| anbn | yes |
| wcwR (marked middle) | yes |
| wwR (no marker) | no |
| {anbn} ∪ {anb2n} | no |
DCFL = the languages of LR(1) grammars (Knuth 1965). Nearly every programming language is designed to be LR(1), or close to it.
Equivalence of two DPDAs is decidable (Sénizergues 1997, Gödel Prize 2002). For NPDAs it is undecidable.
EPS = "ε"
G = { # LL(1) expression grammar (left recursion removed)
"E": [["T", "E'"]],
"E'": [["+", "T", "E'"], [EPS]],
"T": [["F", "T'"]],
"T'": [["*", "F", "T'"], [EPS]],
"F": [["(", "E", ")"], ["id"]],
}
def first_sets(G):
F = {A: set() for A in G}
changed = True
while changed: # iterate to a fixed point
changed = False
for A, prods in G.items():
for rhs in prods:
before = len(F[A])
for X in rhs:
if X == EPS:
F[A].add(EPS); break
if X not in G: # terminal
F[A].add(X); break
F[A] |= F[X] - {EPS}
if EPS not in F[X]:
break
else: # every symbol nullable
F[A].add(EPS)
changed |= len(F[A]) != before
return F
for A, s in first_sets(G).items():
print(f"FIRST({A}) = {sorted(s)}")
Left-to-right scan, Leftmost derivation, 1 token of lookahead. At each variable, the next token alone must pick the rule.
FIRST(α): terminals that can start a string derived from α (plus ε if α ⇒* ε).
FOLLOW(A): terminals that can come right after A.
LL(1) condition: for A → α | β, FIRST sets are disjoint. If β ⇒* ε, then FIRST(α) ∩ FOLLOW(A) = ∅.
No LL(1) grammar may be left-recursive or share a common prefix across alternatives. Fix them by removing left recursion and left factoring: A → αβ | αγ becomes A → αA', A' → β | γ.
FIRST(E) = ['(', 'id']
FIRST(E') = ['+', 'ε']
FIRST(T) = ['(', 'id']
FIRST(T') = ['*', 'ε']
FIRST(F) = ['(', 'id']
import re
class Parser:
def __init__(self, text):
self.toks = re.findall(r"\d+|[-+*/()]", text) + ["$"]
self.i = 0
def peek(self): return self.toks[self.i]
def eat(self, t=None):
tok = self.toks[self.i]
if t and tok != t:
raise SyntaxError(f"expected {t!r}, got {tok!r}")
self.i += 1
return tok
def expr(self):
node = self.term()
while self.peek() in ("+", "-"): # loop = left assoc
node = (self.eat(), node, self.term())
return node
def term(self):
node = self.factor()
while self.peek() in ("*", "/"):
node = (self.eat(), node, self.factor())
return node
def factor(self):
if self.peek() == "(":
self.eat("("); node = self.expr(); self.eat(")")
return node
if self.peek() == "-":
self.eat(); return ("neg", self.factor())
return int(self.eat())
def parse(self):
tree = self.expr(); self.eat("$")
return tree
def ev(t):
if isinstance(t, int): return t
if t[0] == "neg": return -ev(t[1])
op, a, b = t
a, b = ev(a), ev(b)
return {"+": a + b, "-": a - b, "*": a * b, "/": a // b}[op]
t = Parser("2 + 3 * (4 - 1) - 5").parse()
print(t)
print(ev(t), ev(Parser("8 - 3 - 2").parse()))
try:
Parser("2 * (3 + 4").parse()
except SyntaxError as e:
print("SyntaxError:", e)
('-', ('+', 2, ('*', 3, ('-', 4, 1))), 5)
6 3
SyntaxError: expected ')', got '$'
expr → term (('+'|'-') term)*
term → factor (('*'|'/') factor)*
factor → NUMBER | '(' expr ')' | '-' factor
peek() is the 1-token lookahead. A while loop builds left-assoc trees: 8-3-2 = 3.Grammar: E → E+T | T, T → T*F | F, F → id. Input id+id*id.
| Stack | Input | Action |
|---|---|---|
| $ | id+id*id$ | shift |
| $ id | +id*id$ | reduce F → id |
| $ F | +id*id$ | reduce T → F |
| $ T | +id*id$ | reduce E → T |
| $ E | +id*id$ | shift |
| $ E + | id*id$ | shift |
| $ E + id | *id$ | reduce F → id |
| $ E + F | *id$ | reduce T → F |
| $ E + T | *id$ | shift (not reduce!) |
| $ E + T * | id$ | shift |
| $ E + T * id | $ | reduce F → id |
| $ E + T * F | $ | reduce T → T*F |
| $ E + T | $ | reduce E → E+T |
| $ E | $ | accept |
Left-to-right scan, Rightmost derivation in reverse. Shift pushes a token. Reduce replaces a right side on top (a handle) by its variable.
A DFA over the stack contents (the LR(0) item automaton) plus lookahead decides each step. The tables are built by a generator.
| Family | Lookahead | Used by |
|---|---|---|
| LR(0) / SLR(1) | none / FOLLOW | teaching |
| LALR(1) | merged LR(1) | yacc, bison, PLY |
| LR(1) / IELR | full | bison %define lr.type |
| GLR | forks on conflict | bison GLR, tree-sitter |
LL(1) ⊂ LR(1). LR handles left recursion natively. Conflicts (shift/reduce, reduce/reduce) signal that the grammar is ambiguous or needs more lookahead.
| Question | Status | How |
|---|---|---|
| w ∈ L(G)? | decidable | CYK, O(n3) |
| L(G) = ∅? | decidable | is S generating? |
| L(G) infinite? | decidable | cycle in the cleaned CNF grammar |
| L(G) = Σ*? | undecidable | encode TM computations |
| L(G1) = L(G2)? | undecidable | from universality |
| L(G1) ∩ L(G2) = ∅? | undecidable | from PCP |
| G ambiguous? | undecidable | from PCP |
| L(G) regular? | undecidable | Greibach's theorem |
| ¬L(G) is CF? | undecidable | Greibach's theorem |
Write a TM run as C1 # C2R # C3 # C4R … Each configuration is a string.
The strings that are not valid accepting runs form a CFL. A PDA can guess one spot where consecutive configurations disagree. Reversing every other one makes the check fit a stack.
So L(G) = Σ* iff the TM accepts nothing. That is undecidable.
No tool can decide in general whether two grammars match or whether a grammar is ambiguous. Parser generators report LR conflicts instead: a safe, conservative stand-in.
See the Turing Machines deck for the undecidability proofs.
The standard pipeline:
"Declare before use" and type rules are not context-free. They are checked after parsing, on the tree.
Language specs publish their grammar: Python's Grammar/python.gram, the Java Language Spec, the C standard, Go's EBNF.
value → object | array | STRING
| NUMBER | true | false | null
object → { } | { members }
members → pair | pair , members
pair → STRING : value
array → [ ] | [ elements ]
elements → value | value , elements
As written, { } | { members } and pair | pair , members share a first token, so it is not quite LL(1). Left-factor it (members → pair ( , pair )*) and it is. Then a recursive-descent parser of about 100 lines handles all of it. RFC 8259 states it in ABNF.
(?R) goes beyond regular.Try each one before you open the answer.
The grammar is: aaa has two trees, (aa)a and a(aa). The language a+ is not. S → a S | a is unambiguous.
The first is: S → X Y, X → aXb | ε, Y → cYd | ε. The second is not. Its pairs cross, and one stack cannot match crossing pairs. Pump apbpcpdp to prove it.
No. {anbncm} ∩ {ambncn} = {anbncn}. But a CFL intersected with a regular language is always a CFL.
Each A → BC step adds one variable. Going from 1 to n variables takes n − 1 such steps. Then n steps of A → a turn them into terminals.
Push one mark per a. After the first b, switch to a state that only pops, one per b. Accept if you finish the input without popping $. Leftover marks are fine.
| Mistake | Fix |
|---|---|
| "The grammar is ambiguous, so the language is." | Often another grammar is not. Only rare languages are inherently ambiguous, like {aibjck : i=j or j=k}. |
| "In CFL pumping, v and y are both non-empty." | Only |vy| ≥ 1. One of them may be empty. |
| "vxy sits at the start, like regular pumping." | No. vxy can be any window of length ≤ p. Check every place. |
| "DPDAs and PDAs are equal, like DFAs and NFAs." | No. wwR needs a guess. |
| "CFLs are closed under complement." | No. Only the deterministic ones (DCFLs) are. |
| Type | Grammar rules | Machine | Example |
|---|---|---|---|
| 3 Regular | A → aB | a | DFA / NFA | a*b* |
| — DCFL | LR(1) | DPDA | anbn |
| 2 Context-free | A → α | PDA | wwR |
| 1 Context-sensitive | αAβ → αγβ | linear-bounded TM | anbncn |
| 0 Recursively enumerable | any α → β | Turing machine | halting set |
Next deck: Turing machines and undecidability.
| Term | Meaning |
|---|---|
| Variable / non-terminal | A symbol that gets rewritten, like S or E. |
| Terminal | A symbol of the final string. Never rewritten. |
| Rule / production | A → α: replace A by α. |
| Derivation ⇒* | A chain of rewrites from the start variable. |
| Leftmost derivation | Always rewrite the leftmost variable. One per parse tree. |
| Parse tree | The tree of rule uses. Its leaves spell the string. |
| Ambiguous grammar | Some string has two different parse trees. |
| Inherently ambiguous | A language where every grammar is ambiguous. |
| Nullable | A variable that can derive ε. |
| Unit rule | A → B, one variable on the right. |
| Term | Meaning |
|---|---|
| CNF | Rules A → BC or A → a only. Binary trees. |
| GNF | Every rule starts with a terminal. |
| CYK | O(n3) table parser for CNF grammars. |
| PDA | Finite automaton plus one stack. Matches CFGs. |
| DPDA / DCFL | Deterministic PDA and its languages. Same as LR(1). |
Bottom marker $ | Pushed first. Seeing it means the stack is empty. |
| FIRST / FOLLOW | Tokens that can start / come after a variable. Drive LL(1). |
| LL(1) | Top-down parse, one token of lookahead. |
| LR(1) | Bottom-up shift-reduce parse, one token of lookahead. |
| Left recursion | A → Aα. Breaks top-down parsers. |