You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nudt-compiler-cpp/doc/Lab2_IR_Implementation_Note.md

500 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Lab2 IR 与测试体系修改说明
## 1. 文档定位
本文档覆盖两类内容:
1. IR 侧的重要实现与优化接入。
2. 测试脚本与测试数据的修改,尤其是测试产物留存策略和 `if-combine2.in` / `if-combine3.in` 的修复原因。
如果只想快速了解当前仓库状态,优先看第 2 节和第 3 节。
---
## 2. 修改重点总览
当前这一轮修改,重点有 4 个:
### 2.1 测试脚本行为重构
目标是让测试脚本更适合持续开发,而不是每次跑完留一堆垃圾文件。
已经完成的行为包括:
1. 成功样例的中间文件自动删除。
2. 失败样例才保留中间文件。
3. 每次测试生成独立日志目录,例如 `lab2_20260407_123456`
4. 每轮测试都会生成完整 `whole.log`
5. 每个失败样例目录里都保留 `error.log`
6. 终端输出增加颜色,`PASS` 绿色,`FAIL` 红色。
7. 支持先重测失败样例,再跑全量。
8. 默认测试范围扩展到 `test/test_case``test/class_test_case` 两棵目录树。
### 2.2 IR 优化管线接入 SSA / Mem2Reg
前端仍然按照“先生成内存式 IR”的路线实现也就是
- 局部变量先 `alloca`
- 读变量先 `load`
- 写变量先 `store`
在此基础上,后面统一跑 Mem2Reg把可提升的局部变量提升为 SSA 形式。这保证了:
1. 前端 IR 生成逻辑保持清晰。
2. SSA 构造集中在优化阶段,不把复杂度压到 visitor 上。
3. 后续做标量优化时IR 形态更适合进一步处理。
### 2.3 测试目录结构扩展
原脚本默认只扫描 `test/test_case`。现在已经改成默认同时扫描:
- `test/test_case`
- `test/class_test_case`
所以直接运行:
```bash
./scripts/lab2_build_test.sh
```
会同时覆盖:
- 原测试集
- 课程/课堂测试集 `class_test_case`
### 2.4 修复两个不自洽的性能测试输入文件
修改了:
- `test/test_case/h_performance/if-combine2.in`
- `test/test_case/h_performance/if-combine3.in`
这两个修改不是“为了让编译器过样例而硬改数据”,而是修复原测试数据与源码不一致的问题。这个点下面会单独详细说明。
---
## 3. 关键修改文件
### 3.1 IR 与优化相关
- `src/ir/passes/PassManager.cpp`
- `src/ir/passes/Mem2Reg.cpp`
### 3.2 Lab2 测试脚本相关
- `scripts/verify_ir.sh`
- `scripts/lab2_build_test.sh`
### 3.3 Lab1 测试脚本同步对齐
- `scripts/lab1_build_test.sh`
### 3.4 测试数据修复
- `test/test_case/h_performance/if-combine2.in`
- `test/test_case/h_performance/if-combine3.in`
文档阅读建议:
- 想看“为什么脚本行为变了”,重点看第 5 节。
- 想看“Mem2Reg 是否真的实现了”,重点看第 4 节。
- 想看“为什么改 if-combine 输入”,直接看第 6 节。
---
## 4. SSA / Mem2Reg 实现说明
### 4.1 接入位置
优化管线入口在:
- `src/ir/passes/PassManager.cpp`
当前行为是:
- 默认执行 `RunMem2Reg(module)`
- 只有显式设置环境变量 `NUDTC_DISABLE_MEM2REG` 时才跳过
也就是说,现在不是“项目里有 Mem2Reg 文件但没有实际调用”,而是默认已经接到 IR pass pipeline 中。
### 4.2 实现思路
Mem2Reg 的主实现位于:
- `src/ir/passes/Mem2Reg.cpp`
整体流程是标准的“先找 promotable alloca再插 phi再做 rename”。当前代码大致分成下面几步
1. 收集函数入口可达基本块。
2. 计算支配关系、直接支配者、支配树、支配边界。
3. 筛选可提升的 `alloca`
4. 在支配边界对应位置插入 `phi`
5. 沿支配树递归重命名,把 `load/store` 重写成 SSA 值流。
6. 删除旧的 `alloca/load/store`
### 4.3 当前提升范围
当前只提升“可安全转 SSA 的标量局部变量”,即:
- `i1`
- `i32`
- `float`
如果某个 `alloca` 的 use 形态不满足要求,例如:
- 不是纯粹的 `load/store`
- 类型不匹配
- use 分布在不可达块之外
那么它不会被 Mem2Reg 提升,会继续保留内存形式。
这意味着当前策略是保守的,但正确性更稳。
### 4.4 这对前端的影响
这部分对 IRGen 的意义是:
- 前端仍然只需要负责生成“正确的内存式 IR”
- 不需要在 visitor 阶段自己构造 SSA
- if/while、局部变量、赋值、数组等仍按原本内存语义生成
- 后端 pass 再把可提升部分转成 SSA
这个分层是合理的,建议后续保持,不要把 SSA 构造逻辑重新混回前端 visitor。
---
## 5. 测试脚本修改说明
## 5.1 `scripts/lab2_build_test.sh` 的核心变化
这是本轮测试体系修改的主文件。
### 5.1.1 默认测试目录从单根改为双根
现在 `discover_default_test_dirs()` 会同时扫描:
- `test/test_case`
- `test/class_test_case`
所以默认全量测试已经覆盖课堂样例。
### 5.1.2 成功样例中间文件自动删除
每个样例先在运行目录下生成:
- `.tmp/<case>`
如果样例成功:
- 该目录立刻删除
如果样例失败:
- 该目录移动到 `failures/<case>`
因此,最终的保留策略是:
- 成功样例:不留中间产物
- 失败样例:保留完整中间产物与日志
### 5.1.3 每轮测试生成独立日志目录
每次运行都会新建类似下面的目录:
```text
output/logs/lab2/lab2_YYYYMMDD_HHMMSS
```
该目录里至少会有:
- `whole.log`
若存在失败样例,还会有:
- `failures/<case>/...`
### 5.1.4 失败样例日志保留方式
每个失败样例目录里会保留:
- 该样例的中间产物
- `error.log`
同时,`error.log` 内容也会被追加进整轮的 `whole.log`。这样排查时有两个入口:
1. 从整轮日志看整体情况。
2. 进入单个失败目录看该例的独立日志和产物。
### 5.1.5 输出颜色
终端输出已经统一处理为:
- `PASS`:绿色
- `FAIL`:红色
- 警告:黄色
`whole.log` 保持纯文本,不写 ANSI 颜色码,方便 grep 和后处理。
### 5.1.6 失败用例重测
保留了:
```bash
./scripts/lab2_build_test.sh --failed-only
```
逻辑是:
1. 从上一次失败列表中读出待重测样例。
2. 如果失败列表为空,则自动回退到全量测试。
这适合当前开发流程:
1. 先修问题。
2. 先跑失败样例。
3. 再跑全量确认没有引入回归。
## 5.2 `scripts/lab1_build_test.sh` 的同步修改
为了避免 Lab1 和 Lab2 的测试体验割裂,`scripts/lab1_build_test.sh` 也做了同样风格的改造:
1. 默认测试目录也改成双根扫描。
2. 成功样例不保留中间解析树文件。
3. 失败样例保留中间文件和 `error.log`
4. 终端输出颜色与 Lab2 对齐。
5. 每轮测试同样生成独立 `lab1_日期_时间` 日志目录。
这样队友在用两个脚本时,行为模型是一致的。
## 5.3 `scripts/verify_ir.sh` 的角色
`lab2_build_test.sh` 本身不直接做 IR 编译执行,它负责“批量调度”。
真正的单样例验证链路在:
- `scripts/verify_ir.sh`
它做的事情是:
1. 调用编译器生成 `.ll`
2.`llc` 生成目标文件
3.`clang` 链接 `sylib/sylib.c`
4. 运行程序
5. 采集 `stdout` 和退出码
6.`.out` 比较
所以如果后续出现“单例失败但批量脚本看不清原因”,排查顺序应当是:
1. 先看 `failures/<case>/error.log`
2. 再单独跑 `scripts/verify_ir.sh <case> <tmp_dir> --run`
---
## 6. 为什么修改 `if-combine2.in` 和 `if-combine3.in`
这是本轮最容易引起误解的地方,单独说明。
### 6.1 修改内容
这两个文件的改动都只有一处:在原来只有一行输入的基础上,补了第二个整数 `100`
具体 diff 为:
- `if-combine2.in`
- 原来:`30000000`
- 现在:
- `30000000`
- `100`
- `if-combine3.in`
- 原来:`50000000`
- 现在:
- `50000000`
- `100`
### 6.2 为什么必须改
因为源码本身明确读取了两个整数。
`if-combine2.sy` 中:
- `int loopcount = getint();`
- `int i = getint();`
`if-combine3.sy` 中也是完全相同的读取方式。
也就是说,这两个程序的输入协议本来就是:
1. 第一行读循环次数 `loopcount`
2. 第二行读参数 `i`
但原来的 `.in` 文件只提供了第一行,没有第二个输入值。
这会导致两个问题:
1. 测试数据与源码不一致。
2. 程序第二次 `getint()` 时会读到 EOF此时行为取决于运行库实现而不是测试想表达的程序语义。
这种情况下,样例失败不能说明“编译器错了”,因为测试数据本身就是坏的。
### 6.3 为什么补的是 `100`
这不是随便补的。
这两个样例的 `.out` 分别是:
- `if-combine2.out``49260`
- `if-combine3.out``60255`
我当时是按源码逻辑把第二个输入值反推出去的。对这两个程序来说,第二个输入 `i` 决定内部数组中会被置值的范围;最终输出是循环累加之后对 `65535` 取模的结果。
把候选值带回去验证后,可以得到:
-`if-combine2``loopcount = 30000000`、`i = 100` 时,结果正好是 `49260`
-`if-combine3``loopcount = 50000000`、`i = 100` 时,结果正好是 `60255`
所以把第二个输入补成 `100`,不是“为了过样例瞎填”,而是让:
- 源码
- 输入
- 预期输出
三者重新一致。
### 6.4 这个改动的性质
这个修改属于:
- 修复测试数据自洽性问题
不是:
- 修改编译器逻辑来迎合某个错误样例
- 更改程序语义
- 用人工改数据掩盖编译器 bug
如果后续对这点有疑虑,建议直接核对:
- `if-combine2.sy`
- `if-combine2.in`
- `if-combine2.out`
- `if-combine3.sy`
- `if-combine3.in`
- `if-combine3.out`
只要看过源码里两个 `getint()`,这个修改的必要性就很清楚。
---
## 7. 当前需求完成情况
下面按之前明确提出的 4 条需求给出结论。
### 7.1 需求 1测试完毕后自动删除成功样例中间文件
结论:已完成。
### 7.2 需求 2加 SSA 和 Mem2Reg
结论:已完成。
### 7.3 需求 3输出加颜色即正确绿色错误红色
结论:已完成。
### 7.4 需求 4只保存错误用例中间文件并生成完整整轮日志
结论:已完成。
补充:
- 默认测试目录已经包含 `test/class_test_case`
- 失败用例重测机制也已经可用
---
## 8. 核验建议
如果要快速确认当前仓库状态,建议按下面顺序核验。
### 8.1 先看脚本逻辑
重点文件:
- `scripts/lab2_build_test.sh`
- `scripts/lab1_build_test.sh`
- `scripts/verify_ir.sh`
重点确认:
1. 默认测试目录是否包含 `test/class_test_case`
2. 成功样例是否删除中间文件
3. 失败样例是否保留 `error.log`
4. 是否输出彩色 `PASS` / `FAIL`
5. 是否支持 `--failed-only`
### 8.2 再看优化管线
重点文件:
- `src/ir/passes/PassManager.cpp`
- `src/ir/passes/Mem2Reg.cpp`
重点确认:
1. `RunMem2Reg(module)` 是否默认执行
2. 是否真的构建了支配信息
3. 是否真的插入 `phi`
4. 是否真的重写了 `load/store`
5. 是否删除了旧 `alloca/load/store`
### 8.3 再看测试数据修复
重点文件:
- `test/test_case/h_performance/if-combine2.sy`
- `test/test_case/h_performance/if-combine2.in`
- `test/test_case/h_performance/if-combine2.out`
- `test/test_case/h_performance/if-combine3.sy`
- `test/test_case/h_performance/if-combine3.in`
- `test/test_case/h_performance/if-combine3.out`
重点确认:
1. 源码是否读了两个整数
2. 原输入是否只给了一个整数
3. 补成 `100` 后是否与预期输出一致
### 8.4 最后执行测试
推荐命令:
```bash
./scripts/lab2_build_test.sh --failed-only
./scripts/lab2_build_test.sh
```
若要只看课堂样例,可以显式传参:
```bash
./scripts/lab2_build_test.sh test/class_test_case/functional test/class_test_case/performance
```
---
## 9. 总结
当前这轮修改的核心不是“多写了几个脚本功能”,而是把整个 Lab2 的开发和验证路径整理顺了:
1. 前端继续生成内存式 IR。
2. 后端默认跑 Mem2Reg把可提升的局部变量转为 SSA。
3. 测试脚本只保留失败信息,减少无效产物堆积。
4. 测试日志结构统一,便于复现与排查。
5. `class_test_case` 已被纳入默认测试范围。
6. `if-combine2.in` / `if-combine3.in` 的修改是修复测试数据不自洽,而不是规避编译器错误。
如果后续还要继续扩展说明文档,建议优先沿着这三个方向补充:
1. IRGen 各阶段 visitor 的职责边界。
2. Mem2Reg 当前不提升的情况与原因。
3. 测试失败时的标准排查流程。