Rust 编译器 MIR Visitor 指南:用 Visitor 与 MutVisitor 遍历、分析并改写 MIR
发布时间:2026/9/12 3:56:08 锦皓数字建站

Rust 编译器 MIR Visitor 指南用 Visitor 与 MutVisitor 遍历、分析并改写 MIR【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust导读MIRMid-level Intermediate Representation中间表示是 Rust 编译器在类型检查之后、代码生成之前使用的核心程序表示而 MIR visitor 则是围绕它提供的一套标准遍历机制你可以用它扫描Body中的任意元素语句、终结符、局部变量、操作数……来做分析也可以直接原地改写 MIR。本文将结合当前仓库compiler/rustc_middle/src/mir/visit.rs与compiler/rustc_mir_transform/src/prettify.rs中的真实实现完整讲解Visitor/MutVisitor两个 trait 的用法、visit_foo与super_foo的协作约定、宏生成原理以及compiler/rustc_middle/src/mir/traversal.rs提供的 CFG 遍历工具读完后你将能够独立实现一个自定义 MIR 优化或分析 pass。MIR visitor 是什么MIR visitor 是一个用于遍历 MIR 的便捷工具它既能让你在遍历过程中查找感兴趣的东西分析也能让你修改MIR改写。其核心定义位于compiler/rustc_middle/src/mir/visit.rs的rustc_middle::mir::visit模块中。该模块包含两个visitor trait它们由同一个宏make_mir_visitor!生成见该文件第 68 行的宏定义与第 1276~1277 行的两次调用Visitor以Bodytcx共享引用为输入回调时给出共享引用适用于只读分析MutVisitor以mut Bodytcx可变引用为输入回调时给出可变引用允许原地改写 MIR。由于两者来自同一份宏模板方法签名与遍历逻辑完全一致唯一的差别是引用的可变性。源码模块注释compiler/rustc_middle/src/mir/visit.rs顶部明确了三条约定对每一个 MIR 类型都引入一对visit_foo与super_foo方法visit_foo默认调用super_foosuper_foo默认解构foo并逐个调用相应的visit_foo。这套约定与面向对象语言中子类重写visit方法、再调用super获得默认行为的模式一致这也是该文件顶部注释中 Just as in an OO language 的含义所在。如何实现一个自定义 visitor实现 visitor 分两步。第一步创建一个类型用来表示你的 visitor并让它持有遍历 MIR 时所需的状态例如TyCtxtstruct MyVisitor... { tcx: TyCtxttcx, ... }第二步为该类型实现Visitor或MutVisitortrait。以下面的改写型 visitor 为例impltcx MutVisitortcx for MyVisitor { fn visit_foo(mut self, ...) { ... self.super_foo(...); } }其中...处写入你希望在遇到foo元素时执行的逻辑如果还需要继续递归遍历foo的内部结构就调用对应的super_foo方法。一个关键规则是永远不要重写super_foo——它承担的是默认的深度遍历职责重写它会破坏遍历完整性。需要自定义行为时改写对应的visit_foo然后在方法内调用self.super_foo(...)来回到默认行为。实战示例LocalFinder 如何找出可重排序的局部变量原文档给出的入门示例是 [LocalFinder]它通过只实现visit_local这一个方法找出可作为重排候选的局部变量。在当前仓库中LocalFinder位于compiler/rustc_mir_transform/src/prettify.rs第 115 行起服务于ReorderLocalspass——该 pass 会把局部变量重排成使用顺序即首次被赋值或引用的Location越小的局部变量编号越小但不重排函数参数与返回占位RETURN_PLACE。LocalFinder的结构体与状态struct LocalFinder { map: IndexVecLocal, Local, // 记录遇到过的 Local按首次出现顺序排列 seen: DenseBitSetLocal, // 去重避免同一 Local 被记录多次 } impl LocalFinder { fn track(mut self, l: Local) { if self.seen.insert(l) { self.map.push(l); } } }它的Visitor实现非常精简只覆盖了一个方法impltcx Visitortcx for LocalFinder { fn visit_local(mut self, l: Local, context: PlaceContext, _location: Location) { // 排除非使用场景避免 StorageLive 影响 Local 的排序位置 // 因为 StorageLive 出现的位置并不代表该 Local 真正被赋值的时间。 if context.is_use() { self.track(l); } } }这个例子体现了 visitor 的两个关键点按需覆盖不需要重写所有方法只覆盖visit_local就能拦截遍历中遇到的每一个Local上下文参数PlaceContext告诉你在当前使用场景下该Local是被读、被写还是被用于存储标记等使你能像上面那样过滤掉非使用non-use场景。在ReorderLocals::run_pass中visitor 是这样被驱动的compiler/rustc_mir_transform/src/prettify.rs第 54~73 行先用visit_basic_block_data对每个基本块做一次遍历收集顺序再补上遍历中未出现的局部变量例如只出现在调试信息里的最后把map反转为LocalUpdater使用的置换映射。LocalFinder是一个只读Visitor的典型范例它只用Body收集信息不修改任何东西。只读与可变两个 trait 的协作模式MutVisitor型 visitor 的经典例子是同一文件中的LocalUpdatercompiler/rustc_mir_transform/src/prettify.rs第 138~151 行impltcx MutVisitortcx for LocalUpdatertcx { fn tcx(self) - TyCtxttcx { self.tcx } fn visit_local(mut self, l: mut Local, _: PlaceContext, _: Location) { *l self.map[*l]; } }它与LocalFinder形成了鲜明的对照方面LocalFinder只读LocalUpdater改写实现的 traitVisitorMutVisitor输入Bodytcxmut Bodytcxvisit_local参数l: Local值传递l: mut Local可变引用行为收集、记录信息就地改写局部变量编号MutVisitor的 trait 要求实现fn tcx(self) - TyCtxttcx方法源码中LocalUpdater通过字段持有tcx并返回之。这种先只读分析、再可变改写的两阶段模式在 MIR 优化中非常常见LocalFinder先计算出置换映射LocalUpdater再把它应用到整个 body。源码级深入make_mir_visitor 宏与穷尽匹配的设计哲学compiler/rustc_middle/src/mir/visit.rs共有约 1500 行绝大部分代码由make_mir_visitor!宏模板第 68 行起一次定义、两次实例化避免了Visitor与MutVisitor两套代码的重复维护。宏通过$($mutability)?参数把是否有mut注入到所有签名中macro_rules! make_mir_visitor { ($visitor_trait_name:ident, $($mutability:ident)?) { ... } } make_mir_visitor!(Visitor,); // 无 mut只读 make_mir_visitor!(MutVisitor, mut); // 带 mut可变宏模板中还嵌套了extra_body_methods!第 1035 行起为MutVisitor额外生成visit_body_preserves_cfg等方法。从源码结构看这些方法专门服务于不改动 CFG 结构的改写型 pass——例如ReorderLocals就调用updater.visit_body_preserves_cfg(body)来重写整个 body 中的Local编号同时保证基本块之间的跳转结构不被破坏。另一个值得关注的设计细节是super_basic_block_data的实现compiler/rustc_middle/src/mir/visit.rs第 296~322 行fn super_basic_block_data( mut self, block: BasicBlock, data: $($mutability)? BasicBlockDatatcx ) { let BasicBlockData { statements, after_last_stmt_debuginfos, terminator, is_cleanup: _ } data; // 依次以递增的 statement_index 构造 Location访问每条语句…… // 之后访问 after_last_stmt_debuginfos 与 terminator }这里刻意使用let BasicBlockData { ... } data的穷尽模式匹配来解构字段而不是在方法体里写data.statements。这样做的目的源码注释明确说明是一旦有人给BasicBlockData增加了新字段编译器就会强制要求修订这段代码从而但愿调用上正确的新增 visit 方法。因此该文件的维护约定非常严格——所有 match 必须在字段和变体上穷尽既不能用..跳过字段也不能用_跳过变体只有确实无需访问的字段如is_cleanup才允许用_匹配。同时遍历中为每条语句构造的Location { block, statement_index }也是 MIR 中定位语句的唯一坐标visit_statement/visit_terminator都会携带它。常用 visit 方法速览Visitor/MutVisitortrait 中提供了覆盖 MIR 各层结构的方法核心的包括visit_body/visit_basic_block_data遍历整个函数体及其基本块visit_statement/visit_terminator遍历语句与终结符是绝大多数 pass 的切入点visit_assign/visit_rvalue/visit_operand深入赋值语句、右值与操作数visit_place/visit_local访问 place存储位置与局部变量其中visit_place额外携带PlaceContext说明使用方式visit_const_operand/visit_ty/visit_span等访问常量、类型与源码位置span。从super_terminator的实现第 516 行起可以看到终结符各变体的默认遍历行为Return会以NonMutatingUseContext::Move访问返回占位RETURN_PLACE且MutVisitor下断言它不可被改写SwitchInt访问判别值操作数Call/TailCall依次访问函数操作数、实参操作数并标记目的 placeDrop以MutatingUseContext::Drop访问被 drop 的 place。需要针对特定终结符做分析或改写时重写visit_terminator并匹配TerminatorKind即可。MIR CFG 的遍历工具traversal 模块除了逐元素遍历的 visitorcompiler/rustc_middle/src/mir/traversal.rs提供了按控制流图CFG顺序遍历基本块的函数适合需要全局控制流视角的 passpreorder前序遍历。每个节点在其某个前驱被访问之后才被访问等价于对 CFG 做深度优先搜索并按发现时间返回节点从START_BLOCK出发利用DenseBitSet去重、Vec作为工作栈第 22~76 行。postorder后序遍历。每个节点在其所有后继回边可达的后继除外被访问之后才被访问等价于按完成时间返回实现使用visit_stack栈维护(基本块, 后继迭代器)并借助traverse_successor不断下探第 97~211 行。reverse_postorder后序遍历的反转由postorder(body).rev()直接得到第 221~226 行这是许多数据流分析偏好的顺序。mono_reachable_reverse_postorder单态化版本的后序反转遍历需要TyCtxt与Instance以便精确计算终结符的后继第 228~240 行适用于需要区分调用目标的具体实例monomorphized场景。文档中的示例图说明了两种顺序的差异对一个从A分叉到B、C再汇合到D的图前序遍历结果为A B D C或A C D B后序遍历结果为D B C A或D C B A。选择哪种顺序取决于你的分析对依赖方向的要求例如反向数据流分析通常需要前序正向数据流分析则常用后序或反后序。何时用 Visitor何时用 traversal两者服务于不同层级的需求Visitor面向 MIR 的树状/嵌套结构无论元素位于哪个基本块只要在遍历路径上就会被回调适合找出所有Local改写所有Operand这类与位置无关的操作traversal面向 MIR 的图状结构CFG它只按基本块为单位给出顺序适合数据流分析、块级顺序敏感的处理。实际优化 pass 常将两者结合用traversal决定基本块的访问顺序用Visitor或直接按块索引处理块内细节而像LocalFinder这样的场景则用visit_basic_block_data一次性遍历整个 body 并在回调中按需筛选。理解这两套工具的分工是编写高质量 MIR pass如rustc_mir_transform中的各类优化、以及编译器自身的各种分析的基础。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。