Files
nudt-compiler-cpp/doc/Lab2-实验记录.md

314 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 生成链路可用、典型功能样例可运行验证”的状态。阻塞实验验收的主问题已经被定位并解决,代码结构也比原来更清晰,后续继续做运行库、优化与更大规模回归时会更稳。