📝 CFG Parsing Fundamentals
🎯 What is CFG Parsing?
Context-Free Grammar (CFG) parsing is the process of analyzing a string of tokens to determine its grammatical structure according to a context-free grammar. It is fundamental to compiler design, natural language processing, and formal language theory.
Key concept: A parser takes a sequence of tokens and produces a parse tree that shows how the tokens derive from the grammar's start symbol.
Deep Explanation
CFG parsing is the heart of compiler construction. It bridges the gap between lexical analysis (recognizing tokens) and semantic analysis (understanding meaning).
Why it matters: Parsing is essential for programming languages, query languages, configuration languages, and natural language understanding.
# CFG definition
class ContextFreeGrammar:
def __init__(self):
self.non_terminals = set()
self.terminals = set()
self.productions = {}
self.start_symbol = None
def add_production(self, lhs, rhs):
"""Add a production rule: lhs -> rhs"""
if lhs not in self.productions:
self.productions[lhs] = []
self.productions[lhs].append(rhs)
self.non_terminals.add(lhs)
for symbol in rhs:
if not symbol.isupper(): # Convention: terminals are lowercase
self.terminals.add(symbol)
def parse(self, tokens):
"""Parse a sequence of tokens"""
pass
# Example: Simple arithmetic grammar
# S -> E
# E -> E + T | E - T | T
# T -> T * F | T / F | F
# F -> ( E ) | num
grammar = ContextFreeGrammar()
grammar.start_symbol = 'S'
grammar.add_production('S', ['E'])
grammar.add_production('E', ['E', '+', 'T'])
grammar.add_production('E', ['E', '-', 'T'])
grammar.add_production('E', ['T'])
grammar.add_production('T', ['T', '*', 'F'])
grammar.add_production('T', ['T', '/', 'F'])
grammar.add_production('T', ['F'])
grammar.add_production('F', ['(', 'E', ')'])
grammar.add_production('F', ['num'])📋 Grammar Types
🎯 Chomsky Hierarchy
The Chomsky hierarchy classifies grammars into four types: Type 0 (Unrestricted), Type 1 (Context-Sensitive), Type 2 (Context-Free), and Type 3 (Regular).
Grammar Classification
Type 0: Unrestricted grammars (Turing machines). Type 1: Context-sensitive grammars (αAβ → αγβ). Type 2: Context-free grammars (A → γ). Type 3: Regular grammars (A → aB or A → a).
CFG: Productions have a single non-terminal on the left side.
# Grammar type classification
class GrammarType:
@staticmethod
def is_regular(grammar):
# Check if grammar is regular (Type 3)
for lhs, productions in grammar.productions.items():
for rhs in productions:
if len(rhs) == 0:
continue
if len(rhs) == 1:
if rhs[0] in grammar.terminals:
continue
else:
return False
elif len(rhs) == 2:
if rhs[0] in grammar.terminals and rhs[1] in grammar.non_terminals:
continue
else:
return False
else:
return False
return True
@staticmethod
def is_context_free(grammar):
# Check if grammar is context-free (Type 2)
for lhs, productions in grammar.productions.items():
# LHS must be a single non-terminal
if len(lhs) != 1 or lhs not in grammar.non_terminals:
return False
return True
@staticmethod
def is_context_sensitive(grammar):
# Check if grammar is context-sensitive (Type 1)
# Productions must have |α| ≤ |γ| (non-contracting)
for lhs, productions in grammar.productions.items():
for rhs in productions:
if len(rhs) == 0 and lhs != grammar.start_symbol:
continue
# Context-sensitive condition
if len(rhs) < len(lhs):
return False
return True🌳 Parse Trees
🎯 Representing Structure
A parse tree (or derivation tree) visually represents the syntactic structure of a string according to a grammar. The root is the start symbol, internal nodes are non-terminals, and leaves are terminals.
Tree Properties
Root: Start symbol. Internal nodes: Non-terminals. Leaves: Terminals (in order). Derivation: Each internal node expands according to a production. Yield: The string of leaves (the parsed input).
Ambiguity: Multiple parse trees for the same string.
# Parse tree implementation
class ParseTreeNode:
def __init__(self, symbol, children=None):
self.symbol = symbol
self.children = children if children else []
self.is_leaf = len(self.children) == 0
def add_child(self, child):
self.children.append(child)
return self
def get_yield(self):
"""Get the string of leaves"""
if self.is_leaf:
return self.symbol
result = []
for child in self.children:
result.append(child.get_yield())
return ''.join(result)
def __repr__(self):
if self.is_leaf:
return self.symbol
children_str = ', '.join(repr(c) for c in self.children)
return f"{self.symbol}({children_str})"
# Example: Parse tree for "3 + 5 * 2"
# E -> E + T
# T -> T * F
# F -> num
# num -> 3 | 5 | 2
tree = ParseTreeNode('E')
t1 = ParseTreeNode('T')
f1 = ParseTreeNode('F')
num1 = ParseTreeNode('3')
f1.add_child(num1)
t1.add_child(f1)
tree.add_child(t1)
tree.add_child(ParseTreeNode('+'))
tree.add_child(ParseTreeNode('T'))
# Print tree
print(tree)
print("Yield:", tree.get_yield())🔝 Top-Down Parsing
🎯 Building from the Root
Top-down parsing starts from the start symbol and expands non-terminals using productions to match the input string. It constructs the parse tree from the root to the leaves.
Top-Down Methods
Algorithm: Start with start symbol, attempt to match tokens. Predictive parsing: Lookahead to choose productions. Recursive descent: Implement each non-terminal as a function. LL(k): Left-to-right scan, leftmost derivation, k tokens lookahead.
Challenges: Left recursion must be eliminated, factoring needed.
# Top-down parsing (recursive descent)
class RecursiveDescentParser:
def __init__(self, tokens):
self.tokens = tokens
self.position = 0
self.current_token = tokens[0] if tokens else None
def advance(self):
self.position += 1
if self.position < len(self.tokens):
self.current_token = self.tokens[self.position]
else:
self.current_token = None
def match(self, expected):
if self.current_token and self.current_token == expected:
self.advance()
return True
return False
def parse_S(self):
# S -> E
return self.parse_E()
def parse_E(self):
# E -> T E'
return self.parse_T() and self.parse_E_prime()
def parse_E_prime(self):
# E' -> + T E' | - T E' | ε
if self.match('+') or self.match('-'):
return self.parse_T() and self.parse_E_prime()
return True # ε
def parse_T(self):
# T -> F T'
return self.parse_F() and self.parse_T_prime()
def parse_T_prime(self):
# T' -> * F T' | / F T' | ε
if self.match('*') or self.match('/'):
return self.parse_F() and self.parse_T_prime()
return True # ε
def parse_F(self):
# F -> ( E ) | num
if self.match('('):
result = self.parse_E()
return result and self.match(')')
elif self.current_token and self.current_token.isdigit():
self.advance()
return True
return False
def parse(self):
result = self.parse_S()
return result and self.current_token is None🔻 Bottom-Up Parsing
🎯 Building from the Leaves
Bottom-up parsing starts from the input tokens and reduces them using productions until the start symbol is reached. It constructs the parse tree from the leaves to the root.
Bottom-Up Methods
Algorithm: Shift tokens onto stack, reduce when production matches. Shift-reduce parsing: Core bottom-up algorithm. LR(k): Left-to-right scan, rightmost derivation, k lookahead. Operator precedence: Uses operator priorities.
Advantages: More powerful than top-down, handles left recursion naturally.
# Shift-reduce parser (simplified)
class ShiftReduceParser:
def __init__(self, grammar, tokens):
self.grammar = grammar
self.tokens = tokens
self.stack = []
self.position = 0
def shift(self):
# Shift current token onto stack
token = self.tokens[self.position]
self.stack.append(token)
self.position += 1
def reduce(self):
# Find production matching top of stack
for lhs, productions in self.grammar.productions.items():
for rhs in productions:
if len(rhs) <= len(self.stack):
# Check if stack top matches rhs
if self.stack[-len(rhs):] == rhs:
# Reduce: replace rhs with lhs
self.stack = self.stack[:-len(rhs)]
self.stack.append(lhs)
return True
return False
def parse(self):
self.tokens.append('$') # End marker
while True:
# Try to reduce first (if possible)
reduced = self.reduce()
# If can't reduce, shift
if not reduced:
if self.position < len(self.tokens):
self.shift()
else:
break
# Check if we reduced to start symbol
return len(self.stack) == 1 and self.stack[0] == self.grammar.start_symbol
# LR(0) item
class LR0Item:
def __init__(self, lhs, rhs, dot_position=0):
self.lhs = lhs
self.rhs = rhs
self.dot = dot_position
def next_symbol(self):
if self.dot < len(self.rhs):
return self.rhs[self.dot]
return None
def advance_dot(self):
return LR0Item(self.lhs, self.rhs, self.dot + 1)
def __eq__(self, other):
return (self.lhs == other.lhs and
self.rhs == other.rhs and
self.dot == other.dot)
def __repr__(self):
before = self.rhs[:self.dot]
after = self.rhs[self.dot:]
return f"{self.lhs} -> {' '.join(before)} . {' '.join(after)}"📊 LL Parsing
🎯 Predictive Top-Down Parsing
LL parsing is a top-down parsing method that uses lookahead to decide which production to apply. LL(1) uses one token of lookahead.
LL Features
LL(1) grammar: Predictive parsing with one token lookahead. Parse table: Maps (non-terminal, token) to production. FIRST and FOLLOW sets: Used to construct the parse table. No left recursion: Must be eliminated.
Advantages: Simple, efficient, easy to implement.
# LL(1) parser implementation
class LL1Parser:
def __init__(self, grammar):
self.grammar = grammar
self.parse_table = {}
self.build_parse_table()
def compute_first(self, symbol):
# Compute FIRST set for a symbol
first = set()
if symbol in self.grammar.terminals:
first.add(symbol)
elif symbol in self.grammar.non_terminals:
for production in self.grammar.productions.get(symbol, []):
for sym in production:
sym_first = self.compute_first(sym)
first.update(sym_first - {'ε'})
if 'ε' not in sym_first:
break
else:
first.add('ε')
return first
def compute_follow(self, non_terminal):
# Compute FOLLOW set for a non-terminal
follow = set()
if non_terminal == self.grammar.start_symbol:
follow.add('$')
for lhs, productions in self.grammar.productions.items():
for production in productions:
for i, symbol in enumerate(production):
if symbol == non_terminal:
# Find rest of production
rest = production[i+1:]
if rest:
# Add FIRST(rest) to FOLLOW
for sym in rest:
sym_first = self.compute_first(sym)
follow.update(sym_first - {'ε'})
if 'ε' not in sym_first:
break
else:
# All of rest can derive ε
follow.update(self.compute_follow(lhs))
else:
# At end of production
if lhs != non_terminal:
follow.update(self.compute_follow(lhs))
return follow
def build_parse_table(self):
# Build LL(1) parse table
for non_terminal in self.grammar.non_terminals:
for production in self.grammar.productions.get(non_terminal, []):
first_set = self.compute_first_set(production)
for terminal in first_set:
if terminal != 'ε':
self.parse_table[(non_terminal, terminal)] = production
if 'ε' in first_set:
for terminal in self.compute_follow(non_terminal):
self.parse_table[(non_terminal, terminal)] = production
def parse(self, tokens):
# Parse using LL(1) parse table
stack = ['$', self.grammar.start_symbol]
tokens.append('$')
pos = 0
while stack:
top = stack[-1]
token = tokens[pos]
if top in self.grammar.terminals:
if top == token:
stack.pop()
pos += 1
else:
return False
elif top == '$':
return token == '$'
else:
# Non-terminal
production = self.parse_table.get((top, token))
if production:
stack.pop()
# Push right-hand side in reverse order
for symbol in reversed(production):
if symbol != 'ε':
stack.append(symbol)
else:
return False
return True📈 LR Parsing
🎯 Powerful Bottom-Up Parsing
LR parsing is the most powerful bottom-up parsing method. It handles all deterministic context-free grammars and is used in many compilers.
LR Types
SLR(1): Simple LR with one lookahead. CLR(1): Canonical LR (most powerful). LALR(1): Lookahead LR (practical compromise). LR(0): No lookahead (limited).
Tools: Yacc, Bison, ANTLR generate LR parsers.
# LR parser states and actions
class LRItem:
def __init__(self, lhs, rhs, dot, lookahead=None):
self.lhs = lhs
self.rhs = rhs
self.dot = dot
self.lookahead = lookahead
def next_symbol(self):
if self.dot < len(self.rhs):
return self.rhs[self.dot]
return None
def advance(self):
return LRItem(self.lhs, self.rhs, self.dot + 1, self.lookahead)
class LRParser:
def __init__(self, grammar):
self.grammar = grammar
self.states = []
self.build_states()
self.build_actions()
def closure(self, items):
# Compute LR closure
closure_set = set(items)
changed = True
while changed:
changed = False
for item in list(closure_set):
symbol = item.next_symbol()
if symbol and symbol in self.grammar.non_terminals:
for prod in self.grammar.productions.get(symbol, []):
new_item = LRItem(symbol, prod, 0, item.lookahead)
if new_item not in closure_set:
closure_set.add(new_item)
changed = True
return closure_set
def goto(self, state, symbol):
# Compute GOTO transition
items = []
for item in state:
if item.next_symbol() == symbol:
items.append(item.advance())
return self.closure(items)
def build_states(self):
# Build LR states
start_prod = [self.grammar.start_symbol + self.grammar.start_symbol]
start_item = LRItem('S\'', start_prod, 0)
start_state = self.closure({start_item})
self.states.append(start_state)
# Build transitions
transitions = []
for state in self.states:
for symbol in self.grammar.terminals | self.grammar.non_terminals:
next_state = self.goto(state, symbol)
if next_state and next_state not in self.states:
self.states.append(next_state)
def parse(self, tokens):
# LR parsing (simplified)
stack = [0]
tokens.append('$')
pos = 0
while True:
state = stack[-1]
token = tokens[pos]
# Look up action
action = self.actions.get((state, token))
if action is None:
return False
if action[0] == 'shift':
stack.append(action[1])
pos += 1
elif action[0] == 'reduce':
# Reduce by production
lhs, rhs = action[1]
for _ in range(len(rhs)):
stack.pop()
state = stack[-1]
stack.append(self.goto_state(state, lhs))
elif action[0] == 'accept':
return True🔄 Recursive Descent
🎯 Direct Implementation
Recursive descent is a straightforward top-down parsing technique where each non-terminal is implemented as a function that directly recognizes the grammar.
Recursive Descent Features
Implementation: One function per non-terminal. Prediction: Often requires backtracking or lookahead. Backtracking: Try alternatives, backtrack on failure. Recursive: Naturally handles nested structures.
Advantages: Easy to understand, flexible, good error messages.
# Recursive descent parser with backtracking
class RecursiveDescentParser:
def __init__(self, tokens):
self.tokens = tokens
self.position = 0
self.saved_positions = []
def save_position(self):
self.saved_positions.append(self.position)
def restore_position(self):
self.position = self.saved_positions.pop()
def peek(self):
if self.position < len(self.tokens):
return self.tokens[self.position]
return None
def match(self, expected):
if self.peek() == expected:
self.position += 1
return True
return False
def parse_S(self):
# S -> E
return self.parse_E()
def parse_E(self):
# E -> T ( + T | - T )*
self.save_position()
if self.parse_T():
while True:
if self.match('+'):
if self.parse_T():
continue
else:
self.restore_position()
return False
elif self.match('-'):
if self.parse_T():
continue
else:
self.restore_position()
return False
else:
return True
self.restore_position()
return False
def parse_T(self):
# T -> F ( * F | / F )*
self.save_position()
if self.parse_F():
while True:
if self.match('*'):
if self.parse_F():
continue
else:
self.restore_position()
return False
elif self.match('/'):
if self.parse_F():
continue
else:
self.restore_position()
return False
else:
return True
self.restore_position()
return False
def parse_F(self):
# F -> ( E ) | num
if self.match('('):
if self.parse_E() and self.match(')'):
return True
return False
elif self.peek() and self.peek().isdigit():
self.position += 1
return True
return False
def parse(self):
result = self.parse_S()
return result and self.position == len(self.tokens)🧠 Earley Parsing
🎯 General CFG Parsing
The Earley parser is a general-purpose parsing algorithm for any context-free grammar. It runs in O(n³) time and can handle ambiguous and left-recursive grammars.
Earley Features
Chart parser: Maintains sets of states. Items: (production, dot position, origin). Operations: Predict, Scan, Complete. Handles: All CFGs, including ambiguous and left-recursive.
Complexity: O(n³) for ambiguous grammars, O(n²) for unambiguous.
# Earley parser implementation
class EarleyParser:
def __init__(self, grammar):
self.grammar = grammar
self.charts = []
def predict(self, state, chart):
# Predict non-terminal symbols
next_symbol = state.next_symbol()
if next_symbol in self.grammar.non_terminals:
for production in self.grammar.productions.get(next_symbol, []):
new_state = EarleyState(next_symbol, production, 0, state.origin)
if new_state not in chart:
chart.append(new_state)
def scan(self, state, token):
# Scan terminal symbols
next_symbol = state.next_symbol()
if next_symbol == token:
return EarleyState(state.lhs, state.rhs, state.dot + 1, state.origin)
return None
def complete(self, state, chart_index):
# Complete a non-terminal
for chart in self.charts[state.origin:]:
for prev_state in chart:
if prev_state.next_symbol() == state.lhs:
if prev_state.dot < len(prev_state.rhs):
new_state = EarleyState(prev_state.lhs, prev_state.rhs, prev_state.dot + 1, prev_state.origin)
if new_state not in self.charts[chart_index]:
self.charts[chart_index].append(new_state)
def parse(self, tokens):
# Initialize chart
self.charts = [[] for _ in range(len(tokens) + 1)]
# Add start state
start_state = EarleyState(self.grammar.start_symbol, self.grammar.productions[self.grammar.start_symbol][0], 0, 0)
self.charts[0].append(start_state)
# Process tokens
for i in range(len(tokens) + 1):
chart = self.charts[i]
for j, state in enumerate(chart):
# Predict phase
self.predict(state, chart)
# Scan phase
if i < len(tokens):
scanned = self.scan(state, tokens[i])
if scanned and scanned not in self.charts[i + 1]:
self.charts[i + 1].append(scanned)
# Complete phase
if state.dot == len(state.rhs):
self.complete(state, i)
# Check if start symbol is complete
for state in self.charts[-1]:
if state.lhs == self.grammar.start_symbol and state.dot == len(state.rhs) and state.origin == 0:
return True
return False
class EarleyState:
def __init__(self, lhs, rhs, dot, origin):
self.lhs = lhs
self.rhs = rhs
self.dot = dot
self.origin = origin
def next_symbol(self):
if self.dot < len(self.rhs):
return self.rhs[self.dot]
return None
def __eq__(self, other):
return (self.lhs == other.lhs and
self.rhs == other.rhs and
self.dot == other.dot and
self.origin == other.origin)📐 CYK Algorithm
🎯 Dynamic Programming Parsing
The CYK algorithm (Cocke-Younger-Kasami) is a dynamic programming algorithm for parsing Chomsky Normal Form (CNF) grammars in O(n³) time.
CYK Features
CNF required: Productions must be A → BC or A → a. Dynamic programming: Table of substrings. Cell: Set of non-terminals deriving substring. Building: Combine adjacent substrings.
Applications: Grammar analysis, RNA folding (with modifications).
# CYK algorithm implementation
def convert_to_cnf(grammar):
# Convert grammar to Chomsky Normal Form
# (Simplified - assumes grammar is already close to CNF)
cnf_grammar = {'productions': {}, 'terminals': set(), 'non_terminals': set()}
for lhs, productions in grammar.productions.items():
for rhs in productions:
if len(rhs) == 1 and rhs[0] in grammar.terminals:
# Terminal production: A -> a (already CNF)
if lhs not in cnf_grammar['productions']:
cnf_grammar['productions'][lhs] = []
cnf_grammar['productions'][lhs].append(rhs)
elif len(rhs) == 2 and rhs[0] in grammar.non_terminals and rhs[1] in grammar.non_terminals:
# Non-terminal production: A -> BC (already CNF)
if lhs not in cnf_grammar['productions']:
cnf_grammar['productions'][lhs] = []
cnf_grammar['productions'][lhs].append(rhs)
else:
# Need conversion (simplified - just pass through)
pass
return cnf_grammar
def cyk_parse(grammar, tokens):
# Convert grammar to CNF
cnf_grammar = convert_to_cnf(grammar)
n = len(tokens)
# DP table: table[i][j] = set of non-terminals deriving tokens[i:j]
table = [[set() for _ in range(n + 1)] for _ in range(n + 1)]
# Fill length 1 substrings (terminal productions)
for i, token in enumerate(tokens):
for lhs, productions in cnf_grammar['productions'].items():
for rhs in productions:
if len(rhs) == 1 and rhs[0] == token:
table[i][i + 1].add(lhs)
# Fill longer substrings
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length
for k in range(i + 1, j):
# Combine substrings table[i][k] and table[k][j]
for lhs, productions in cnf_grammar['productions'].items():
for rhs in productions:
if len(rhs) == 2:
B, C = rhs
if B in table[i][k] and C in table[k][j]:
table[i][j].add(lhs)
# Check if start symbol is in table[0][n]
return grammar.start_symbol in table[0][n]
# Example usage
grammar = {
'productions': {
'S': [['NP', 'VP']],
'NP': [['Det', 'N']],
'VP': [['V', 'NP']],
'Det': [['the'], ['a']],
'N': [['cat'], ['dog'], ['mat']],
'V': [['chased'], ['sat']]
},
'terminals': {'the', 'a', 'cat', 'dog', 'mat', 'chased', 'sat'},
'non_terminals': {'S', 'NP', 'VP', 'Det', 'N', 'V'},
'start_symbol': 'S'
}
result = cyk_parse(grammar, ['the', 'cat', 'chased', 'a', 'dog'])
print("Parsed:", result)🔀 Ambiguity
🎯 Multiple Parse Trees
A grammar is ambiguous if a string can be derived in multiple ways (multiple parse trees). Ambiguity must be resolved for reliable parsing.
Handling Ambiguity
Causes: Left recursion, multiple productions, precedence issues. Resolutions: Modify grammar, use precedence rules, choose one derivation. Classic example: "I saw the man with the telescope" has multiple interpretations.
Applications: Natural language processing, programming language design.
# Detecting and resolving ambiguity
class AmbiguityDetector:
def __init__(self, grammar):
self.grammar = grammar
self.ambiguous_grammars = set()
def is_ambiguous(self, string, max_parse=3):
# Count parse trees for a string
count = self.count_parses(string, max_parse)
return count > 1
def count_parses(self, string, max_count=3):
# Count number of parse trees (simplified)
trees = self.parse_all(string, max_count)
return len(trees)
def parse_all(self, string, max_count):
# Generate all parse trees (simplified)
trees = []
# Implementation uses Earley or similar to find all parses
return trees
# Resolve ambiguity with precedence
class PrecedenceResolver:
def __init__(self, precedence):
self.precedence = precedence # {'+': 1, '*': 2}
def resolve_ambiguity(self, parse_trees):
# Choose parse tree based on precedence
if not parse_trees:
return None
# Evaluate each tree based on precedence rules
best_tree = None
best_score = -1
for tree in parse_trees:
score = self.evaluate_tree(tree)
if score > best_score:
best_score = score
best_tree = tree
return best_tree
def evaluate_tree(self, tree):
# Score tree based on operator precedence
score = 0
# Implementation would traverse tree
return score
# Example: Dangling else ambiguity
# Grammar:
# S -> if E then S | if E then S else S | other
# This is ambiguous for "if a then if b then c else d"
# Resolution: "else" binds to nearest "if"🛡️ Error Handling
🎯 Dealing with Invalid Input
Error handling in parsing deals with syntax errors in the input. It involves detection, reporting, and recovery.
Error Recovery Strategies
Error detection: Identify when input doesn't match grammar. Panic mode: Skip tokens until a synchronization point. Phrase-level recovery: Replace erroneous tokens. Error productions: Add error productions to grammar.
Reporting: Provide helpful error messages with location.
# Error handling in parsers
class ErrorRecoveryParser:
def __init__(self, grammar):
self.grammar = grammar
self.errors = []
self.sync_set = {';', '}', ')'}
def error(self, message, token, position):
self.errors.append({
'message': message,
'token': token,
'position': position
})
def panic_mode(self, tokens, position):
# Skip tokens until synchronization point
while position < len(tokens):
if tokens[position] in self.sync_set:
return position
position += 1
return position
def phrase_level_recovery(self, expected, token):
# Try to fix common errors
if token == ';' and expected == ',':
self.error("Expected ',' found ';'", token, 0)
return True
return False
def parse_with_recovery(self, tokens):
pos = 0
while pos < len(tokens):
try:
self.parse_tokens(tokens, pos)
break
except SyntaxError as e:
self.error(str(e), tokens[pos], pos)
pos = self.panic_mode(tokens, pos + 1)
return self.errors
# Error reporting
class ErrorReporter:
@staticmethod
def format_error(error, source_line):
message = error['message']
token = error['token']
position = error['position']
line = source_line.split('\n')[position]
pointer = ' ' * len(line) + '^'
return f"Error at position {position}:\n{line}\n{pointer}\n{message}"
# Example error handlers
def report_syntax_error(line, pos, expected, found):
return f"""Syntax Error:
{line}
{' ' * pos}^
Expected '{expected}', found '{found}'"""
💼 Applications
🎯 Where Parsing is Used
CFG parsing is fundamental to compilers, interpreters, natural language processing, and data format processing.
Application Domains
Compilers: Parsing source code. Interpreters: Evaluating expressions. NLP: Parsing natural language sentences. Data formats: JSON, XML, CSV parsing. Query languages: SQL, GraphQL parsing.
Tools: ANTLR, Yacc, Bison, XText.
# Application: SQL parser (simplified)
class SQLParser:
def __init__(self):
self.grammar = self.build_sql_grammar()
self.parser = RecursiveDescentParser(None) # Simplified
def build_sql_grammar(self):
# Simplified SQL grammar
return {
'query': [['SELECT', 'columns', 'FROM', 'table', 'where_clause']],
'columns': [['*'], ['column_list']],
'column_list': [['column', 'rest_columns']],
'rest_columns': [['', ',' 'column', 'rest_columns']],
'table': [['identifier']],
'where_clause': [[''], ['WHERE', 'condition']],
'condition': [['identifier', 'op', 'value']],
'op': [['='], ['>'], ['<'], ['!=']]
}
def parse_sql(self, query):
tokens = self.tokenize(query)
return self.parser.parse(tokens)
# Application: JSON parser
class JSONParser:
def parse(self, json_string):
self.tokens = self.tokenize(json_string)
self.position = 0
return self.parse_value()
def parse_value(self):
token = self.peek()
if token == '{':
return self.parse_object()
elif token == '[':
return self.parse_array()
elif token.isdigit():
return self.parse_number()
elif token == 'true' or token == 'false':
return self.parse_boolean()
elif token == 'null':
return self.parse_null()
elif token.startswith('"'):
return self.parse_string()
def parse_object(self):
obj = {}
self.expect('{')
if self.peek() != '}':
while True:
key = self.parse_string()
self.expect(':')
value = self.parse_value()
obj[key] = value
if self.peek() != ',':
break
self.expect(',')
self.expect('}')
return obj
def parse_array(self):
arr = []
self.expect('[')
if self.peek() != ']':
while True:
arr.append(self.parse_value())
if self.peek() != ',':
break
self.expect(',')
self.expect(']')
return arr
# Application: Natural language parser
class NLPParser:
def __init__(self, grammar):
self.parser = EarleyParser(grammar)
def parse_sentence(self, words):
return self.parser.parse(words)
def get_parse_tree(self, words):
# Build parse tree from parsing result
pass🚀 Future Directions
🎯 The Future of Parsing
Parsing continues to evolve with neural parsing, incremental parsing, and integration with machine learning.
Emerging Trends
Neural parsing: Using neural networks for parsing. Incremental parsing: Parse partial input in real-time. Grammar learning: Automatically learn grammars. Fuzzy parsing: Handle imperfect input. Interactive parsing: Real-time feedback.
Applications: Code completion, auto-correction, natural language interfaces.
# Future: Neural parser
import torch
import torch.nn as nn
class NeuralParser(nn.Module):
def __init__(self, vocab_size, hidden_size, num_layers=2):
super().__init__()
self.embedding = nn.Embedding(vocab_size, hidden_size)
self.lstm = nn.LSTM(hidden_size, hidden_size, num_layers, bidirectional=True)
self.classifier = nn.Linear(hidden_size * 2, vocab_size)
def forward(self, tokens):
embedded = self.embedding(tokens)
output, _ = self.lstm(embedded)
return self.classifier(output)
def parse(self, tokens):
# Neural parsing
with torch.no_grad():
logits = self.forward(tokens)
predictions = torch.argmax(logits, dim=-1)
return predictions
# Future: Incremental parser
class IncrementalParser:
def __init__(self, grammar):
self.grammar = grammar
self.state = None
self.partial_tree = None
def process_token(self, token):
# Update parse state with new token
self.state = self.update_state(self.state, token)
self.partial_tree = self.build_partial_tree(self.state)
return self.partial_tree
def get_completions(self):
# Suggest possible completions
return self.state.get_possible_tokens()
# Future: Grammar learning
class GrammarLearner:
def __init__(self, corpus):
self.corpus = corpus
self.grammar = None
def learn(self):
# Learn grammar from examples
# Uses algorithms like SEQUITUR or ADIOS
pass
# Future: Interactive parser (IDE-style)
class InteractiveParser:
def __init__(self, grammar):
self.grammar = grammar
self.parser = EarleyParser(grammar)
def parse_async(self, text, callback):
# Parse text as it's typed
# Provide real-time feedback
for i in range(len(text)):
partial = text[:i+1]
result = self.parser.parse(partial)
callback(i, partial, result)