# 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/` 如果样例成功: - 该目录立刻删除 如果样例失败: - 该目录移动到 `failures/` 因此,最终的保留策略是: - 成功样例:不留中间产物 - 失败样例:保留完整中间产物与日志 ### 5.1.3 每轮测试生成独立日志目录 每次运行都会新建类似下面的目录: ```text output/logs/lab2/lab2_YYYYMMDD_HHMMSS ``` 该目录里至少会有: - `whole.log` 若存在失败样例,还会有: - `failures//...` ### 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//error.log` 2. 再单独跑 `scripts/verify_ir.sh --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. 测试失败时的标准排查流程。