|
|
# 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. 测试失败时的标准排查流程。 |