Rust 编译器源码解析:opaque type(`impl Trait`)的隐藏类型推断全流程
Rust 编译器源码解析opaque typeimpl 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 typeRust 用两种语法引入 opaque type返回位置的impl TraitRPITfn foo() - impl IteratorItem u32类型别名impl TraitTAITtype 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 SeqT impl IntoIteratorItem T; #[define_opaque(Seq)] pub fn produce_singletonT(t: T) - SeqT { vec![t] } #[define_opaque(Seq)] pub fn produce_doubletonT(t: T, u: T) - SeqT { vec![t, u] } } fn is_sendT: Send(_: T) {} pub fn main() { let elems m::produce_singleton(22); is_send(elems); for elem in elems { println!(elem {:?}, elem); } }这里SeqT是 opaque type其定义作用域是模块m隐藏类型为VecT由produce_singleton与produce_doubleton两个定义使用点共同约束得出。在main中opaque type 已处于其定义作用域之外is_send(elems)需要证明Seqi32: SendSend并不在impl Trait声明的 bound 列表中需通过 auto-trait 泄漏机制推断for循环脱糖后要求SeqT: IntoIterator该约束可直接由 opaque type 自身声明的 bound 满足。关于 TAIT 的更多语法与定义使用点规则可参考开发指南姊妹篇 opaque-types-type-alias-impl-trait.md。类型检查mainopaque type 在定义作用域之外的行为for 循环的脱糖直接使用声明的 boundfor elem in elems被脱糖为IntoIterator::into_iter(elems)。elems的类型是SeqT因此类型检查器注册一条SeqT: IntoIterator的 obligation。由于SeqT本身就是impl IntoIteratorItem T这条 obligation平凡可满足——这与U: Foo的 where 约束让U平凡满足Foo的原理一致opaque type 声明的 bound 对类型检查器可见并被直接用来满足 obligation。elem的类型被推断为SeqT 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_taitcompiler/rustc_hir_analysis/src/collect/type_of/opaque.rs会通过tcx.hir_walk_toplevel_module(mut locator)遍历顶层模块的 HIR借助TaitConstraintLocator同文件 L101-L128逐项检查每个 item 是否具有 typeck 结果、是否定义了该 opaquetcx.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_typecompiler/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_typeL177-L197调用insert_hidden_type写入 opaque type 存储并调用add_item_bounds_for_hidden_typeL297-L385为隐藏类型注册额外 obligation要求隐藏类型 well-formed修复了 #114728并将 opaque 的 item bounds 中对该 opaque 自身的引用替换为隐藏类型后注册为新目标确保 bound 最终在具体类型上成立。已存在值时的处理insert_hidden_typeL218-L295若发现该 opaque 已注册过隐藏类型则用eq(DefineOpaqueTypes::Yes, prev, hidden_ty)将新旧隐藏类型相等化MIR borrowck 阶段PostTypeckUntilBorrowck则与 HIR typeck 推断出的类型tcx.type_of_opaque_hir_typeck做相等化。底层存储由OpaqueTypeTable::registercompiler/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.mdm member of [c_1..c_N]表示 regionm必须等于候选 region 之一。例如fn make(a: a u32, b: b u32) - impl Traita, b { .. }其隐藏类型只允许捕获a或b脱糖后即type MakeReturnx, y impl Traitx, y; fn make(a: a u32, b: b u32) - MakeReturna, b { .. }求解时借用检查器综合下界0必须 outlive 的类型、上界必须 outlive0的类型与最小选择规则从候选集中挑出唯一/最小解。候选 region 目前总是当前函数的生命周期参数见 rust-lang/rust#61773。向后兼容 hacksreplace_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()则直接原样返回不做任何替换。共有三个兼容 hackHack 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. implR: 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 TraitAssoc impl Send { || 42 }开发指南注释解释R是F上的::Outputprojection 这一事实会生成一个中间推断变量之后与实际发现的Assoc类型比较impl Send随后与这个推断变量比较导致推断变量被设为impl Send而非隐藏类型。由于推断变量上已经注册了使其满足Trait::Assoc的: Duhbound 的 obligation即使 opaque type 本身或隐藏类型不实现Duh也能编译。惰性 TAITlazy 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),仅供参考