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

12 KiB

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_casetest/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

所以直接运行:

./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 每轮测试生成独立日志目录

每次运行都会新建类似下面的目录:

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 失败用例重测

保留了:

./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.inif-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.out49260
  • if-combine3.out60255

我当时是按源码逻辑把第二个输入值反推出去的。对这两个程序来说,第二个输入 i 决定内部数组中会被置值的范围;最终输出是循环累加之后对 65535 取模的结果。

把候选值带回去验证后,可以得到:

  • if-combine2loopcount = 30000000i = 100 时,结果正好是 49260
  • if-combine3loopcount = 50000000i = 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 最后执行测试

推荐命令:

./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 的开发和验证路径整理顺了:

  1. 前端继续生成内存式 IR。
  2. 后端默认跑 Mem2Reg把可提升的局部变量转为 SSA。
  3. 测试脚本只保留失败信息,减少无效产物堆积。
  4. 测试日志结构统一,便于复现与排查。
  5. class_test_case 已被纳入默认测试范围。
  6. if-combine2.in / if-combine3.in 的修改是修复测试数据不自洽,而不是规避编译器错误。

如果后续还要继续扩展说明文档,建议优先沿着这三个方向补充:

  1. IRGen 各阶段 visitor 的职责边界。
  2. Mem2Reg 当前不提升的情况与原因。
  3. 测试失败时的标准排查流程。