314 lines
10 KiB
Markdown
314 lines
10 KiB
Markdown
# Lab2 实验记录:中间表示生成
|
||
|
||
## 1. 实验目标
|
||
|
||
本次 Lab2 的目标是在已有的 SysY 前端基础上,补齐语义检查与 IR 生成流程,使编译器能够把更完整的 SysY 程序翻译为 LLVM 风格 IR,并通过 `llc/clang` 验证生成结果的正确性。
|
||
|
||
本次完成工作的重点包括:
|
||
|
||
- 扩展 IR 类型系统与指令系统,支持 `float`、数组、分支、函数调用、GEP、类型转换等基础能力。
|
||
- 扩展 Sema,支持嵌套作用域、左值绑定、函数调用绑定与内建库函数预声明。
|
||
- 完成表达式、控制流、函数、数组与全局变量的 IR 生成逻辑。
|
||
- 修复全局初始化常量求值、短路求值、数组寻址、IR 打印格式等会直接阻塞 Lab2 验证的关键问题。
|
||
|
||
## 2. 代码改动范围
|
||
|
||
本次实验主要修改了以下模块:
|
||
|
||
- `src/sem` 与 `include/sem`
|
||
- `src/ir` 与 `include/ir`
|
||
- `src/irgen` 与 `include/irgen`
|
||
- 新增本文档 `doc/Lab2-实验记录.md`
|
||
|
||
其中:
|
||
|
||
- `sem` 负责名称绑定、作用域和语义信息准备。
|
||
- `ir` 负责 IR 基础设施、Builder 与 Printer。
|
||
- `irgen` 负责把 ANTLR 语法树翻译成 IR。
|
||
|
||
## 3. 完成过程
|
||
|
||
### 3.1 先确认问题边界
|
||
|
||
开始时先阅读了实验文档 `doc/Lab2-中间表示生成.md`,然后检查了以下实现:
|
||
|
||
- `IRGenDecl.cpp`
|
||
- `IRGenExp.cpp`
|
||
- `IRGenStmt.cpp`
|
||
- `IRGenFunc.cpp`
|
||
- `IRBuilder.cpp`
|
||
- `IRPrinter.cpp`
|
||
- `Sema.cpp`
|
||
|
||
在初始状态下,代码已经完成了大部分 Lab2 框架,但仍存在两个会直接导致失败的问题:
|
||
|
||
1. 全局常量初始化时,`EvalConstExpr` 实际上仍然调用了运行时的 `EvalExpr`,从而在没有插入点时进入 `builder_.CreateLoad/CreateBinary/...`,最终报错:
|
||
`IRBuilder 未设置插入点`
|
||
2. 数组相关的指针/聚合类型处理不一致,局部数组、多维数组与数组参数传递时很容易触发 `LoadInst 不支持的指针类型` 或生成错误的 GEP。
|
||
|
||
为了避免只靠静态阅读猜问题,随后先执行了构建与最小样例验证,确认真实失败点。
|
||
|
||
### 3.2 建立回归基线
|
||
|
||
首先重新构建项目:
|
||
|
||
```bash
|
||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||
cmake --build build -j 4
|
||
```
|
||
|
||
然后针对典型样例做验证:
|
||
|
||
- `simple_add.sy`
|
||
- `05_arr_defn4.sy`
|
||
- `95_float.sy`
|
||
|
||
结果表明:
|
||
|
||
- `95_float.sy` 会因为全局常量路径错误触发 `IRBuilder 未设置插入点`
|
||
- `05_arr_defn4.sy` 会因为数组寻址/存储类型不一致导致崩溃
|
||
|
||
这一步的作用是把问题从“感觉哪里有问题”缩小到“常量求值路径”和“数组存储/寻址路径”两条主线。
|
||
|
||
## 4. 关键困难与解决办法
|
||
|
||
### 4.1 困难一:全局初始化错误地走了运行时 IRBuilder 路径
|
||
|
||
#### 现象
|
||
|
||
像下面这种代码在全局或常量初始化中会崩溃:
|
||
|
||
```c
|
||
const float PI = 3.1415926;
|
||
const int A = 1 + 2;
|
||
```
|
||
|
||
原因是原来的 `EvalConstExpr` 虽然名字叫“常量求值”,但内部还是直接调用了 `EvalExpr`。一旦表达式中包含需要访问变量、二元运算、短路逻辑等节点,就会落入 `builder_` 创建指令的逻辑,而此时全局作用域没有任何基本块插入点。
|
||
|
||
#### 解决办法
|
||
|
||
把编译期常量求值彻底独立出来:
|
||
|
||
- 为 `EvalConstExpr` 单独实现一套常量求值 Visitor。
|
||
- 常量路径只返回 `ConstantInt` / `ConstantFloat`,绝不生成 IR 指令。
|
||
- 支持:
|
||
- 整数/浮点字面量
|
||
- 括号表达式
|
||
- `+`、`-`、`!`
|
||
- `* / % + -`
|
||
- 比较运算
|
||
- `&& ||`
|
||
- 标量 `const` 的引用
|
||
- 在全局和常量初始化中,只允许使用 `EvalConstExpr` 的结果。
|
||
|
||
#### 效果
|
||
|
||
修复后:
|
||
|
||
- 全局初始化不再依赖插入点
|
||
- `95_float.sy` 中的全局常量能够稳定生成
|
||
- 短路表达式在常量上下文中只做纯编译期求值,不会试图分配 `alloca`
|
||
|
||
### 4.2 困难二:数组变量、数组参数与标量变量的“存储语义”混乱
|
||
|
||
#### 现象
|
||
|
||
原实现里,`alloca/load/store/GEP` 对类型的理解不统一:
|
||
|
||
- 标量变量需要的是“指向标量的槽位”
|
||
- 局部数组需要的是“聚合对象的基址”
|
||
- 数组形参在 SysY 中本质上是指针,不应按局部数组同样处理
|
||
|
||
如果把这些情况混在一起,就会出现:
|
||
|
||
- `load` 试图从数组类型直接取值
|
||
- GEP 基类型和索引序列不匹配
|
||
- 局部数组访问、多维数组访问、数组实参传递行为错误
|
||
|
||
#### 解决办法
|
||
|
||
做了三层拆分:
|
||
|
||
1. 标量与数组分离
|
||
- 标量局部变量使用真正的标量槽位:`i32*` 或 `float*`
|
||
- 数组局部变量保留聚合基址
|
||
|
||
2. 普通数组与数组形参分离
|
||
- 局部/全局数组通过多级 GEP 沿数组维度寻址
|
||
- 数组形参按“指针退化”处理,访问时根据剩余维度计算偏移
|
||
|
||
3. 左值取址与值求值分离
|
||
- `GetLValuePtr` 只负责拿地址
|
||
- `visitLValueExp` 根据左值是否仍是数组来决定是 `load` 还是数组退化传参
|
||
|
||
#### 效果
|
||
|
||
修复后:
|
||
|
||
- `simple_add.sy` 恢复正常
|
||
- `05_arr_defn4.sy` 可以生成并运行
|
||
- 多维数组和数组形参的寻址逻辑更加稳定
|
||
|
||
### 4.3 困难三:局部数组花括号初始化语义不正确
|
||
|
||
#### 现象
|
||
|
||
`05_arr_defn4.sy` 虽然在中期已经不再崩溃,但运行结果仍然错误,退出码从预期的 `21` 变成了 `13`。这说明不是寻址崩了,而是初始化布局错了。
|
||
|
||
问题根源在于:
|
||
|
||
- 一部分初始化按“子数组递进”处理
|
||
- 一部分初始化又按“标量扁平展开”处理
|
||
|
||
两套逻辑混用后,多维数组初始化次序就会乱掉。
|
||
|
||
#### 解决办法
|
||
|
||
把局部数组初始化统一改成“聚合初始化 + 标量游标”方案:
|
||
|
||
- 先统一做零初始化
|
||
- 再对花括号初始化维护一个标量游标
|
||
- 标量初始化时按当前扁平偏移定位到实际元素
|
||
- 子聚合初始化时按当前对齐边界进入对应子数组
|
||
|
||
这套逻辑与 SysY/LLVM 前端常见的聚合初始化处理方式更接近。
|
||
|
||
#### 效果
|
||
|
||
修复后 `05_arr_defn4.sy` 的 IR 可以通过 `verify_ir.sh --run`,输出与预期一致。
|
||
|
||
### 4.4 困难四:IR 文本虽然能打印,但 LLVM 后端不一定接受
|
||
|
||
#### 现象
|
||
|
||
在进入 `verify_ir.sh` 阶段后,又暴露出一批“IR 生成没崩,但 LLVM 不认”的问题:
|
||
|
||
- 内建函数被打印成了空定义,而不是声明
|
||
- 浮点常量打印格式不符合 LLVM 期望
|
||
- `icmp/fcmp` 的结果在打印和后续使用中对 `i1/i32` 处理不一致
|
||
- 自动临时名使用纯数字,打乱后会违反 LLVM 的编号要求
|
||
- 基本块名重复
|
||
- `getelementptr` 打印时的基类型信息不正确
|
||
|
||
#### 解决办法
|
||
|
||
对 IR 基础设施做了系统修正:
|
||
|
||
- `Function` 不再默认创建入口块,只有真正定义函数时才建 `entry`
|
||
- `IRPrinter` 对没有基本块的函数输出 `declare`
|
||
- 自动临时名改成 `t0/t1/...`,避免 LLVM 对纯数字 SSA 名称的严格顺序约束
|
||
- 比较结果按布尔值打印和消费
|
||
- `if/while/and/or` 生成的块名追加唯一后缀
|
||
- 修复 float 常量、GEP、Cast、Call 等打印格式
|
||
|
||
#### 效果
|
||
|
||
修复后:
|
||
|
||
- `simple_add.sy`
|
||
- `13_sub2.sy`
|
||
- `29_break.sy`
|
||
- `36_op_priority2.sy`
|
||
- `05_arr_defn4.sy`
|
||
|
||
都已经可以通过 `verify_ir.sh --run`。
|
||
|
||
### 4.5 困难五:`95_float.sy` 的最终运行验证仍受运行库缺失影响
|
||
|
||
#### 现象
|
||
|
||
在修完 IR 生成与打印问题后,`95_float.sy` 已经可以:
|
||
|
||
- 成功生成 IR
|
||
- 通过 `llc` 生成目标文件
|
||
|
||
但在最终链接阶段仍会失败,原因不是 IR 错误,而是仓库中的 `sylib/sylib.c` 当前只是空壳,没有提供:
|
||
|
||
- `getfloat`
|
||
- `putfloat`
|
||
- `getfarray`
|
||
- `putfarray`
|
||
- `putch`
|
||
- `putint`
|
||
|
||
等符号的真实实现。
|
||
|
||
#### 解决办法
|
||
|
||
本次提交中没有擅自扩展运行库,而是把问题明确定位为“Lab2 IR 生成正确,但运行时依赖未补齐”。这样可以把 Lab2 编译器部分与后续运行库实现清晰分开。
|
||
|
||
#### 影响
|
||
|
||
`95_float.sy` 当前的状态是:
|
||
|
||
- IR 生成正确
|
||
- LLVM 后端接受
|
||
- 最终运行依赖运行库补全
|
||
|
||
## 5. 本次实现的主要能力
|
||
|
||
本次实验结束后,编译器已经具备以下 Lab2 关键能力:
|
||
|
||
- 全局变量/常量 IR 生成
|
||
- 局部变量 IR 生成
|
||
- `int/float` 常量与表达式生成
|
||
- 基本算术与比较运算
|
||
- 类型转换:`sitofp`、`fptosi`、`zext`
|
||
- `if-else`
|
||
- `while`
|
||
- `break/continue`
|
||
- 函数定义与函数调用
|
||
- 标量参数与数组参数
|
||
- 多维数组寻址
|
||
- 局部数组零初始化与花括号初始化
|
||
- 短路求值
|
||
- LLVM 可接受的 IR 文本打印
|
||
|
||
## 6. 验证结果
|
||
|
||
本次已完成的回归包括:
|
||
|
||
```bash
|
||
./scripts/verify_ir.sh test/test_case/functional/simple_add.sy /tmp/ir_simple --run
|
||
./scripts/verify_ir.sh test/test_case/functional/13_sub2.sy /tmp/ir_sub2 --run
|
||
./scripts/verify_ir.sh test/test_case/functional/29_break.sy /tmp/ir_break --run
|
||
./scripts/verify_ir.sh test/test_case/functional/36_op_priority2.sy /tmp/ir_op --run
|
||
./scripts/verify_ir.sh test/test_case/functional/05_arr_defn4.sy /tmp/ir_arr --run
|
||
```
|
||
|
||
这些样例均已通过。
|
||
|
||
另外:
|
||
|
||
```bash
|
||
./build/bin/compiler --emit-ir test/test_case/functional/95_float.sy
|
||
```
|
||
|
||
可以成功生成 IR,且 IR 能通过 `llc`,说明浮点常量、浮点表达式、浮点比较、类型转换与数组传参路径已经基本打通。
|
||
|
||
## 7. 本次实验中的经验总结
|
||
|
||
本次 Lab2 最核心的经验有三点:
|
||
|
||
1. 编译期常量求值和运行时 IR 生成必须严格分离。
|
||
只要两条路径混在一起,全局初始化和常量表达式一定会出错。
|
||
|
||
2. 数组不能按“只是更大的标量”处理。
|
||
数组对象、数组形参、数组元素地址、数组退化指针这几个概念必须明确区分。
|
||
|
||
3. “能打印 IR”不等于“LLVM 能接受 IR”。
|
||
最后一定要走一遍 `llc/clang`,否则很多类型和格式问题会被掩盖。
|
||
|
||
## 8. 后续可继续完善的方向
|
||
|
||
虽然本次已经完成了 Lab2 的主体工作,但还可以继续完善:
|
||
|
||
- 为 `sylib` 补齐实际运行库实现,打通 `95_float` 等 I/O 样例的最终运行
|
||
- 为全局数组初始化补完整的常量聚合表示,而不是目前以标量初始化为主
|
||
- 进一步统一 IR 中布尔类型的内部表示,减少 `i1/i32` 的兼容分支
|
||
- 继续批量回归 `test/test_case` 下更多样例,补齐剩余边界情况
|
||
|
||
## 9. 结论
|
||
|
||
本次 Lab2 已经从“完成约 90%,但被全局初始化与数组/短路问题卡住”的状态,推进到“核心 IR 生成链路可用、典型功能样例可运行验证”的状态。阻塞实验验收的主问题已经被定位并解决,代码结构也比原来更清晰,后续继续做运行库、优化与更大规模回归时会更稳。
|