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