的构造、存储与调试)
Rustc 的 THIR 详解类型化高层中间表示Typed High-Level IR的构造、存储与调试【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust导读THIRTyped High-Level Intermediate Representation类型化高层中间表示是 rustc 编译器在类型检查之后生成的又一中间表示它是 MIR 构建、模式穷尽性检查与不安全性检查的共同基石。本文以 rustc 源码仓库中 THIR 章节文档 为核心结合 THIR 数据类型定义 与 THIR 构造实现 等源码系统讲解 THIR 与 HIR/MIR 的关系、独有的数据结构设计、临时存储与生命周期管理并通过-Zunprettythir-flat的真实输出逐字段拆解一个完整示例。读完本文你将能够读懂任意fn体对应的 THIR 输出理解ExprId/StmtId/BlockId等索引体系并掌握在 rustc 内部使用与调试 THIR 的方法。THIR 是什么类型检查之后的下一个 IRTHIR 的全称是Typed High-Level Intermediate Representation类型化高层中间表示旧称 HAIRHigh-Level Abstract IR。它由 rustc 在类型检查完成后生成是介于 HIR 与 MIR 之间的一层表示。截至仓库当前文档标注2024 年 1 月THIR 主要被用于三处MIR 构建compiler/rustc_mir_build/src/builder/mod.rs第 77 行通过tcx.thir_body(def)取得 THIR 后向下构建 MIR模式穷尽性检查check_match.rs 同样基于tcx.thir_body不安全性检查check_unsafety.rs。正如其名THIR 是 HIR 的降级lowered版本——HIR 中各个节点在类型检查完成后被逐一填入具体类型从而形成 THIR。可以这样理解 rustc 的 IR 流水线源码 AST → HIR语法树无类型 → THIR带类型的高层 IR → MIR显式控制流与内存操作 → LLVM IRTHIR 站在类型已经确定、但控制流与借用细节尚未展开的中间地带因此比 HIR 更实又比 MIR 更高。THIR 与 HIR 的四个关键区别文档明确列出了 THIR 区别于 HIR 的四个特征它们共同决定了 THIR 在编译器中的地位1. 只表示可执行代码bodies与 MIR 类似THIR 只表示 body函数体、const初始化器、static初始化器等即所有 body owners 都会生成 THIR。因此 THIR 中没有struct、trait这类条目item的表示——它们不是可执行代码。从 thir.rs 的BodyTy枚举可以看出 THIR 只关心三种 body 形态pub enum BodyTytcx { Const(Tytcx), Fn(FnSigtcx), GlobalAsm(Tytcx), }2. 临时存储、按需生成、用完即弃每个 body 的 THIR 只被临时保存不再需要时立即释放而不是像 HIR 那样驻留到编译结束。这一设计的原因文档说得很直白如果把一个 crate 中所有 body 的 THIR 同时放在内存里会非常沉重would be very heavy。下文会看到它通过内存 arena 与Steal机制实现。3. 显式化更多信息额外 desugaringTHIR 除了给所有节点填上类型之外还比 HIR 多做了一层脱糖desugaring自动引用autoref与自动解引用autoderef被显式化方法调用method calls与重载运算符overloaded operators被转换为普通函数调用plain function calls析构作用域destruction scopes被显式标记。这一点在 thir.rs 的模块级注释 中得到印证ExprKind相比hir::ExprKind简单不少方法调用和重载运算符在 HIR 中还存在但在 THIR 中已被预期转换为ExprKind::Call。这意味着语法糖的消除有一部分被提前到了 THIR 阶段而不是全部推迟到 MIR 构建。4. 语句、表达式、match 臂、块、参数分开存储THIR 将 statements、expressions、match arms、blocks、parameters 分别存放在不同的数组中互相之间通过索引如ExprId引用。例如stmts数组中的语句通过ExprId引用exprs数组中的表达式。这种扁平数组 索引互引的布局是IndexVec的典型用法它让内存紧凑、便于遍历也天然支持后续的增量分析与复制。THIR 的数据结构Thir 容器与五类元素THIR 的类型定义位于 compiler/rustc_middle/src/thir.rs。其核心是一个Thir容器结构体通过一个thir_with_elements!宏第 44-100 行批量生成五个 newtype 索引与对应的IndexVec存储thir_with_elements! { arms: ArmId Armtcx a{}, blocks: BlockId Block b{}, exprs: ExprId Exprtcx e{}, stmts: StmtId Stmttcx s{}, params: ParamId Paramtcx p{}, }展开后Thirtcx大致形如pub struct Thirtcx { pub body_type: BodyTytcx, pub attributes: FxIndexMapExprId, ThinVecAttributeKind, pub arms: IndexVecArmId, Armtcx, // a{} pub blocks: IndexVecBlockId, Block, // b{} pub exprs: IndexVecExprId, Exprtcx, // e{} pub stmts: IndexVecStmtId, Stmttcx, // s{} pub params: IndexVecParamId, Paramtcx,// p{} }宏还为每个索引实现了Index因此Thir可以直接用ExprId、StmtId等下标索引第 83-90 行。Debug 格式化时e0、b0、s0这样的前缀宏中的e{}、b{}、s{}参数正是我们在-Zunprettythir-flat输出中看到的编号形式。几个关键数据结构Block第 124-140 行包含targeted_by_break块自身是否带标签、region_scope、span、stmtsBox[StmtId]、尾部表达式expr: OptionExprId以及safety_mode: BlockSafety。BlockSafety枚举第 192-199 行区分Safe、编译器生成的BuiltinUnsafe与用户显式写的ExplicitUnsafe(HirId)——这为不安全性检查提供了直接依据。Param第 110-122 行描述一个经过类型检查的函数参数包括参数模式pat、推断出的类型ty、显式类型标注的 span、是否为selfself_kind以及用于 lint 的hir_id。ExprKindtcx第 271 行起THIR 表达式的核心枚举例如Scope显式标记析构作用域并记录HirId与region_scopeIf表示if表达式Call表示函数调用方法调用与重载运算符在 THIR 阶段已被降级为它。THIR 如何被构造thir_body 查询与 ThirBuildCxTHIR 的构造入口是thir_body函数。它是一个查询query实现在 compiler/rustc_mir_build/src/thir/cx/mod.rs 第 16-57 行pub(crate) fn thir_bodytcx( tcx: TyCtxttcx, owner_def: LocalDefId, ) - Result(tcx StealThirtcx, ExprId), ErrorGuaranteed { let body tcx.hir_body_owned_by(owner_def); let mut cx: ThirBuildCxtcx ThirBuildCx::new(tcx, owner_def); if let Some(reported) cx.typeck_results.tainted_by_errors { return Err(reported); } // 先降级参数使参数相关错误最先暴露 ... let expr cx.mirror_expr(body.value); Ok((tcx.alloc_steal_thir(cx.thir), expr)) }它返回一个二元组(tcx StealThirtcx, ExprId)。其中第一个元素是包在Steal里的Thir——Steal是 rustc 数据结构的常用封装允许该查询结果被偷走steal一次从而避免复制整个 THIR配合临时存储策略降低峰值内存第二个元素是整个 body 根表达式的ExprId即入口表达式消费者从它出发沿索引遍历整棵表达式森林。thir_body查询在 compiler/rustc_mir_build/src/lib.rs 第 24-26 行 被注册到查询提供者providers上providers.queries.thir_body thir::cx::thir_body;构造过程由ThirBuildCxtcx第 60-114 行驱动。它的核心职责包括通过tcx.typeck(def)取回类型检查结果TypeckResultsTHIR 中的类型正是从这里填充而来根据 body owner 的种类函数/闭包、const/static、全局汇编确定BodyTy函数使用liberated_fn_sigs取得完全替换了绑定生命周期后的签名const/static使用node_type取得其揭示类型而非 body 的调整后类型两者在子类型关系下可能不同注释中给出了static X: fn(static ()) foo;的反例通过mirror_expr将 HIR 表达式镜像成 THIR 表达式逐步填充cx.thir处理闭包环境参数closure_env_param、显式参数explicit_params以及协程coroutine缺失的 resume 参数。构造完成后内存方面文档给出的建议是把 THIR 分配在内存 arena 中丢弃drop这个 arena 即销毁 THIR。因为thir_body返回的是StealThir消费方如 MIR 构建可以 steal 后获得所有权随后随作用域结束而释放从而控制峰值内存。调试 THIR-Zunpretty 与 thir-flatrustc 提供了专门的调试入口给rustc传入-Zunprettythir-flat即可得到整个 body 的扁平化 THIR 调试输出。该选项定义于 compiler/rustc_session/src/options.rs 第 2954-2957 行与它并列的还有hir、hir,typed带类型的 HIR、hir-tree原始 HIR 树、thir-tree、mirMIR、mir-cfgGraphviz 格式的 MIR等。也就是说除了thir-flat你还可以用-Zunprettythir-tree查看树状结构的 THIR。thir-flat的实现位于 compiler/rustc_mir_build/src/thir/print.rs 第 22-26 行本质上是调用thir_body查询并对StealThir做{:#?}美化调试打印pub fn thir_flat(tcx: TyCtxt_, owner_def: LocalDefId) - String { match super::cx::thir_body(tcx, owner_def) { Ok((thir, _)) format!({:#?}, thir.steal()), Err(_) error.into(), } }对以下示例程序fn main() { let x 1 2; }在当前仓库对应版本文档标注日期 2026 年 7 月下rustc -Zunprettythir-flat会输出如下 THIR 表示。这段输出是理解 THIR 数据结构的最佳教材下面按区块逐段解读。逐字段解读一个 main 函数的 THIR 全貌头部body 元信息DefId(0:3 ~ main[26fd]::main): Thir { body_type: Fn( fn(), ), attributes: {}, // no match arms arms: [], blocks: [ Block { targeted_by_break: false, region_scope: Node(1), span: main.rs:1:11: 3:2 (#0), stmts: [ s0, ], expr: None, safety_mode: Safe, }, ],body_type: Fn(fn())这是BodyTy::Fn签名是无参无返回的fn()attributes: {}该 body 上没有附加属性arms: []没有match所以 match 臂数组为空blocks数组第一个元素b0对应main的函数体块stmts引用s0尾部表达式为None因为main末尾没有表达式只有一个let语句safety_mode: Safe普通安全块。exprs 数组8 个表达式exprs是理解 THIR 的核心它把let x 1 2;拆成了 8 个带类型的表达式节点e0字面量 1Expr { kind: Literal { lit: Spanned { node: Int(Pu128(1), Unsuffixed), span: main.rs:2:13: 2:14 (#0), }, neg: false, }, ty: i32, temp_scope_id: 4, span: main.rs:2:13: 2:14 (#0), },字面量被表示为Literal内部Int(Pu128(1), Unsuffixed)表示未带后缀的整型字面量 1neg: false表示不是负号。注意ty: i32——类型检查后未加后缀的整数默认落到i32这就是 THIR 相比 HIR 的填类型体现。e1围绕字面量 1 的作用域ScopeExpr { kind: Scope { region_scope: Node(4), hir_id: HirId(DefId(0:3 ~ main[26fd]::main).4), // reference to expression 0 above value: e0, }, ty: i32, temp_scope_id: 4, ... },Scope是 THIR 显式化析构作用域的方式region_scope: Node(4)记录区域作用域hir_id保留对应 HIR 节点用于 lint 等场景value: e0通过ExprId引用真正的表达式。这正是表达式以索引互相引用的典型样例。e2 / e3字面量 2 及其作用域// expression 2, literal 2 Expr { kind: Literal { lit: Spanned { node: Int(Pu128(2), Unsuffixed), span: main.rs:2:17: 2:18 (#0) }, neg: false }, ty: i32, temp_scope_id: 5, span: main.rs:2:17: 2:18 (#0), }, // expression 3, scope surrounding literal 2 Expr { kind: Scope { region_scope: Node(5), hir_id: ..., value: e2 }, ... },与 e0/e1 完全对称span 落在2:17: 2:18即源码中2的位置。e4二元加法1 2Expr { kind: Binary { op: Add, // references to scopes surrounding literals above lhs: e1, rhs: e3, }, ty: i32, temp_scope_id: 3, span: main.rs:2:13: 2:18 (#0), },Binary { op: Add, lhs: e1, rhs: e3 }是整棵表达式的核心加法运算符的两个操作数直接以ExprId引用前面两个作用域表达式。可以注意到这里的op: Add仍是运算符形态——重载运算符如作用于自定义类型在 THIR 阶段才会被降级为Call而内建i32的加法保留为Binary。e5围绕加法的作用域Expr { kind: Scope { region_scope: Node(3), hir_id: ..., value: e4 }, ty: i32, temp_scope_id: 3, span: main.rs:2:13: 2:18 (#0), },let语句的初始化器最终引用的是这个e5见下方 stmts 区块中initializer: Some(e5)。e6 / e7块及其作用域// expression 6, block around statement Expr { kind: Block { block: b0 }, ty: (), temp_scope_id: 8, span: main.rs:1:11: 3:2 (#0), }, // expression 7, scope around block in expression 6 Expr { kind: Scope { region_scope: Node(8), hir_id: ..., value: e6 }, ty: (), ... },函数体块自身也是一个表达式类型为()通过kind: Block { block: b0 }反向引用blocks数组中的块外层再包一层Scopee7作为 body 的根表达式。注意temp_scope_id字段THIR 中每个表达式都带有一个临时作用域编号用于描述求值产生的临时值何时析构这是 MIR 构建阶段生成Drop与作用域指令的重要输入。stmts 数组let 语句stmts: [ Stmt { kind: Let { remainder_scope: Remainder { block: 1, first_statement_index: 0}, init_scope: Node(2), pattern: Pat { ty: i32, span: main.rs:2:9: 2:10 (#0), extra: None, kind: Binding { name: x, mode: BindingMode(No, Not), var: LocalVarId(HirId(DefId(0:3 ~ main[26fd]::main).7)), ty: i32, subpattern: None, is_primary: true, is_shorthand: false, }, }, initializer: Some(e5), else_block: None, hir_id: HirId(DefId(0:3 ~ main[26fd]::main).6), span: main.rs:2:5: 2:18 (#0), }, }, ], params: [], }kind: Letlet语句。remainder_scope描述该语句之后剩余代码的区域作用域block: 1, first_statement_index: 0即块b1中的第 0 条语句之后init_scope: Node(2)是初始化器求值的作用域pattern绑定模式Binding { name: x, ... }mode: BindingMode(No, Not)表示非ref非mut的普通值绑定var: LocalVarId(HirId(...).7)指向局部变量的 HIR idty: i32即变量x的类型initializer: Some(e5)初始化器指向exprs数组中的 e5即1 2加外层作用域——这就是语句与表达式通过索引互联的直接体现params: []main无参数。从这份输出可以清楚地看到 THIR 的设计哲学HIR 是一棵树THIR 是一片按类别分仓的索引化森林。所有的ExprId、StmtId、BlockId交叉引用构成了一个紧凑、可随机访问、便于遍历与分析的数据结构为 MIR 构建阶段奠定了干净的输入。谁在消费 THIR从文档到源码的印证文档提到 THIR 用于 MIR 构建、穷尽性检查与不安全性检查仓库源码可以逐一印证消费者源码位置用途MIR 构建compiler/rustc_mir_build/src/builder/mod.rs#L77tcx.thir_body(def)取 THIR 后逐表达式构建 MIR穷尽性/可达性检查compiler/rustc_mir_build/src/thir/pattern/check_match.rs#L33基于 THIR 的模式与 match 臂做穷尽性检查不安全性检查compiler/rustc_mir_build/src/check_unsafety.rs#L1053-L1061遍历 THIR 定位 unsafe 操作及其所在块的safety_mode尾调用检查compiler/rustc_mir_build/src/check_tail_calls.rs#L15需要 THIR 判断函数是否可能递归其中不安全性检查尤其能体现 THIR 的价值因为Block结构自带safety_modeSafe/BuiltinUnsafe/ExplicitUnsafe(HirId)检查器可以在不接触 MIR 的情况下直接根据表达式所在块的 safety 模式判断 unsafe 操作的合法性并报告精确的 span。结语与延伸阅读THIR 是 rustc 中小而专的一层 IR它只活到被消费为止临时存储 Steal只为 body 而生无 item 表示却在类型填充、脱糖autoref/autoderef、方法调用降级、析构作用域显式化与索引化布局上做了大量工作使得下游的 MIR 构建、穷尽性检查和不安全性检查都能获得一个类型完备、语义干净的输入。如果你想继续深入阅读 HIR 章节 了解 THIR 的上游输入以及 MIR 构建章节 了解 THIR 的下游消费直接阅读 THIR 数据类型定义 的ExprKind枚举它完整列出了 THIR 支持的表达式形态If、Call、Binary、Match、Loop、Closure、InlineAsm等阅读 THIR 构造实现 与 表达式镜像逻辑跟踪mirror_expr如何逐节点生成 THIR亲自动手写一个包含match、闭包、unsafe块的函数分别用rustc -Zunprettythir-flat与-Zunprettythir-tree对比两种输出形态观察arms、safety_mode、Scope等字段如何随源码结构变化。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考