25 KiB
Lab2 IR / IRGen 实现说明
1. 文档目的
这份文档用于说明当前 Lab2 实现中具体改了哪些地方、为什么这样改、最终达到了什么效果、如何验证。
文档分成两类内容:
- 手工实现的核心代码修改 这些是本次完成 Lab2 功能时真正依赖的实现。
- 工作树中额外出现的改动或生成物 这些文件虽然在当前仓库状态中显示为 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-elsewhilebreak / 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.hsrc/ir/Context.cppsrc/ir/Type.cppsrc/ir/Value.cppsrc/ir/GlobalValue.cppsrc/ir/BasicBlock.cppsrc/ir/Function.cppsrc/ir/Module.cppsrc/ir/Instruction.cppsrc/ir/IRBuilder.cppsrc/ir/IRPrinter.cpp
3.2 符号表 / 语义层
include/sem/SymbolTable.hsrc/sem/SymbolTable.cppinclude/sem/Sema.hsrc/sem/Sema.cpp
3.3 IR 生成层
include/irgen/IRGen.hsrc/irgen/IRGenDriver.cppsrc/irgen/IRGenFunc.cppsrc/irgen/IRGenDecl.cppsrc/irgen/IRGenExp.cppsrc/irgen/IRGenStmt.cpp
3.4 构建 / 验证 / 运行时支持
src/frontend/CMakeLists.txtsrc/sem/CMakeLists.txtsrc/irgen/CMakeLists.txtscripts/verify_ir.shscripts/lab2_build_test.shsylib/sylib.hsylib/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/storebr/condbr/retcallgepzextmemsetphi(接口保留,但当前 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/storebr/condbr/retcallgepphizextmemset
重点效果:
- 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. 内建函数预注册
注册以下函数签名:
getintgetchgetfloatgetarraygetfarrayputintputchputfloatputarrayputfarraystarttimestoptime
作用:
- 用户代码中调用这些函数时,不需要先写定义
- 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/floatParseArrayDims():解析数组维度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():调用memsetStoreLocalArrayElements():按扁平槽位逐个生成 GEP + store
作用:
- 避免手写整棵数组清零循环
- 局部大数组初始化开销和 IR 规模都更合理
4.3.5 src/irgen/IRGenExp.cpp
这个文件负责表达式、常量表达式、左值 / 右值、数组寻址、函数调用等。
主要修改内容:
A. 常量表达式求值
实现了:
EvalConstExpEvalConstAddExpEvalConstMulExpEvalConstUnaryExpEvalConstPrimaryExpEvalConstLVal
用途:
- 数组维度解析
- 常量初始化求值
- const 传播到全局 / 局部 const
B. 数值与类型转换
实现了:
CastScalar()CastToCondition()NormalizeLogicalValue()ConvertConst()
效果:
- int / float 混合运算时自动转换
- 条件表达式统一转成可分支的 i1
- 比较结果可继续转成 int 使用
C. 普通表达式生成
实现了:
EmitExpEmitAddExpEmitMulExpEmitUnaryExpEmitPrimaryExpEmitRelExpEmitEqExp
支持:
- 字面量
- 括号
- 变量读取
- 一元
+ - ! - 二元算术与比较
- int / float 混合运算
D. 逻辑与条件短路
实现:
EmitCondEmitLOrCondEmitLAndCond
方式:
- 直接用 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跳当前循环 endcontinue跳当前循环 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 差异
- 文件末尾换行差异
这个修改解决了两个现实问题:
- 仓库原有
build/bin/compiler可能是 parse-only 构建,不能--emit-ir - 某些
.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.javasrc/antlr4/.antlr/SysYLexer.javasrc/antlr4/.antlr/SysYListener.javasrc/antlr4/.antlr/SysYParser.java
这些属于 ANTLR 生成物,不是手工维护的功能核心。
5.3 当前工作树里出现但不是本次功能重点的文件
以下文件在当前仓库状态中也可能显示有 diff:
include/ir/IR_org.hinclude/ir/utils.hsrc/antlr4/SysY.g4scripts/lab1_build_test.shthird_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.sy09_func_defn.sy11_add2.sy13_sub2.sy15_graph_coloring.sy22_matrix_multiply.sy25_scope3.sy29_break.sy36_op_priority2.sy95_float.sysimple_add.sy
performance
01_mm2.sy02_mv3.sy03_sort1.sy2025-MYO-20.syfft0.sygameoflife-oscillator.syif-combine3.sylarge_loop_array_2.sytranspose0.syvector_mul3.sy
最终批量结果:
21 PASS / 0 FAIL / total 21
7. 如何验证
7.1 编译
cmake -S . -B build_lab2
cmake --build build_lab2 -j2
7.2 单个用例验证
./scripts/verify_ir.sh test/test_case/functional/simple_add.sy output/check/simple_add --run
这个命令会自动做:
compiler --emit-irllc -opaque-pointersclang + sylib- 运行程序
- 对比
.out与退出码
7.3 批量验证
./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. 建议你和队友核验时的顺序
建议按下面顺序看,会最快:
- 先看
include/irgen/IRGen.h了解 IRGen 的总辅助结构和接口 - 再看
IRGenFunc.cpp了解顶层预注册、函数生成、参数绑定 - 再看
IRGenDecl.cpp了解变量 / const / 数组 / 初始化逻辑 - 再看
IRGenExp.cpp了解表达式、左值、数组地址和调用 - 最后看
IRGenStmt.cpp了解 if/while/break/continue 的控制流生成 - 核验时直接跑
./scripts/lab2_build_test.sh --save-ir
这样会比一开始就读 IR 层全部类定义更容易抓住主线。
10. 当前结论
当前版本已经满足本轮 Lab2 的核心目标:
- SysY IR 已定义并实现到可执行验证程度
- visitor 一遍生成 IR 已完成
- 关键语言子集与测试集均已通过
- 实现思路与“边查符号表边生成 IR、一遍完成”的要求一致
如果后续你们还要继续扩展:
- 可以在此基础上继续补更严的语义检查
- 可以继续推进 SSA / phi
- 可以继续做 MIR / 汇编后端
- 但对当前 Lab2 来说,IR 生成主线已经可以作为稳定基线继续迭代