Context-Free Grammars
& Pushdown Automata

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.

CFG

Rewrite rules. One variable on the left.

PDA

A finite automaton plus one stack.

Parsing

CYK, LL(1), LR: find the tree.

Limits

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+).

Roadmap WHERE WE GO

  1. Why go beyond regular?
  2. CFG: formal definition
  3. Derivations
  4. Parse trees
  5. Ambiguity
  6. Fixing precedence
  7. The dangling else
  8. Cleaning a grammar
  9. Chomsky normal form
  10. Greibach normal form
  11. CYK: the idea
  12. CYK in Python
  13. Stacks and nesting
  14. PDA: formal definition
  15. A PDA for anbn
  16. A PDA simulator in Python
  17. Nondeterminism & CFG → PDA
  18. CFG ↔ PDA equivalence
  19. Pumping lemma for CFLs
  20. anbncn is not context-free
  21. Closure properties
  22. DPDA vs NPDA
  23. LL(1) and FIRST sets
  24. Recursive descent in Python
  25. LR: shift-reduce parsing
  26. Decision problems
  27. CFGs in the wild
  28. Chomsky hierarchy & summary

Why Go Beyond Regular? MOTIVATION

What a DFA cannot do

A DFA has a fixed number of states. So it cannot count without bound.

  • { anbn : n ≥ 0 } is not regular (pumping lemma).
  • Balanced parentheses (()(())) are not regular.
  • Nested if blocks, JSON arrays, HTML tags: all nest to any depth.

The fix

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).

Decidable (Turing machines) Context-free (PDA) Regular (DFA) a*b* (ab)* aⁿbⁿ balanced ( ) aⁿbⁿcⁿ

Each ring strictly contains the one inside. We climb one ring in this deck.

Context-Free Grammar DEFINITION

A context-free grammar is a 4-tuple G = (V, Σ, R, S):

  • V: a finite set of variables (non-terminals).
  • Σ: a finite set of terminals, disjoint from V.
  • R: a finite set of rules A → α with A ∈ V and α ∈ (V ∪ Σ)*.
  • S ∈ V: the start variable.

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.

Examples

GrammarLanguage
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
T → T*F | F
F → (E) | id
arithmetic expressions

A → α | β is shorthand for two rules. This notation is BNF (Backus–Naur form), first used for ALGOL 60.

Derivations REWRITING

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.

Deriving (())() 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.

Parse Trees STRUCTURE

A parse tree for G:

  • The root is labeled S.
  • Each inner node A with children X1 … Xk matches a rule A → X1…Xk.
  • Leaves are terminals or ε. Read left to right, they spell the yield.

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 +.

Tree for id + id * id

idFTE+idFT*idFTE

Grammar: E → E+T | T, T → T*F | F, F → id. The * sits deeper, so it is evaluated first.

Designing a Grammar: Patterns HOW TO THINK

Five building blocks

GoalPattern
Either X or YS → X | Y
X then YS → X Y
Any number of XR → X R | ε
Matched pairs, same countS → a S b | ε
Pairs around a middle partS → 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".

Common mistakes

  • Too loose. S → aS | Sb | ε gives all of a*b*, not anbn.
  • Forgetting the base case. Without | ε (or a short string), no derivation ever ends.
  • Not testing both ways. Check the grammar makes every string you want, and only those.

Worked: { aibjck : k = i + j }

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

Derive 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.

Try it: a grammar for { aib2i : i ≥ 0 }

S → a S b b | ε. Each step adds one a and two b's.

Try it: strings with equal numbers of a's and 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.

Ambiguity TWO TREES

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

aE+aEE*aEE

(a+a)*a  ✗

aE+aE*aEEE

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.

Fixing Precedence & Associativity LAYERED GRAMMAR

One variable per precedence level

E → E + T | E - T | T        # lowest: + -
T → T * F | T / F | F        # middle: * /
F → ( E ) | - F | num         # highest: atoms
  • Precedence: a T can never contain a bare +. So * binds tighter.
  • Left associativity: E → E + T recurses on the left. So a-b-c parses as (a-b)-c.
  • Right associativity (for ** or =): recurse on the right, P → F ** P | F.

Now a - a - a has one tree

aFTE-aFTE-aFTE

The language is unchanged. Only the trees change. Parser generators like yacc and bison offer a shortcut: keep the ambiguous grammar and declare %left '+' '-', %left '*' '/'.

The Dangling Else CLASSIC BUG

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?

Inner (C, Java)

if a then
  if b then x
  else y

Outer

if a then
  if b then x
else y

Every mainstream language picks the inner if: "an else matches the nearest unmatched then".

Unambiguous fix: matched vs unmatched

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.

Other fixes in practice

  • LR parsers: resolve the shift/reduce conflict by shifting. Same effect.
  • Change the language: end if (Ada), braces required (Go, Rust, Swift), indentation (Python).

Cleaning a Grammar PREPROCESSING

1. Useless symbols

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

2. ε-rules

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.

3. Unit rules

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 ε).

Chomsky Normal Form CNF

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.

Conversion

  1. START: add S0 → S.
  2. DEL: remove ε-rules. UNIT: remove unit rules.
  3. TERM: in long rules, replace terminal a by a new Xa → a.
  4. BIN: split A → B1B2…Bk into a chain of pairs.

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).

Example: 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

Why CNF is useful

  • Every parse tree is binary. A string of length n ≥ 1 takes exactly 2n − 1 steps.
  • Binary trees make the pumping lemma proof clean.
  • It enables the CYK dynamic program (next).

Worked Example: Full CNF Conversion STEP BY STEP

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 | ε

1. START

New start, so S never appears on a right side as the start.

S0 → S
S  → a S b | ε

2. DEL: remove ε-rules

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

3. UNIT: remove S0 → S

Copy the rules of S up to S0.

S0 → a S b | a b | ε
S  → a S b | a b

4. TERM, then 5. BIN

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

Check it

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.

Greibach Normal Form GNF

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.

Consequences

  • Each step emits exactly one terminal. So |w| = n takes exactly n steps.
  • No left recursion at all. Top-down parsers never loop forever.
  • It gives a PDA with no ε-moves: read a, pop A, push B1…Bk.

Key tool: removing left recursion

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.

CYK: The Idea DYNAMIC PROGRAMMING

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.

Table for aabb (grammar from the CNF slide)

ℓ \ i1: a2: a3: b4: b
4S
3∅C
2∅S∅
1AABB
  • T[2,2]: ab = A·B, and S → AB.
  • T[2,3]: abb = S·B, and C → SB.
  • T[1,4]: aabb = A·C, and S → AC. Accept.

CYK in Python CODE

# 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']

Reading the loops

  • Three nested loops over l, i, k give the n3.
  • The inner loop over rules gives the |G| factor.
  • Short spans are filled before long ones. Each cell only reads shorter spans.

Getting the tree too

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.

Stacks and Nesting INTUITION

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.

([]{()}) read head (left to right only) finite control states q0, q1, ... push/pop ({($ stack (top) After reading "([]{(": [ was pushed and popped, three opens remain.

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.

Pushdown Automaton DEFINITION

A PDA is a 7-tuple P = (Q, Σ, Γ, δ, q0, Z0, F):

  • Q states, Σ input alphabet, Γ stack alphabet.
  • δ : Q × (Σ ∪ {ε}) × (Γ ∪ {ε}) → Pfin(Q × Γ*).
  • q0 start state, Z0 initial stack symbol, F ⊆ Q accept states.

(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)

Two ways to accept

ModeAccept 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 PDA for anbn EXAMPLE

q0q1qf a, ε → A b, A → ε ε, ε → ε ε, $ → $ stack starts as $ · label "input, pop → push"
  • q0: push an A for each a.
  • Guess the middle with a free move to q1.
  • q1: pop an A for each b.
  • Seeing $ means the counts match. Move to qf.

Trace on aabb

StateInput leftStackMove
q0aabb$read a, push A
q0abbA$read a, push A
q0bbAA$ε to q1
q1bbAA$read b, pop A
q1bA$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.

A PDA Simulator in Python CODE

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']

How it works

  • A configuration is (state, pos, stack), the stack as a string with the top first.
  • BFS explores every nondeterministic branch. seen skips repeats.
  • ε-moves can grow the stack forever, so limit caps the search. A real decider would convert to a grammar and run CYK.

Designing a PDA: Even Palindromes WORKED EXAMPLE

Recipe

  1. Push what you must remember for later.
  2. Switch phase with an ε-move. If you cannot see where, guess: nondeterminism.
  3. Pop to match what you pushed.
  4. Push $ first. Seeing it again means the stack is empty.

For { w wR : w ∈ {a,b}* }

StateMoveWhy
q0a, ε → a and b, ε → bsave the first half
q0 → q1ε, ε → εguess the middle
q1a, a → ε and b, b → εmatch in reverse
q1 → qfε, $ → $all matched: accept

Trace on abba (the good guess)

StateInput leftStackMove
q0abba$read a, push a
q0bbaa$read b, push b
q0baba$guess middle, to q1
q1baba$read b, pop b
q1aa$read a, pop a
q1ε$see $, to qf
qfε$accept

A bad guess just dies

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.

Nondeterminism & CFG → PDA CODE

Even palindromes wwR: guess the middle

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.

Any grammar becomes a PDA

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.

CFG ↔ PDA Equivalence THEOREM

Theorem. A language is context-free if and only if some PDA accepts it.

CFG ⇒ PDA (easy)

This is the construction on the last slide.

  • Push S. Loop in one state.
  • Top is variable A: pop it, push some α with A → α (nondeterministic).
  • Top is terminal a: read a and pop.
  • Stack empty (back to $) at end of input: accept.

Invariant: consumed input + stack contents = a leftmost sentential form. So accepting runs match leftmost derivations one to one.

PDA ⇒ CFG (harder)

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).

Pumping Lemma for CFLs LIMITS

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:

  1. u vi x yi z ∈ L for all i ≥ 0
  2. |vy| ≥ 1
  3. |vxy| ≤ p

Proof idea

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.

S R R uvxyz

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).

anbncn Is Not Context-Free PROOF

Proof by contradiction

  1. Suppose L = {anbncn} is CF with pumping length p.
  2. Pick s = apbpcp. It is in L and long enough.
  3. Take any split uvxyz with |vxy| ≤ p, |vy| ≥ 1.
  4. Since |vxy| ≤ p, the window vxy touches at most two of the three letter blocks.
  5. Pump down to uxz. At least one letter count drops. The untouched letter keeps count p. The counts differ.
  6. So uxz ∉ L. Contradiction. ∎

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

Closure Properties ALGEBRA

OperationCFL?Why
Union L1 ∪ L2yesS → S1 | S2
ConcatenationyesS → S1 S2
Kleene staryesS → S S1 | ε
Reversalyesreverse every right side
Homomorphismyessubstitute in the rules
∩ regularyesproduct: PDA × DFA
Intersectionnocounterexample →
ComplementnoDe Morgan →
DifferencenoΣ* − L is complement

Rename variables apart first so the two grammars share none.

Not closed under ∩

L1 = { anbncm }    S → XC, X → aXb | ε, C → cC | ε

L2 = { ambncn }    symmetric

L1 ∩ L2 = { anbncn }   not CF

Not closed under complement

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.

∩ regular is a handy tool

Is L = {w : #a = #b = #c} CF? Intersect with regular a*b*c* and you get anbncn. So L is not CF.

DPDA vs NPDA DETERMINISM

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.

LanguageDCFL?
anbnyes
wcwR (marked middle)yes
wwR (no marker)no
{anbn} ∪ {anb2n}no

DCFLs behave differently

  • Closed under complement (swap accept states, after care with ε-loops). CFLs are not.
  • So a language whose complement is not CF cannot be a DCFL.
  • Not closed under union or reversal.
  • Every DCFL has an unambiguous grammar.
  • Membership in O(n) time.

Why parsers love DCFLs

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.

LL(1) Parsing & FIRST Sets TOP-DOWN

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)}")

LL(1)

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']

Recursive Descent in Python CODE

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 '$'

Grammar → code, one to one

expr   → term (('+'|'-') term)*
term   → factor (('*'|'/') factor)*
factor → NUMBER | '(' expr ')' | '-' factor
  • One method per variable. The call stack is the PDA stack.
  • peek() is the 1-token lookahead. A while loop builds left-assoc trees: 8-3-2 = 3.

LR: Shift-Reduce Parsing BOTTOM-UP

Grammar: E → E+T | T, T → T*F | F, F → id. Input id+id*id.

StackInputAction
$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

How it works

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.

FamilyLookaheadUsed by
LR(0) / SLR(1)none / FOLLOWteaching
LALR(1)merged LR(1)yacc, bison, PLY
LR(1) / IELRfullbison %define lr.type
GLRforks on conflictbison 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.

Decision Problems for CFGs DECIDABILITY

QuestionStatusHow
w ∈ L(G)?decidableCYK, O(n3)
L(G) = ∅?decidableis S generating?
L(G) infinite?decidablecycle in the cleaned CNF grammar
L(G) = Σ*?undecidableencode TM computations
L(G1) = L(G2)?undecidablefrom universality
L(G1) ∩ L(G2) = ∅?undecidablefrom PCP
G ambiguous?undecidablefrom PCP
L(G) regular?undecidableGreibach's theorem
¬L(G) is CF?undecidableGreibach's theorem

The key trick: computation histories

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.

Practical meaning

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.

CFGs in the Wild APPLICATIONS

Compilers

The standard pipeline:

  1. Lexer: regex / DFA → tokens
  2. Parser: CFG → syntax tree
  3. Semantic checks: types, scopes

"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.

JSON

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.

HTML, XML & more

  • XML: well-formed documents nest like parentheses. With a fixed tag set, that is a CFL.
  • HTML5: its parser is a state machine with error recovery, not a CFG. It must accept broken pages.
  • Regex: you cannot match nested HTML with a true regex. PCRE recursion (?R) goes beyond regular.
  • NLP: probabilistic CFGs for sentence structure.
  • Biology: RNA folding uses stochastic CFGs. Paired bases nest.

Check Yourself PRACTICE

Try each one before you open the answer.

1. Is S → S S | a ambiguous? Is its language?

The grammar is: aaa has two trees, (aa)a and a(aa). The language a+ is not. S → a S | a is unambiguous.

2. Is { anbncmdm } context-free? And { anbmcndm }?

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.

3. Is the intersection of two CFLs always context-free?

No. {anbncm} ∩ {ambncn} = {anbncn}. But a CFL intersected with a regular language is always a CFL.

4. Why must a CNF grammar take exactly 2n − 1 steps?

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.

5. Build a PDA idea for { aibj : i ≥ j }.

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.

Mistakes students make

MistakeFix
"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.

Chomsky Hierarchy & Summary RECAP

TypeGrammar rulesMachineExample
3 RegularA → aB | aDFA / NFAa*b*
— DCFLLR(1)DPDAanbn
2 Context-freeA → αPDAwwR
1 Context-sensitiveαAβ → αγβlinear-bounded TManbncn
0 Recursively enumerableany α → βTuring machinehalting set

Further reading

  • Sipser, Introduction to the Theory of Computation, ch. 2.
  • Hopcroft, Motwani, Ullman, Automata Theory, Languages, and Computation, ch. 5–7.
  • Aho, Lam, Sethi, Ullman, Compilers ("Dragon Book"), ch. 4.

Key takeaways

  • A CFG rewrites one variable at a time. Its derivations form parse trees.
  • Ambiguity is a grammar bug. Fix it with layered variables. Detecting it is undecidable.
  • CNF gives binary trees, CYK parsing in O(n3), and the pumping lemma.
  • PDA = finite control + stack. Nondeterministic PDAs match CFGs exactly.
  • DPDAs are weaker. They match LR(1), the class real parsers use.
  • CFLs are closed under ∪, concat, star, but not ∩ or complement.
  • A stack can count one thing at a time. anbncn needs more: a Turing machine.

Next deck: Turing machines and undecidability.

Glossary QUICK REFERENCE

TermMeaning
Variable / non-terminalA symbol that gets rewritten, like S or E.
TerminalA symbol of the final string. Never rewritten.
Rule / productionA → α: replace A by α.
Derivation ⇒*A chain of rewrites from the start variable.
Leftmost derivationAlways rewrite the leftmost variable. One per parse tree.
Parse treeThe tree of rule uses. Its leaves spell the string.
Ambiguous grammarSome string has two different parse trees.
Inherently ambiguousA language where every grammar is ambiguous.
NullableA variable that can derive ε.
Unit ruleA → B, one variable on the right.
TermMeaning
CNFRules A → BC or A → a only. Binary trees.
GNFEvery rule starts with a terminal.
CYKO(n3) table parser for CNF grammars.
PDAFinite automaton plus one stack. Matches CFGs.
DPDA / DCFLDeterministic PDA and its languages. Same as LR(1).
Bottom marker $Pushed first. Seeing it means the stack is empty.
FIRST / FOLLOWTokens 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 recursionA → Aα. Breaks top-down parsers.