# 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 生成主线已经可以作为稳定基线继续迭代