1. 编译器自学路线规划
当自学编译器开发进入第二阶段,意味着已经完成了基础理论学习和简单词法分析器/语法分析器的实现。这个阶段的核心任务是理解并实现中间表示(IR)系统,这是连接前端语法分析和后端代码生成的关键桥梁。
我自己的学习路径是从实现一个简单的算术表达式计算器开始的,用Flex/Bison完成了词法和语法分析。进入第二阶段后,首先面临的选择是:应该直接使用现有IR系统(如LLVM IR)还是自己设计一套简化版的IR?考虑到学习目的,我选择了后者——设计一个类似Relax IR的简化中间表示。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 中间表示(IR)设计要点
2.1 IR的核心作用
中间表示本质上是一种抽象的程序表示,需要满足几个关键特性:
- 与机器无关但易于转换为机器码
- 保留足够多的语义信息
- 便于进行各种优化
- 结构简单利于处理
在Relax IR的设计中,我采用了三地址码的形式,因为这种表示:
- 足够底层以表达各种操作
- 又足够高层以保留源代码语义
- 每条指令最多三个操作数,处理简单
2.2 基本指令集设计
一个最小化的IR指令集需要包含:
c复制// 运算指令
%1 = add i32 %2, %3
%4 = sub i32 %5, %6
// 内存操作
store i32 %val, i32* %ptr
%res = load i32, i32* %ptr
// 控制流
br i1 %cond, label %true, label %false
ret i32 %result
注意:在设计指令集时要特别注意类型系统的处理,这是后续类型检查和优化的基础。我最初忽略了这点,导致后来需要大量重构。
3. 从AST到IR的转换
3.1 转换策略选择
将抽象语法树(AST)转换为IR有两种主要方法:
- 递归下降遍历AST生成IR
- 使用访问者模式转换
我选择了访问者模式,因为:
- 可以保持AST结构不变
- 方便实现多种IR生成策略
- 代码结构更清晰
3.2 表达式转换示例
以算术表达式为例,转换过程如下:
python复制def visit_BinaryOp(self, node):
left = self.visit(node.left)
right = self.visit(node.right)
temp = self.new_temp()
self.emit(f"{temp} = {node.op} {left.type} {left.name}, {right.name}")
return TempValue(temp, left.type)
实际实现时需要注意:
- 临时变量的生命周期管理
- 类型提升的处理
- 副作用表达式的顺序保持
4. 控制流处理
4.1 基本块划分
控制流处理的关键是将代码划分为基本块(Basic Block),具有:
- 单入口单出口
- 无内部跳转
- 顺序执行特性
我的实现方法是:
- 遇到标签或跳转目标时结束当前块
- 遇到跳转指令时结束当前块
- 维护块之间的跳转关系
4.2 条件语句转换
if语句的转换示例:
code复制// 源代码
if (x > 0) {
y = 1;
} else {
y = 2;
}
// 转换后的IR
%cond = icmp sgt i32 %x, 0
br i1 %cond, label %true, label %false
true:
store i32 1, i32* %y
br label %end
false:
store i32 2, i32* %y
br label %end
end:
...
5. 优化实践
5.1 常量传播
实现了一个简单的常量传播优化:
python复制def const_propagate(blocks):
const_map = {}
for block in blocks:
for inst in block.instructions:
if is_const(inst.src1) and is_const(inst.src2):
result = eval_const_expr(inst.op, inst.src1, inst.src2)
const_map[inst.dest] = result
mark_for_removal(inst)
elif inst.dest in const_map:
replace_uses(inst.dest, const_map[inst.dest])
5.2 死代码消除
基本实现步骤:
- 构建def-use链
- 标记没有使用的定义
- 删除没有副作用的死代码
6. 调试与验证
6.1 可视化工具
开发过程中,我编写了几个调试辅助工具:
- IR可视化工具:将IR转换为DOT格式用Graphviz显示
- 控制流图查看器
- 与源代码的交叉引用查看
6.2 测试策略
有效的测试方法包括:
- 单元测试每个IR生成阶段
- 随机生成的测试用例
- 与其他编译器(如Clang)的交叉验证
7. 性能考量
7.1 内存管理
IR系统容易成为内存消耗大户,我的优化措施:
- 使用内存池分配IR节点
- 实现SSA形式减少临时变量
- 及时释放不再使用的IR
7.2 处理大型函数
当处理包含大量基本块的函数时:
- 采用惰性处理策略
- 实现增量式优化
- 必要时分割函数
8. 进阶方向
完成基础IR系统后,可以考虑:
- 实现更多优化pass
- 支持多线程IR生成
- 添加元数据支持
- 实现JIT编译功能
我在这个阶段遇到的最大挑战是类型系统的正确处理。最初的设计过于简单,导致后续添加结构体和指针支持时遇到很多问题。建议在早期就设计一个可扩展的类型系统。
