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

1006 lines
25 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 / IRGen 实现说明
## 1. 文档目的
这份文档用于说明当前 Lab2 实现中**具体改了哪些地方、为什么这样改、最终达到了什么效果、如何验证**。
文档分成两类内容:
1. **手工实现的核心代码修改**
这些是本次完成 Lab2 功能时真正依赖的实现。
2. **工作树中额外出现的改动或生成物**
这些文件虽然在当前仓库状态中显示为 modified / untracked但它们不是当前 Lab2 逻辑实现的核心代码,核验时应与手工实现区分开。
本次实现遵循的总路线是:
- 基于现有前端接口和现有解析树 visitor
- 在 IR 生成阶段**边查符号表边生成 IR一遍完成**
- `CompUnit` 只做轻量预注册:内建函数、全局符号、函数签名
- 局部变量统一走 `alloca + load + store`
- if / while / break / continue 统一走 `BasicBlock + br / condbr`
- 不依赖 SSA / phi 完成功能正确性
---
## 2. 本次实现覆盖的功能范围
当前已经实现并验证通过的 SysY 子集如下:
- `int / float / void`
- 全局变量、全局常量
- 局部变量、局部常量
- 标量与数组
- 标量参数与数组参数
- 整数 / 浮点字面量
- 左值取地址、右值读取
- 赋值语句
- `return`
- 一元运算:`+ - !`
- 二元运算:`+ - * / %`
- 比较运算:`< <= > >= == !=`
- 逻辑运算:`&& ||`(短路求值)
- `if / if-else`
- `while`
- `break / continue`
- 函数定义与函数调用
- 全局 / 局部 / 常量数组初始化
- 内建函数调用:`getint/getch/getfloat/getarray/getfarray/putint/putch/putfloat/putarray/putfarray/starttime/stoptime`
当前没有作为 Lab2 重点去扩展的内容:
- SSA 形式
- phi 合流
- MIR / `--emit-asm` 的完整 lowering
- 后端优化
也就是说,本次提交重点是:
- **定义并实现可打印、可执行、可被 `llc` 接受的 SysY IR**
- **基于 visitor 从解析树一遍生成 IR**
- **通过 `--emit-ir -> llc -> clang -> 运行对拍` 完整验证**
---
## 3. 手工修改的核心文件总览
### 3.1 IR 层
- `include/ir/IR.h`
- `src/ir/Context.cpp`
- `src/ir/Type.cpp`
- `src/ir/Value.cpp`
- `src/ir/GlobalValue.cpp`
- `src/ir/BasicBlock.cpp`
- `src/ir/Function.cpp`
- `src/ir/Module.cpp`
- `src/ir/Instruction.cpp`
- `src/ir/IRBuilder.cpp`
- `src/ir/IRPrinter.cpp`
### 3.2 符号表 / 语义层
- `include/sem/SymbolTable.h`
- `src/sem/SymbolTable.cpp`
- `include/sem/Sema.h`
- `src/sem/Sema.cpp`
### 3.3 IR 生成层
- `include/irgen/IRGen.h`
- `src/irgen/IRGenDriver.cpp`
- `src/irgen/IRGenFunc.cpp`
- `src/irgen/IRGenDecl.cpp`
- `src/irgen/IRGenExp.cpp`
- `src/irgen/IRGenStmt.cpp`
### 3.4 构建 / 验证 / 运行时支持
- `src/frontend/CMakeLists.txt`
- `src/sem/CMakeLists.txt`
- `src/irgen/CMakeLists.txt`
- `scripts/verify_ir.sh`
- `scripts/lab2_build_test.sh`
- `sylib/sylib.h`
- `sylib/sylib.c`
---
## 4. 详细修改说明
## 4.1 IR 层修改
### 4.1.1 `include/ir/IR.h`
这个文件是当前对接的新 IR 主接口,当前实现围绕它进行了系统补全。
主要内容:
- 对齐并完善 `Type / Value / Use / User / Instruction / BasicBlock / Function / Module / IRBuilder / IRPrinter` 的接口声明
- 保留并使用新的 `Opcode` 体系
- 补齐各种指令类的接口:
- 算术与比较
- 一元运算
- `alloca/load/store`
- `br/condbr/ret`
- `call`
- `gep`
- `zext`
- `memset`
- `phi`(接口保留,但当前 IRGen 不依赖它)
-`BasicBlock` 新增了 `Insert()` 模板接口
`BasicBlock::Insert()` 是本次后期一个关键修复点。原因是:
- 如果块内声明变量时直接在当前位置发 `alloca`
- 那么当声明出现在循环体里时,`alloca` 会出现在循环体 BasicBlock 内
- 程序每次循环都会重复增长栈帧,性能样例会出现栈爆或段错误
因此新增 `Insert()` 之后,局部变量一律插入函数入口块的前部,从而保证“语义上是块作用域变量,物理上仍在函数入口块分配栈槽”,这符合典型的内存式 IR 实现方式。
### 4.1.2 `src/ir/Context.cpp`
主要修改:
- 统一临时 SSA 名为 `%t0, %t1, %t2, ...`
- 不再使用可能与 LLVM 解析、编号风格冲突的名字方案
- 继续负责常量缓存与上下文级对象管理
这里修过一个实际问题:
- 先前使用纯数字 SSA 名时,和后续打印后的 IR 在某些场景下不够稳定
- 改成 `%tN` 后,输出更清晰,也更容易排错
### 4.1.3 `src/ir/Type.cpp`
主要修改:
- 补齐标量类型、数组类型、指针语义相关支持
- 为 IRPrinter 和 GEP 推导提供准确类型信息
重点作用:
- 数组全局变量初始化
- 数组 GEP
- 数组参数衰减后的地址计算
- `alloca` 的被分配对象类型确定
### 4.1.4 `src/ir/Value.cpp`
主要修改:
- 完善 `Value` 基类与命名、类型访问逻辑
- 保证 `GlobalValue`、`Instruction`、`Argument` 等可统一当作 `Value*` 使用
### 4.1.5 `src/ir/GlobalValue.cpp`
主要修改:
- 完善全局变量 / 全局常量的 initializer 管理
- 支持记录 object type 与 initializer
重点效果:
- 全局标量和全局数组都可以正确打印成 LLVM 风格 IR
- 全局常量与普通全局变量可以区分
### 4.1.6 `src/ir/BasicBlock.cpp`
主要修改:
- 完善 BasicBlock 的 predecessor / successor 关系维护
-`br/condbr` 打印与控制流组织提供基础
### 4.1.7 `src/ir/Function.cpp`
主要修改:
- 完善函数参数、参数类型、参数对象、入口块、块管理
- 支持 external / non-external 函数区分
重点效果:
- 内建函数以 external declaration 的形式存在
- 用户函数以 define 形式输出
- visitor 生成函数体时可以安全创建和切换 BasicBlock
### 4.1.8 `src/ir/Module.cpp`
主要修改:
- 管理整个模块级别的全局变量和函数列表
- 支持创建、收集并输出 module 内对象
### 4.1.9 `src/ir/Instruction.cpp`
主要修改:
- 补全各类指令对象的构造、operand 管理与 opcode 绑定
- 修正 `CallInst` 构造时 opcode 应为 `Opcode::Call`
- 支持 `GetElementPtrInst`、`MemsetInst`、`ZextInst` 等
### 4.1.10 `src/ir/IRBuilder.cpp`
主要修改:
- 所有 IR 构造尽量统一收敛到 `IRBuilder`
- 完善以下创建接口:
- 常量创建
- 二元 / 一元运算
- `alloca/load/store`
- `br/condbr/ret`
- `call`
- `gep`
- `phi`
- `zext`
- `memset`
重点效果:
- IRGen 侧基本不需要手写 `new Instruction`
- 一致性更强,也更便于后续扩展
### 4.1.11 `src/ir/IRPrinter.cpp`
这个文件是 IR 能否被 `llc` 接受的关键。
主要修改:
- 输出接近 LLVM IR 的文本格式
- 支持全局变量、external declaration、函数定义、基本块、指令打印
- 支持数组类型和数组 initializer 打印
- 支持 `getelementptr`、`alloca/load/store`、比较、调用、分支、转换等打印
- 在需要时输出:
- `declare void @llvm.memset.p0.i32(ptr, i8, i32, i1)`
- 浮点常量按 LLVM 能接受的形式输出
关键修复:
- `llc``ptr` 和浮点常量格式比较敏感
- 当前打印格式已经和 `llc -opaque-pointers` 验证链路对齐
---
## 4.2 符号表 / 语义层修改
## 4.2.1 `include/sem/SymbolTable.h` 与 `src/sem/SymbolTable.cpp`
这一组文件被改成了当前 IR 生成真正使用的符号表。
核心设计:
- 作用域栈:`EnterScope / ExitScope`
- 当前作用域查重:`ContainsInCurrentScope`
- 名字查找:`Lookup`
- 符号项 `SymbolEntry` 保存:
- 符号种类:变量 / 常量 / 函数
- 语义类型:`int / float / void`
- 是否数组
- 是否参数数组
- 数组维度
- 对应的 IR 值指针
- 常量标量值 / 常量数组值
- 函数类型信息
这里严格贯彻了你给的约定:
- 变量项存“地址”
- 局部变量存 `alloca` 结果
- 全局变量存 `GlobalValue*`
- 参数标量存入口块 `alloca` 的地址
- 参数数组存函数参数本身(退化后的指针值)
- 函数项存 `Function*`
- 右值读取时:查符号表,必要时 `load`
- 左值赋值时:直接拿地址 `store`
## 4.2.2 `include/sem/Sema.h` 与 `src/sem/Sema.cpp`
本次没有把 Sema 做成单独的“完整先验检查阶段”,而是保留为一个轻量外壳。
原因:
- 当前 Lab2 的主要策略是“边查符号表边生成 IR一遍完成”
- 因此真正的名字绑定、类型转换、数组维度检查、const 使用限制等逻辑都收在 IRGen 里完成
- `Sema` 在这里主要保留接口兼容性,避免前端主流程被完全推翻
换句话说:
- 不是没有语义检查
- 而是把“本轮实验真正需要的语义检查”前移到了 IR 生成过程中完成
---
## 4.3 IR 生成层修改
## 4.3.1 `include/irgen/IRGen.h`
这个文件被改成当前 IRGen 的总控头文件,声明了整个 visitor 生成需要的辅助结构和函数。
主要内容:
- `TypedValue`
- 统一表示“一个表达式生成后的值”
- 包含 `Value*`、语义类型、是否数组、数组维度
- `LValueInfo`
- 统一表示一个左值解析后的结果
- 包含符号项、地址、类型、是否仍是数组、剩余维度信息等
- `LoopContext`
- 保存当前循环的条件块和退出块
-`break/continue` 使用
- 声明所有生成流程:
- 顶层预注册
- 函数定义生成
- 声明生成
- 表达式生成
- 条件与控制流生成
- 初始化列表展开
- 左值地址计算
- 常量表达式求值
- 新增 `CreateEntryAlloca()` 辅助接口
## 4.3.2 `src/irgen/IRGenDriver.cpp`
主要作用:
- 提供 `GenerateIR()` 入口
- 创建 `Module`
- 建立 `IRGenImpl`
- 从解析树根节点开始 visitor
这是 IR 生成和外部调用之间的桥。
## 4.3.3 `src/irgen/IRGenFunc.cpp`
这个文件负责顶层与函数相关逻辑。
主要修改内容:
### A. 内建函数预注册
注册以下函数签名:
- `getint`
- `getch`
- `getfloat`
- `getarray`
- `getfarray`
- `putint`
- `putch`
- `putfloat`
- `putarray`
- `putfarray`
- `starttime`
- `stoptime`
作用:
- 用户代码中调用这些函数时,不需要先写定义
- IRPrinter 能输出 external declaration
### B. 顶层轻量预注册
`CompUnit` 层先完成:
- 全局声明的名字注册
- 函数签名注册
这样做的目的是:
- 函数体里调用后定义函数时也能正常查到符号
- 全局数组和全局常量在生成时已有符号项可绑定
### C. 函数定义生成
`EmitFunction()` 的流程是:
- 找到预注册好的 `Function*`
- 建立 / 获取入口块
- 设置 builder 插入点
- 进入函数作用域
- 绑定形参
- 生成函数体 block
- 离开函数作用域
- 若当前块无 terminator则补默认返回
### D. 参数绑定
参数绑定严格区分:
- 标量参数:
- 在入口块建 `alloca`
-`store` 参数实值
- 符号表中存这个地址
- 数组参数:
- 不再额外 `alloca`
- 直接把函数参数值当成“退化后的首地址”保存到符号表
- `is_param_array = true`
- 记录剩余维度
这和 SysY / C 风格数组参数是吻合的。
## 4.3.4 `src/irgen/IRGenDecl.cpp`
这个文件是改动最多的文件之一,负责声明、常量、数组初始化与很多辅助逻辑。
主要修改内容:
### A. 基本类型和维度解析
- `ParseBType()`:把文法层 `int/float` 转成语义类型
- `ParseFuncType()`:解析 `void/int/float`
- `ParseArrayDims()`:解析数组维度
- `ParseParamDims()`:解析参数数组维度
### B. 全局声明预注册
`PredeclareGlobalDecl()` 会先把全局变量 / 全局常量注册进符号表。
特别处理:
- 对于全局标量 `const`,在预注册阶段就把它的常量值算出来并存入符号表
这是后面修过的一个关键点,原因是:
- 某些函数参数维度或数组维度依赖全局常量表达式
- 如果顶层 const 值不先写入符号表,后续求值时会失败
### C. 全局变量 / 全局常量生成
- 标量全局:直接生成 initializer
- 数组全局:先把初始化列表摊平成一维,再生成常量数组 initializer
- 全局常量额外记录 `const_scalar``const_array`
### D. 局部变量 / 局部常量生成
#### 原则
- 局部变量统一存地址
- 局部标量:`alloca + (可选初始化) + store`
- 局部数组:
- 先整体清零
- 再按初始化列表逐元素写入
#### 关键修复:入口块 alloca
新增 `CreateEntryAlloca()`,它会:
- 找到当前函数入口块
- 跳过入口块里已有的 `alloca`
- 把新的 `alloca` 插到入口块最前面的 alloca 区域
修复了下面这个真实 bug
- 如果 `while``if` 内部定义局部变量
- 旧做法会把 `alloca` 发在当前块
- 循环每执行一次就新长一层栈帧
- 性能样例会出现段错误
现在该问题已解决。
### E. 初始化列表摊平
实现了三套初始化展开逻辑:
- `FlattenConstInitVal()`:常量数组初始化
- `FlattenInitVal()`:全局变量初始化
- `FlattenLocalInitVal()`:局部数组初始化(保留表达式槽位)
同时实现了内部递归辅助:
- `FlattenConstInitValImpl()`
- `FlattenInitValImpl()`
- `FlattenLocalInitValImpl()`
- `AlignInitializerCursor()`
- `FlattenIndices()`
这里也修过一个关键错误:
- 花括号初始化子对象结束后cursor 必须跳到该子对象末尾
- 否则多维数组初始化会错位
这个问题已经在数组类功能样例中验证过。
### F. 局部数组初始化与清零
- `ZeroInitializeLocalArray()`:调用 `memset`
- `StoreLocalArrayElements()`:按扁平槽位逐个生成 GEP + store
作用:
- 避免手写整棵数组清零循环
- 局部大数组初始化开销和 IR 规模都更合理
## 4.3.5 `src/irgen/IRGenExp.cpp`
这个文件负责表达式、常量表达式、左值 / 右值、数组寻址、函数调用等。
主要修改内容:
### A. 常量表达式求值
实现了:
- `EvalConstExp`
- `EvalConstAddExp`
- `EvalConstMulExp`
- `EvalConstUnaryExp`
- `EvalConstPrimaryExp`
- `EvalConstLVal`
用途:
- 数组维度解析
- 常量初始化求值
- const 传播到全局 / 局部 const
### B. 数值与类型转换
实现了:
- `CastScalar()`
- `CastToCondition()`
- `NormalizeLogicalValue()`
- `ConvertConst()`
效果:
- int / float 混合运算时自动转换
- 条件表达式统一转成可分支的 i1
- 比较结果可继续转成 int 使用
### C. 普通表达式生成
实现了:
- `EmitExp`
- `EmitAddExp`
- `EmitMulExp`
- `EmitUnaryExp`
- `EmitPrimaryExp`
- `EmitRelExp`
- `EmitEqExp`
支持:
- 字面量
- 括号
- 变量读取
- 一元 `+ - !`
- 二元算术与比较
- int / float 混合运算
### D. 逻辑与条件短路
实现:
- `EmitCond`
- `EmitLOrCond`
- `EmitLAndCond`
方式:
- 直接用 BasicBlock 组织短路求值
- `||`:左边为真直接跳 true block
- `&&`:左边为假直接跳 false block
这部分没有用“先算成整数再判断”的偷懒写法,而是直接走控制流。
### E. 左值地址与右值读取
这里严格按你要求的方式做。
#### `ResolveLVal()`
统一解析左值,得到:
- 符号项
- 当前对应地址
- 当前类型
- 是否仍然是数组
- 剩余维度
- 是否是“参数数组且未下标”的特殊情况
#### `GenLValAddr()`
- 如果最终还是数组,不允许当作普通可赋值标量左值使用
- 否则直接返回地址
#### `EmitLValValue()`
- 标量:直接 `load`
- 常量标量:直接生成常量值
- 数组值:按需要做 decay
### F. 数组地址计算
`CreateArrayElementAddr()` 负责统一处理:
- 普通数组
- 参数数组
- 多维数组
规则:
- 普通数组 GEP 需要补首个 `0`
- 参数数组本身已经是退化后的指针,不补额外 `0`
- 下标数不足时保留为“仍然是数组”的中间结果
### G. 函数调用
表达式文件还处理了:
- 通过符号表查函数项
- 实参与形参类型对齐
- 数组实参退化
-`CreateCall`
### H. 一元 `!` 修复
这里修过一个逻辑错误:
- 旧逻辑容易把 `!expr` 处理成“转布尔”而不是“逻辑非”
- 现在已经按 `expr == 0 ? 1 : 0` 的语义实现
## 4.3.6 `src/irgen/IRGenStmt.cpp`
这个文件负责语句和控制流。
主要修改内容:
### A. Block 与作用域
- `EmitBlock()`:进入 / 退出作用域
- `EmitBlockItem()`:分派到声明或语句
### B. 赋值语句
流程:
- `ResolveLVal()` 拿左侧地址
- 检查不能给数组整体赋值
- 检查不能给 const 赋值
- 生成右侧表达式
- 做类型转换
- `store`
### C. return 语句
- `void` 函数:只能 `ret void`
-`void` 函数:必须有返回值,必要时先做类型转换
### D. if / if-else
- 创建 then / else / end block
- 条件用 `EmitCond()` 直接发 `condbr`
- 分支结束后决定是否回跳到 end
- 如果 then / else 都终结,则整个 if 终结
### E. while
- 创建 cond / body / end 三个块
- 先无条件跳 cond
- cond 中发条件分支
- body 未终结时回跳 cond
- 退出时插入点落在 end
### F. break / continue
- 维护 `loop_stack_`
- `break` 跳当前循环 end
- `continue` 跳当前循环 cond
---
## 4.4 构建、验证与运行时支持修改
## 4.4.1 `src/frontend/CMakeLists.txt`
主要修改:
- 让 ANTLR 生成步骤在构建时自动发生
- 直接基于当前 `src/antlr4/SysY.g4` 生成 parser/visitor 代码
作用:
- 新建构建目录后,不需要手工先跑一遍 ANTLR 命令
- 构建更可复现
## 4.4.2 `src/sem/CMakeLists.txt`
主要修改:
- `sem` 链接 `frontend`
原因:
- `Sema` / `SymbolTable` 相关编译过程中需要访问 parser 头文件
## 4.4.3 `src/irgen/CMakeLists.txt`
主要修改:
- `irgen` 链接 `frontend``sem`
原因:
- IRGen 需要 parser 访问能力
- 也需要共享的符号表与语义层声明
## 4.4.4 `sylib/sylib.h` 与 `sylib/sylib.c`
这两份文件提供 end-to-end 验证需要的运行时支持。
实现了:
- 输入:`getint/getch/getfloat/getarray/getfarray`
- 输出:`putint/putch/putfloat/putarray/putfarray`
- 计时:`starttime/stoptime`(当前为无副作用占位实现)
特别处理:
- `getfloat` 使用 `strtof`,可以兼容十进制与十六进制浮点输入形式
- `putfloat` / `putfarray` 使用 `%a` 风格输出,和测试用例格式对齐
## 4.4.5 `scripts/verify_ir.sh`
这个脚本被修成当前最重要的单例验证脚本。
主要修改:
- 优先使用 `./build_lab2/bin/compiler`
- 如果存在 `build/bin/compiler` 但它是 parse-only不再优先选它
- 使用 `llc -opaque-pointers`
-`clang` 链接 `sylib/sylib.c`
- 比较时同时校验:
- 程序标准输出
- 进程退出码
- 比较文本时归一化:
- CRLF 与 LF 差异
- 文件末尾换行差异
这个修改解决了两个现实问题:
1. 仓库原有 `build/bin/compiler` 可能是 parse-only 构建,不能 `--emit-ir`
2. 某些 `.out` 文件的换行风格不统一,不应该被误判成语义错误
## 4.4.6 `scripts/lab2_build_test.sh`
这个脚本被改成当前的批量测试入口。
主要功能:
- 自动配置并编译 `build_lab2`
- 批量遍历 `functional``performance` 目录中的 `.sy`
- 调用 `verify_ir.sh` 完成 end-to-end 测试
- 可选 `--save-ir` 保存每个用例生成的 IR
- 输出 PASS / FAIL 汇总
它的目标是让你们做回归时不用手工一条条敲命令。
---
## 5. 当前工作树中出现、但不属于本次核心实现的改动
下面这些文件 / 目录在当前工作树里可能也显示为 modified 或 untracked但它们和“本次 Lab2 核心逻辑实现”需要区分看待。
### 5.1 生成物和测试产物
这些不是手工业务逻辑代码:
- `build_lab2/`
- `output/lab2/`
- `output/recheck/`
- `output/logs/lab2/`
- `output/lab1/*.tree`
它们的来源是:
- 新构建目录
- 批量验证生成的 IR / 可执行文件 / 日志
- Lab1/Lab2 相关输出
### 5.2 ANTLR 生成文件
- `src/antlr4/.antlr/SysYBaseListener.java`
- `src/antlr4/.antlr/SysYLexer.java`
- `src/antlr4/.antlr/SysYListener.java`
- `src/antlr4/.antlr/SysYParser.java`
这些属于 ANTLR 生成物,不是手工维护的功能核心。
### 5.3 当前工作树里出现但不是本次功能重点的文件
以下文件在当前仓库状态中也可能显示有 diff
- `include/ir/IR_org.h`
- `include/ir/utils.h`
- `src/antlr4/SysY.g4`
- `scripts/lab1_build_test.sh`
- `third_party/antlr4-runtime-4.13.2/runtime/src/**`
这些文件**不是当前 Lab2 一遍式 IR 生成功能的核心实现依赖点**。换句话说:
- 当前功能的正确性不建立在“改 grammar”上
- 当前功能的正确性也不建立在“手工修改第三方 runtime”上
- 如果你们准备整理成最终提交版本,建议把这部分和队友仓库主线再核对一次,确认哪些是已有基线差异、哪些是生成/换行造成的工作树噪声,再决定是否保留
---
## 6. 最终实现效果总结
当前最终效果是:
### 6.1 设计效果
- 实现了基于 visitor 的一遍式 IR 生成
- 符号表在生成过程中实时查询
- 变量统一按地址入表
- 右值统一按需 `load`
- 左值统一按地址 `store`
- 控制流统一走 BasicBlock
- 数组支持到了能覆盖当前测试集的程度
### 6.2 IR 效果
当前 `--emit-ir` 输出的 IR
- 能被 `llc -opaque-pointers` 接受
- 能正确链接 `sylib`
- 能跑出正确结果
### 6.3 验证效果
当前已通过:
#### functional
- `05_arr_defn4.sy`
- `09_func_defn.sy`
- `11_add2.sy`
- `13_sub2.sy`
- `15_graph_coloring.sy`
- `22_matrix_multiply.sy`
- `25_scope3.sy`
- `29_break.sy`
- `36_op_priority2.sy`
- `95_float.sy`
- `simple_add.sy`
#### performance
- `01_mm2.sy`
- `02_mv3.sy`
- `03_sort1.sy`
- `2025-MYO-20.sy`
- `fft0.sy`
- `gameoflife-oscillator.sy`
- `if-combine3.sy`
- `large_loop_array_2.sy`
- `transpose0.sy`
- `vector_mul3.sy`
最终批量结果:
- `21 PASS / 0 FAIL / total 21`
---
## 7. 如何验证
### 7.1 编译
```bash
cmake -S . -B build_lab2
cmake --build build_lab2 -j2
```
### 7.2 单个用例验证
```bash
./scripts/verify_ir.sh test/test_case/functional/simple_add.sy output/check/simple_add --run
```
这个命令会自动做:
1. `compiler --emit-ir`
2. `llc -opaque-pointers`
3. `clang + sylib`
4. 运行程序
5. 对比 `.out` 与退出码
### 7.3 批量验证
```bash
./scripts/lab2_build_test.sh --save-ir
```
效果:
- 自动编译 `build_lab2`
- 跑完整个 `functional + performance`
- 保存生成 IR 到 `output/lab2/`
- 保存日志到 `output/logs/lab2/`
---
## 8. 为什么新建 `build_lab2`
新建 `build_lab2` 的原因不是“源代码需要它”,而是为了**隔离构建配置,保证 Lab2 验证链路稳定**。`build_lab2` 是当前专门服务于 Lab2 IR 生成与验证的一套独立构建目录,命名上直接对应实验用途,也方便和仓库中其他历史构建目录区分。
具体原因如下:
### 8.1 仓库原有 `build/` 目录不是稳定可信的 Lab2 IR 构建目录
我们实际检查过:
- 原有 `build/bin/compiler` 在当前仓库状态下可能是 parse-only 构建
- 它会直接报“当前为 parse-only 构建IR/汇编输出已禁用”
- 这种构建目录不能用于 Lab2 的 `--emit-ir` 验证
### 8.2 CMake 构建目录会缓存配置
CMake 会把很多配置记在构建目录里,例如:
- 编译选项
- 生成器信息
- 特定开关(如 parse-only
- 依赖关系缓存
如果直接复用已有 `build/`
- 很难保证它没有历史残留配置
- 容易出现“代码是新的,但编出来的不是你想要的版本”
- 也可能覆盖你或队友原本留着的构建配置
### 8.3 `build_lab2` 是一个隔离的、可复现的 Lab2 全功能构建目录
我新建它的目标是:
- 不污染原有 `build/`
- 单独得到一个确认支持 `--emit-ir` 的完整编译器
-`verify_ir.sh``lab2_build_test.sh` 都有一致、稳定的目标编译器可用
### 8.4 它只是构建产物目录,不是源码目录
- 它可以随时删除再重新生成
- 不影响源代码逻辑
- 本质上和常见的 `build-debug`、`build-release`、`build-lab2` 是同一类思路
如果后续你们想统一目录,也可以在确认 `build/` 已不是 parse-only 且配置干净之后,再把脚本切回 `build/`。当前保留 `build_lab2` 的主要价值是:**它已经被完整验证过,能稳定跑通当前 Lab2 全套测试。**
---
## 9. 建议你和队友核验时的顺序
建议按下面顺序看,会最快:
1. 先看 `include/irgen/IRGen.h`
了解 IRGen 的总辅助结构和接口
2. 再看 `IRGenFunc.cpp`
了解顶层预注册、函数生成、参数绑定
3. 再看 `IRGenDecl.cpp`
了解变量 / const / 数组 / 初始化逻辑
4. 再看 `IRGenExp.cpp`
了解表达式、左值、数组地址和调用
5. 最后看 `IRGenStmt.cpp`
了解 if/while/break/continue 的控制流生成
6. 核验时直接跑 `./scripts/lab2_build_test.sh --save-ir`
这样会比一开始就读 IR 层全部类定义更容易抓住主线。
---
## 10. 当前结论
当前版本已经满足本轮 Lab2 的核心目标:
- SysY IR 已定义并实现到可执行验证程度
- visitor 一遍生成 IR 已完成
- 关键语言子集与测试集均已通过
- 实现思路与“边查符号表边生成 IR、一遍完成”的要求一致
如果后续你们还要继续扩展:
- 可以在此基础上继续补更严的语义检查
- 可以继续推进 SSA / phi
- 可以继续做 MIR / 汇编后端
- 但对当前 Lab2 来说IR 生成主线已经可以作为稳定基线继续迭代