告别死记硬背:用Python实战模拟龙书习题中的四元式、三元式与间接三元式生成
编译原理常被视为计算机科学中的"高山",而龙书中的习题更是让许多学习者望而生畏。传统的纸上推导方式容易陷入机械记忆的困境,本文将带你用Python构建一个表达式转换器,通过代码实现四元式、三元式等中间表示的自动生成,让抽象概念变得触手可及。
1. 环境准备与基础架构
在开始编码前,我们需要明确几个核心概念。四元式(op, arg1, arg2, result)是最接近机器指令的中间表示,而三元式通过位置引用替代了临时变量,间接三元式则增加了指令列表的灵活性。下面是我们项目的技术选型:
# 所需库 import ast from dataclasses import dataclass from typing import List, Optional, Union # 中间表示基类 @dataclass class IntermediateCode: op: str arg1: Optional[str] = None arg2: Optional[str] = None表达式解析将使用Python内置的ast模块,它能将字符串表达式转换为抽象语法树。我们定义三种中间表示的数据结构:
# 四元式结构 @dataclass class Quadruple(IntermediateCode): result: Optional[str] = None # 三元式结构 @dataclass class Triple(IntermediateCode): pass # 间接三元式结构 @dataclass class IndirectTriple: instructions: List[int] triples: List[Triple]2. 表达式到四元式的转换实现
我们从最基础的四元式生成开始。关键点在于处理运算符优先级和临时变量的管理。以下是一个算术表达式的转换示例:
def expr_to_quadruples(expr: str) -> List[Quadruple]: node = ast.parse(expr, mode='eval') quads = [] temp_counter = 0 def new_temp(): nonlocal temp_counter temp_counter += 1 return f"t{temp_counter}" def visit(node): if isinstance(node, ast.Num): return str(node.n) elif isinstance(node, ast.Name): return node.id elif isinstance(node, ast.UnaryOp): operand = visit(node.operand) temp = new_temp() op = { ast.USub: 'minus', ast.UAdd: 'plus' }[type(node.op)] quads.append(Quadruple(op, operand, None, temp)) return temp elif isinstance(node, ast.BinOp): left = visit(node.left) right = visit(node.right) temp = new_temp() op = { ast.Add: '+', ast.Sub: '-', ast.Mult: '*', ast.Div: '/' }[type(node.op)] quads.append(Quadruple(op, left, right, temp)) return temp else: raise ValueError(f"Unsupported node: {type(node)}") visit(node.body) return quads测试表达式a+-(b+c)的输出结果:
[ Quadruple(op='+', arg1='b', arg2='c', result='t1'), Quadruple(op='minus', arg1='t1', arg2=None, result='t2'), Quadruple(op='+', arg1='a', arg2='t2', result='t3') ]3. 从四元式到三元式的转换
三元式通过位置引用替代了临时变量名,这使得代码优化更加方便。转换时需要维护一个位置映射表:
def quads_to_triples(quads: List[Quadruple]) -> List[Triple]: triples = [] temp_map = {} for i, quad in enumerate(quads): # 处理参数中的临时变量引用 arg1 = temp_map.get(quad.arg1, quad.arg1) arg2 = temp_map.get(quad.arg2, quad.arg2) triple = Triple(quad.op, arg1, arg2) triples.append(triple) # 记录临时变量到位置的映射 if quad.result and quad.result.startswith('t'): temp_map[quad.result] = f"({len(triples)-1})" return triples同样的表达式转换结果:
[ Triple(op='+', arg1='b', arg2='c'), Triple(op='minus', arg1='(0)', arg2=None), Triple(op='+', arg1='a', arg2='(1)') ]4. 间接三元式的实现与优化
间接三元式通过指令列表实现了代码移动的灵活性,特别适合优化阶段的重排序。实现时需要分离指令序列和三元式存储:
def triples_to_indirect(triples: List[Triple]) -> IndirectTriple: # 初始指令列表就是顺序执行 instructions = list(range(len(triples))) return IndirectTriple( instructions=instructions, triples=triples.copy() )这种结构允许我们在不改变实际三元式的情况下,通过调整instructions列表来改变执行顺序。例如优化后的指令序列可能是[0,2,1],表示先执行第一条和第三条,再执行第二条。
5. 高级特性实现与DAG优化
在简单表达式的基础上,我们进一步实现数组访问和函数调用等复杂操作。以a[i]=b*c-b*d为例:
# 数组访问的四元式生成示例 quads = [ Quadruple('*', 'b', 'c', 't1'), Quadruple('*', 'b', 'd', 't2'), Quadruple('-', 't1', 't2', 't3'), Quadruple('[]=', 'a', 'i', 't4'), Quadruple('=', 't3', None, 't4') ]DAG(有向无环图)优化可以识别公共子表达式。我们可以在生成中间代码前先构建DAG:
def build_dag(expr: str) -> dict: node = ast.parse(expr, mode='eval') dag = {'nodes': [], 'edges': []} node_map = {} def visit(node): if isinstance(node, ast.Num): key = str(node.n) elif isinstance(node, ast.Name): key = node.id else: children = [visit(child) for child in ast.iter_child_nodes(node)] key = hash((type(node).__name__, *children)) if key not in node_map: node_map[key] = len(dag['nodes']) dag['nodes'].append({ 'type': type(node).__name__, 'value': getattr(node, 'n', getattr(node, 'id', None)), 'op': getattr(node, 'op', None) }) for child in children: dag['edges'].append((node_map[key], node_map[child])) return key visit(node.body) return dag对于表达式(x+y)-((x+y)*(x-y)),DAG将识别出两个x+y是相同子表达式,只需计算一次。
6. 可视化与调试工具
为了更好理解中间代码的生成过程,我们添加可视化输出功能:
def visualize_quads(quads: List[Quadruple]) -> str: headers = ["op", "arg1", "arg2", "result"] rows = [] for i, quad in enumerate(quads): rows.append([ quad.op, quad.arg1 or "", quad.arg2 or "", quad.result or "" ]) # 生成Markdown表格 table = "| " + " | ".join(headers) + " |\n" table += "|" + "|".join(["---"]*4) + "|\n" for row in rows: table += "| " + " | ".join(str(x) for x in row) + " |\n" return table输出示例:
| op | arg1 | arg2 | result |
|---|---|---|---|
| + | b | c | t1 |
| minus | t1 | t2 | |
| + | a | t2 | t3 |
7. 测试框架与边界情况处理
完善的测试用例能确保转换器的可靠性。我们使用pytest构建测试套件:
import pytest @pytest.mark.parametrize("expr,expected_quads", [ ("a+b", [ Quadruple('+', 'a', 'b', 't1') ]), ("-a", [ Quadruple('minus', 'a', None, 't1') ]), ("a*(b+c)", [ Quadruple('+', 'b', 'c', 't1'), Quadruple('*', 'a', 't1', 't2') ]) ]) def test_expr_to_quads(expr, expected_quads): assert expr_to_quadruples(expr) == expected_quads特殊运算符处理需要特别注意,比如赋值运算和单目运算符:
# 在visit方法中添加特殊处理 if isinstance(node, ast.Assign): if len(node.targets) != 1: raise ValueError("Multiple assignments not supported") target = visit(node.targets[0]) value = visit(node.value) quads.append(Quadruple('=', value, None, target)) return target通过这个项目,你会发现编译原理中的中间代码生成不再是纸上谈兵。当你能用几十行Python代码复现龙书中的经典算法时,那些抽象概念会变得异常清晰。我在实现过程中最大的收获是理解了临时变量在三元式中的巧妙替代——用位置引用代替变量名,这个设计让后续的代码优化变得如此优雅。