Rust 编译器源码解析:opaque type(impl Trait)的隐藏类型推断全流程
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
导读
impl Trait是 Rust 中一种特殊的"不透明类型"(opaque type):使用者只看到它声明的 trait 接口,而背后的真实类型(即 hidden type,隐藏类型)由编译器从定义作用域内的"约束点"推断得出。与普通类型推断不同,这种推断可以跨越函数与函数体进行,是编译器类型系统中最复杂的环节之一。本文基于 rustc 官方开发指南 opaque-types-impl-trait-inference.md,结合本仓库compiler/下的实际源码,系统讲解 rustc 如何推断 opaque type 的隐藏类型:从type_of查询、handle_opaque_type注册机制,到 MIR 借用检查器(borrow checker)作为隐藏类型最终权威的全链路原理,并梳理为兼容历史行为保留的三个向后兼容 hack。
背景:什么是 opaque type 与 hidden type
Rust 用两种语法引入 opaque type:
- 返回位置的
impl Trait(RPIT):fn foo() -> impl Iterator<Item = u32> - 类型别名
impl Trait(TAIT):type Foo = impl Bar;(不稳定特性,需 nightly 与#![feature(type_alias_impl_trait)])
在类型检查时,impl Trait会被脱糖为一个不透明的类型(对应 HIR 中的OpaqueTy)。它对外只暴露声明中列出的 trait 约束,内部的具体类型(hidden type)则从**定义作用域(defining scope)**内的各"定义使用点"(defining use site)推断出来。Opaque type 推断与普通类型推断的最大区别在于:它可以在函数之间、甚至跨函数体工作。
本文的核心运行示例(取自开发指南)如下:
#![feature(type_alias_impl_trait)] mod m { pub type Seq<T> = impl IntoIterator<Item = T>; #[define_opaque(Seq)] pub fn produce_singleton<T>(t: T) -> Seq<T> { vec![t] } #[define_opaque(Seq)] pub fn produce_doubleton<T>(t: T, u: T) -> Seq<T> { vec![t, u] } } fn is_send<T: Send>(_: &T) {} pub fn main() { let elems = m::produce_singleton(22); is_send(&elems); for elem in elems { println!("elem = {:?}", elem); } }这里Seq<T>是 opaque type,其定义作用域是模块m;隐藏类型为Vec<T>,由produce_singleton与produce_doubleton两个定义使用点共同约束得出。在main中,opaque type 已处于其定义作用域之外:is_send(&elems)需要证明Seq<i32>: Send(Send并不在impl Trait声明的 bound 列表中,需通过 auto-trait 泄漏机制推断);for循环脱糖后要求Seq<T>: IntoIterator,该约束可直接由 opaque type 自身声明的 bound 满足。
关于 TAIT 的更多语法与定义使用点规则,可参考开发指南姊妹篇 opaque-types-type-alias-impl-trait.md。
类型检查main:opaque type 在定义作用域之外的行为
for 循环的脱糖:直接使用声明的 bound
for elem in elems被脱糖为IntoIterator::into_iter(elems)。elems的类型是Seq<T>,因此类型检查器注册一条Seq<T>: IntoIterator的 obligation。由于Seq<T>本身就是impl IntoIterator<Item = T>,这条 obligation平凡可满足——这与U: Foo的 where 约束让U平凡满足Foo的原理一致:opaque type 声明的 bound 对类型检查器可见,并被直接用来满足 obligation。elem的类型被推断为<Seq<T> as IntoIterator>::Item,即T。整个过程中,类型检查器完全不关心隐藏类型是什么。
is_send调用:auto-trait 的"揭示"(reveal)
当需要证明 auto trait bound 时,rustc 先重复上述过程,检查该 auto trait 是否出现在 opaque type 的 bound 列表中;若失败,则仅针对这一条 trait bound揭示 opaque type 的隐藏类型(而不是泛泛地全部揭示)。揭示动作通过调用type_of查询完成:该查询以 opaque type 的DefId为参数,内部会向定义作用域内的各定义函数请求隐藏类型并返回(详见下文"type_of查询内部"一节)。
完整流程图
整个main的类型检查步骤可用如下流程图概括(源自开发指南):
type_of查询内部:如何汇总隐藏类型
当type_of查询作用于 opaque typeO时,它返回隐藏类型。该隐藏类型由O的定义作用域内每个约束函数的结果组合而成(对应源码 compiler/rustc_hir_analysis/src/collect/type_of.rs 中的type_of_opaque分派逻辑,按 opaque 来源区分为 TAIT、关联 opaque、RPIT 三类处理)。
流程如下(开发指南中的流程图):
从源码看,find_opaque_ty_constraints_for_tait(compiler/rustc_hir_analysis/src/collect/type_of/opaque.rs)会通过tcx.hir_walk_toplevel_module(&mut locator)遍历顶层模块的 HIR,借助TaitConstraintLocator(同文件 L101-L128)逐项检查:每个 item 是否具有 typeck 结果、是否定义了该 opaque(tcx.opaque_types_defined_by(item_def_id)),若命中则取该 item 的 hidden type 并校验与已发现类型的一致性,不一致时报告错误。而 RPIT 的分支find_opaque_ty_constraints_for_rpit(同文件 L237-L287)在 HIR typeck 阶段直接从tcx.typeck(owner_def_id)的tables.hidden_types取结果;在 MIR borrowck 阶段则调用tcx.mir_borrowck(owner_def_id),从借用检查器的输出中取隐藏类型,取不到时回退到 HIR typeck 的结果。
值得注意的是,定义使用点之间对隐藏类型的校验同样发生在 MIR borrowck 阶段:add_hidden_type(compiler/rustc_borrowck/src/region_infer/opaque_types/mod.rs)在同一个 opaque 已存在隐藏类型时进行比较,若类型不同则构建 mismatch 错误诊断(build_mismatch_error),并同时说明不同定义使用点必须产生完全相同的类型。
将 opaque type 与其他类型关联:handle_opaque_type
隐藏类型被约束的核心入口只有一个:InferCtxt::handle_opaque_type,位于 compiler/rustc_infer/src/infer/opaque_types/mod.rs。它接收两个类型(参数顺序只影响诊断信息,任意一方应为 opaque type),处理流程如下(开发指南流程图):
关键分支逻辑在源码中均有对应实现:
- 两个 opaque type 同时定义:当
b也是 opaque 且与a同属可定义范围(can_define_opaque_ty)且来源为 TAIT 时,报告OpaqueHiddenTypeDiag错误(源码 L139-L159)。注意开发指南特别说明:RPIT 场景中async { 42 }这类"同时定义"是无害的,因此检查限定在 TAIT 上。 - 是否处于定义作用域:通过
self.can_define_opaque_ty(def_id)判断。只有处于定义作用域(或查询内部)时才能注册隐藏类型;否则返回None,交由另一方尝试处理。 - 注册与去重:
register_hidden_type(L177-L197)调用insert_hidden_type写入 opaque type 存储,并调用add_item_bounds_for_hidden_type(L297-L385)为隐藏类型注册额外 obligation:要求隐藏类型 well-formed(修复了 #114728),并将 opaque 的 item bounds 中对该 opaque 自身的引用替换为隐藏类型后注册为新目标,确保 bound 最终在具体类型上成立。 - 已存在值时的处理:
insert_hidden_type(L218-L295)若发现该 opaque 已注册过隐藏类型,则用eq(DefineOpaqueTypes::Yes, prev, hidden_ty)将新旧隐藏类型相等化;MIR borrowck 阶段(PostTypeckUntilBorrowck)则与 HIR typeck 推断出的类型(tcx.type_of_opaque_hir_typeck)做相等化。
底层存储由OpaqueTypeTable::register(compiler/rustc_infer/src/infer/opaque_types/table.rs)实现:若键已存在则替换旧值并返回旧类型,同时把变更记录进撤销日志(UndoLog::OpaqueTypes),保证推断回溯的一致性。
与查询(queries)的交互
opaque type 的推断与 rustc 的查询体系有微妙的交互。核心事实是:查询在执行时无法判断自身是否处于定义作用域内,因此一律假定处于定义作用域内。
具体机制(开发指南原文要点):
- 已注册的隐藏类型被存放在
QueryResponse结构体的opaque_types字段中,由take_opaque_types_for_query_response读取出来。在 compiler/rustc_infer/src/infer/canonical/query_response.rs 中可以看到对应实现:查询响应构建时会通过take_opaque_types()(L160-L173 附近)收集本次查询中新产生的 opaque 约束写入QueryResponse.opaque_types。 - 当
QueryResponse在query_response_substitution_guess中被实例化到周围的InferCtxt时,每条隐藏类型约束都会再次调用handle_opaque_type进行转换。对应实现位于unify_query_response_instantiation_guess附近的循环(L527-L545):对query_response.value.opaque_types中的每个(a, b),实例化变量后用eq(DefineOpaqueTypes::Yes, ...)重新相等化——注释说明这里刻意用equate而非直接注册隐藏值,因为隐藏类型可能是被约束成 opaque 本身的推断变量,需要把 opaque 的泛型参数与隐藏类型版本中的泛型参数做相等化。
开发指南还指出一处"怪异":实例化后的 opaque type 存在先后顺序——若两个 opaque type 相互比较,必须选择其中一个作为"获得隐藏类型赋值"的一方,rustc 选择被视为expected的那个。但真实场景中两个 opaque type 可能都有定义性使用。当查询结果被实例化时,这个选择会从使用该查询的上下文重新评估;最终上下文(函数的 typeck、MIR borrowck 或 wf-checks)才知道哪个 opaque type 真正可以被实例化,并据此正确处理。
在 MIR 借用检查器内部
MIR borrow check 通过nll_relate关联类型,并且只关心 region(生命周期)。任何类型关系都会触发隐藏类型的绑定,因此借用检查器所做的与类型检查器相同——但它会忽略明显是死代码的部分(如 panic 之后)。
借用检查器是隐藏类型的最终权威(source of truth),原因在于:只有它能正确弄清楚隐藏类型上的各个生命周期分别对应 opaque type 声明上的哪些生命周期。这背后的机制在 compiler/rustc_borrowck/src/region_infer/opaque_types/mod.rs 的compute_definition_site_hidden_types中得到体现:该函数收集定义作用域内所有 opaque 的定义使用点,把ProvisionalHiddenType统一映射回 opaque 的定义参数(definition-site 表示),并对同一 opaque 的多个使用点做一致性校验。
隐藏类型对生命周期的限制本质上是member constraints(成员约束)在起作用。member constraints 的完整原理见开发指南 borrow-check/region-inference/member-constraints.md:'m member of ['c_1..'c_N]表示 region'm必须等于候选 region 之一。例如:
fn make(a: &'a u32, b: &'b u32) -> impl Trait<'a, 'b> { .. }其隐藏类型只允许捕获'a或'b,脱糖后即:
type MakeReturn<'x, 'y> = impl Trait<'x, 'y>; fn make(a: &'a u32, b: &'b u32) -> MakeReturn<'a, 'b> { .. }求解时借用检查器综合下界('0必须 outlive 的类型)、上界(必须 outlive'0的类型)与最小选择规则,从候选集中挑出唯一/最小解。候选 region 目前总是当前函数的生命周期参数(见 rust-lang/rust#61773。
向后兼容 hacks:replace_opaque_types_with_inference_vars
返回位置的impl Trait存在一些不属于任何 RFC、很可能是"意外稳定化"的历史怪癖。为了支持它们,rustc 使用replace_opaque_types_with_inference_vars重新引入旧行为,其实现位于 compiler/rustc_infer/src/infer/opaque_types/mod.rs:它用BottomUpFolder遍历类型,将当前可定义(can_define_opaque_ty)且无逃逸绑定变量的 opaque 类型替换为新的推断变量,并为每个替换注册一条OpaqueReturnType(None)原因的 obligation。注意:该 hack 只作用于旧求解器——源码第 33-36 行明确,若使用下一代 trait 求解器(next_trait_solver())则直接原样返回,不做任何替换。
共有三个兼容 hack:
Hack 1:所有返回点共享同一个推断变量
所有返回点共享同一个推断变量,因此某个返回点只有在另一个返回点使用了具体类型时才能编译:
fn foo() -> impl Debug { if false { return std::iter::empty().collect(); } vec![42] }if false分支的返回类型无法单独确定,但由于它与主返回路径共享推断变量,vec![42]的具体类型会约束整个函数。
Hack 2:关联类型相等约束(associated type equality constraints)
对impl Trait的关联类型相等约束可以使用,只要隐藏类型满足关联类型上的 trait bound 即可——opaqueimpl Trait签名本身不必满足它们:
trait Duh {} impl Duh for i32 {} trait Trait { type Assoc: Duh; } // the fact that `R` is the `::Output` projection on `F` causes // an intermediate inference var to be generated which is then later // compared against the actually found `Assoc` type. impl<R: Duh, F: FnMut() -> R> Trait for F { type Assoc = R; } // The `impl Send` here is then later compared against the inference var // created, causing the inference var to be set to `impl Send` instead of // the hidden type. We already have obligations registered on the inference // var to make it uphold the `: Duh` bound on `Trait::Assoc`. The opaque // type does not implement `Duh`, even if its hidden type does. // Lazy TAIT would error out, but we inserted a hack to make it work again, // keeping backwards compatibility. fn foo() -> impl Trait<Assoc = impl Send> { || 42 }开发指南注释解释:R是F上的::Outputprojection 这一事实会生成一个中间推断变量,之后与实际发现的Assoc类型比较;impl Send随后与这个推断变量比较,导致推断变量被设为impl Send而非隐藏类型。由于推断变量上已经注册了使其满足Trait::Assoc的: Duhbound 的 obligation,即使 opaque type 本身(或隐藏类型)不实现Duh也能编译。惰性 TAIT(lazy TAIT)本应报错,但 hack 使其恢复工作以保持向后兼容。
Hack 3:闭包不能为父函数的impl Trait创建隐藏类型
闭包无法为其父函数的impl Trait创建隐藏类型。开发指南指出:这一点目前基本无关紧要,因为 Hack 1 引入了推断变量,闭包只看到推断变量;但如果修复 Hack 1,这个问题就会浮现。
总结:一条跨越类型检查与借用检查的推断链路
综合全文,rustc 推断 opaque type 隐藏类型的完整链路可以概括为:
- HIR typeck 阶段:类型检查器遇到
impl Trait的 opaque 类型时,通过handle_opaque_type在OpaqueTypeStorage中注册ProvisionalHiddenType;同时为隐藏类型补充 well-formed 与 item bounds 相关 obligation。 - 查询封装:opaque 约束随
QueryResponse.opaque_types跨查询传递,实例化时再次经handle_opaque_type还原到调用方上下文,并重新评估"哪个 opaque 获得隐藏类型"。 - MIR borrowck 阶段:借用检查器经
nll_relate处理类型关系、通过 member constraints 求解隐藏类型中各生命周期的对应关系,并调用compute_definition_site_hidden_types汇总定义使用点,成为隐藏类型的最终权威。 type_of查询:任何需要揭示隐藏类型的位置(如 auto-trait 泄漏检查)调用type_of,经find_opaque_ty_constraints_for_*系列函数回溯各定义函数,取回并一致性校验后的隐藏类型。- 兼容层:对返回位置
impl Trait的意外稳定化行为,通过replace_opaque_types_with_inference_vars(旧求解器路径)保持向后兼容。
这条链路同时跨越类型检查器与借用检查器,正是"opaque type 推断可以跨函数工作"这一特性的实现根基。深入阅读本文引用的源码文件,可以继续追踪OpaqueTypeStorage的撤销日志机制、member constraints 的 SCC 图求解等更底层的细节。
【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考