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