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

25 KiB

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 基类与命名、类型访问逻辑
  • 保证 GlobalValueInstructionArgument 等可统一当作 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
  • 支持 GetElementPtrInstMemsetInstZextInst

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 打印
  • 支持 getelementptralloca/load/store、比较、调用、分支、转换等打印
  • 在需要时输出:
    • declare void @llvm.memset.p0.i32(ptr, i8, i32, i1)
  • 浮点常量按 LLVM 能接受的形式输出

关键修复:

  • llcptr 和浮点常量格式比较敏感
  • 当前打印格式已经和 llc -opaque-pointers 验证链路对齐

4.2 符号表 / 语义层修改

4.2.1 include/sem/SymbolTable.hsrc/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.hsrc/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_scalarconst_array

D. 局部变量 / 局部常量生成

原则

  • 局部变量统一存地址
  • 局部标量:alloca + (可选初始化) + store
  • 局部数组:
    • 先整体清零
    • 再按初始化列表逐元素写入

关键修复:入口块 alloca

新增 CreateEntryAlloca(),它会:

  • 找到当前函数入口块
  • 跳过入口块里已有的 alloca
  • 把新的 alloca 插到入口块最前面的 alloca 区域

修复了下面这个真实 bug

  • 如果 whileif 内部定义局部变量
  • 旧做法会把 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 链接 frontendsem

原因:

  • IRGen 需要 parser 访问能力
  • 也需要共享的符号表与语义层声明

4.4.4 sylib/sylib.hsylib/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
  • 批量遍历 functionalperformance 目录中的 .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 编译

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

这个命令会自动做:

  1. compiler --emit-ir
  2. llc -opaque-pointers
  3. clang + sylib
  4. 运行程序
  5. 对比 .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.shlab2_build_test.sh 都有一致、稳定的目标编译器可用

8.4 它只是构建产物目录,不是源码目录

  • 它可以随时删除再重新生成
  • 不影响源代码逻辑
  • 本质上和常见的 build-debugbuild-releasebuild-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 生成主线已经可以作为稳定基线继续迭代