Rust 编译器新 Trait 求解器:Trait 目标与 NormalizesTo 目标的候选偏好(Candidate Preference)机制详解

Rust 编译器新 Trait 求解器:Trait 目标与 NormalizesTo 目标的候选偏好(Candidate Preference)机制详解 Rust 编译器新 Trait 求解器Trait 目标与 NormalizesTo 目标的候选偏好Candidate Preference机制详解【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust在 Rust 编译器新版 trait 求解器next trait solver位于compiler/rustc_next_trait_solver中证明一个 trait 目标往往存在多种可行的证明路径编译器必须决定优先采用哪一种。本文基于 Rust 官方开发者指南中的 Candidate preference 文档结合当前仓库的实际源码完整讲解这套候选偏好规则为什么ParamEnvwhere 子句会优先于Impl、为什么 trivial 的BuiltinImpl拥有最高优先级、trait 目标与NormalizesTo归一化目标的偏好逻辑有何不同以及这些偏好各自修复了哪些经典的 lifetime 错误、类型推断问题又埋下了哪些已知缺陷。读完后你将能够阅读merge_trait_candidates与assemble_and_merge_candidates的实现理解每条偏好规则背后为什么必须这样的证据链并在遇到 winnowing 相关的 UI 测试时快速定位其成因。背景Candidate 与 CandidateSource证明Trait目标和NormalizesTo目标有多种方式每一种可行的方式在求解器内部被称做一个Candidate候选。当多个候选同时适用时编译器并不会平等对待它们而是按照一套固定的优先级筛选 合并winnowing出一个最终结果。用来记录这个候选是从哪来的的信息存放在CandidateSource中。CandidateSource的完整定义位于 rustc_type_ir 的求解模块它包含五种来源Impl(I::ImplId)用户手写的 implBuiltinImpl(BuiltinImplSource)编译器生成的内置 implauto trait、Sized、DiscriminantKind等其子分类定义在 BuiltinImplSource包括Trivial无嵌套要求的 trivial 内置 impl、Misc、Objecttrait object 内置 impl和TraitUpcastingParamEnv(ParamEnvSource)来自函数参数环境的假设即 where 子句。ParamEnvSource又分为NonGlobal与Global非全局的 where 子句被优先preferred eagerly全局的则除非同时存在非全局候选否则不参与竞争AliasBound(AliasBoundKind)当 self 类型是别名类型opaque 类型或投影类型时即便不知道其具体底层类型我们也能确定该别名的声明上界一定成立。AliasBoundKind区分SelfBounds与NonSelfBounds是否沿投影的 self 类型递归得到的CoherenceUnknowable仅在 coherence 模式下注册代表下游可能出现的、无法预知的 impl。为什么 coherence 模式下要合并所有候选需要特别强调的是候选偏好在 coherence 检查中是不健全unsound的——偏好可能导致错误的推断或 region生命周期约束。因此在 coherence 模式下求解器不做任何偏好筛选而是直接尝试合并全部候选如果能合并成功说明所有候选给出了相同约束结果一致否则报告歧义flounder因为重叠的候选意味着用户代码违反了 coherence 规则。这一点在merge_trait_candidates的入口 中可以直接看到函数第一步就检查self.typing_mode().is_coherence()若为 true 则跳过所有偏好逻辑直接走try_merge_candidates/flounder。Trait 目标的偏好规则merge_trait_candidatesTrait 目标的候选合并发生在fn merge_trait_candidates。它接收一个candidate_preference_mode由CandidatePreferenceMode::compute计算普通 trait 用Defaultsizedness/auto/default trait 用Marker后者会把 AliasBound 的优先级提到 where 子句之前以及候选列表按如下顺序执行偏好。第一优先级trivial 的 BuiltinImplCandidateSource::BuiltinImpl(BuiltinImplSource::Trivial)trivial 内置 impl 是指那些对任何良构类型well-formed types都必然成立的内置 impl。由于它必然适用用它证明目标绝不可能比用其他候选引入更多约束因此它拥有最高优先级——在源码中它被单独提取并直接返回不参与任何合并trait_goals.rs 中的注释明确说明这是 #53123 的修复。目前只有Sized和MetaSized的 impl 被归为 trivial。这条规则是为了防止如下模式产生 lifetime 错误trait TraitT: Sized {} impla Traitu32 for a str {} impla Traiti32 for a str {} fn is_sizedT: Sized(_: T) {} fn fooa, b, T(x: b str) where a str: TraitT, { // Elaborating the a str: TraitT where-bound results in a // a str: Sized where-bound. We do not want to prefer this // over the builtin impl. is_sized(x); }如果优先用了 where 子句推导出的a str: Sized会把a与b错误地关联起来而 trivial 的Sized内置 impl 没有嵌套目标不会引入任何约束。文档也指出了该偏好的一处已知局限当内置 impl 存在依赖非参数 where 子句的嵌套目标时这个偏好会出错。例如struct MyTypea, T: ?Sized(a (), T); fn is_sizedT() {} fn fooa, T: ?Sized() where (MyTypea, T,): Sized, MyTypestatic, T: Sized, { // The where-bound is trivial while the builtin Sized impl for tuples // requires proving MyTypea, T: Sized which can only be proven by // using the where-clause, adding an unnecessary static constraint. is_sized::(MyTypea, T,)(); //~^ ERROR lifetime may not live long enough }第二优先级ParamEnv非全局 where 子句一旦存在至少一个非全局的ParamEnv候选求解器就优先使用全部ParamEnv候选包括全局的并丢弃其他类型的候选。源码实现见 trait_goals.rs 中has_non_global_where_bounds分支它把所有ParamEnv候选提取出来单独合并若无法合并则直接报歧义而不再看Impl等其他候选。全局where 子句的定义一个 where 子句是全局的当且仅当它不含 HRTBfora且不包含任何泛型参数可以包含static。之所以这样区分是因为非全局 where 子句涉及函数自身的泛型变量编译器对它们的约束能力有限而用户对 where 子句的控制力最强——求解器偏好错了用户可以最直接地调整自己的 bound所以 where 子句优先。对Impl候选的偏好这条偏好首先是必要的它能避免如下 region 错误trait Traita {} implT Traitstatic for T {} fn impls_traita, T: Traita() {} fn fooa, T: Traita() { impls_trait::a, T(); }如果优先用 blanket implimplT Traitstatic for T会引入a: static这样的错误约束。此外被遮蔽shadowed的 impl 在当前的求解器中会产生歧义循环参见 trait-system-refactor-initiative 的 issue #76。没有这条偏好时为避免不完整incompleteness求解器只能对由 where 子句产生 region 约束的情形报歧义错误trait Super { type SuperAssoc; } trait Trait: SuperSuperAssoc Self::TraitAssoc { type TraitAssoc; } implT, U Trait for T where T: SuperSuperAssoc U, { type TraitAssoc U; } fn overflowT: Trait() { // We can use the elaborated SuperSuperAssoc Self::TraitAssoc where-bound // to prove the where-bound of the T: Trait implementation. This currently results in // overflow. let x: T as Trait::TraitAssoc; }这条偏好造成问题的情况同样被如实记录。它引发的历史 issue 数量非常多最著名的是 rust-lang/rust #24066其中多数是where 子句本来应该引导类型推断时仍被无条件优先导致的。例如trait TraitT { fn call_me(self, x: T) {} } implT Traitu32 for T {} implT Traiti32 for T {} fn bugT: TraitU, U(x: T) { x.call_me(1u32); //~^ ERROR mismatched types }即便只在where 子句不引导推断时才启用该偏好仍可能产生错误的 lifetime 约束trait Traita {} impla Traita for a str {} fn impls_traita, T: Traita(_: T) {} fn fooa, b(x: b str) where a str: Traitb { // Need to prove x str: Traitb with b: x. impls_trait::b, _(x); //~^ ERROR lifetime may not live long enough }对AliasBound候选的偏好ParamEnv 也优先于 AliasBound这是为了规避如下 region 错误trait Bounda {} trait Traita { type Assoc: Bounda; } fn impls_boundb, T: Boundb() {} fn fooa, b, c, T() where T: Traita, forhr T::Assoc: Boundhr, { impls_bound::b, T::Assoc(); impls_bound::c, T::Assoc(); }同时它也可能引入不必要约束例如trait Bounda {} trait Traita { type Assoc: Bounda; } fn impls_boundb, T: Boundb() {} fn fooa, b, T() where T: forhr Traithr, T as Traitb::Assoc: Bounda, { // Using the where-bound for T as Traita::Assoc: Bounda // unnecessarily equates T as Traita::Assoc with the // T as Traitb::Assoc from the env. impls_bound::a, T as Traita::Assoc(); // For a T as Traitb::Assoc: Boundb the self type of the // where-bound matches, but the arguments of the trait bound dont. impls_bound::b, T as Traitb::Assoc(); }为什么全局 where 子句不享有偏好全局 where 子句要么被某个 impl 完全蕴含要么不可满足不可满足时无所谓用哪个候选被完全蕴含时用 impl 去证明该 trait 目标不会引入任何额外约束。因此对 trait 目标而言偏好全局 where 子句几乎只有在使用static的边角场景有用trait A { fn test(self); } fn foo(x: dyn A) where dyn A static: A, // Using this bound would lead to a lifetime error. { x.test(); }更重要的是在归一化关联类型时优先使用 impl可以防止全局 where 子句遮蔽shadowimpl。目前没有任何已知问题源于impl 优先于全局 where 子句这一选择。为什么仍然要考虑全局 where 子句既然全局 where 子句在竞争中被淘汰为什么还要组装它们因为它们用来削弱非全局 where 子句带来的推断引导。在没有全局 where 子句时求解器会优先使用非全局 where 子句——即使存在同样适用的 impl——这种过度的推断引导会导致如下代码无法编译而一旦加入全局 bound该引导被禁用fn checkColor(color: Color) where Vec: IntoColor Intof32, { let _: f32 Vec.into(); // Without the global Vec: Intof32 bound wed // eagerly use the non-global Vec: IntoColor bound // here, causing this to fail. } struct Vec; impl FromVec for f32 { fn from(_: Vec) - Self { loop {} } }在源码中这个保留/丢弃行为发生在 merge_trait_candidates 末尾当所有候选都是全局 where 子句时结果仍被标记为TraitGoalProvenVia::ParamEnv保证 rigid projection 行为正确否则在合并前把所有全局ParamEnv候选过滤掉。第三优先级AliasBoundCandidateSource::AliasBound优先于Impl。目前这条偏好被用来引导类型推断典型受益代码是impl Trait返回值的链式调用pub trait Dyn { type Word: Intou64; fn d_tag(self) - Self::Word; fn tag32(self) - Optionu32 { self.d_tag().into().try_into().ok() // prove Self::Word: Into?0 and then select a method // on ?0, needs eager inference. } }fn impl_trait() - impl Intou32 { 0u16 } fn main() { // There are two possible types for x: // - u32 by using the alias bound of impl Intou32 // - impl Intou32, i.e. u16, by using implT FromT for T // // We infer the type of x to be u32 even though this is not // strictly necessary and can even lead to surprising errors. let x impl_trait().into(); println!({}, std::mem::size_of_val(x)); }文档作者对这条偏好的态度是坦率的我个人认为这条偏好并不理想 。它同时能规避因 region 约束导致的歧义例如impls_bound::_, T::Assoc()中a与static的抉择但作者并不确定实践中是否有人依赖后者。特殊项BuiltinImpl(Object)——已知的 unsound 偏好trait object 的内置 impl 会被优先于用户手写的 impl但源码注释明确标注这是不健全的且计划在未来移除对应 issue #57893 与 PR #141347。在源码中这一行为由unsound_prefer_builtin_dyn_impl实现注释写明这是为了与旧 trait 求解器行为兼容的hack以避免阻塞新求解器的稳定化该偏好严格弱于旧行为仅在没有推断约束only region constraints时偏向内置 impl且只用于 trait 目标不用于归一化因此理论上可被利用的 unsoundness 只剩依赖 lifetime 的用户手写 impl这一种情形coherence 模式下该函数直接返回不做任何偏好。这意味着该偏好目前只影响 trait 目标可被构造出的 unsoundness 限于 lifetime 相关场景属于为了兼容暂时保留的技术债。NormalizesTo 目标的偏好规则assemble_and_merge_candidates归一化NormalizesTo目标有自己的一套偏好逻辑实现在fn assemble_and_merge_candidates。它的核心思路由 TraitGoalProvenVia 枚举体现归一化一个投影时只沿用它所对应 trait 目标被证明时所用的那种候选类别——特别是如果 trait 目标是通过ParamEnv候选证明的就不允许用 impl 去归一化TraitGoalProvenVia的文档注释直接说明了这一点动机见 trait-system-refactor-initiative #125。assemble_and_merge_candidates的函数注释还给出了这个设计的直观例子trait Traita { type Assoc; } impla, T: a Traita for T { type Assoc u32; } fn with_bounda, T: Traita(_value: T::Assoc) {}with_bound的 where 子句没有指定关联类型归一化T as Traita::Assoc只能借助那个 impl而 impl 会附加T: a约束造成不必要的 lifetime 错误。既然用户明确写了T: Traita成立求解器反而把该 alias 保持为 rigid刚性拒绝归一化。总是考虑 AliasBound 候选即使 trait 目标是通过ParamEnv候选证明的只要 where 子句没有指定对应的关联项associated item归一化时仍会考虑AliasBound候选而不是把 alias 直接视为 rigid。例如trait Super { type Assoc; } trait Bound { type Assoc: SuperAssoc u32; } trait Trait: Super {} // Elaborating the environment results in a T::Assoc: Super where-bound. // This where-bound must not prevent normalization via the SuperAssoc u32 // item bound. fn heckT: BoundAssoc: Trait(x: T::Assoc as Super::Assoc) - u32 { x }环境推导出的T::Assoc: Superwhere 子句不能阻止通过SuperAssoc u32的 item bound 完成归一化。该行为对应的回归测试在 alias-bound-shadowed-by-env.rs。使用这类 alias 也可能引入额外 region 约束issue #133044 记录的相关问题trait Bounda { type Assoc; } trait Trait { type Assoc: Boundstatic, Assoc u32; } fn hecka, T: TraitAssoc: Bounda(x: T::Assoc as Bounda::Assoc) { // Normalizing the associated type requires T::Assoc: Boundstatic as it // uses the Boundstatic alias-bound instead of keeping the alias rigid. drop(x); }ParamEnv 优先于 AliasBound反过来当 where 子句确实指定了关联项时优先使用 where 子句// Make sure we prefer the I::IntoIterator: IteratorItem () // where-bound over the I::Intoiterator: IteratorItem I::Item // alias-bound. trait Iterator { type Item; } trait IntoIterator { type Item; type IntoIter: IteratorItem Self::Item; } fn normalizeI: IteratorItem ()() {} fn fooI() where I: IntoIterator, I::IntoIter: IteratorItem (), { // We need to prefer the I::IntoIterator: IteratorItem () // where-bound over the I::Intoiterator: IteratorItem I::Item // alias-bound. normalize::I::IntoIter(); }这条规则的回归测试是 norm-where-bound-gt-alias-bound.rs源码中同样有对应注释引用它见 assembly/mod.rs 中We still need to prefer where-bounds over alias-bounds的candidates.retain逻辑。总是考虑 where 子句即使 trait 目标是通过impl证明的归一化时只要存在ParamEnv候选依然优先使用它们源码 assembly/mod.rs 中TraitGoalProvenVia::Misc分支 里的candidates.retain就是这个逻辑。优先orphanedwhere 子句在 GAT 与 RPITITreturn-positionimpl Traitin traits的 item bound 检查中fn check_type_bounds求解器会把一些orphaned的Projection子句注入ParamEnv。这些孤立的 ParamEnv 候选必须优先于 impl 和其他 where 子句#![feature(associated_type_defaults)] trait Foo { // We should be able to prove that i32: BazSelf because of // the impl below, which requires that Self::Bar(): Eqi32 // which is true, because we assume forT Self::BarT i32. type BarT: BazSelf i32; } trait BazT: ?Sized {} implT: Foo ?Sized BazT for i32 where T::Bar(): Eqi32 {} trait EqT {} implT EqT for T {}文档作者坦言这条偏好的全部必要场景尚未完全弄清还没能把它利用出什么有趣的东西。优先全局 where 子句归一化时全局 where 子句反而优先于 impl这是为了让如下代码可编译虽然作者不确定实践中是否有人依赖它trait Id { type This; } implT Id for T { type This T; } fn fooT(x: T) - u32 as Id::This where u32: IdThis T, { x }其代价是归一化可能产生额外的 region 约束同样关联 issue #133044trait Trait { type Assoc; } impl Trait for u32 { type Assoc u32; } fn trait_boundT: Trait() {} fn normalizeT: TraitAssoc u32() {} fn fooa() where static u32: TraitAssoc u32, { trait_bound::a u32(); // ok, proven via impl normalize::a u32(); // error, proven via where-bound }Trait where 子句遮蔽 implrigid 投影的另一层含义综合上述规则归一化关联项时的完整行为是如果对应的 trait 目标是通过ParamEnv或AliasBound候选证明的归一化就不考虑 impl也就是说对于不约束关联类型的 where 子句关联类型保持rigid刚性。这有两大动机。避免 impl 带来的不一致 region 约束trait Traita { type Assoc; } impl Traitstatic for u32 { type Assoc u32; } fn barb, T: Traitb() - T::Assoc { todo!() } fn fooa() where u32: Traita, { // Normalizing the return type would use the impl, proving // the T: Trait where-bound would use the where-bound, resulting // in different region constraints. bar::_, u32(); }如果不遮蔽 implbar的返回类型归一化会走 impl引入static约束而T: Trait目标本身走 where 子句同一程序里同一事实出现两套 region 约束必然产生 spurious 错误。规避 RPITIT 的type_of循环当存在 where 子句时必须避免 impl 候选否则 RPITITtrait 中的impl Trait返回值会触发查询循环issue #139762 的修复正是处理这个循环#139788 是作者认为更彻底的解法方向——移除 RPITIT 计算中依赖的 auto-trait leakageuse std::future::Future; pub trait ReactiveFunction: Send { type Output; fn invoke(self) - Self::Output; } trait AttributeValue { fn resolve(self) - impl FutureOutput () Send; } implF, V AttributeValue for F where F: ReactiveFunctionOutput V, V: AttributeValue, { async fn resolve(self) { // Were awaiting V as AttributeValue::{synthetic#0} here. // Normalizing that one via the the impl were currently in // relies on collect_return_position_impl_trait_in_trait_tys which // ends up relying on auto-trait leakage when checking that the // opaque return type of this function implements the Send item // bound of the trait definition. self.invoke().await() } }推论trait 定义内不能用到 blanket impl 的关联类型由where 子句遮蔽 impl直接得到的一个可观察行为trait 定义里的T: TraitSelf: Trait假设会阻止Self as Trait::Assoc通过 blanket impl 归一化为T。文档作者认为这似乎是个还算合理的限制trait EqT {} implT EqT for T {} struct IsEqualT: EqU, U(T, U); trait Trait: Sized { type Assoc; fn foo() - IsEqualSelf, Self::Assoc { //~^ ERROR the trait bound Self: EqSelf as Trait::Assoc is not satisfied todo!() } } implT Trait for T { type Assoc T; }总结偏好规则全景与源码索引把文档和源码放在一起候选偏好的完整决策顺序可以概括为场景偏好顺序源码位置Trait 目标typeck/普通模式1. 非全局 where 子句存在 → 只用 ParamEnv2. 无 → AliasBound 优先于 Impl3. trivial BuiltinImpl 最先直接胜出merge_trait_candidatesTrait 目标sizedness/auto/defaultMarker模式未嵌套的 AliasBound 优先于 where 子句CandidatePreferenceModeTrait object 内置 impl仅当无推断约束时偏向BuiltinImpl(Object)标记为 unsound、待移除unsound_prefer_builtin_dyn_implCoherence 目标不做任何偏好合并全部候选失败即歧义merge_trait_candidates 入口NormalizesTo 目标沿用TraitGoalProvenViaParamEnv/AliasBound 证明 → 禁用 impl、保持 rigidwhere 子句指定关联项 → ParamEnv 优先未指定 → 仍考虑 AliasBoundassemble_and_merge_candidates、TraitGoalProvenVia理解这套机制的关键是抓住文档反复强调的一个权衡偏好规则本质上是在牺牲理论上的完整合并把所有候选合并才是严格正确的做法换取1可预期的 lifetime 约束、2类型推断所需的早期引导、3避免求解循环与溢出。几乎每条规则都附带这条偏好修了什么和这条偏好又造成了什么的两类最小反例这种规则 双向反例的写法本身就是该文档最大的价值。想进一步验证这些行为可以直接阅读 tests/ui/traits/next-solver/normalization-shadowing/alias-bound-shadowed-by-env.rs 与 tests/ui/traits/winnowing/norm-where-bound-gt-alias-bound.rs 这两个回归测试它们分别钉住了alias-bound 不应被 env 遮蔽和归一化时 where 子句应压过 alias-bound两条关键决策。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考