Rust 编译器 HIR 类型检查深度解析:类型收集(Type Collection)与函数体类型检查的实现
2026/9/12 15:55:40 网站建设 项目流程

Rust 编译器 HIR 类型检查深度解析:类型收集(Type Collection)与函数体类型检查的实现

【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust

本文基于 rustc 开发指南中 HIR Type checking 章节,讲解 Rust 编译器前端最核心的两个阶段:把 HIR 中用户书写的hir::Ty语法类型转换为编译器内部表示Ty<'tcx>类型收集(Collection),以及在rustc_hir_typeck中完成的函数体类型检查(Type Checking)。读完后,你将理解这两阶段为什么按 crate 与查询(query)拆分、rustc_hir_analysistype_of/generics_of/clauses_of等查询各自负责什么,以及类型检查阶段中类型推断、强制转换(Coercion)与方法查找(Method Lookup)在源码中的落点。

两个 crate、两条职责线

rustc 开发指南将 HIR 阶段的类型处理拆分为两个 crate(见 章节原文):

  • rustc_hir_analysis:包含 "type collection"(类型收集)及其相关功能,源码位于 compiler/rustc_hir_analysis,其中收集逻辑集中在 collect.rs 及其子模块;
  • rustc_hir_typeck:实现函数**体(body)**的检查,即真正的 type checking,源码位于 compiler/rustc_hir_typeck。

这两个 crate 都大量依赖 类型推断(type inference) 与 trait 求解(trait solving) 这两套基础设施,这也是 rustc 前端能"边检查边推断"的根本原因。

rustc_hir_typeck的源码目录可以直观看到函数体检查覆盖面的宽度:expr.rs 负责表达式检查,cast.rs、callee.rs、closure.rs、loops.rs、pat.rs、_match.rs 分别处理对应的语法结构,而 coercion.rs、expectation.rs、method 目录则对应下文的强制转换与方法查找,check.rs 是整个 item 级检查的入口,fn_ctxt 目录中的FnCtxt是检查单个函数体时的上下文对象。

类型收集:从hir::TyTy<'tcx>

类型收集的核心任务在 原文 中定义得很清楚:

Type "collection" is the process of converting the types found in the HIR (hir::Ty), which represent the syntactic things that the user wrote, into the internal representation used by the compiler (Ty<'tcx>).

即:把 HIR 中的hir::Ty——它只表示"用户写了什么"的语法层类型——转换为编译器内部表示Ty<'tcx>。同样的转换也作用于 where 子句及函数签名中的其他成分。

原文用一个最小示例展示了二者的差别:

struct Foo { } fn foo(x: Foo, y: self::Foo) { ... } // ^^^ ^^^^^^^^^

两个参数xy的"类型"其实是同一个,但它们在 HIR 中是两个不同的hir::Ty节点:span 不同,路径的编码方式也不同(一个是裸路径Foo,一个是限定路径self::Foo)。而当这两个节点被"收集"为Ty<'tcx>之后,它们会被表示为完全相同的内部类型——路径差异在 lowering 后消失,只剩下类型本身。

collect.rs 开头的模块文档注释与开发指南的表述完全一致,可以把它当作权威定义:

Collection is the process of determining the type and other external details of each item in Rust. Collection is specifically concerned withinter-proceduralthings -- for example, for a function definition, collection will figure out the type and signature of the function, but it will not visit thebodyof the function in any way, nor examine type annotations on local variables (that's the job of type checking).

这里有两个关键点值得展开:

  1. 收集是"过程间"(inter-procedural)的。对于函数定义,收集阶段会确定该函数的类型和签名,但完全不进入函数体,也不检查局部变量的类型标注——那是类型检查(type checking)的工作。这条职责边界正是两个 crate 拆分的原因:收集产出的信息(类型、泛型参数、where 子句)可以被其他 item 引用,因此必须先行可用;而函数体检查只针对单个函数。
  2. 收集是一组查询(queries)的集合。原文指出:"Collection is defined as a bundle of queries for computing information about the various functions, traits, and other items in the crate being compiled",并建议参见 查询系统章节。

源码印证:collect模块的查询族

在 collect.rs 的provide函数中,可以看到"查询集合"的具体形态——它向 rustc 的查询引擎注册了一批提供函数(provider):

  • type_of:item 的类型(函数得到FnDef类型,ADT 得到其 ADT 类型等);
  • generics_ofclauses_of及其explicit_*变体:泛型参数集合与 where 子句;
  • item_boundsitem_self_boundsitem_non_self_bounds:item 的约束条件;
  • trait_defadt_deffn_sig:trait 定义、ADT 定义与函数签名;
  • const_of_itemconst_param_defaultcoroutine_kind等。

这些查询各自独立、按需求值,是 rustc 查询驱动(query-driven)架构的典型体现:收集不再是传统意义上"扫一遍所有 item"的单向 pass,而是由使用方按需拉取各 item 的对外可见信息。原文也提到,collect模块是这一过程的详细出处。

type_of的实现位于 collect/type_of.rs。从源码结构看,type_of(tcx, def_id)先处理 RPITIT(trait 中返回位置impl Trait)等特殊情形,然后构造ItemCtxt并按 HIR 节点类型分发:trait item / impl item 中的函数会构造Ty::new_fn_def(即绑定到该DefId的函数定义类型,配合 late-bound 变量绑定器);常量、关联类型、static等则通过ItemCtxt内部的HirTyLowerer(定义在 hir_ty_lowering.rs)把hir::Ty逐节点 lower 为Ty<'tcx>;遇到"可推断类型"(suggable infer type,例如static mut X: _)时还会调用infer_placeholder_type从常量体反推占位类型。这正是"同一个self::FooFoo最终收敛到同一个内部类型"这一行为的落点:lower 过程解析路径、消解泛型实参后,输出只保留类型语义本身。

collect目录下的其余子模块同样对应原文所说的"related functionality":generics_of.rs(泛型参数收集)、clauses_of.rs(where 子句收集)、item_bounds.rs(item 约束)、resolve_bound_vars.rs(绑定变量解析)以及 type_of/opaque.rs(不透明类型的 lowering)。

函数体类型检查:rustc_hir_typeck的职责

原文的 "HIR Type checking" 章节以一句 TODO 收尾("actually talk about type checking",对应 rustc-dev-guide 的 issue #1161),即原文本身主要成文于类型收集部分。为完整覆盖该主题,下面结合同目录的 Coercions 与 Method lookup 两篇姊妹文档,以及rustc_hir_typeck源码,补全类型检查阶段的核心内容。

类型检查阶段处理原文中明确排除在收集之外的那些"过程内"事务:函数体内的表达式、局部变量标注、模式、控制流等。检查在FnCtxt(函数检查上下文,fn_ctxt 目录)中进行,它持有推断上下文与正在累积的TypeckResults。其中两个最复杂的机制是强制转换与方法查找。

强制转换(Coercions):one-to-one 与 LUB 两类转换点

强制转换是"把值隐式转换为另一种类型"的操作,**转换点(coercion site)**是允许隐式转换发生的语法位置。rustc 中转换点分两类(见 coercions.md):

let one_to_one_coercion: &u32 = &mut 8; // one-to-one:&mut u32 -> &u32 let lub_coercion = match my_bool { true => &mut 10, false => &12, // LUB:&mut i32 与 &i32 统一收敛为 &i32 };

one-to-one 转换:从一个已知源类型转换到已知目标类型,调用FnCtxt::coerce即可(实现在 coercion.rs 的Coerce相关逻辑中,如coerce_to_refcoerce_unsizedcoerce_from_fn_item等方法)。

LUB(Least-Upper-Bound,最小上界)转换:把一组源类型转换为一个尚且未知的目标类型,并且由转换过程本身产生这个目标类型。通用三步流程:

let mut coerce = CoerceMany::new(initial_lub_ty); // 1. 创建 CoerceMany,选定初始 LUB for expr in exprs { let expr_ty = fcx.check_expr_with_expectation(expr, expectation); coerce.coerce(fcx, &cause, expr, expr_ty); // 2. 逐个检查表达式并登记其类型 } let final_ty = coerce.complete(fcx); // 3. 完成 LUB,得到最终类型

三步分别对应 coercion.rs 中CoerceMany结构的new/coerce/complete方法,CoerceMany存储 LUB 转换所需的全部状态。细节要点:

  • 初始 LUB 来自Expectation:创建CoerceMany需要一个initial_lub,它来自表达式的类型期望(Expectation,定义在 expectation.rs),使得 LUB 计算中的推断约束能传播给后续被检查的表达式;没有可用期望时就新建一个推断变量。若期望本身就是推断变量,则不直接用它作为初始 LUB,而是另建推断变量?y——否则中间表达式会过早地把?x约束为第一个分支的类型(例如FnDef(a)),最终统一?x eq fn() -> ()时就会失败。
  • 没有 HIR 节点的情况:例如不带操作数的break/return,需要让()参与 LUB,此时用CoerceMany::coerce_forced_unit
  • try_find_coercion_lub是"给两个类型算出新 LUB 类型"的核心逻辑(FnCtxt的方法),有三条路径:把当前 LUB 与新类型都转为函数指针、把其一转换为另一个、或计算两者的共同超类型。例如match三分支foo / foo / bar的 LUB 过程:第一步特殊处理,直接把第一个分支FnDef(Foo)强转为初始推断变量?x,推出?x = FnDef(Foo);第二、三步再依次与FnDef(Foo)FnDef(Bar)计算 LUB,最终得到fn() -> ()
  • use_lub字段:one-to-one 转换的实现被 LUB 转换复用,Coerce结构上的use_lub字段切换行为——one-to-one 场景在强转失败时回退到普通子类型判定,而 LUB 场景必须计算共同超类型use_lub置位时)。共同超类型的计算使用 rustc_infer 中的特殊类型关系LatticeOp,它在遇到高阶类型(HRTB)时切换为不变性(invariance),强制两个高阶类型的绑定器等价,从而避免"选最泛绑定器"这一难题,并保证 LUB 计算不依赖求值顺序。
  • 转换结果落地为 adjustments:每次成功强转都会向进行中的TypeckResults记录一系列adjustments(如 unsize、autoderef)。构建 THIR 时这些 adjustments 被展开为显式步骤;此后再无"强制转换"概念,MIR 中只有显式 cast 与子类型关系。
  • 回退到子类型FnCtxt::coerceCoerceMany::coerce在强转失败后都会尝试子类型判定(one-to-one 尝试源是目标的子类型,LUB 尝试计算共同超类型),因此强转失败后无需再单独尝试子类型。
  • never 类型的特殊性:从!强转到推断变量会得到一个NeverToAny转换(目标即该推断变量),这与把推断变量统一!有微妙差别——统一要求?x真的等于!,而NeverToAny允许?x被推断为任意类型。因此当 LUB 的初始类型是推断变量时,必须走强转而不能用子类型。
  • 探针(probe)注意事项:强转有副作用(会写入 adjustments),不能被探针回滚。在probe内调用FnCtxt::coerce是错误的;commit_if_ok包裹coerce且成功后不回滚才是正确用法;CoerceMany绝不应出现在probe/commit_if_ok内部。

方法查找(Method Lookup):Probe 与 Confirm 两阶段

receiver.method(...)形式的调用,本质上被转换为等价的完全限定调用:trait 方法为Trait::method(ADJ(receiver), ...),固有方法为ReceiverType::method(ADJ(receiver), ...),其中ADJ通常是"一系列 autoderef 之后可能再 autoref"的调整(如&**receiver),有时还包含 unsizing 等强转(如[T; n][T])。整个查找分两个阶段(见 method-lookup.md,实现在 method 目录):

  1. Probe(探测):决定调用哪个方法、接收者如何调整。探测阶段产出一个Pick结果,刻意不含推断变量等状态,以便在多个调用点之间缓存复用。
  2. Confirm(确认):把探测结果"应用"下去——更新 side table、统一类型变量,即所有带副作用的动作都发生在此阶段。

探测阶段内部又分三步:

  • Steps 生成:对接收者类型逐步解引用直到不可再解引用,并追加一个可选的 unsize 步。例如Rc<Box<[T; 3]>>产生Rc<Box<[T; 3]>> → Box<[T; 3]> → [T; 3] → [T]
  • 候选收集:沿各 step 收集Candidate,分两类。**固有候选(inherent)**来自接收者类型本身的impl(无需 import,且固有 impl 只能定义在类型所在 crate 内);**扩展候选(extension)**来自已导入 trait 的 impl(即 extension method)。
  • 候选搜索:沿 steps 向下逐个匹配,每个 step 同时尝试 autoref 与 auto-mut-ref 两种接收者形态;对每种形态,先固有候选后扩展候选。同组内多个候选匹配时报告错误(同一 trait 的多个 impl 视为一次匹配),否则取第一个命中者。命中后还要递归检查 impl 上的 where 子句:若匹配(或无法排除匹配)则选定该方法,否则继续向下探测。

小结与延伸入口

回到 原文 的核心结论:rustc 把 HIR 阶段的类型工作切分为过程间的类型收集(rustc_hir_analysiscollect查询族:type_ofgenerics_ofclauses_ofitem_bounds等,把hir::Tylower 为Ty<'tcx>)与过程内的函数体检查(rustc_hir_typeckFnCtxt驱动,含强制转换与方法查找两大机制)。原文对"实际展开讲 type checking"留了 TODO,而仓库中同目录的 Coercions 与 Method lookup 已经承担了这部分内容,并与 compiler/rustc_hir_typeck/src/coercion.rs、compiler/rustc_hir_typeck/src/method 的源码相互印证。

如需继续深入,建议按以下顺序阅读仓库文档:query 系统(理解收集查询如何被按需求值)、type inference(理解检查过程中的变量统一)、trait resolution(理解候选匹配中 where 子句检查背后的 trait 求解)。

【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询