课题21:IR验证器(7条规则)
难度:高 | 类型:项目实战 | 源文件:
scratchv/analysis/ir_verifier.py| 行数:~500 状态:✅ 已完成
概述
实现一个IR验证器,在优化前后检查IR的合法性(变量定义使用、跳转标签存在、类型一致、控制流完整性)。
理解背景
是什么?
IR 验证器检查 ScratchV IR Program 是否符合正确性规则。它应该在每次优化 pass 前后运行——确保优化没有破坏 IR。
验证 7 条规则: 1. def-before-use: 所有操作数必须在使用前定义 2. label-existence: 跳转目标标签必须存在 3. block-termination: 每个基本块必须以终止指令结束 4. type-consistency: 运算操作数类型必须兼容 5. control-flow-integrity: 不可达代码检测 6. ssa-validity: 每个变量只赋值一次 7. entry-existence: 函数必须有至少一个基本块
为什么?
优化 pass 是 IR→IR 的变换,很容易引入错误: - 死代码消除可能误删还在使用的变量 - 常量折叠可能改变类型 - 循环不变量外提可能破坏 SSA
验证器是安全网——在 IR 进入后端(产生不可逆的机器码)之前,拦截所有结构性问题。
核心概念
错误级别
| 级别 | 含义 |
|---|---|
| ERROR | 确定会导致编译失败或运行时错误 |
| WARNING | 可能有问题但不一定(如类型不匹配) |
VerificationError 结构
@dataclass
class VerificationError:
level: ErrorLevel # ERROR 或 WARNING
message: str # 人类可读描述
function_name: str | None # 所在函数
block_name: str | None # 所在基本块
instruction_index: int | None # 指令索引
value_name: str | None # 有问题的值
rule: str | None # 规则标识符
详细任务
- 设计IR合法性规则:
- 变量必须先定义再使用。
- 跳转标签必须存在对应
LABEL。
- 基本块必须以跳转(
BR、JMP)或RET结尾。
- 二元运算的操作数类型一致。
- 实现遍历
Program、Function、BasicBlock的验证函数。
- 检查控制流:无条件跳转后不能有后继指令,有条件跳转后恰好有两个分支。
- 集成到编译器主流程,在每次优化Pass前后自动调用验证器,出错时输出详细报告并停止编译。
- 添加
--verify-ir命令行开关。
交付产物
ir_verifier.py模块
- 测试用例(合法和非法IR)
- 文档:验证规则列表、如何扩展
代码走读
在编译管线中使用
from scratchv.analysis.ir_verifier import IRVerifier
# 优化前验证
pre_errors = IRVerifier(program).verify()
assert not pre_errors, f"IR invalid: {pre_errors}"
# 运行优化
run_optimizations(program)
# 优化后验证
post_errors = IRVerifier(program).verify()
assert not post_errors, f"Optimization broke IR: {post_errors}"
快速验证
from scratchv.analysis.ir_verifier import verify_ir
passed, errors = verify_ir(program)
if not passed:
for err in errors:
print(f"[{err.level.name}] {err.message}")
def-before-use 检查
def _check_def_before_use(self, func):
defined = set() # 已定义的变量
for block in func.blocks:
for i, instr in enumerate(block.instructions):
for op in instr.operands:
if op.name not in defined and not op.constant:
self._add_error(ErrorLevel.ERROR,
f"value '{op.name}' used before definition",
func_name=func.name, block_name=block.name,
instruction_index=i, value_name=op.name,
rule="def-before-use")
if instr.dest:
defined.add(instr.dest.name)
SSA 检查
def _check_ssa(self, func):
assigned = set()
for block in func.blocks:
for instr in block.instructions:
if instr.dest:
if instr.dest.name in assigned:
self._add_error(ErrorLevel.ERROR,
f"value '{instr.dest.name}' assigned multiple times",
rule="ssa-validity")
assigned.add(instr.dest.name)
block-termination 检查
@dataclass
class VerificationError:
level: ErrorLevel # ERROR 或 WARNING
message: str # 人类可读描述
function_name: str | None # 所在函数
block_name: str | None # 所在基本块
instruction_index: int | None # 指令索引
value_name: str | None # 有问题的值
rule: str | None # 规则标识符
- 设计IR合法性规则:
- 变量必须先定义再使用。
- 跳转标签必须存在对应
LABEL。 - 基本块必须以跳转(
BR、JMP)或RET结尾。 - 二元运算的操作数类型一致。
- 实现遍历
Program、Function、BasicBlock的验证函数。 - 检查控制流:无条件跳转后不能有后继指令,有条件跳转后恰好有两个分支。
- 集成到编译器主流程,在每次优化Pass前后自动调用验证器,出错时输出详细报告并停止编译。
- 添加
--verify-ir命令行开关。
交付产物
ir_verifier.py模块
- 测试用例(合法和非法IR)
- 文档:验证规则列表、如何扩展
代码走读
在编译管线中使用
from scratchv.analysis.ir_verifier import IRVerifier
# 优化前验证
pre_errors = IRVerifier(program).verify()
assert not pre_errors, f"IR invalid: {pre_errors}"
# 运行优化
run_optimizations(program)
# 优化后验证
post_errors = IRVerifier(program).verify()
assert not post_errors, f"Optimization broke IR: {post_errors}"
快速验证
from scratchv.analysis.ir_verifier import verify_ir
passed, errors = verify_ir(program)
if not passed:
for err in errors:
print(f"[{err.level.name}] {err.message}")
def-before-use 检查
def _check_def_before_use(self, func):
defined = set() # 已定义的变量
for block in func.blocks:
for i, instr in enumerate(block.instructions):
for op in instr.operands:
if op.name not in defined and not op.constant:
self._add_error(ErrorLevel.ERROR,
f"value '{op.name}' used before definition",
func_name=func.name, block_name=block.name,
instruction_index=i, value_name=op.name,
rule="def-before-use")
if instr.dest:
defined.add(instr.dest.name)
SSA 检查
def _check_ssa(self, func):
assigned = set()
for block in func.blocks:
for instr in block.instructions:
if instr.dest:
if instr.dest.name in assigned:
self._add_error(ErrorLevel.ERROR,
f"value '{instr.dest.name}' assigned multiple times",
rule="ssa-validity")
assigned.add(instr.dest.name)
block-termination 检查
ir_verifier.py模块在编译管线中使用
from scratchv.analysis.ir_verifier import IRVerifier
# 优化前验证
pre_errors = IRVerifier(program).verify()
assert not pre_errors, f"IR invalid: {pre_errors}"
# 运行优化
run_optimizations(program)
# 优化后验证
post_errors = IRVerifier(program).verify()
assert not post_errors, f"Optimization broke IR: {post_errors}"
快速验证
from scratchv.analysis.ir_verifier import verify_ir
passed, errors = verify_ir(program)
if not passed:
for err in errors:
print(f"[{err.level.name}] {err.message}")
def-before-use 检查
def _check_def_before_use(self, func):
defined = set() # 已定义的变量
for block in func.blocks:
for i, instr in enumerate(block.instructions):
for op in instr.operands:
if op.name not in defined and not op.constant:
self._add_error(ErrorLevel.ERROR,
f"value '{op.name}' used before definition",
func_name=func.name, block_name=block.name,
instruction_index=i, value_name=op.name,
rule="def-before-use")
if instr.dest:
defined.add(instr.dest.name)
SSA 检查
def _check_ssa(self, func):
assigned = set()
for block in func.blocks:
for instr in block.instructions:
if instr.dest:
if instr.dest.name in assigned:
self._add_error(ErrorLevel.ERROR,
f"value '{instr.dest.name}' assigned multiple times",
rule="ssa-validity")
assigned.add(instr.dest.name)
block-termination 检查
from scratchv.analysis.ir_verifier import IRVerifier
# 优化前验证
pre_errors = IRVerifier(program).verify()
assert not pre_errors, f"IR invalid: {pre_errors}"
# 运行优化
run_optimizations(program)
# 优化后验证
post_errors = IRVerifier(program).verify()
assert not post_errors, f"Optimization broke IR: {post_errors}"
from scratchv.analysis.ir_verifier import verify_ir
passed, errors = verify_ir(program)
if not passed:
for err in errors:
print(f"[{err.level.name}] {err.message}")
def-before-use 检查
def _check_def_before_use(self, func):
defined = set() # 已定义的变量
for block in func.blocks:
for i, instr in enumerate(block.instructions):
for op in instr.operands:
if op.name not in defined and not op.constant:
self._add_error(ErrorLevel.ERROR,
f"value '{op.name}' used before definition",
func_name=func.name, block_name=block.name,
instruction_index=i, value_name=op.name,
rule="def-before-use")
if instr.dest:
defined.add(instr.dest.name)
SSA 检查
def _check_ssa(self, func):
assigned = set()
for block in func.blocks:
for instr in block.instructions:
if instr.dest:
if instr.dest.name in assigned:
self._add_error(ErrorLevel.ERROR,
f"value '{instr.dest.name}' assigned multiple times",
rule="ssa-validity")
assigned.add(instr.dest.name)
block-termination 检查
def _check_def_before_use(self, func):
defined = set() # 已定义的变量
for block in func.blocks:
for i, instr in enumerate(block.instructions):
for op in instr.operands:
if op.name not in defined and not op.constant:
self._add_error(ErrorLevel.ERROR,
f"value '{op.name}' used before definition",
func_name=func.name, block_name=block.name,
instruction_index=i, value_name=op.name,
rule="def-before-use")
if instr.dest:
defined.add(instr.dest.name)
def _check_ssa(self, func):
assigned = set()
for block in func.blocks:
for instr in block.instructions:
if instr.dest:
if instr.dest.name in assigned:
self._add_error(ErrorLevel.ERROR,
f"value '{instr.dest.name}' assigned multiple times",
rule="ssa-validity")
assigned.add(instr.dest.name)
block-termination 检查
每个基本块的最后一条指令必须是终结指令(RETURN, BR, BR_IF 之一),否则控制流会"掉进"下一个块——虽然是合法的,但通常是 bug。
动手练习
练习 1: 故意破坏 IR
手动构造一个有 def-before-use 错误的 IR,用验证器检测。
练习 2: 写一个优化 pass 并验证
写一个简单的常量折叠 pass,在优化前后各调用一次验证器。
练习 3: 添加新规则
添加一条验证规则:检查所有 CONV 指令的 kernel_shape 属性是否存在且合法。
常见坑
| 坑 | 说明 |
|---|---|
| 常量豁免 | 常量(value.constant == True)不需要 def-before-use 检查——它们天然"已定义" |
| 副作用的终结指令 | RETURN/BR/BR_IF 后的指令是不可达的,但不是所有 pass 都会清理 |
| 验证器性能 | 大型 IR(如 CNN 展开后的全循环 IR)验证可能较慢,开发时可选择性跳过 |
进阶阅读
- 龙书第 8.5 节:Peephole Optimization(优化正确性验证)
- LLVM Verifier:
llvm::verifyFunction() — 业界参考实现
- 相关 topic: 课题3 — IR 系统 | 课题4 — IR 优化器框架 | 课题11 — 控制流图生成器
12周每周目标
- W1:学习项目IR的数据结构(
ir.py中的Instruction, BasicBlock, Function等)。
- W2:设计验证规则列表,按类别(变量、标签、类型、控制流)组织。
- W3:实现变量定义-使用检查:遍历每个基本块,维护变量定义集合,检测未定义使用。
- W4:实现标签存在性检查:收集所有
LABEL指令的目标,检查跳转指令的目标是否存在。
- W5:实现基本块结尾检查:确保每个块以跳转或返回结尾,否则报错。
- W6:实现类型一致性检查:二元运算的两个操作数类型相同,比较操作结果类型为整数等。
- W7:实现控制流完整性检查:无条件跳转后不能有后继指令,有条件跳转后有两个后继块。
- W8:将验证器封装为函数
verify_ir(program),返回错误列表。
- W9:集成到编译器主流程,在解析后、每个优化Pass后、代码生成前调用验证器。
- W10:添加
--verify-ir命令行开关,默认开启(或仅在DEBUG模式开启)。
- W11:编写测试用例:构造非法IR(如缺失标签、类型不匹配),验证验证器能捕获。
- W12:撰写文档,包含所有验证规则及示例。
llvm::verifyFunction() — 业界参考实现- W1:学习项目IR的数据结构(
ir.py中的Instruction,BasicBlock,Function等)。 - W2:设计验证规则列表,按类别(变量、标签、类型、控制流)组织。
- W3:实现变量定义-使用检查:遍历每个基本块,维护变量定义集合,检测未定义使用。
- W4:实现标签存在性检查:收集所有
LABEL指令的目标,检查跳转指令的目标是否存在。 - W5:实现基本块结尾检查:确保每个块以跳转或返回结尾,否则报错。
- W6:实现类型一致性检查:二元运算的两个操作数类型相同,比较操作结果类型为整数等。
- W7:实现控制流完整性检查:无条件跳转后不能有后继指令,有条件跳转后有两个后继块。
- W8:将验证器封装为函数
verify_ir(program),返回错误列表。 - W9:集成到编译器主流程,在解析后、每个优化Pass后、代码生成前调用验证器。
- W10:添加
--verify-ir命令行开关,默认开启(或仅在DEBUG模式开启)。 - W11:编写测试用例:构造非法IR(如缺失标签、类型不匹配),验证验证器能捕获。
- W12:撰写文档,包含所有验证规则及示例。