๐ง CFG Fundamentals
๐ฏ What is a Context-Free Grammar?
A Context-Free Grammar (CFG) is a formal system for generating strings. It consists of rewrite rules that replace a single nonterminal symbol with a sequence of symbols. CFGs are more powerful than regular expressions โ they can describe nested structures like balanced parentheses, arithmetic expressions, and programming language syntax.
Why CFGs Matter
Power: Beyond regular languages (can count). Nesting: Handles recursive structures. Compilers: Syntax analysis. Parsing: CYK, Earley, LL, LR. Natural language: Sentence structure. Foundation: Chomsky hierarchy (Type 2).
# Context-Free Grammar โ the big picture
print("""
=== What Makes a Grammar Context-Free? ===
A rule replaces ONE nonterminal:
CONTEXT-FREE: A โ ฮฑ (A is a single nonterminal)
CONTEXT-SENSITIVE: ฮฑAฮฒ โ ฮฑฮณฮฒ (A depends on context)
"Context-free" means the replacement of A
doesn't depend on what's around it.
=== Real-World Example: Arithmetic Expressions ===
Grammar G:
E โ E + T | T
T โ T * F | F
F โ ( E ) | id
This generates expressions like:
id
id + id
id * id
(id + id) * id
id + id * id
The nesting of parentheses is what makes this
impossible for regular expressions!
=== The Chomsky Hierarchy ===
Type 0: Recursively Enumerable (Turing machines)
Type 1: Context-Sensitive (Linear bounded automata)
Type 2: Context-Free (Pushdown automata) โ CFGs
Type 3: Regular (Finite automata)
CFGs are strictly more powerful than regular languages.
""")
# ANALOGY
print("=== Analogy: Recipe Book ===")
print("""
A CFG is like a recipe book with variables:
Dish โ Main Course | Dessert
Main Course โ Protein + Side
Protein โ Chicken | Fish | Tofu
Side โ Rice | Salad | Potato
Dessert โ Cake | Ice Cream
You start with "Dish" and apply rules
until you get a concrete meal.
Similarly, a CFG starts with S (start symbol)
and applies rules until you get a string of terminals.
The "context-free" part means each rule
can be applied regardless of context.
""")
# LANGUAGE CLASSIFICATION
print("=== Language Classification ===")
print("""
REGULAR LANGUAGES (Type 3):
- Recognized by DFA/NFA
- Described by regex
- Examples: a*b*, (ab)*
- Cannot count or nest
CONTEXT-FREE LANGUAGES (Type 2):
- Recognized by PDA
- Described by CFG
- Examples: a^n b^n, balanced parens, arithmetic
- Can count (one counter) and nest
CONTEXT-SENSITIVE (Type 1):
- Recognized by LBA
- Examples: a^n b^n c^n
- Can count multiple things
RECURSIVELY ENUMERABLE (Type 0):
- Recognized by Turing machines
- Everything computable
""")
# PYTHON CFG EXAMPLE
print("=== Python: Simple CFG ===")
import random
class CFG:
"""A simple context-free grammar."""
def __init__(self, start, rules):
self.start = start
self.rules = rules # dict: nonterminal -> list of productions
def generate(self, symbol=None, max_depth=10):
"""Generate a random string from the grammar."""
if symbol is None:
symbol = self.start
if max_depth <= 0:
# Try to terminate
if symbol not in self.rules:
return symbol
# Pick shortest production
prods = self.rules[symbol]
prods = sorted(prods, key=lambda p: (len(p), p))
return self.generate(prods[0], 0)
if symbol not in self.rules:
return symbol # terminal
# Pick a random production
prod = random.choice(self.rules[symbol])
# Generate each symbol in the production
result = []
for s in prod:
result.append(self.generate(s, max_depth - 1))
return ''.join(result)
def derive(self, string):
"""Check if string can be derived (brute force, limited)."""
# BFS over derivations
from collections import deque
queue = deque([(self.start, [self.start])])
visited = set()
for _ in range(10000):
if not queue:
break
current, path = queue.popleft()
if current == string:
return True, path
if current in visited:
continue
visited.add(current)
if len(current) > len(string) + 5:
continue
# Find first nonterminal
for i, c in enumerate(current):
if c in self.rules:
for prod in self.rules[c]:
new = current[:i] + prod + current[i+1:]
if len(new) <= len(string) + 5:
queue.append((new, path + [new]))
break
return False, None
# Example: Balanced parentheses
paren_grammar = CFG('S', {
'S': ['(S)', 'SS', '()'],
})
print("Balanced Parentheses Grammar:")
print(" S โ (S) | SS | ()")
print("\nGenerated strings:")
for _ in range(8):
s = paren_grammar.generate(max_depth=5)
print(f" '{s}'")
# Example: a^n b^n
anbn_grammar = CFG('S', {
'S': ['aSb', ''],
})
print("\na^n b^n Grammar:")
print(" S โ aSb | ฮต")
print("\nGenerated strings:")
for _ in range(6):
s = anbn_grammar.generate(max_depth=5)
print(f" '{s}'")
# Check derivation
print("\nDerivation check:")
test_strings = ['()', '(())', '()()', '((()))', '(()']
for s in test_strings:
result, path = paren_grammar.derive(s)
if result:
print(f" '{s}' โ โ derivable")
print(f" Path: {' โ '.join(path[:6])}{'...' if len(path) > 6 else ''}")
else:
print(f" '{s}' โ โ not derivable")
print("""
=== Key Properties ===
1. GENERATIVE
CFGs generate strings through derivations.
2. RECURSIVE
Rules can refer to themselves (S โ aSb).
This enables nesting and counting.
3. CONTEXT-FREE
Each rule replaces a single nonterminal.
No dependence on surrounding symbols.
4. AMBIGUOUS
Some strings have multiple parse trees.
(See ambiguity section)
5. PARSING
Given a string, find a derivation.
CYK: O(nยณ), Earley: O(nยณ), LL/LR: O(n)
""")๐ Formal Definition
๐ฏ The 4-Tuple
A CFG is formally defined as a 4-tuple G = (V, ฮฃ, R, S), where each component plays a specific role in generating strings.
The Components
V: Finite set of nonterminals (variables). ฮฃ: Finite set of terminals (disjoint from V). R: Finite set of production rules. S: Start symbol (S โ V). L(G): Language generated by G. Derivation: Sequence of rule applications.
# Formal Definition of CFG
from dataclasses import dataclass, field
from typing import Set, Dict, List, Tuple, FrozenSet
@dataclass
class CFG:
"""
Formal CFG as 4-tuple G = (V, ฮฃ, R, S)
V: nonterminals (variables)
ฮฃ: terminals
R: production rules (V โ (V โช ฮฃ)*)
S: start symbol
"""
V: FrozenSet[str] # Nonterminals
Sigma: FrozenSet[str] # Terminals
R: Dict[str, List[Tuple[str, ...]]] # Rules
S: str # Start symbol
def __post_init__(self):
# Validate
assert self.S in self.V, "Start symbol must be a nonterminal"
assert self.V & self.Sigma == frozenset(), "V and ฮฃ must be disjoint"
for lhs, productions in self.R.items():
assert lhs in self.V, f"LHS {lhs} must be nonterminal"
for rhs in productions:
for symbol in rhs:
assert symbol in self.V or symbol in self.Sigma, \
f"Symbol {symbol} not in V โช ฮฃ"
def __repr__(self):
lines = [f"G = (V, ฮฃ, R, S)"]
lines.append(f" V = {sorted(self.V)}")
lines.append(f" ฮฃ = {sorted(self.Sigma)}")
lines.append(f" S = {self.S}")
lines.append(f" R:")
for lhs in sorted(self.R.keys()):
for rhs in self.R[lhs]:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
lines.append(f" {lhs} โ {rhs_str}")
return '\n'.join(lines)
# Example: Arithmetic expressions
expr_cfg = CFG(
V=frozenset({'E', 'T', 'F'}),
Sigma=frozenset({'id', '+', '*', '(', ')'}),
R={
'E': [('E', '+', 'T'), ('T',)],
'T': [('T', '*', 'F'), ('F',)],
'F': [('(', 'E', ')'), ('id',)],
},
S='E'
)
print(expr_cfg)
# DERIVATION
def derive_one_step(cfg, sentential_form):
"""Find all one-step derivations from a sentential form."""
results = []
for i, symbol in enumerate(sentential_form):
if symbol in cfg.V:
for rhs in cfg.R.get(symbol, []):
new_form = sentential_form[:i] + rhs + sentential_form[i+1:]
results.append((i, symbol, rhs, new_form))
return results
def leftmost_derivation(cfg, target, max_steps=20):
"""Find a leftmost derivation of target."""
start = (cfg.S,)
def helper(form, steps, path):
if steps > max_steps:
return None
# Check if form is all terminals
if all(s in cfg.Sigma for s in form):
if ''.join(form) == target:
return path + [form]
return None
# Find leftmost nonterminal
for i, symbol in enumerate(form):
if symbol in cfg.V:
for rhs in cfg.R[symbol]:
new_form = form[:i] + rhs + form[i+1:]
result = helper(new_form, steps + 1, path + [new_form])
if result:
return result
break # only expand leftmost
return None
return helper(start, 0, [start])
# Test derivation
print("\n=== Leftmost Derivation ===")
target = 'id+id*id'
path = leftmost_derivation(expr_cfg, target)
if path:
print(f"Target: {target}")
print("Derivation:")
for i, form in enumerate(path):
form_str = ' '.join(form) if form else 'ฮต'
print(f" Step {i}: {form_str}")
else:
print(f"Could not derive {target} in 20 steps")
# LANGUAGE OF A CFG
print("""
=== Language of a CFG ===
L(G) = { w โ ฮฃ* | S โ* w }
The language of a CFG is the set of all strings
of terminals derivable from the start symbol.
=== Examples ===
G1: S โ aSb | ฮต
L(G1) = { a^n b^n | n โฅ 0 }
G2: S โ (S) | SS | ฮต
L(G2) = balanced parentheses
G3: E โ E+T | T
T โ T*F | F
F โ (E) | id
L(G3) = arithmetic expressions
=== Key Facts ===
1. Every regular language is context-free.
2. Not every CFL is regular (e.g., a^n b^n).
3. CFLs are closed under union, concat, star.
4. CFLs are NOT closed under intersection.
5. Membership is decidable: O(nยณ) via CYK.
""")
# MORE EXAMPLES
print("=== More CFG Examples ===")
print("""
1. PALINDROMES over {a, b}:
S โ aSa | bSb | a | b | ฮต
L = { w | w = w^R }
2. EQUAL a's and b's:
S โ aSb | bSa | SS | ฮต
L = { w | #a(w) = #b(w) }
3. a^i b^j with i โ j:
S โ A | B
A โ aAb | aA | a (more a's)
B โ aBb | Bb | b (more b's)
4. ARITHMETIC EXPRESSIONS:
E โ E + T | T
T โ T * F | F
F โ ( E ) | id
5. PROGRAMMING LANGUAGE:
Program โ StmtList
StmtList โ Stmt StmtList | ฮต
Stmt โ id = Expr ; | if ( Expr ) Stmt | { StmtList }
Expr โ ...
""")
# PYTHON PARSER SIMULATION
print("=== Python: CYK-style Membership (simplified) ===")
def cfg_membership(cfg, string, max_len=20):
"""
Check if string is in L(G) using BFS over derivations.
(Not efficient, but works for small examples.)
"""
from collections import deque
# Convert string to tuple of chars for easier handling
if len(string) > max_len:
return False
start = (cfg.S,)
queue = deque([start])
visited = set()
for _ in range(100000):
if not queue:
break
form = queue.popleft()
if form in visited:
continue
visited.add(form)
# Check if form is the target string (all terminals)
if all(s in cfg.Sigma for s in form):
if ''.join(form) == string:
return True
continue
# Check length bound
terminal_len = sum(1 for s in form if s in cfg.Sigma)
if terminal_len > len(string):
continue
# Expand leftmost nonterminal
for i, symbol in enumerate(form):
if symbol in cfg.V:
for rhs in cfg.R[symbol]:
new_form = form[:i] + rhs + form[i+1:]
if len(new_form) <= len(string) + 5:
queue.append(new_form)
break # leftmost only
return False
# Test with balanced parentheses
print("Balanced parentheses grammar:")
print(" S โ (S) | SS | ()")
for s in ['()', '(())', '()()', '((()))', '(()', ')(', '()()()']:
result = cfg_membership(paren_grammar, s, max_len=8)
print(f" '{s}' โ {'โ' if result else 'โ'}")
# Test with a^n b^n
print("\na^n b^n grammar:")
print(" S โ aSb | ฮต")
for s in ['', 'ab', 'aabb', 'aaabbb', 'aab', 'abb', 'ba']:
result = cfg_membership(anbn_grammar, s, max_len=8)
print(f" '{s}' โ {'โ' if result else 'โ'}")
# TEST WITH EXPRESSIONS
print("\nArithmetic expressions:")
for s in ['id', 'id+id', 'id*id', 'id+id*id', '(id+id)*id', 'id+', '+id']:
result = cfg_membership(expr_cfg, s, max_len=10)
print(f" '{s}' โ {'โ' if result else 'โ'}")๐ Derivations
๐ฏ How Strings Are Generated
A derivation is a sequence of rule applications that transforms the start symbol into a string of terminals. Derivations show how a string is generated by the grammar.
Derivation Types
Leftmost: Always expand leftmost nonterminal. Rightmost: Always expand rightmost nonterminal. Mixed: Any order. Sentential form: Intermediate string. โ*: Zero or more steps. Parse tree: Tree representation of derivation.
# Derivations in detail
print("=" * 60)
print("DERIVATIONS")
print("=" * 60)
print("""
=== Definition ===
A derivation is a sequence:
ฮฑโ โ ฮฑโ โ ฮฑโ โ ... โ ฮฑโ
where each ฮฑแตขโโ is obtained from ฮฑแตข by
replacing a nonterminal using a production rule.
Notation:
ฮฑ โ ฮฒ (one step)
ฮฑ โ* ฮฒ (zero or more steps)
ฮฑ โ+ ฮฒ (one or more steps)
=== Example ===
Grammar:
E โ E + T | T
T โ T * F | F
F โ ( E ) | id
Derivation of "id + id * id":
LEFTMOST:
E
โ E + T
โ T + T
โ F + T
โ id + T
โ id + T * F
โ id + F * F
โ id + id * F
โ id + id * id
RIGHTMOST:
E
โ E + T
โ E + T * F
โ E + T * id
โ E + F * id
โ E + id * id
โ T + id * id
โ F + id * id
โ id + id * id
Both are valid derivations of the same string.
""")
# TYPES OF DERIVATIONS
print("=== Types of Derivations ===")
print("""
1. LEFTMOST DERIVATION
- Always expand the leftmost nonterminal
- Notation: โ_lm
- Unique for unambiguous grammars
- Used by LL parsers
2. RIGHTMOST DERIVATION
- Always expand the rightmost nonterminal
- Notation: โ_rm
- Reverse of leftmost
- Used by LR parsers
3. MIXED DERIVATION
- Any order of expansion
- Multiple possible sequences
- All produce the same parse tree
=== Key Fact ===
For a given parse tree, there is:
- Exactly ONE leftmost derivation
- Exactly ONE rightmost derivation
But there may be multiple mixed derivations.
""")
# DERIVATION IN CODE
print("=== Derivation in Code ===")
class CFG:
def __init__(self, start, rules):
self.start = start
self.rules = rules
def is_terminal(self, symbol):
return symbol not in self.rules and symbol != 'ฮต'
def leftmost_derivation(self, target, max_steps=30):
"""Find leftmost derivation of target."""
start = (self.start,)
def expand(form, steps):
if steps > max_steps:
return None
# Check if all terminals
if all(self.is_terminal(s) for s in form):
if ''.join(form) == target:
return [form]
return None
# Find leftmost nonterminal
for i, symbol in enumerate(form):
if symbol in self.rules:
for rhs in self.rules[symbol]:
rhs_tuple = rhs if rhs else ('ฮต',)
if rhs_tuple == ('ฮต',):
rhs_tuple = ()
new_form = form[:i] + rhs_tuple + form[i+1:]
result = expand(new_form, steps + 1)
if result:
return [form] + result
break
return None
return expand(start, 0)
def rightmost_derivation(self, target, max_steps=30):
"""Find rightmost derivation of target."""
start = (self.start,)
def expand(form, steps):
if steps > max_steps:
return None
if all(self.is_terminal(s) for s in form):
if ''.join(form) == target:
return [form]
return None
# Find rightmost nonterminal
for i in range(len(form) - 1, -1, -1):
if form[i] in self.rules:
for rhs in self.rules[form[i]]:
rhs_tuple = rhs if rhs else ('ฮต',)
if rhs_tuple == ('ฮต',):
rhs_tuple = ()
new_form = form[:i] + rhs_tuple + form[i+1:]
result = expand(new_form, steps + 1)
if result:
return [form] + result
break
return None
return expand(start, 0)
# Example grammar
grammar = CFG('E', {
'E': [('E', '+', 'T'), ('T',)],
'T': [('T', '*', 'F'), ('F',)],
'F': [('(', 'E', ')'), ('id',)],
})
# Leftmost derivation
print("Grammar: E โ E+T | T; T โ T*F | F; F โ (E) | id")
print("\nTarget: id+id*id")
lm = grammar.leftmost_derivation('id+id*id')
if lm:
print("\nLeftmost derivation:")
for i, form in enumerate(lm):
form_str = ' '.join(form) if form else 'ฮต'
print(f" {i}: {form_str}")
rm = grammar.rightmost_derivation('id+id*id')
if rm:
print("\nRightmost derivation:")
for i, form in enumerate(rm):
form_str = ' '.join(form) if form else 'ฮต'
print(f" {i}: {form_str}")
# SENTENTIAL FORMS
print("""
=== Sentential Forms ===
A sentential form is any string derivable from S.
S โ* ฮฑ (ฮฑ is a sentential form)
Types:
- Left-sentential: from leftmost derivation
- Right-sentential: from rightmost derivation
- Sentence: sentential form with only terminals
=== Example ===
For E โ E+T | T:
E (start, sentential form)
E + T (sentential form)
T + T (sentential form)
F + T (sentential form)
id + T (sentential form)
id + F (sentential form)
id + id (sentence โ all terminals)
""")
# DERIVATION TREES
print("""
=== Derivation Trees (Parse Trees) ===
A derivation tree shows the structure:
- Root: start symbol
- Internal nodes: nonterminals
- Leaves: terminals
- Children of A: RHS of production A โ ...
Example for id + id * id:
E
/|\
E + T
| /|\
T T * F
| | |
F F id
| |
id id
Each internal node corresponds to a rule application.
The yield of the tree is the derived string.
""")
# DERIVATION EXAMPLES
print("=== More Derivation Examples ===")
print("""
Grammar: S โ aSb | ฮต
Derivation of aabb:
S โ aSb โ aaSbb โ aabb
Grammar: S โ (S) | SS | ฮต
Derivation of (()):
S โ (S) โ (SS) โ ((S)S) โ (()S) โ (())
Derivation of ()():
S โ SS โ ()S โ ()(S) โ ()()
Grammar: S โ 0S1 | 01
Derivation of 000111:
S โ 0S1 โ 00S11 โ 000S111 โ 000111
""")
# LEFT VS RIGHT DERIVATION
print("=== Leftmost vs Rightmost ===")
print(f"{'Aspect':<30} {'Leftmost':<25} {'Rightmost':<25}")
print("-" * 80)
print(f"{'Expansion order':<30} {'Leftmost first':<25} {'Rightmost first':<25}")
print(f"{'Notation':<30} {'โ_lm':<25} {'โ_rm':<25}")
print(f"{'Used by':<30} {'LL parsers':<25} {'LR parsers':<25}")
print(f"{'Parse tree':<30} {'Preorder':<25} {'Reverse postorder':<25}")
print(f"{'Unique for':<30} {'Unambiguous grammars':<25} {'Unambiguous grammars':<25}")
print(f"{'Equivalent':<30} {'Yes (same tree)':<25} {'Yes (same tree)':<25}")
# IMPORTANT THEOREM
print("""
=== Important Theorem ===
For any grammar G and string w โ L(G):
w has a leftmost derivation
โบ
w has a rightmost derivation
โบ
w has a parse tree
All three are equivalent representations
of the same underlying structure.
This is why parsing works:
- We can parse left-to-right (LL)
- Or right-to-left (LR)
- Or build a tree
- All give the same result (for unambiguous grammars)
""")๐ณ Parse Trees
๐ฏ Tree Representation of Derivations
A parse tree (or derivation tree) is a tree that represents the syntactic structure of a string according to a grammar. The root is the start symbol, internal nodes are nonterminals, and leaves are terminals.
Parse Tree Properties
Root: Start symbol S. Internal: Nonterminals. Leaves: Terminals (or ฮต). Yield: Concatenation of leaves. Children: RHS of production. Structure: Captures nesting and precedence.
# Parse Trees in detail
print("=" * 60)
print("PARSE TREES")
print("=" * 60)
print("""
=== Definition ===
A parse tree for a CFG G = (V, ฮฃ, R, S) is a tree where:
1. Root is labeled S (start symbol)
2. Internal nodes are labeled with nonterminals
3. Leaves are labeled with terminals or ฮต
4. If node A has children Xโ, Xโ, ..., Xโ,
then A โ XโXโ...Xโ is a production in R
=== Yield ===
The yield of a parse tree is the string obtained
by reading the leaves left to right.
w โ L(G) โบ there exists a parse tree for w
=== Example ===
Grammar: E โ E + T | T
T โ T * F | F
F โ ( E ) | id
Parse tree for "id + id * id":
E
/|\
/ | \
E + T
| /|\
T T * F
| | |
F F id
| |
id id
Yield: id + id * id โ
""")
# PARSE TREE IN CODE
print("=== Parse Tree Implementation ===")
class ParseTreeNode:
def __init__(self, symbol, children=None):
self.symbol = symbol
self.children = children or []
def is_leaf(self):
return len(self.children) == 0
def yield_string(self):
"""Get the yield of the tree."""
if self.is_leaf():
return self.symbol if self.symbol != 'ฮต' else ''
return ''.join(child.yield_string() for child in self.children)
def __repr__(self, level=0):
indent = " " * level
result = f"{indent}{self.symbol}\n"
for child in self.children:
result += child.__repr__(level + 1)
return result
def to_ascii(self):
"""Pretty ASCII tree."""
lines = []
def build(node, prefix="", is_last=True):
connector = "โโโ " if is_last else "โโโ "
lines.append(prefix + connector + node.symbol)
new_prefix = prefix + (" " if is_last else "โ ")
for i, child in enumerate(node.children):
build(child, new_prefix, i == len(node.children) - 1)
lines.append(self.symbol)
for i, child in enumerate(self.children):
build(child, "", i == len(self.children) - 1)
return '\n'.join(lines)
# Build parse tree for id + id * id
# E โ E + T
# E โ T โ F โ id
# T โ T * F
# T โ F โ id
# F โ id
tree = ParseTreeNode('E', [
ParseTreeNode('E', [
ParseTreeNode('T', [
ParseTreeNode('F', [ParseTreeNode('id')])
])
]),
ParseTreeNode('+'),
ParseTreeNode('T', [
ParseTreeNode('T', [
ParseTreeNode('F', [ParseTreeNode('id')])
]),
ParseTreeNode('*'),
ParseTreeNode('F', [ParseTreeNode('id')])
])
])
print("Parse tree for 'id + id * id':")
print(tree.to_ascii())
print(f"\nYield: {tree.yield_string()}")
# PARSE TREE FOR BALANCED PARENS
print("\n=== Parse Tree for Balanced Parentheses ===")
# S โ (S) | SS | ฮต
# Parse tree for "(())"
paren_tree = ParseTreeNode('S', [
ParseTreeNode('('),
ParseTreeNode('S', [
ParseTreeNode('('),
ParseTreeNode('S', []), # ฮต
ParseTreeNode(')')
]),
ParseTreeNode(')')
])
print("Parse tree for '(())':")
print(paren_tree.to_ascii())
print(f"\nYield: {paren_tree.yield_string()}")
# AMBIGUITY AND PARSE TREES
print("""
=== Parse Trees and Ambiguity ===
A grammar is AMBIGUOUS if some string has
TWO OR MORE different parse trees.
Example:
Grammar: E โ E + E | E * E | id
String: id + id * id
Tree 1: (+ is root)
E
/|\
E + E
| |
id E*E
| |
id id
Value: id + (id * id) = id + id*id
Tree 2: (* is root)
E
/|\
E * E
/|\ |
E + E id
| |
id id
Value: (id + id) * id = id + id*id
Both trees yield the same string!
This grammar is ambiguous.
=== Why Ambiguity Matters ===
1. COMPILERS
- Need unambiguous grammar
- Otherwise, meaning is unclear
- Precedence and associativity
2. PARSING
- Multiple parse trees = multiple meanings
- Harder to parse efficiently
3. SOLUTION
- Rewrite grammar to be unambiguous
- Use precedence rules
- Use associativity rules
""")
# PARSE TREE PROPERTIES
print("=== Parse Tree Properties ===")
print("""
1. YIELD
The string derived from the tree.
Root-to-leaf concatenation.
2. HEIGHT
Longest path from root to leaf.
Related to derivation length.
3. DEPTH
Level of a node.
Root is depth 0.
4. ARITY
Number of children.
For CFG: any number โฅ 0.
5. FRONTIER
The leaves (terminals).
Same as yield.
=== Relationship to Derivations ===
Parse tree โบ Leftmost derivation
Parse tree โบ Rightmost derivation
For unambiguous grammar:
Each string has exactly ONE parse tree
(and hence one leftmost and one rightmost derivation)
""")
# PARSE TREE VISUALIZATION
print("=== Parse Tree Visualization ===")
print("""
For grammar S โ aSb | ฮต:
Parse tree for "aaabbb":
S
/|\
a S b
/|\
a S b
/|\
a S b
|
ฮต
Yield: a a a b b b = aaabbb โ
For grammar E โ E+T | T:
T โ T*F | F
F โ (E) | id
Parse tree for "(id + id) * id":
E
|
T
/|\
T * F
| |
F id
/|\
( E )
|
E+T
/|\
T + T
| |
F F
| |
id id
Yield: ( id + id ) * id โ
""")
# COUNTING PARSE TREES
print("=== Counting Parse Trees ===")
def count_derivations(grammar, target, max_steps=15):
"""Count the number of distinct parse trees (derivations)."""
from functools import lru_cache
memo = {}
def count(symbol, s, i, j):
"""Count ways to derive s[i:j] from symbol."""
key = (symbol, i, j)
if key in memo:
return memo[key]
if i == j:
# Empty string: check if symbol โ ฮต
result = 1 if symbol in grammar.rules and () in [tuple(r) for r in grammar.rules[symbol]] else 0
memo[key] = result
return result
total = 0
for rhs in grammar.rules.get(symbol, []):
if len(rhs) == 1 and rhs[0] in grammar.rules:
# A โ B
total += count(rhs[0], s, i, j)
elif len(rhs) == 2:
# A โ BC
for k in range(i + 1, j):
total += count(rhs[0], s, i, k) * count(rhs[1], s, k, j)
elif len(rhs) == 1 and rhs[0] not in grammar.rules:
# A โ terminal
if j == i + 1 and rhs[0] == s[i]:
total += 1
elif len(rhs) == 0:
# A โ ฮต
if i == j:
total += 1
memo[key] = total
return total
return count(grammar.start, target, 0, len(target))
# Example: ambiguous expression grammar
ambig_expr = CFG('E', {
'E': [('E', '+', 'E'), ('E', '*', 'E'), ('id',)],
})
print("Ambiguous grammar: E โ E + E | E * E | id")
print("\nNumber of parse trees:")
for s in ['id', 'id+id', 'id+id*id', 'id*id+id']:
n = count_derivations(ambig_expr, s)
print(f" '{s}' โ {n} parse tree(s)")
print("""
For 'id+id*id', there are 2 parse trees:
- (id + id) * id
- id + (id * id)
This grammar is ambiguous!
""")
# UNAMBIGUOUS VERSION
print("=== Unambiguous Version ===")
unambig_expr = CFG('E', {
'E': [('E', '+', 'T'), ('T',)],
'T': [('T', '*', 'F'), ('F',)],
'F': [('id',)],
})
print("Unambiguous grammar:")
print(" E โ E + T | T")
print(" T โ T * F | F")
print(" F โ id")
print("\nNumber of parse trees:")
for s in ['id', 'id+id', 'id+id*id', 'id*id+id']:
n = count_derivations(unambig_expr, s)
print(f" '{s}' โ {n} parse tree(s)")
print("""
Now each string has exactly ONE parse tree.
The grammar enforces:
- * has higher precedence than +
- + is left-associative
- * is left-associative
""")โ ๏ธ Ambiguity
๐ฏ When Grammars Have Multiple Meanings
A grammar is ambiguous if some string in its language has two or more different parse trees. Ambiguity is a property of the grammar, not the language โ some languages have both ambiguous and unambiguous grammars.
Ambiguity Types
Grammar ambiguity: A grammar with multiple parse trees. Language ambiguity: All grammars for L are ambiguous (inherently ambiguous). Parse tree: Different structures. Derivation: Multiple leftmost derivations. Precedence: Usually causes ambiguity. Associativity: Also a common cause.
# Ambiguity in detail
print("=" * 60)
print("AMBIGUITY")
print("=" * 60)
print("""
=== Definition ===
A CFG G is AMBIGUOUS if there exists a string
w โ L(G) with two or more distinct parse trees.
Equivalently:
w has two or more distinct leftmost derivations.
=== Example ===
Grammar: E โ E + E | E * E | id
String: id + id * id
Parse Tree 1: Parse Tree 2:
E E
/|\ /|\
E + E E * E
| | /|\ |
id E * E E + E id
| | | |
id id id id
Tree 1: id + (id * id) Tree 2: (id + id) * id
Both trees yield "id + id * id"
But they mean different things!
This grammar is AMBIGUOUS.
""")
# AMBIGUITY IN CODE
print("=== Detecting Ambiguity ===")
class CFG:
def __init__(self, start, rules):
self.start = start
self.rules = rules
def count_parse_trees(self, target, max_steps=15):
"""Count distinct parse trees for target."""
from functools import lru_cache
memo = {}
def count(symbol, i, j, depth=0):
if depth > max_steps:
return 0
key = (symbol, i, j, depth)
if key in memo:
return memo[key]
if i == j:
# ฮต production
if symbol in self.rules:
for rhs in self.rules[symbol]:
if rhs == () or rhs == ('ฮต',):
memo[key] = 1
return 1
memo[key] = 0
return 0
total = 0
for rhs in self.rules.get(symbol, []):
if rhs == () or rhs == ('ฮต',):
continue
if len(rhs) == 1:
if rhs[0] in self.rules:
total += count(rhs[0], i, j, depth + 1)
else:
if j == i + 1 and rhs[0] == target[i]:
total += 1
elif len(rhs) == 2:
for k in range(i + 1, j):
total += count(rhs[0], i, k, depth + 1) * count(rhs[1], k, j, depth + 1)
memo[key] = total
return total
return count(self.start, 0, len(target))
# Ambiguous grammar
ambig = CFG('E', {
'E': [('E', '+', 'E'), ('E', '*', 'E'), ('id',)],
})
print("Ambiguous grammar: E โ E + E | E * E | id")
print("\nCounting parse trees:")
test_strings = ['id', 'id+id', 'id+id*id', 'id*id+id', 'id+id+id']
for s in test_strings:
n = ambig.count_parse_trees(s)
status = "AMBIGUOUS" if n > 1 else "unambiguous"
print(f" '{s}' โ {n} tree(s) โ {status}")
# UNAMBIGUOUS VERSION
print("\n=== Unambiguous Version ===")
unambig = CFG('E', {
'E': [('E', '+', 'T'), ('T',)],
'T': [('T', '*', 'F'), ('F',)],
'F': [('id',)],
})
print("Unambiguous grammar:")
print(" E โ E + T | T")
print(" T โ T * F | F")
print(" F โ id")
print("\nCounting parse trees:")
for s in test_strings:
n = unambig.count_parse_trees(s)
print(f" '{s}' โ {n} tree(s)")
# WHY AMBIGUITY MATTERS
print("""
=== Why Ambiguity Matters ===
1. COMPILERS
- Ambiguous grammar โ ambiguous meaning
- Need unambiguous grammar for parsing
- Precedence and associativity
2. NATURAL LANGUAGE
- Ambiguity is common
- "I saw the man with the telescope"
- Multiple interpretations
3. PARSING ALGORITHMS
- LL/LR parsers need unambiguous grammars
- CYK/Earley handle ambiguity (list all trees)
- Ambiguity increases complexity
4. LANGUAGE DESIGN
- Programming languages avoid ambiguity
- Use precedence rules
- Use associativity rules
""")
# CAUSES OF AMBIGUITY
print("=== Common Causes of Ambiguity ===")
print("""
1. OPERATOR PRECEDENCE
E โ E + E | E * E | id
Is id + id * id = (id+id)*id or id+(id*id)?
2. ASSOCIATIVITY
E โ E - E | id
Is id - id - id = (id-id)-id or id-(id-id)?
3. DANGLING ELSE
S โ if E then S | if E then S else S | ...
Is else matched with inner or outer if?
4. DERIVATION ORDER
Multiple leftmost derivations
(same as multiple parse trees)
=== Solutions ===
1. PRECEDENCE
E โ E + T | T
T โ T * F | F
F โ id
2. ASSOCIATIVITY
E โ E + T | T (left-associative)
E โ T + E | T (right-associative)
3. REWRITE GRAMMAR
Eliminate ambiguity by restructuring rules.
""")
# DANGLING ELSE EXAMPLE
print("=== Dangling Else Example ===")
print("""
Ambiguous grammar:
S โ if E then S
| if E then S else S
| other
String: if E1 then if E2 then S1 else S2
Parse 1: else matches INNER if
if E1 then (if E2 then S1 else S2)
Parse 2: else matches OUTER if
if E1 then (if E2 then S1) else S2
Solution (unambiguous grammar):
S โ M | U
M โ if E then M else M | other
U โ if E then S
| if E then M else U
This forces else to match the nearest if.
""")
# INHERENT AMBIGUITY
print("=== Inherent Ambiguity ===")
print("""
Some languages are INHERENTLY AMBIGUOUS:
Every grammar for the language is ambiguous.
Example:
L = { a^i b^j c^k | i = j or j = k }
This language is inherently ambiguous.
No matter how you write the grammar,
some string will have multiple parse trees.
Proof: Requires advanced techniques
(see Ogden's lemma for CFLs).
=== Key Facts ===
1. Ambiguity is undecidable for CFGs.
(No algorithm can decide if a CFG is ambiguous.)
2. Some languages are inherently ambiguous.
3. For practical purposes:
- Most programming languages have unambiguous grammars
- We design them carefully
- We use precedence and associativity
""")
# AMBIGUITY IN PRACTICE
print("=== Ambiguity in Practice ===")
print("""
1. EXPRESSION GRAMMARS
Most common source of ambiguity.
Solved by layered grammar (E, T, F).
2. IF-THEN-ELSE
Dangling else problem.
Solved by grammar restructuring.
3. NATURAL LANGUAGE
"Time flies like an arrow"
- Time (verb) flies (noun)...
- Time (noun) flies (verb)...
Multiple meanings.
4. PROGRAMMING LANGUAGES
C, Java, Python: carefully designed
to be unambiguous (mostly).
5. PARSER GENERATORS
Yacc/Bison: detect ambiguity
ANTLR: resolves with precedence rules
Happy: reports conflicts
""")
# SUMMARY TABLE
print("=== Summary ===")
print(f"{'Aspect':<30} {'Ambiguous':<25} {'Unambiguous':<25}")
print("-" * 80)
print(f"{'Parse trees':<30} {'Multiple':<25} {'Unique':<25}")
print(f"{'Leftmost derivation':<30} {'Multiple':<25} {'Unique':<25}")
print(f"{'Meaning':<30} {'Unclear':<25} {'Clear':<25}")
print(f"{'Parsing':<30} {'Complex':<25} {'Efficient':<25}")
print(f"{'Compilers':<30} {'Bad':<25} {'Good':<25}")
print(f"{'Example':<30} {'E โ E+E | E*E':<25} {'E โ E+T | T':<25}")๐งน Grammar Simplification
๐ฏ Cleaning Up Grammars
Grammar simplification removes unnecessary symbols and rules without changing the language. This makes grammars easier to work with and is a prerequisite for Chomsky Normal Form.
Simplification Steps
1. Remove ฮต-productions: Eliminate A โ ฮต. 2. Remove unit productions: Eliminate A โ B. 3. Remove useless symbols: Unreachable and non-generating. 4. Remove duplicates: Redundant rules. 5. Result: Simplified grammar. Preserves: L(G) unchanged (except maybe ฮต).
# Grammar Simplification
print("=" * 60)
print("GRAMMAR SIMPLIFICATION")
print("=" * 60)
print("""
=== Why Simplify? ===
1. EASIER TO ANALYZE
Fewer rules and symbols.
2. REQUIRED FOR CNF
Chomsky Normal Form needs no ฮต or unit rules.
3. PARSING EFFICIENCY
Simpler grammars parse faster.
4. PROOF SIMPLICITY
Fewer cases to consider.
=== Order Matters ===
1. Remove ฮต-productions
2. Remove unit productions
3. Remove useless symbols
(both unreachable and non-generating)
Doing these in the right order preserves the language.
""")
# 1. REMOVE ฮต-PRODUCTIONS
print("=== 1. Remove ฮต-Productions ===")
print("""
A rule A โ ฮต is an ฮต-production.
PROCEDURE:
1. Find all nullable nonterminals (can derive ฮต).
2. For each rule with nullable symbols, add new rules
with those symbols omitted.
3. Remove all A โ ฮต (except possibly S โ ฮต).
EXAMPLE:
S โ AB
A โ aA | ฮต
B โ bB | ฮต
Nullable: A, B, S
New rules:
S โ AB | A | B | ฮต
A โ aA | a
B โ bB | b
Remove ฮต (except S โ ฮต if needed):
S โ AB | A | B
A โ aA | a
B โ bB | b
""")
def find_nullable(cfg):
"""Find all nullable nonterminals."""
nullable = set()
# Iteratively find nullable
changed = True
while changed:
changed = False
for lhs, productions in cfg.rules.items():
if lhs in nullable:
continue
for rhs in productions:
if rhs == () or rhs == ('ฮต',):
nullable.add(lhs)
changed = True
break
if all(s in nullable for s in rhs):
nullable.add(lhs)
changed = True
break
return nullable
def remove_epsilon(cfg):
"""Remove ฮต-productions."""
nullable = find_nullable(cfg)
new_rules = {}
for lhs, productions in cfg.rules.items():
new_prods = set()
for rhs in productions:
if rhs == () or rhs == ('ฮต',):
continue # skip ฮต
# Generate all combinations of omitting nullable symbols
from itertools import combinations
# Find positions of nullable symbols
nullable_positions = [i for i, s in enumerate(rhs) if s in nullable]
# Generate all subsets
for r in range(len(nullable_positions) + 1):
for omit in combinations(nullable_positions, r):
new_rhs = tuple(s for i, s in enumerate(rhs) if i not in omit)
if new_rhs: # don't add empty
new_prods.add(new_rhs)
# Also add original if no nullable
if not nullable_positions:
new_prods.add(rhs)
if new_prods:
new_rules[lhs] = list(new_prods)
# Handle start symbol
if cfg.start in nullable:
# Add S โ ฮต only if ฮต โ L
if cfg.start not in new_rules:
new_rules[cfg.start] = []
new_rules[cfg.start].append(())
return CFG(cfg.start, new_rules)
# Example
cfg1 = CFG('S', {
'S': [('A', 'B')],
'A': [('a', 'A'), ()],
'B': [('b', 'B'), ()],
})
print("Original grammar:")
print(" S โ AB")
print(" A โ aA | ฮต")
print(" B โ bB | ฮต")
print(f"\nNullable: {find_nullable(cfg1)}")
cfg1_noeps = remove_epsilon(cfg1)
print("\nAfter removing ฮต-productions:")
for lhs, prods in sorted(cfg1_noeps.rules.items()):
for rhs in prods:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
print(f" {lhs} โ {rhs_str}")
# 2. REMOVE UNIT PRODUCTIONS
print("\n=== 2. Remove Unit Productions ===")
print("""
A unit production is A โ B (single nonterminal).
PROCEDURE:
1. For each A, find all B such that A โ* B (using only unit rules).
2. For each such B and each B โ ฮฑ (non-unit),
add A โ ฮฑ.
3. Remove all unit productions.
EXAMPLE:
S โ A | B
A โ a
B โ b
Unit pairs: (S,A), (S,B)
Add:
S โ a (from A โ a)
S โ b (from B โ b)
Remove unit rules:
S โ a | b
A โ a
B โ b
""")
def find_unit_pairs(cfg):
"""Find all pairs (A, B) where A โ* B via unit rules."""
units = {A: set() for A in cfg.rules}
# Direct unit rules
for lhs, productions in cfg.rules.items():
for rhs in productions:
if len(rhs) == 1 and rhs[0] in cfg.rules:
units[lhs].add(rhs[0])
# Transitive closure
changed = True
while changed:
changed = False
for A in cfg.rules:
for B in list(units[A]):
for C in units.get(B, set()):
if C not in units[A]:
units[A].add(C)
changed = True
return units
def remove_unit_productions(cfg):
"""Remove unit productions."""
unit_pairs = find_unit_pairs(cfg)
new_rules = {}
for lhs in cfg.rules:
new_prods = set()
# Add non-unit productions
for rhs in cfg.rules[lhs]:
if len(rhs) == 1 and rhs[0] in cfg.rules:
continue # skip unit
new_prods.add(rhs)
# Add productions from unit-reachable nonterminals
for B in unit_pairs[lhs]:
for rhs in cfg.rules.get(B, []):
if len(rhs) == 1 and rhs[0] in cfg.rules:
continue
new_prods.add(rhs)
if new_prods:
new_rules[lhs] = list(new_prods)
return CFG(cfg.start, new_rules)
# Example
cfg2 = CFG('S', {
'S': [('A',), ('B',)],
'A': [('a',)],
'B': [('b',)],
})
print("Original grammar:")
print(" S โ A | B")
print(" A โ a")
print(" B โ b")
unit_pairs = find_unit_pairs(cfg2)
print(f"\nUnit pairs: {unit_pairs}")
cfg2_nounit = remove_unit_productions(cfg2)
print("\nAfter removing unit productions:")
for lhs, prods in sorted(cfg2_nounit.rules.items()):
for rhs in prods:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
print(f" {lhs} โ {rhs_str}")
# 3. REMOVE USELESS SYMBOLS
print("\n=== 3. Remove Useless Symbols ===")
print("""
A symbol is USELESS if it's:
- Not generating (can't derive a terminal string), OR
- Not reachable (can't be reached from S)
PROCEDURE:
1. Find generating symbols (can derive terminals).
2. Remove non-generating symbols and their rules.
3. Find reachable symbols (from S).
4. Remove unreachable symbols and their rules.
EXAMPLE:
S โ A B | a
A โ a
B โ b
C โ c (unreachable)
D โ D (non-generating)
Generating: S, A, B, C (D is not)
Reachable: S, A, B (C is not)
Remove D (non-generating), C (unreachable):
S โ A B | a
A โ a
B โ b
""")
def find_generating(cfg):
"""Find all generating nonterminals."""
generating = set()
changed = True
while changed:
changed = False
for lhs, productions in cfg.rules.items():
if lhs in generating:
continue
for rhs in productions:
if all(s not in cfg.rules or s in generating for s in rhs):
generating.add(lhs)
changed = True
break
return generating
def find_reachable(cfg, generating):
"""Find all reachable symbols from S."""
reachable = {cfg.start}
queue = [cfg.start]
while queue:
symbol = queue.pop(0)
for rhs in cfg.rules.get(symbol, []):
for s in rhs:
if s not in reachable:
reachable.add(s)
if s in cfg.rules:
queue.append(s)
return reachable
def remove_useless(cfg):
"""Remove useless symbols."""
generating = find_generating(cfg)
# Keep only generating rules
rules_gen = {}
for lhs, productions in cfg.rules.items():
if lhs not in generating:
continue
new_prods = []
for rhs in productions:
if all(s not in cfg.rules or s in generating for s in rhs):
new_prods.append(rhs)
if new_prods:
rules_gen[lhs] = new_prods
cfg_gen = CFG(cfg.start, rules_gen)
# Find reachable
reachable = find_reachable(cfg_gen, generating)
# Keep only reachable rules
rules_final = {}
for lhs, productions in cfg_gen.rules.items():
if lhs not in reachable:
continue
new_prods = []
for rhs in productions:
if all(s not in cfg_gen.rules or s in reachable for s in rhs):
new_prods.append(rhs)
if new_prods:
rules_final[lhs] = new_prods
return CFG(cfg.start, rules_final)
# Example
cfg3 = CFG('S', {
'S': [('A', 'B'), ('a',)],
'A': [('a',)],
'B': [('b',)],
'C': [('c',)],
'D': [('D',)],
})
print("Original grammar:")
for lhs, prods in sorted(cfg3.rules.items()):
for rhs in prods:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
print(f" {lhs} โ {rhs_str}")
print(f"\nGenerating: {find_generating(cfg3)}")
cfg3_clean = remove_useless(cfg3)
print("\nAfter removing useless symbols:")
for lhs, prods in sorted(cfg3_clean.rules.items()):
for rhs in prods:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
print(f" {lhs} โ {rhs_str}")
# FULL SIMPLIFICATION PIPELINE
print("\n=== Full Simplification Pipeline ===")
def simplify_cfg(cfg):
"""Apply all simplification steps."""
cfg = remove_epsilon(cfg)
cfg = remove_unit_productions(cfg)
cfg = remove_useless(cfg)
return cfg
# Test on a complex grammar
complex_cfg = CFG('S', {
'S': [('A', 'B'), ('C',)],
'A': [('a', 'A'), ()],
'B': [('b', 'B'), ()],
'C': [('c',)],
'D': [('d',)], # unreachable
'E': [('E',)], # non-generating
})
print("Complex grammar:")
for lhs, prods in sorted(complex_cfg.rules.items()):
for rhs in prods:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
print(f" {lhs} โ {rhs_str}")
simplified = simplify_cfg(complex_cfg)
print("\nAfter full simplification:")
for lhs, prods in sorted(simplified.rules.items()):
for rhs in prods:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
print(f" {lhs} โ {rhs_str}")
# IMPORTANT NOTES
print("""
=== Important Notes ===
1. ORDER MATTERS
- Remove ฮต first, then unit, then useless.
- Wrong order can leave useless symbols.
2. LANGUAGE PRESERVATION
- L(G) is preserved (except maybe ฮต).
- If ฮต โ L, keep S โ ฮต.
3. TERMINATION
- All procedures terminate.
- Grammar size may grow temporarily.
4. USES
- Preparation for CNF/GNF.
- Parsing optimization.
- Proof simplification.
5. LIMITATIONS
- Some grammars can't be made unambiguous.
- Simplification doesn't remove ambiguity.
""")๐ Chomsky Normal Form
๐ฏ A Standard Form for CFGs
Chomsky Normal Form (CNF) is a restricted form of CFG where every production is either A โ BC (two nonterminals) or A โ a (one terminal). CNF is used by the CYK parsing algorithm.
CNF Rules
Form 1: A โ BC (B, C nonterminals). Form 2: A โ a (a terminal). Optional: S โ ฮต (if ฮต โ L). No: Mixed rules, long rules, ฮต, units. Every CFG: Has an equivalent CNF. CYK: Requires CNF.
# Chomsky Normal Form
print("=" * 60)
print("CHOMSKY NORMAL FORM (CNF)")
print("=" * 60)
print("""
=== Definition ===
A CFG is in Chomsky Normal Form if every rule is:
1. A โ BC (two nonterminals)
2. A โ a (one terminal)
Plus optionally:
3. S โ ฮต (only if ฮต โ L)
=== No Other Forms Allowed ===
โ A โ ฮต (except S)
โ A โ B (unit)
โ A โ Bc (mixed)
โ A โ BCD (longer than 2)
โ A โ aB (mixed)
=== Conversion Steps ===
1. Simplify grammar (remove ฮต, unit, useless)
2. Handle terminals in long rules
3. Break long rules into binary rules
4. Result: CNF
=== Why CNF? ===
- CYK algorithm requires CNF
- Parsing complexity analysis
- Proof simplicity
- Every CFG has equivalent CNF
""")
# CNF CONVERSION IN CODE
print("=== CNF Conversion Algorithm ===")
class CFG:
def __init__(self, start, rules):
self.start = start
self.rules = rules
def __repr__(self):
lines = []
for lhs in sorted(self.rules.keys()):
for rhs in self.rules[lhs]:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
lines.append(f" {lhs} โ {rhs_str}")
return '\n'.join(lines)
def to_cnf(cfg):
"""Convert CFG to Chomsky Normal Form."""
# Step 0: Simplify (assume already simplified for this demo)
new_rules = {}
counter = [0]
def new_nonterminal():
counter[0] += 1
return f"X{counter[0]}"
# Step 1: Handle terminals in RHS of length > 1
# Replace each terminal a with a new nonterminal Ta โ a
terminal_map = {}
for lhs, productions in cfg.rules.items():
for rhs in productions:
if len(rhs) >= 2:
for symbol in rhs:
if symbol not in cfg.rules: # terminal
if symbol not in terminal_map:
ta = new_nonterminal()
terminal_map[symbol] = ta
if ta not in new_rules:
new_rules[ta] = [(symbol,)]
# Step 2: Rewrite rules with terminals replaced
intermediate_rules = {}
for lhs, productions in cfg.rules.items():
new_prods = []
for rhs in productions:
if len(rhs) >= 2:
new_rhs = tuple(
terminal_map.get(s, s) if s not in cfg.rules else s
for s in rhs
)
new_prods.append(new_rhs)
else:
new_prods.append(rhs)
intermediate_rules[lhs] = new_prods
# Step 3: Break long rules (length > 2) into binary rules
final_rules = dict(new_rules)
for lhs, productions in intermediate_rules.items():
new_prods = []
for rhs in productions:
if len(rhs) <= 2:
new_prods.append(rhs)
else:
# Break A โ B1 B2 ... Bk into
# A โ B1 X1
# X1 โ B2 X2
# ...
# Xk-2 โ Bk-1 Bk
current_lhs = lhs
for i in range(len(rhs) - 2):
xi = new_nonterminal()
if i == 0:
new_prods.append((rhs[0], xi))
else:
final_rules[current_lhs] = [(rhs[i], xi)]
current_lhs = xi
# Last pair
final_rules[current_lhs] = [(rhs[-2], rhs[-1])]
if new_prods:
final_rules[lhs] = new_prods
return CFG(cfg.start, final_rules)
# Example: Convert to CNF
print("Original grammar:")
print(" S โ a B c")
print(" B โ b")
original = CFG('S', {
'S': [('a', 'B', 'c')],
'B': [('b',)],
})
print(original)
cnf = to_cnf(original)
print("\nAfter CNF conversion:")
print(cnf)
# MORE COMPLEX EXAMPLE
print("\n=== More Complex Example ===")
complex = CFG('S', {
'S': [('A', 'B', 'C', 'D')],
'A': [('a',)],
'B': [('b',)],
'C': [('c',)],
'D': [('d',)],
})
print("Original grammar:")
print(complex)
cnf_complex = to_cnf(complex)
print("\nAfter CNF conversion:")
print(cnf_complex)
# CNF PROPERTIES
print("""
=== Properties of CNF ===
1. DERIVATION LENGTH
For string of length n:
- Exactly 2n-1 steps in any derivation
- n-1 steps using A โ BC
- n steps using A โ a
2. PARSE TREE
- Binary tree (each internal node has 2 children)
- Height โค n (for string of length n)
- Exactly 2n-1 internal nodes
3. CYK ALGORITHM
- Requires CNF
- O(nยณ) time
- O(nยฒ) space
4. EVERY CFG
- Has an equivalent CNF
- Conversion may increase size
- Preserves language
=== Example Derivation in CNF ===
Grammar:
S โ AB
A โ a
B โ b
Derivation of "ab":
S โ AB โ aB โ ab
(3 steps for n=2: 2n-1 = 3 โ)
""")
# GREIBACH NORMAL FORM
print("=== Greibach Normal Form (GNF) ===")
print("""
Another normal form:
A โ a ฮฑ where a is terminal, ฮฑ is (V)*
Every rule starts with exactly one terminal.
Properties:
- Every CFG has equivalent GNF
- Derivation length = n (for string of length n)
- Useful for top-down parsing
- No left recursion
Comparison:
CNF: A โ BC | a
GNF: A โ a ฮฑ
Both are normal forms for CFGs.
CNF is used for CYK (bottom-up).
GNF is used for top-down parsing.
""")
# CONVERSION COMPLEXITY
print("=== Conversion Complexity ===")
print(f"{'Step':<35} {'Complexity':<20}")
print("-" * 55)
print(f"{'Simplify (ฮต, unit, useless)':<35} {'O(|G|ยฒ)':<20}")
print(f"{'Handle terminals':<35} {'O(|G|)':<20}")
print(f"{'Break long rules':<35} {'O(|G|)':<20}")
print(f"{'Total':<35} {'O(|G|ยฒ)':<20}")
print()
print("Grammar size may increase:")
print(" - Each long rule โ multiple binary rules")
print(" - Each terminal in long rule โ new nonterminal")
print(" - Worst case: |G'| = O(|G|ยฒ)")
# CNF LIMITATIONS
print("=== CNF Limitations ===")
print("""
1. GRAMMAR SIZE
CNF conversion can significantly increase
the number of rules and nonterminals.
2. READABILITY
CNF grammars are hard for humans to read.
Use only for parsing algorithms.
3. AMBIGUITY
CNF doesn't remove ambiguity.
An ambiguous grammar becomes ambiguous CNF.
4. ฮต-PRODUCTIONS
Only S โ ฮต allowed.
Other ฮต-productions must be eliminated.
5. PRACTICAL USE
- CYK algorithm
- Theoretical proofs
- Not for hand-written parsers
""")
# SUMMARY
print("=== Summary ===")
print("""
CNF:
A โ BC (binary nonterminal)
A โ a (terminal)
Conversion:
1. Simplify
2. Handle terminals
3. Break long rules
Properties:
- Every CFG has equivalent CNF
- 2n-1 steps for length n
- Binary parse tree
- Used by CYK
Key insight:
Any CFG can be transformed into CNF
without changing the language.
This enables uniform parsing algorithms.
""")๐ CYK Algorithm
๐ฏ Parsing with Dynamic Programming
The CYK algorithm (Cocke-Younger-Kasami) determines if a string can be generated by a CFG in Chomsky Normal Form. It uses dynamic programming in O(nยณ) time.
CYK Table
Table: T[i][j] = nonterminals deriving s[i..j]. Base: T[i][i] = {A | A โ s[i]}. Inductive: T[i][j] from T[i][k] and T[k+1][j]. Accept: S โ T[0][n-1]. Complexity: O(nยณ) time, O(nยฒ) space. Requires: CNF grammar.
# CYK Algorithm
print("=" * 60)
print("CYK ALGORITHM")
print("=" * 60)
print("""
=== Overview ===
CYK (Cocke-Younger-Kasami) determines membership
in a CFL using dynamic programming.
Input: Grammar G in CNF, string w = aโaโ...aโ
Output: True if w โ L(G), False otherwise
=== Algorithm ===
1. Build table T[i][j] = set of nonterminals
that derive substring w[i..j].
2. Base case: For each i:
T[i][i] = { A | A โ aแตข is a rule }
3. Inductive case: For length L = 2 to n:
For each i, j = i+L-1:
T[i][j] = { A | A โ BC, B โ T[i][k], C โ T[k+1][j] }
4. Accept if S โ T[0][n-1].
=== Complexity ===
Time: O(nยณ ร |G|)
Space: O(nยฒ ร |G|)
=== Example ===
Grammar (CNF):
S โ AB | BC
A โ BA | a
B โ CC | b
C โ AB | a
String: "baaba"
Table (bottom-up):
""")
# CYK IMPLEMENTATION
print("=== CYK Implementation ===")
class CNFGrammar:
"""CFG in Chomsky Normal Form."""
def __init__(self, start, rules):
self.start = start
self.rules = rules # lhs -> list of rhs (tuples)
def is_cnf(self):
"""Check if grammar is in CNF."""
for lhs, productions in self.rules.items():
for rhs in productions:
if len(rhs) == 1:
# Must be terminal
if rhs[0] in self.rules:
return False
elif len(rhs) == 2:
# Both must be nonterminals
if rhs[0] not in self.rules or rhs[1] not in self.rules:
return False
elif len(rhs) == 0:
# Only S โ ฮต allowed
if lhs != self.start:
return False
else:
return False
return True
def cyk_parse(grammar, string):
"""
CYK algorithm for membership testing.
Returns (accepted, table).
"""
n = len(string)
if n == 0:
# Check if S โ ฮต
return (grammar.start in grammar.rules and
any(rhs == () for rhs in grammar.rules[grammar.start])), {}
# Initialize table: T[i][j] = set of nonterminals
# representing substring from i to j (inclusive)
T = [[set() for _ in range(n)] for _ in range(n)]
# Base case: substrings of length 1
for i in range(n):
char = string[i]
for lhs, productions in grammar.rules.items():
for rhs in productions:
if len(rhs) == 1 and rhs[0] == char:
T[i][i].add(lhs)
# Inductive case: substrings of length 2 to n
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
# Try all split points
for k in range(i, j):
# Check all binary rules
for lhs, productions in grammar.rules.items():
for rhs in productions:
if len(rhs) == 2:
B, C = rhs
if B in T[i][k] and C in T[k+1][j]:
T[i][j].add(lhs)
# Check if start symbol is in T[0][n-1]
accepted = grammar.start in T[0][n-1]
return accepted, T
def print_cyk_table(T, string):
"""Pretty print CYK table."""
n = len(string)
print(f"\nCYK Table for '{string}':")
print()
# Header
print(" ", end="")
for j in range(n):
print(f"{string[j]:>8}", end="")
print()
print(" " + "-" * (8 * n + 4))
for i in range(n):
print(f"{string[i]:>3} |", end="")
for j in range(n):
if i <= j:
cell = ','.join(sorted(T[i][j])) if T[i][j] else 'โ
'
print(f"{cell:>8}", end="")
else:
print(f"{'':>8}", end="")
print()
# Example grammar (CNF)
grammar = CNFGrammar('S', {
'S': [('A', 'B'), ('B', 'C')],
'A': [('B', 'A'), ('a',)],
'B': [('C', 'C'), ('b',)],
'C': [('A', 'B'), ('a',)],
})
print("Grammar (CNF):")
for lhs, prods in sorted(grammar.rules.items()):
for rhs in prods:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
print(f" {lhs} โ {rhs_str}")
print(f"\nIs CNF: {grammar.is_cnf()}")
# Test strings
test_strings = ['baaba', 'baab', 'ab', 'a', 'b', 'ba']
print("\n=== Membership Testing ===")
for s in test_strings:
accepted, T = cyk_parse(grammar, s)
result = "โ ACCEPT" if accepted else "โ REJECT"
print(f" '{s}' โ {result}")
# Show table for one string
accepted, T = cyk_parse(grammar, 'baaba')
print_cyk_table(T, 'baaba')
# TRACE THE ALGORITHM
print("=== Algorithm Trace for 'baaba' ===")
print("""
Step 1: Length 1 (base case)
T[0][0] = {A, C} (b โ B? No, wait...)
Let's check: b โ B (rule B โ b)
So T[0][0] = {B}
a โ A, C (rules A โ a, C โ a)
So T[1][1] = {A, C}
a โ A, C
T[2][2] = {A, C}
b โ B
T[3][3] = {B}
a โ A, C
T[4][4] = {A, C}
Step 2: Length 2
T[0][1] for "ba":
split k=0: T[0][0]={B}, T[1][1]={A,C}
Rules: S โ AB (B in T[0][0], A in T[1][1]) โ โ S
S โ BC (B in T[0][0], C in T[1][1]) โ โ S
A โ BA (B in T[0][0], A in T[1][1]) โ โ A
C โ AB (A? not in T[0][0]) โ
T[0][1] = {S, A}
T[1][2] for "aa":
T[1][1]={A,C}, T[2][2]={A,C}
Rules: S โ AB (A in T[1][1], B? not in T[2][2]) โ
S โ BC (B? not in T[1][1]) โ
A โ BA (B? not in T[1][1]) โ
B โ CC (C in T[1][1], C in T[2][2]) โ โ B
C โ AB (A in T[1][1], B? not in T[2][2]) โ
T[1][2] = {B}
T[2][3] for "ab":
T[2][2]={A,C}, T[3][3]={B}
S โ AB (A in T[2][2], B in T[3][3]) โ โ S
S โ BC (B? not in T[2][2]) โ
A โ BA (B? not in T[2][2]) โ
C โ AB (A in T[2][2], B in T[3][3]) โ โ C
T[2][3] = {S, C}
T[3][4] for "ba":
T[3][3]={B}, T[4][4]={A,C}
S โ AB (B in T[3][3], A in T[4][4]) โ โ S
S โ BC (B in T[3][3], C in T[4][4]) โ โ S
A โ BA (B in T[3][3], A in T[4][4]) โ โ A
T[3][4] = {S, A}
Step 3: Length 3
... (continue)
Final: S โ T[0][4] โ ACCEPT
""")
# CYK WITH PARSE TREE RECONSTRUCTION
print("=== CYK with Parse Tree Reconstruction ===")
def cyk_with_tree(grammar, string):
"""CYK that also reconstructs a parse tree."""
n = len(string)
if n == 0:
if grammar.start in grammar.rules:
for rhs in grammar.rules[grammar.start]:
if rhs == ():
return True, (grammar.start, [])
return False, None
# T[i][j] = dict: nonterminal -> (split_k, left_tree, right_tree)
T = [[{} for _ in range(n)] for _ in range(n)]
# Base case
for i in range(n):
char = string[i]
for lhs, productions in grammar.rules.items():
for rhs in productions:
if len(rhs) == 1 and rhs[0] == char:
T[i][i][lhs] = (None, char, None)
# Inductive
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
for k in range(i, j):
for lhs, productions in grammar.rules.items():
for rhs in productions:
if len(rhs) == 2:
B, C = rhs
if B in T[i][k] and C in T[k+1][j]:
if lhs not in T[i][j]:
T[i][j][lhs] = (k, B, C)
if grammar.start in T[0][n-1]:
# Reconstruct tree
def build(i, j, symbol):
entry = T[i][j].get(symbol)
if entry is None:
return None
if i == j:
return ParseTreeNode(symbol, [ParseTreeNode(entry[1])])
k, B, C = entry
left = build(i, k, B)
right = build(k+1, j, C)
return ParseTreeNode(symbol, [left, right])
tree = build(0, n-1, grammar.start)
return True, tree
return False, None
class ParseTreeNode:
def __init__(self, symbol, children=None):
self.symbol = symbol
self.children = children or []
def to_ascii(self):
lines = []
def build(node, prefix="", is_last=True):
connector = "โโโ " if is_last else "โโโ "
lines.append(prefix + connector + node.symbol)
new_prefix = prefix + (" " if is_last else "โ ")
for i, child in enumerate(node.children):
build(child, new_prefix, i == len(node.children) - 1)
lines.append(self.symbol)
for i, child in enumerate(self.children):
build(child, "", i == len(self.children) - 1)
return '\n'.join(lines)
# Test tree reconstruction
accepted, tree = cyk_with_tree(grammar, 'baaba')
if accepted:
print("Parse tree for 'baaba':")
print(tree.to_ascii())
else:
print("'baaba' not in language")
# CYK COMPLEXITY
print("""
=== CYK Complexity ===
Time: O(nยณ ร |G|)
- nยฒ substrings
- n split points per substring
- |G| rules to check
Space: O(nยฒ ร |G|)
- nยฒ table cells
- Each cell stores up to |G| nonterminals
For a fixed grammar, O(nยณ) time and O(nยฒ) space.
=== Optimizations ===
1. BIT VECTORS
Represent sets of nonterminals as bit vectors.
2. EARLEY PARSING
O(nยณ) worst case, O(n) for many grammars.
3. PACKRAT PARSING
Memoization + backtracking.
4. VALIANT'S ALGORITHM
O(n^ฯ) where ฯ < 2.373 (matrix multiplication).
=== Comparison ===
Algorithm Time Space Grammar
CYK O(nยณ) O(nยฒ) CNF
Earley O(nยณ) O(nยฒ) Any CFG
LL(k) O(n) O(1) LL(k)
LR(k) O(n) O(1) LR(k)
""")๐ค CFG โ PDA
๐ฏ Grammars and Pushdown Automata
Context-free grammars and pushdown automata (PDAs) are equivalent: every CFG has a corresponding PDA, and vice versa. PDAs are the "machines" for CFLs, just as DFAs are for regular languages.
Equivalence
CFG โ PDA: Nondeterministic PDA. PDA โ CFG: Construct grammar from PDA. PDA: FSM + stack. Stack: Unbounded memory. Nondeterminism: Required for equivalence. Deterministic PDAs: Weaker (DCFL).
# CFG โ PDA Equivalence
print("=" * 60)
print("CFG โ PDA EQUIVALENCE")
print("=" * 60)
print("""
=== Pushdown Automaton (PDA) ===
A PDA is a 7-tuple M = (Q, ฮฃ, ฮ, ฮด, qโ, Zโ, F):
Q: finite states
ฮฃ: input alphabet
ฮ: stack alphabet
ฮด: Q ร (ฮฃ โช {ฮต}) ร ฮ โ P(Q ร ฮ*) (transition function)
qโ: start state
Zโ: initial stack symbol
F: accept states
Key difference from DFA:
- Has a STACK (unbounded memory)
- Can push/pop symbols
- Nondeterministic in general
=== Equivalence Theorem ===
L is context-free
โบ
L is accepted by some PDA
This is the CFL analog of:
L is regular โบ L is accepted by some DFA
=== Two Acceptance Modes ===
1. ACCEPT BY FINAL STATE
- End in an accept state
- Stack contents irrelevant
2. ACCEPT BY EMPTY STACK
- End with empty stack
- State irrelevant
Both modes are equivalent!
""")
# PDA IMPLEMENTATION
print("=== PDA Implementation ===")
class PDA:
"""
Pushdown Automaton.
Transitions: (state, input_symbol, stack_top) -> list of (next_state, stack_push)
"""
def __init__(self, states, input_alphabet, stack_alphabet,
transitions, start_state, start_stack, accept_states):
self.Q = states
self.Sigma = input_alphabet
self.Gamma = stack_alphabet
self.delta = transitions
self.q0 = start_state
self.Z0 = start_stack
self.F = accept_states
def accepts(self, input_string, max_steps=1000):
"""Simulate PDA (nondeterministic)."""
# Configuration: (state, remaining_input, stack)
initial = (self.q0, input_string, (self.Z0,))
from collections import deque
queue = deque([(initial, 0)])
visited = set()
while queue:
(state, remaining, stack), steps = queue.popleft()
if steps > max_steps:
continue
key = (state, remaining, stack)
if key in visited:
continue
visited.add(key)
# Check acceptance
if state in self.F and not remaining:
return True
# Also accept by empty stack
if not stack:
return True
# Try epsilon transitions
if stack:
top = stack[0]
for next_state, push in self.delta.get((state, 'ฮต', top), []):
new_stack = tuple(push) + stack[1:]
queue.append(((next_state, remaining, new_stack), steps + 1))
# Try consuming input
if remaining and stack:
symbol = remaining[0]
top = stack[0]
for next_state, push in self.delta.get((state, symbol, top), []):
new_stack = tuple(push) + stack[1:]
queue.append(((next_state, remaining[1:], new_stack), steps + 1))
return False
# Example: PDA for a^n b^n
print("=== Example: PDA for a^n b^n ===")
print("""
States: {q0, q1, q2}
Input: {a, b}
Stack: {Z, A}
Start: q0, Z
Accept: {q2}
Transitions:
(q0, a, Z) โ (q0, AZ) push A
(q0, a, A) โ (q0, AA) push A
(q0, b, A) โ (q1, ฮต) pop A, switch to q1
(q1, b, A) โ (q1, ฮต) pop A
(q1, ฮต, Z) โ (q2, Z) accept
This PDA:
- Pushes A for each 'a'
- Pops A for each 'b'
- Accepts when stack has only Z and input is done
""")
pda_anbn = PDA(
states={'q0', 'q1', 'q2'},
input_alphabet={'a', 'b'},
stack_alphabet={'Z', 'A'},
transitions={
('q0', 'a', 'Z'): [('q0', ('A', 'Z'))],
('q0', 'a', 'A'): [('q0', ('A', 'A'))],
('q0', 'b', 'A'): [('q1', ())],
('q1', 'b', 'A'): [('q1', ())],
('q1', 'ฮต', 'Z'): [('q2', ('Z',))],
},
start_state='q0',
start_stack='Z',
accept_states={'q2'}
)
print("Testing a^n b^n PDA:")
for s in ['', 'ab', 'aabb', 'aaabbb', 'aab', 'abb', 'ba', 'aaabbb']:
result = pda_anbn.accepts(s)
print(f" '{s}' โ {'โ' if result else 'โ'}")
# CFG TO PDA CONSTRUCTION
print("\n=== CFG to PDA Construction ===")
print("""
Given CFG G = (V, ฮฃ, R, S), construct PDA M:
M = ({q0, q1}, ฮฃ, V โช ฮฃ โช {Z}, ฮด, q0, Z, {q1})
Transitions:
1. (q0, ฮต, Z) โ (q1, S Z) [start: push S]
2. For each rule A โ ฮฑ:
(q1, ฮต, A) โ (q1, ฮฑ^R) [expand A]
3. For each terminal a:
(q1, a, a) โ (q1, ฮต) [match terminal]
4. (q1, ฮต, Z) โ (q1, ฮต) [accept: empty stack]
Idea:
- Push start symbol
- Nondeterministically expand nonterminals
- Match terminals with input
- Accept when stack is empty
This PDA accepts by empty stack.
""")
def cfg_to_pda(cfg):
"""Convert CFG to PDA (accept by empty stack)."""
states = {'q0', 'q1'}
stack_alphabet = set(cfg.rules.keys()) | set('Z') | set()
for prods in cfg.rules.values():
for rhs in prods:
stack_alphabet |= set(rhs)
transitions = {}
# Start: push S
transitions[('q0', 'ฮต', 'Z')] = [('q1', (cfg.start, 'Z'))]
# Expand nonterminals (push RHS in reverse)
for lhs, prods in cfg.rules.items():
for rhs in prods:
key = ('q1', 'ฮต', lhs)
if key not in transitions:
transitions[key] = []
# Push in reverse order (stack is LIFO)
transitions[key].append(('q1', tuple(reversed(rhs))))
# Match terminals
terminals = set()
for prods in cfg.rules.values():
for rhs in prods:
for s in rhs:
if s not in cfg.rules:
terminals.add(s)
for a in terminals:
transitions[('q1', a, a)] = [('q1', ())]
# Accept: empty stack
transitions[('q1', 'ฮต', 'Z')] = [('q1', ())]
return PDA(states, terminals, stack_alphabet, transitions, 'q0', 'Z', {'q1'})
# Example: Convert a^n b^n grammar
anbn_grammar = CFG('S', {
'S': [('a', 'S', 'b'), ()],
})
pda_from_cfg = cfg_to_pda(anbn_grammar)
print("CFG: S โ aSb | ฮต")
print("\nConverted PDA:")
print(f" States: {pda_from_cfg.Q}")
print(f" Input: {pda_from_cfg.Sigma}")
print(f" Stack: {pda_from_cfg.Gamma}")
print(f" Transitions: {len(pda_from_cfg.delta)}")
print("\nTesting:")
for s in ['', 'ab', 'aabb', 'aaabbb', 'aab', 'abb', 'ba']:
result = pda_from_cfg.accepts(s)
print(f" '{s}' โ {'โ' if result else 'โ'}")
# PDA TO CFG (sketch)
print("""
=== PDA to CFG (Sketch) ===
Given PDA M, construct CFG G:
Idea: Nonterminals are triples [p, A, q]
meaning: "from state p with A on stack,
go to state q and pop A"
Rules:
1. For each state q: [q, A, q] โ ฮต
2. For transition (p, a, A) โ (r, BโBโ...Bโ):
[p, A, q] โ a [r, Bโ, qโ] [qโ, Bโ, qโ] ... [qโ, Bโ, q]
Start symbol: [qโ, Zโ, q_accept]
This is complex but shows the equivalence.
In practice, we usually go CFG โ PDA.
""")
# DETERMINISTIC PDAs
print("=== Deterministic PDAs (DPDAs) ===")
print("""
A DPDA has at most one transition per configuration.
|ฮด(q, a, Z)| โค 1
DPDAs are WEAKER than nondeterministic PDAs!
Languages accepted by DPDAs:
- Called DETERMINISTIC CFLs (DCFLs)
- Strict subset of CFLs
- Include most programming languages
- Parsed by LR(k) parsers
Example of CFL not accepted by DPDA:
L = { ww^R | w โ {a,b}* }
(palindromes)
Requires nondeterminism to guess the middle.
Example of DCFL:
L = { a^n b^n | n โฅ 0 }
Deterministic: push a's, then pop b's.
=== Comparison ===
Type Machine Languages
DFA Finite states Regular
DPDA Stack (det.) DCFL
PDA Stack (nondet.) CFL
LBA Linear bounded CSL
TM Infinite tape RE
""")
# SUMMARY TABLE
print("=== Summary ===")
print(f"{'Aspect':<30} {'CFG':<25} {'PDA':<25}")
print("-" * 80)
print(f"{'Type':<30} {'Grammar':<25} {'Machine':<25}")
print(f"{'Memory':<30} {'Rules':<25} {'Stack':<25}")
print(f"{'Nondeterminism':<30} {'Inherent':<25} {'Allowed':<25}")
print(f"{'Acceptance':<30} {'Derivation':<25} {'Final state/empty stack':<25}")
print(f"{'Deterministic?':<30} {'N/A':<25} {'DPDA (weaker)':<25}")
print(f"{'Closure โช':<30} {'Yes':<25} {'Yes':<25}")
print(f"{'Closure โฉ':<30} {'No':<25} {'No':<25}")
print(f"{'Membership':<30} {'O(nยณ) CYK':<25} {'O(nยณ)':<25}")
print(f"{'Example':<30} {'S โ aSb':<25} {'Push/pop a/b':<25}")๐ Closure Properties
๐ฏ What Operations Preserve CFLs?
Context-free languages are closed under union, concatenation, Kleene star, and more. But they are NOT closed under intersection or complement.
Closure Properties of CFLs
Closed under: Union, concat, star, reversal, homomorphism. NOT closed under: Intersection, complement. Closed under: โฉ regular. Proof: Constructive with CFGs. Key: Intersection of two CFLs may not be CFL. Example: anbncn.
# Closure Properties of CFLs
print("=" * 60)
print("CLOSURE PROPERTIES OF CFLs")
print("=" * 60)
print("""
=== Operations that PRESERVE Context-Freeness ===
โ UNION: L1 โช L2
Construction: New start S โ S1 | S2
G = (V1 โช V2 โช {S}, ฮฃ, R1 โช R2 โช {S โ S1 | S2}, S)
โ CONCATENATION: L1 ยท L2
Construction: New start S โ S1 S2
G = (V1 โช V2 โช {S}, ฮฃ, R1 โช R2 โช {S โ S1 S2}, S)
โ KLEENE STAR: L*
Construction: New start S โ S S1 | ฮต
G = (V1 โช {S}, ฮฃ, R1 โช {S โ S S1 | ฮต}, S)
โ REVERSAL: L^R
Construction: Reverse all RHS in rules
A โ ฮฑ becomes A โ ฮฑ^R
โ HOMOMORPHISM: h(L)
Construction: Apply h to all terminals in rules
A โ a becomes A โ h(a)
โ INVERSE HOMOMORPHISM: hโปยน(L)
More complex, but still CFL.
โ INTERSECTION WITH REGULAR: L โฉ R
Construction: Product of PDA and DFA.
(Key: CFL โฉ Regular = CFL)
""")
# CLOSURE CONSTRUCTIONS IN CODE
print("=== Closure Constructions ===")
class CFG:
def __init__(self, start, rules):
self.start = start
self.rules = rules
def __repr__(self):
lines = []
for lhs in sorted(self.rules.keys()):
for rhs in self.rules[lhs]:
rhs_str = ' '.join(rhs) if rhs else 'ฮต'
lines.append(f" {lhs} โ {rhs_str}")
return '\n'.join(lines)
def union_cfg(g1, g2, new_start='S_union'):
"""L(G1) โช L(G2)"""
new_rules = dict(g1.rules)
for lhs, prods in g2.rules.items():
if lhs in new_rules:
new_rules[lhs] = new_rules[lhs] + prods
else:
new_rules[lhs] = prods
# Add new start
new_rules[new_start] = [(g1.start,), (g2.start,)]
return CFG(new_start, new_rules)
def concat_cfg(g1, g2, new_start='S_concat'):
"""L(G1) ยท L(G2)"""
new_rules = dict(g1.rules)
for lhs, prods in g2.rules.items():
if lhs in new_rules:
new_rules[lhs] = new_rules[lhs] + prods
else:
new_rules[lhs] = prods
new_rules[new_start] = [(g1.start, g2.start)]
return CFG(new_start, new_rules)
def star_cfg(g, new_start='S_star'):
"""L(G)*"""
new_rules = dict(g.rules)
new_rules[new_start] = [(new_start, g.start), ()]
return CFG(new_start, new_rules)
def reverse_cfg(g, new_start=None):
"""L(G)^R"""
new_rules = {}
for lhs, prods in g.rules.items():
new_prods = []
for rhs in prods:
new_prods.append(tuple(reversed(rhs)))
new_rules[lhs] = new_prods
return CFG(g.start, new_rules)
# Example
g1 = CFG('S1', {'S1': [('a', 'S1', 'b'), ()]}) # a^n b^n
g2 = CFG('S2', {'S2': [('c', 'S2', 'd'), ()]}) # c^n d^n
print("G1: a^n b^n")
print(g1)
print("\nG2: c^n d^n")
print(g2)
union = union_cfg(g1, g2)
print("\nUnion (G1 โช G2):")
print(union)
concat = concat_cfg(g1, g2)
print("\nConcatenation (G1 ยท G2):")
print(concat)
star = star_cfg(g1)
print("\nStar (G1*):")
print(star)
# NON-CLOSURE
print("""
=== Operations that DO NOT Preserve Context-Freeness ===
โ INTERSECTION: L1 โฉ L2
Counterexample:
L1 = { aโฟbโฟcแต | n, m โฅ 0 } (CFL)
L2 = { aโฟbแตcแต | n, m โฅ 0 } (CFL)
L1 โฉ L2 = { aโฟbโฟcโฟ | n โฅ 0 } (NOT CFL)
โ COMPLEMENT: ฮฃ* \ L
Counterexample:
L = { ww | w โ {a,b}* } is NOT CFL.
But complement of L is also NOT CFL.
(Actually, if L were CFL and complement CFL,
then L โฉ complement would be CFL, but...)
More precisely:
CFLs are not closed under complement.
Proof: If they were, then since they're closed
under union, they'd be closed under intersection
(by De Morgan). But they're not.
โ DIFFERENCE: L1 \ L2
Since L1 \ L2 = L1 โฉ complement(L2),
and intersection/complement fail,
difference also fails.
""")
# INTERSECTION WITH REGULAR
print("=== Intersection with Regular ===")
print("""
KEY THEOREM: CFL โฉ Regular = CFL
If L is context-free and R is regular,
then L โฉ R is context-free.
Proof idea:
- Take PDA for L
- Take DFA for R
- Build product PDA
- States: Q_PDA ร Q_DFA
- Accept: both accept
This is useful for proving languages are NOT CFL:
If L โฉ R is not CFL (and R is regular),
then L is not CFL.
Example:
L = { aโฑbสฒcแต | i < j < k }
R = a*b*c* (regular)
L โฉ R = { aโฑbสฒcแต | i < j < k } = known not CFL
Therefore L is not CFL.
""")
# SUMMARY TABLE
print("=== Closure Summary Table ===")
print(f"{'Operation':<30} {'Regular':<15} {'CFL':<15}")
print("-" * 60)
print(f"{'Union':<30} {'โ':<15} {'โ':<15}")
print(f"{'Intersection':<30} {'โ':<15} {'โ':<15}")
print(f"{'Complement':<30} {'โ':<15} {'โ':<15}")
print(f"{'Difference':<30} {'โ':<15} {'โ':<15}")
print(f"{'Concatenation':<30} {'โ':<15} {'โ':<15}")
print(f"{'Kleene Star':<30} {'โ':<15} {'โ':<15}")
print(f"{'Reversal':<30} {'โ':<15} {'โ':<15}")
print(f"{'Homomorphism':<30} {'โ':<15} {'โ':<15}")
print(f"{'Inverse Homomorphism':<30} {'โ':<15} {'โ':<15}")
print(f"{'โฉ Regular':<30} {'โ':<15} {'โ':<15}")
print("""
=== Key Insight ===
CFLs are "almost" closed under everything,
EXCEPT intersection and complement.
This is why:
- aโฟbโฟ is CFL (one counter)
- aโฟbโฟcโฟ is NOT CFL (two counters)
- { ww } is NOT CFL (copying)
- { ww^R } IS CFL (palindrome โ one stack)
The stack gives ONE counter.
To recognize aโฟbโฟcโฟ, you'd need TWO counters,
which requires more than a stack.
=== Decision Problems ===
Membership: Decidable (CYK, O(nยณ))
Emptiness: Decidable (check generating)
Finiteness: Decidable
Equivalence: UNDECIDABLE
Ambiguity: UNDECIDABLE
Universality: UNDECIDABLE
For regular languages:
All these are decidable!
""")๐ผ Applications
๐ฏ CFGs in the Real World
Context-free grammars are the backbone of compiler design, natural language processing, and data format specification. Every programming language has a CFG.
Application Domains
Compilers: Syntax analysis. Parsing: LL, LR, CYK, Earley. NLP: Sentence structure. Data formats: JSON, XML, YAML. Protocols: Message formats. Bioinformatics: RNA folding.
# Applications of CFGs
print("=" * 60)
print("APPLICATIONS OF CFGs")
print("=" * 60)
# 1. COMPILERS
print("""
=== 1. Compilers ===
The most important application.
Pipeline:
Source code โ Lexer โ Tokens โ Parser โ AST โ Code gen
Parser uses CFG:
- Grammar specifies syntax
- Parser checks if tokens form valid program
- Builds parse tree (AST)
Example: Expression grammar
E โ E + T | T
T โ T * F | F
F โ ( E ) | id
This handles:
- Precedence (* before +)
- Associativity (left)
- Parentheses (nesting)
Tools:
- Yacc/Bison (LR)
- ANTLR (LL)
- JavaCC
- Happy (Haskell)
""")
# 2. PARSING ALGORITHMS
print("""
=== 2. Parsing Algorithms ===
CFGs enable systematic parsing:
TOP-DOWN:
- Recursive descent
- LL(1), LL(k)
- Predictive parsing
BOTTOM-UP:
- LR(0), SLR(1), LALR(1), LR(1)
- Shift-reduce parsing
- Yacc/Bison
GENERAL:
- CYK (O(nยณ), CNF)
- Earley (O(nยณ), any CFG)
- GLR (generalized LR)
Each has trade-offs:
- LL: simple, fast, limited grammars
- LR: powerful, fast, complex tables
- CYK: general, slow
- Earley: general, medium
""")
# 3. NATURAL LANGUAGE
print("""
=== 3. Natural Language Processing ===
CFGs model sentence structure:
S โ NP VP
NP โ Det N | Det Adj N | NP PP
VP โ V NP | V PP | V
PP โ P NP
Example: "The cat sat on the mat"
S
โโโ NP
โ โโโ Det: The
โ โโโ N: cat
โโโ VP
โโโ V: sat
โโโ PP
โโโ P: on
โโโ NP
โโโ Det: the
โโโ N: mat
Used in:
- Parsing (syntax)
- Machine translation
- Information extraction
- Question answering
""")
# 4. DATA FORMATS
print("""
=== 4. Data Formats ===
Many data formats are context-free:
JSON:
value โ object | array | string | number | true | false | null
object โ { members } | { }
members โ pair | pair , members
pair โ string : value
array โ [ elements ] | [ ]
elements โ value | value , elements
XML:
element โ content
content โ element | text | content content
YAML:
(indentation-based, but still CFG)
These require nesting, so regex can't parse them!
""")
# 5. PROTOCOLS
print("""
=== 5. Network Protocols ===
Message formats:
HTTP:
request โ method SP url SP version CRLF headers CRLF body
headers โ header | header CRLF headers
header โ name : value
Email (RFC 5322):
message โ headers CRLF body
headers โ header | header CRLF headers
SIP, SMTP, FTP:
All have CFG-based message formats.
Why?
- Nested structures
- Variable-length fields
- Context-dependent parsing
""")
# 6. BIOINFORMATICS
print("""
=== 6. Bioinformatics ===
RNA secondary structure:
- RNA folds into shapes
- Base pairs: A-U, G-C
- Nested structure = CFG
Grammar for RNA:
S โ a S u | c S g | g S c | u S a | S S | ฮต
This models:
- Stems (base pairs)
- Loops
- Branches
Used in:
- RNA folding prediction
- Sequence analysis
- Drug design
""")
# 7. PROGRAMMING LANGUAGES
print("""
=== 7. Programming Languages ===
Every language has a CFG:
Python:
file_input โ (NEWLINE | stmt)* ENDMARKER
stmt โ simple_stmt | compound_stmt
compound_stmt โ if_stmt | while_stmt | for_stmt | ...
if_stmt โ 'if' test ':' suite ('elif' test ':' suite)*
['else' ':' suite]
Java:
CompilationUnit โ PackageDeclaration? ImportDeclaration*
TypeDeclaration*
TypeDeclaration โ ClassDeclaration | InterfaceDeclaration
ClassDeclaration โ Modifier* 'class' Identifier ...
C:
translation_unit โ external_declaration+
external_declaration โ function_definition | declaration
function_definition โ declaration_specifiers declarator
compound_statement
These grammars are published in language specs.
""")
# 8. TOOLS
print("""
=== 8. Tools ===
PARSER GENERATORS:
- Yacc/Bison (C)
- ANTLR (Java, Python, C++, ...)
- JavaCC (Java)
- Happy (Haskell)
- PLY (Python)
- PEG.js (JavaScript)
PARSING LIBRARIES:
- Lark (Python)
- pyparsing (Python)
- Parsec (Haskell)
- Nom (Rust)
- Boost.Spirit (C++)
Each takes a grammar and generates a parser.
Example (Lark):
grammar = '''
start: expr
?expr: expr "+" term -> add
| term
?term: term "*" factor -> mul
| factor
?factor: "(" expr ")"
| NUMBER
%import common.NUMBER
%import common.WS
%ignore WS
'''
""")
# 9. SUMMARY TABLE
print("=== Application Summary ===")
print(f"{'Domain':<25} {'Use Case':<30} {'CFG Role':<25}")
print("-" * 80)
print(f"{'Compilers':<25} {'Syntax analysis':<30} {'Grammar specification':<25}")
print(f"{'Parsing':<25} {'Parse algorithms':<30} {'Input grammar':<25}")
print(f"{'NLP':<25} {'Sentence parsing':<30} {'Syntactic structure':<25}")
print(f"{'Data formats':<25} {'JSON, XML':<30} {'Format definition':<25}")
print(f"{'Protocols':<25} {'Message parsing':<30} {'Protocol grammar':<25}")
print(f"{'Bioinformatics':<25} {'RNA folding':<30} {'Nested structure':<25}")
print(f"{'Languages':<25} {'Language design':<30} {'Language spec':<25}")
print(f"{'Tools':<25} {'Parser generators':<30} {'Input grammar':<25}")
# 10. CASE STUDY: PYTHON GRAMMAR
print("""
=== Case Study: Python Grammar (excerpt) ===
From Python's official Grammar file:
single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
file_input: (NEWLINE | stmt)* ENDMARKER
eval_input: testlist NEWLINE* ENDMARKER
stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt |
with_stmt | funcdef | classdef | decorated | async_stmt
if_stmt: 'if' test ':' suite ('elif' test ':' suite)*
['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'@'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom_expr ['**' factor]
This grammar defines ALL of Python's syntax!
""")๐ Advanced Topics
๐ฏ Beyond Basic CFGs
CFGs are the foundation for many advanced topics: parsing algorithms, grammar transformations, formal language theory, and modern applications.
Advanced Directions
Earley: General parsing. GLR: Generalized LR. PEG: Parsing expression grammars. Probabilistic: PCFGs for NLP. Tree: Tree adjoining grammars. Quantum: Quantum grammars.
# Advanced Topics in CFGs
print("=" * 60)
print("ADVANCED TOPICS")
print("=" * 60)
# 1. EARLEY PARSING
print("""
=== 1. Earley Parsing ===
A general parsing algorithm for ANY CFG.
Idea: Dynamic programming with "dotted rules."
Items: A โ ฮฑ ยท ฮฒ (ฮฑ parsed, ฮฒ remaining)
- Predictor: expand nonterminals
- Scanner: match terminals
- Completer: combine completed items
Complexity:
O(nยณ) for general CFGs
O(nยฒ) for unambiguous grammars
O(n) for LL(k)/LR(k) grammars
Advantages:
- Handles any CFG
- Handles ambiguity (lists all parses)
- Handles left recursion
- Online (processes left-to-right)
Used in:
- NLP (natural language parsing)
- Some compiler frontends
- Grammar development tools
""")
# 2. GLR PARSING
print("""
=== 2. GLR Parsing ===
Generalized LR parsing.
Idea: When LR parser encounters a conflict,
fork the parse stack.
Handles:
- Ambiguous grammars
- Non-LR grammars
- Multiple parse trees
Complexity: O(nยณ) worst case
Used in:
- C++ parsers (some)
- Elkhound (C++)
- Bison with GLR mode
- Parsing ambiguous languages
""")
# 3. PEG (PARSING EXPRESSION GRAMMARS)
print("""
=== 3. PEG ===
Parsing Expression Grammars (Ford, 2004).
Different from CFG:
- Ordered choice (not unordered)
- Greedy matching
- No ambiguity by design
Operators:
e1 e2 Sequence
e1 / e2 Ordered choice
e* Zero or more
e+ One or more
e? Optional
&e And-predicate
!e Not-predicate
Advantages:
- No ambiguity
- Simple to implement
- Good for DSLs
Used in:
- PEG.js (JavaScript)
- Pest (Rust)
- LPeg (Lua)
- Python's pegen
Note: PEGs are not CFGs!
They recognize a different class of languages.
""")
# 4. PROBABILISTIC CFGs
print("""
=== 4. Probabilistic CFGs (PCFGs) ===
Each rule has a probability:
A โ ฮฑ with probability p
Sum of probabilities for each A = 1.
Used in NLP:
- Parsing ambiguous sentences
- Choosing most likely parse
- Language modeling
Example:
S โ NP VP [0.8]
S โ VP [0.2]
NP โ Det N [0.6]
NP โ N [0.4]
P(tree) = product of rule probabilities
CKY can be extended to find most probable parse.
""")
# 5. TREE ADJOINING GRAMMARS
print("""
=== 5. Tree Adjoining Grammars (TAG) ===
More powerful than CFGs.
Idea: Combine tree fragments using:
- Substitution (replace leaf with tree)
- Adjunction (insert tree into tree)
Generates:
- All CFLs
- Some non-CFLs (e.g., aโฟbโฟcโฟdโฟ)
- Mildly context-sensitive languages
Used in:
- NLP (some theories)
- Treebanks
- Computational linguistics
Complexity: O(nโถ) parsing
""")
# 6. QUANTUM GRAMMARS
print("""
=== 6. Quantum Grammars ===
Quantum context-free grammars:
- Rules have quantum amplitudes
- Superposition of derivations
- Measurement at the end
Research area:
- Quantum natural language processing
- Quantum compiler design
- Theoretical computer science
Still experimental.
""")
# 7. MODERN PARSING
print("""
=== 7. Modern Parsing Trends ===
1. NEURAL PARSING
- Deep learning for parsing
- No explicit grammar
- Still uses CFG concepts
2. PARSING WITH TRANSFORMERS
- Attention-based parsing
- Fast, accurate
- Black-box models
3. HYBRID APPROACHES
- CFG + neural
- Best of both worlds
4. INCREMENTAL PARSING
- Online, left-to-right
- Important for interactive systems
5. MULTILINGUAL PARSING
- Universal Dependencies
- Cross-lingual transfer
""")
# 8. OPEN PROBLEMS
print("""
=== 8. Open Problems ===
1. EFFICIENT GENERAL PARSING
- Can we parse any CFG in O(nยฒ)?
- Valiant's algorithm: O(n^ฯ)
- Lower bounds unclear
2. AMBIGUITY RESOLUTION
- How to choose the right parse?
- Statistical vs rule-based
- Neural approaches
3. GRAMMAR INDUCTION
- Learn CFG from examples
- Active learning
- Still hard
4. PARSING NATURAL LANGUAGE
- Garden path sentences
- Center embedding
- Human parsing limitations
5. QUANTUM PARSING
- Quantum algorithms for parsing
- Potential speedups
- Early stage
""")
# 9. RECOMMENDATIONS
print("""
=== Recommendations ===
For compiler developers:
1. Master LL and LR parsing
2. Understand CFG transformations
3. Learn parser generators
4. Handle ambiguity carefully
For NLP practitioners:
1. Learn PCFGs and CKY
2. Understand Earley parsing
3. Explore neural parsing
4. Study treebanks
For researchers:
1. Explore efficient parsing
2. Study grammar induction
3. Investigate quantum grammars
4. Work on multilingual parsing
For students:
1. Implement CYK from scratch
2. Build a recursive descent parser
3. Study Yacc/Bison
4. Learn about ambiguity
""")
# 10. FINAL THOUGHTS
print("""
=== Final Thoughts ===
CFGs are:
โ FOUNDATIONAL
Type 2 in Chomsky hierarchy.
โ POWERFUL
Can express nesting and counting.
โ PRACTICAL
Used in every compiler.
โ THEORETICAL
Rich theory (normal forms, closure).
โ EXTENSIBLE
PCFGs, TAGs, quantum grammars.
โ EVOLVING
Neural parsing, grammar induction.
Key takeaway:
CFGs are the sweet spot between
regular expressions (too weak)
and Turing machines (too powerful).
They capture exactly what's needed for
most programming languages and
many natural language phenomena.
"Context-free grammars are the
backbone of computer science."
โ Anonymous
Go build a parser!
""")
# QUICK REFERENCE
print("""
=== Quick Reference ===
CFG: G = (V, ฮฃ, R, S)
V: nonterminals
ฮฃ: terminals
R: rules A โ ฮฑ
S: start symbol
Derivation: S โ* w
Leftmost: expand left first
Rightmost: expand right first
Parse tree:
Root: S
Internal: nonterminals
Leaves: terminals
Ambiguity:
Multiple parse trees for same string
Inherent: every grammar is ambiguous
CNF: A โ BC | a
CYK: O(nยณ) parsing
PDA: CFG equivalent
Stack = unbounded memory
Closure:
โ Union, concat, star, reversal
โ Intersection, complement
Decision problems:
Membership: Decidable
Emptiness: Decidable
Equivalence: Undecidable
Ambiguity: Undecidable
""")