第79集:LL(1)分析器实现
学习目标
- 理解LL(1)预测分析器的基本结构和工作原理
- 掌握基于LL(1)分析表实现预测分析器的方法
- 学会处理分析过程中的各种情况(匹配、推导、错误)
- 通过实际代码实现巩固LL(1)分析器的实现技巧
- 掌握测试LL(1)分析器的方法
一、LL(1)预测分析器的基本结构
LL(1)预测分析器是一种基于LL(1)分析表的自顶向下分析器,它由以下几个部分组成:
1. 输入缓冲区
输入缓冲区用于存放待分析的Token序列,以结束符 $ 结尾。输入指针用于跟踪当前正在处理的Token。
2. 分析栈
分析栈用于存放文法符号,初始时包含开始符号和结束符 $。栈顶符号是当前正在处理的符号。
3. LL(1)分析表
LL(1)分析表是预测分析器的核心,它指导分析器在分析过程中选择正确的产生式。
4. 输出
分析器的输出通常是语法分析的结果,如成功/失败信息、语法树等。
二、LL(1)预测分析器的工作原理
LL(1)预测分析器的工作过程是一个循环过程,直到分析成功或失败:
工作流程
- 初始化:将开始符号压入栈,输入指针指向第一个Token
- 循环:
a. 取出栈顶符号X
b. 如果X是结束符$:
i. 如果当前输入Token也是$,分析成功
ii. 否则,分析失败
c. 如果X是终结符:
i. 如果X与当前输入Token匹配,输入指针前进一位
ii. 否则,分析失败
d. 如果X是非终结符:
i. 查看分析表M[X, a],其中a是当前输入Token
ii. 如果M[X, a]是产生式X → α,将X弹出栈,将α逆序压入栈
iii. 如果M[X, a]是错误,分析失败
关键操作
- 栈操作:压入、弹出符号
- Token匹配:比较栈顶终结符与当前输入Token
- 产生式选择:根据分析表选择正确的产生式
- 错误处理:检测和报告语法错误
三、LL(1)预测分析器的代码实现
1. 分析器类的设计
首先,我们需要设计一个LL(1)预测分析器类,包含输入处理、栈操作、分析表查询等功能。
class LL1Parser:
def __init__(self, parsing_table, start_symbol):
"""
初始化LL(1)分析器
parsing_table: LL(1)分析表,格式为 {非终结符: {终结符: 产生式}}
start_symbol: 开始符号
"""
self.parsing_table = parsing_table
self.start_symbol = start_symbol
self.stack = []
self.input_tokens = []
self.current_token_index = 0
self.steps = [] # 用于记录分析过程
def parse(self, tokens):
"""
开始分析
tokens: 输入Token列表,以('$', '$')结尾
"""
# 初始化
self.input_tokens = tokens
self.current_token_index = 0
self.stack = [self.start_symbol, '$']
self.steps = []
# 记录初始状态
self._record_step()
# 开始分析循环
while True:
# 获取栈顶符号
top = self.stack[-1]
# 获取当前输入Token
current_token = self.input_tokens[self.current_token_index]
current_terminal = current_token[0]
# 情况1:栈顶是结束符$
if top == '$':
if current_terminal == '$':
# 分析成功
self._record_step("分析成功")
return True
else:
# 分析失败:输入未结束
self._record_step(f"分析失败:输入未结束,当前Token: {current_token}")
return False
# 情况2:栈顶是终结符
elif top not in self.parsing_table:
if top == current_terminal:
# 匹配成功,弹出栈顶,前进输入指针
self.stack.pop()
self.current_token_index += 1
self._record_step(f"匹配终结符 {top}")
else:
# 匹配失败
self._record_step(f"分析失败:不匹配,期望 {top},实际 {current_terminal}")
return False
# 情况3:栈顶是非终结符
else:
# 查看分析表
if current_terminal not in self.parsing_table[top]:
# 分析表中无对应产生式
self._record_step(f"分析失败:无对应产生式,非终结符 {top},终结符 {current_terminal}")
return False
# 获取产生式
production = self.parsing_table[top][current_terminal]
if production is None:
# 分析表中无对应产生式
self._record_step(f"分析失败:无对应产生式,非终结符 {top},终结符 {current_terminal}")
return False
# 弹出栈顶非终结符
self.stack.pop()
# 将产生式右部逆序压入栈
lhs, rhs = production
if rhs != ['ε']:
# 逆序压入,保持分析顺序
for symbol in reversed(rhs):
self.stack.append(symbol)
# 记录步骤
production_str = f"{lhs} → {' '.join(rhs)}"
self._record_step(f"应用产生式: {production_str}")
def _record_step(self, action=""):
"""
记录分析步骤
"""
stack_str = ' '.join(self.stack)
input_str = ' '.join([f"{t[0]}" for t in self.input_tokens[self.current_token_index:]])
self.steps.append({
"stack": stack_str,
"input": input_str,
"action": action
})
def print_steps(self):
"""
打印分析步骤
"""
print("\n分析过程:")
print(f"{'步骤':<5} {'栈':<30} {'输入':<30} {'动作':<40}")
print("-" * 110)
for i, step in enumerate(self.steps):
print(f"{i+1:<5} {step['stack']:<30} {step['input']:<30} {step['action']:<40}")2. 构建LL(1)分析表
在实现分析器之前,我们需要先构建LL(1)分析表。这里我们使用之前构造的分析表作为示例:
def build_parsing_table():
"""
构建LL(1)分析表
"""
parsing_table = {
'E': {
'id': ('E', ['T', "E'"]),
'(': ('E', ['T', "E'"])
},
"E'": {
'+': ("E'", ['+', 'T', "E'"]),
')': ("E'", ['ε']),
'$': ("E'", ['ε'])
},
'T': {
'id': ('T', ['F', "T'"]),
'(': ('T', ['F', "T'"])
},
"T'": {
'+': ("T'", ['ε']),
'*': ("T'", ['*', 'F', "T'"]),
')': ("T'", ['ε']),
'$': ("T'", ['ε'])
},
'F': {
'id': ('F', ['id']),
'(': ('F', ['(', 'E', ')'])
}
}
return parsing_table3. 测试示例
现在我们来测试LL(1)分析器,分析输入串 id + id * id $:
def test_ll1_parser():
"""
测试LL(1)分析器
"""
# 构建分析表
parsing_table = build_parsing_table()
# 创建分析器
parser = LL1Parser(parsing_table, 'E')
# 测试用例1:id + id * id $
tokens1 = [
('id', 'id'),
('+', '+'),
('id', 'id'),
('*', '*'),
('id', 'id'),
('$', '$')
]
print("测试用例1:id + id * id $")
result1 = parser.parse(tokens1)
print(f"分析结果: {'成功' if result1 else '失败'}")
parser.print_steps()
# 测试用例2:(id + id) * id $
tokens2 = [
('(', '('),
('id', 'id'),
('+', '+'),
('id', 'id'),
(')', ')'),
('*', '*'),
('id', 'id'),
('$', '$')
]
print("\n测试用例2:(id + id) * id $")
result2 = parser.parse(tokens2)
print(f"分析结果: {'成功' if result2 else '失败'}")
parser.print_steps()
# 测试用例3:错误示例 - 缺少右括号
tokens3 = [
('(', '('),
('id', 'id'),
('+', '+'),
('id', 'id'),
('*', '*'),
('id', 'id'),
('$', '$')
]
print("\n测试用例3:错误示例 - 缺少右括号")
result3 = parser.parse(tokens3)
print(f"分析结果: {'成功' if result3 else '失败'}")
parser.print_steps()
if __name__ == "__main__":
test_ll1_parser()三、LL(1)分析器的工作过程详解
1. 测试用例1:id + id * id $ 的分析过程
| 步骤 | 栈 | 输入 | 动作 |
|---|---|---|---|
| 1 | E $ | id + id * id $ | 应用产生式: E → T E' |
| 2 | T E' $ | id + id * id $ | 应用产生式: T → F T' |
| 3 | F T' E' $ | id + id * id $ | 应用产生式: F → id |
| 4 | id T' E' $ | id + id * id $ | 匹配终结符 id |
| 5 | T' E' $ | + id * id $ | 应用产生式: T' → ε |
| 6 | E' $ | + id * id $ | 应用产生式: E' → + T E' |
| 7 | + T E' $ | + id * id $ | 匹配终结符 + |
| 8 | T E' $ | id * id $ | 应用产生式: T → F T' |
| 9 | F T' E' $ | id * id $ | 应用产生式: F → id |
| 10 | id T' E' $ | id * id $ | 匹配终结符 id |
| 11 | T' E' $ | * id $ | 应用产生式: T' → * F T' |
| 12 | * F T' E' $ | * id $ | 匹配终结符 * |
| 13 | F T' E' $ | id $ | 应用产生式: F → id |
| 14 | id T' E' $ | id $ | 匹配终结符 id |
| 15 | T' E' $ | $ | 应用产生式: T' → ε |
| 16 | E' $ | $ | 应用产生式: E' → ε |
| 17 | $ | $ | 分析成功 |
2. 测试用例2:(id + id) * id $ 的分析过程
| 步骤 | 栈 | 输入 | 动作 |
|---|---|---|---|
| 1 | E $ | ( id + id ) * id $ | 应用产生式: E → T E' |
| 2 | T E' $ | ( id + id ) * id $ | 应用产生式: T → F T' |
| 3 | F T' E' $ | ( id + id ) * id $ | 应用产生式: F → ( E ) |
| 4 | ( E ) T' E' $ | ( id + id ) * id $ | 匹配终结符 ( |
| 5 | E ) T' E' $ | id + id ) * id $ | 应用产生式: E → T E' |
| 6 | T E' ) T' E' $ | id + id ) * id $ | 应用产生式: T → F T' |
| 7 | F T' E' ) T' E' $ | id + id ) * id $ | 应用产生式: F → id |
| 8 | id T' E' ) T' E' $ | id + id ) * id $ | 匹配终结符 id |
| 9 | T' E' ) T' E' $ | + id ) * id $ | 应用产生式: T' → ε |
| 10 | E' ) T' E' $ | + id ) * id $ | 应用产生式: E' → + T E' |
| 11 | + T E' ) T' E' $ | + id ) * id $ | 匹配终结符 + |
| 12 | T E' ) T' E' $ | id ) * id $ | 应用产生式: T → F T' |
| 13 | F T' E' ) T' E' $ | id ) * id $ | 应用产生式: F → id |
| 14 | id T' E' ) T' E' $ | id ) * id $ | 匹配终结符 id |
| 15 | T' E' ) T' E' $ | ) * id $ | 应用产生式: T' → ε |
| 16 | E' ) T' E' $ | ) * id $ | 应用产生式: E' → ε |
| 17 | ) T' E' $ | ) * id $ | 匹配终结符 ) |
| 18 | T' E' $ | * id $ | 应用产生式: T' → * F T' |
| 19 | * F T' E' $ | * id $ | 匹配终结符 * |
| 20 | F T' E' $ | id $ | 应用产生式: F → id |
| 21 | id T' E' $ | id $ | 匹配终结符 id |
| 22 | T' E' $ | $ | 应用产生式: T' → ε |
| 23 | E' $ | $ | 应用产生式: E' → ε |
| 24 | $ | $ | 分析成功 |
四、LL(1)分析器的优化
1. 错误处理优化
在实际应用中,我们需要更健壮的错误处理机制,以便在遇到错误时能够提供更有用的错误信息:
def enhance_error_handling(self):
"""
增强错误处理
"""
# 1. 提供更详细的错误信息
# 2. 尝试错误恢复
# 3. 统计错误数量
pass2. 性能优化
对于大型文法,我们可以进行一些性能优化:
def optimize_performance(self):
"""
优化性能
"""
# 1. 使用更高效的数据结构存储分析表
# 2. 避免不必要的字符串操作
# 3. 使用预计算的信息加速分析过程
pass3. 构建语法树
在实际编译器中,分析器通常需要构建语法树,以便后续的语义分析和代码生成:
def build_ast(self, tokens):
"""
分析并构建抽象语法树
"""
# 初始化
self.input_tokens = tokens
self.current_token_index = 0
self.stack = [self.start_symbol, '$']
self.ast_stack = [] # 用于构建语法树
# 分析过程中构建语法树
# ...
return self.ast_stack[0] if self.ast_stack else None五、LL(1)分析器的优缺点
优点
- 确定性分析:LL(1)分析器是确定性的,没有回溯,分析过程清晰可预测
- 分析效率高:分析过程是线性的,时间复杂度为O(n),其中n是输入串的长度
- 错误定位准确:当分析失败时,可以精确定位错误位置
- 易于实现:基于分析表的实现方法相对简单直接
- 适合手写:对于小型文法,可以手动构建分析表并实现分析器
缺点
- 文法限制:只能处理LL(1)文法,很多自然文法不是LL(1)的
- 分析表大小:对于复杂的文法,分析表可能非常大,占用大量内存
- 需要预处理:需要消除左递归和提取左公因子,这增加了文法设计的复杂性
- 错误恢复能力有限:当遇到错误时,恢复能力相对有限
- 难以处理复杂语言:对于具有复杂语法特性的语言,LL(1)分析器可能不够强大
六、LL(1)分析器的应用场景
1. 小型语言编译器
对于小型语言或领域特定语言(DSL),LL(1)分析器是一个不错的选择,因为它实现简单,分析效率高。
2. 编译器前端原型
在编译器开发的早期阶段,可以使用LL(1)分析器快速构建前端原型,验证文法设计的正确性。
3. 教育和学习
LL(1)分析器是编译原理课程中的重要内容,通过实现LL(1)分析器,可以深入理解语法分析的基本原理。
4. 简单表达式解析
对于简单的表达式解析,如配置文件、查询语言等,LL(1)分析器足够强大且易于实现。
七、完整代码示例
下面是一个完整的LL(1)分析器实现,包括分析表构建、分析过程、错误处理和语法树构建:
class ASTNode:
"""抽象语法树节点"""
def __init__(self, node_type, children=None, value=None):
self.type = node_type
self.children = children or []
self.value = value
def __repr__(self):
return f"ASTNode({self.type}, {self.children}, {self.value})"
class LL1ParserWithAST:
"""带语法树构建的LL(1)分析器"""
def __init__(self, parsing_table, start_symbol):
self.parsing_table = parsing_table
self.start_symbol = start_symbol
self.stack = []
self.input_tokens = []
self.current_token_index = 0
self.ast_stack = [] # 用于构建语法树
self.steps = []
def parse(self, tokens):
"""开始分析"""
# 初始化
self.input_tokens = tokens
self.current_token_index = 0
self.stack = [self.start_symbol, '$']
self.ast_stack = []
self.steps = []
# 记录初始状态
self._record_step()
# 开始分析循环
while True:
# 获取栈顶符号
top = self.stack[-1]
# 获取当前输入Token
current_token = self.input_tokens[self.current_token_index]
current_terminal = current_token[0]
# 情况1:栈顶是结束符$
if top == '$':
if current_terminal == '$':
# 分析成功
self._record_step("分析成功")
return self.ast_stack[0] if self.ast_stack else True
else:
# 分析失败
self._record_step(f"分析失败:输入未结束")
return False
# 情况2:栈顶是终结符
elif top not in self.parsing_table:
if top == current_terminal:
# 匹配成功,弹出栈顶,前进输入指针
self.stack.pop()
# 对于终结符,创建叶子节点
self.ast_stack.append(ASTNode(top, value=current_token[1]))
self.current_token_index += 1
self._record_step(f"匹配终结符 {top}")
else:
# 匹配失败
self._record_step(f"分析失败:不匹配")
return False
# 情况3:栈顶是非终结符
else:
# 查看分析表
if current_terminal not in self.parsing_table[top]:
# 分析表中无对应产生式
self._record_step(f"分析失败:无对应产生式")
return False
# 获取产生式
production = self.parsing_table[top][current_terminal]
lhs, rhs = production
# 弹出栈顶非终结符
self.stack.pop()
# 将产生式右部逆序压入栈
if rhs != ['ε']:
for symbol in reversed(rhs):
self.stack.append(symbol)
# 记录步骤
production_str = f"{lhs} → {' '.join(rhs)}"
self._record_step(f"应用产生式: {production_str}")
# 构建语法树节点
if rhs != ['ε']:
# 标记需要为当前非终结符收集子节点
self.stack.append(f"@{lhs}")
def _record_step(self, action=""):
"""记录分析步骤"""
stack_str = ' '.join(self.stack)
input_str = ' '.join([f"{t[0]}" for t in self.input_tokens[self.current_token_index:]])
self.steps.append({
"stack": stack_str,
"input": input_str,
"action": action
})
# 检查是否需要构建语法树节点
if self.stack and self.stack[-1].startswith('@'):
# 弹出标记
non_terminal = self.stack.pop()[1:]
# 收集子节点
children = []
while self.ast_stack and not (isinstance(self.ast_stack[-1], str) and self.ast_stack[-1] == f"@{non_terminal}"):
children.insert(0, self.ast_stack.pop())
# 创建节点
node = ASTNode(non_terminal, children)
self.ast_stack.append(node)
def print_steps(self):
"""
打印分析步骤
"""
print("\n分析过程:")
print(f"{'步骤':<5} {'栈':<40} {'输入':<40} {'动作':<40}")
print("-" * 130)
for i, step in enumerate(self.steps):
print(f"{i+1:<5} {step['stack']:<40} {step['input']:<40} {step['action']:<40}")
# 测试带语法树构建的分析器
def test_ll1_parser_with_ast():
"""
测试带语法树构建的LL(1)分析器
"""
# 构建分析表
parsing_table = build_parsing_table()
# 创建分析器
parser = LL1ParserWithAST(parsing_table, 'E')
# 测试用例:id + id * id $
tokens = [
('id', 'x'),
('+', '+'),
('id', 'y'),
('*', '*'),
('id', 'z'),
('$', '$')
]
print("测试用例:id + id * id $")
ast = parser.parse(tokens)
print(f"分析结果: {'成功' if ast else '失败'}")
parser.print_steps()
if ast:
print("\n构建的抽象语法树:")
print_ast(ast)
def print_ast(node, indent=0):
"""
打印抽象语法树
"""
prefix = " " * indent
if node.children:
print(f"{prefix}{node.type}")
for child in node.children:
print_ast(child, indent + 1)
else:
print(f"{prefix}{node.type}: {node.value}")
if __name__ == "__main__":
test_ll1_parser_with_ast()七、LL(1)分析器的最佳实践
1. 文法设计
- 消除左递归:确保文法没有左递归,避免分析表冲突
- 提取左公因子:消除文法中的左公因子,提高分析效率
- 保持简单:设计文法时尽量保持简单,避免复杂的产生式
- 测试文法:使用小例子测试文法的LL(1)性质
2. 分析表构建
- 自动生成:对于复杂的文法,使用工具自动生成分析表
- 验证分析表:检查分析表中是否有冲突,确保文法是LL(1)的
- 优化存储:对于大型分析表,考虑使用压缩技术减小存储空间
3. 分析器实现
- 模块化设计:将分析器分为多个模块,如输入处理、栈操作、错误处理等
- 详细的错误信息:提供详细的错误信息,包括错误位置、预期Token等
- 错误恢复:实现基本的错误恢复机制,尽可能发现更多错误
- 构建语法树:在分析过程中构建语法树,方便后续的语义分析
4. 测试
- 单元测试:为分析器的各个组件编写单元测试
- 集成测试:测试完整的分析过程
- 边界测试:测试边界情况,如空输入、超长输入等
- 错误测试:测试各种语法错误的处理
八、自测题
- LL(1)预测分析器的基本结构是什么?
- LL(1)预测分析器的工作原理是什么?
- 如何基于LL(1)分析表实现预测分析器?
- LL(1)分析器的优点和缺点是什么?
- 如何在LL(1)分析器中构建语法树?
- 编写一个LL(1)分析器,分析输入串
id * (id + id) $,并写出分析过程。
九、总结与展望
本集详细介绍了LL(1)分析器的实现方法,包括分析器的结构、工作原理、代码实现以及测试示例。LL(1)分析器是一种基于分析表的预测分析器,它具有确定性分析、分析效率高、错误定位准确等优点。
在实现LL(1)分析器时,需要注意以下几点:
- 构建正确的LL(1)分析表:分析表是分析器的核心,必须确保其正确性
- 正确处理栈操作:栈操作是分析过程的关键,必须确保压栈和弹栈的顺序正确
- 提供详细的错误信息:当分析失败时,提供详细的错误信息,帮助用户定位问题
- 构建语法树:在实际编译器中,分析器通常需要构建语法树,以便后续的语义分析和代码生成
- 优化性能:对于大型文法,可以进行一些性能优化,如使用更高效的数据结构存储分析表
在接下来的几集中,我们将继续学习语法分析的其他方法:
- 预测分析中的错误恢复:介绍如何在预测分析中实现错误恢复
- 自底向上分析概述:介绍移进-归约分析和LR分析的基本概念
- LR(0)分析法:详细介绍LR(0)分析的基本原理和实现方法
通过这些学习,我们将能够掌握各种语法分析方法的实现技巧,为构建完整的编译器打下坚实的基础。
十、参考资料
- 《编译原理》(龙书)- Alfred V. Aho 等
- 《现代编译原理》(虎书)- Andrew W. Appel
- 《编译器设计》- Keith D. Cooper 等
- LL(1)分析器 - 维基百科
- 预测分析器 - 编译原理教材
- 《编程语言实现模式》- Terence Parr