ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Rustc 的 Canonicalization 机制:如何将 trait 查询从推理上下文中隔离

Rustc 的 Canonicalization 机制:如何将 trait 查询从推理上下文中隔离 Rustc 的 Canonicalization 机制如何将 trait 查询从推理上下文中隔离【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust导读Canonicalization规范化是 rustc trait 求解系统实现canonical queries的关键前置技术它把一条包含未绑定推理变量的 trait 查询如?A: Foostatic, ?B从它所属的类型检查上下文中隔离出来转换为与上下文无关的标准形式从而支持全局缓存、环检测与结果复用。阅读本文后你将掌握 canonicalization 的核心思想、完整的数据流查询规范化 → 实例化求解 → 结果规范化 → 结果回填并能对照 rustc 源码Canonical 数据结构、canonicalizer 实现理解其底层原理。Canonicalization 是什么从上下文隔离推理值Canonicalization 的核心思想建立在推理变量inference variable的两态模型之上在 rustc 的类型推断中每一个推理变量都始终处于两种状态之一未绑定unbound我们还不知道它最终是什么类型绑定bound它已经被统一unify为某个具体的值。因此要对一个包含类型/生命周期types/regions的数据结构T做隔离只需遍历它找出其中出现的未绑定变量把它们替换为 canonical variables规范变量从 0 开始、按固定顺序编号大体上是从左到右但顺序本身并不重要只要保持一致即可。例如设类型X (?T, ?U)其中?T与?U是两个互不相同的未绑定推理变量那么X的规范形式canonical form就是(?0, ?1)其中?0与?1就是所谓的canonical placeholders规范占位符。关键在于类型Y (?U, ?T)同样规范化成(?0, ?1)而类型Z (?T, ?T)会规范化成(?0, ?0)(?U, ?U)也一样。也就是说推理变量的具体身份并不重要——除非它们被重复使用。重复出现的推理变量在规范化后必须是同一个规范变量这样才能保留查询内部的共享结构。为什么需要规范化缓存、环检测与求解复用规范化的直接收益体现在三方面缓存如果两条 trait 查询具有相同的规范形式那么它们会得到相同的答案。该答案以规范变量?0、?1表达之后可以再映射回调用者原始的变量?T、?U。这正是 rustc 全局查询缓存能够命中u32: Trait?x与u32: Trait?y这类不同变量、同一目标查询的依据见下一代求解器的规范查询章节。环检测在 trait 求解递归深入时借助规范化形式可以识别出循环依赖。上下文无关性求解器只依赖查询本身而不依赖调用现场的具体推理状态。规范化查询Canonicalizing the query假设我们要证明如下 trait 查询?A: Foostatic, ?B其中?A与?B都是未绑定的推理变量。这条查询包含两个未绑定变量同时还含有一个生命周期static。这里有一个容易忽略的细节trait 系统在求解时一般会忽略生命周期的具体身份将它们一视同仁。因此在规范化时我们也会把任何自由生命周期free lifetime替换成规范变量。注意这里的static实际上是一个自由生命周期变量——我们并不是在整个程序的类型上下文typing context中考虑它而只是在当前这条 trait 引用trait reference的上下文里考虑它。从数学上讲我们没有对整个程序做全称量化quantifying而只是对这一条义务obligation做量化。于是规范化得到?0: Foo?1, ?2有时我们也写作带绑定量词的形式forT,L,T { ?0: Foo?1, ?2 }这个for给出了其中每个规范变量的种类信息T表示类型变量所以?0、?2是类型L表示生命周期变量所以?1是生命周期。canonicalize方法同时还会返回一个CanonicalVarValues数组OV记录每个被规范化的变量对应的原始值original values[?A, static, ?B]这个向量 OV 在后续处理查询响应query response时至关重要——源码中它对应的正是CanonicalVarValues结构体其定义是pub struct CanonicalVarValuesI: Interner { pub var_values: I::GenericArgs, }其文档注释明确说明当你规范化一个值V时会得到这个向量保存被规范变量替换掉的原始值之后需要用它在合适的位置实例化规范化后的查询响应。源码印证查询规范化的两种策略在 canonicalizer.rs 中InferCtxt::canonicalize_query的实现揭示了查询规范化的真实规则对查询值本体使用CanonicalizeAllFreeRegions策略——所有自由区域生命周期一律规范化注释中给出的例子正是T: Traitstatic→T: Trait?0并附映射?0 → static对param_env参数环境则单独走缓存并使用CanonicalizeFreeRegionsOtherThanStatic策略——源码注释FIXME(#118965)说明不规范化 param_env 中出现的static生命周期因为它们在 trait 选择中被特殊对待。这与主文档的叙述完全一致查询输入中出现的自由生命周期全部被当作规范变量处理。执行查询Executing the query构造出规范查询之后接下来就是求解它。求解过程的基本步骤是创建一个全新的推理上下文fresh inference context在该上下文中**实例化instantiate**规范查询为每个规范变量建立一个替换substitutionS把规范形式中的每个规范变量映射为一个新鲜的推理变量类型种类匹配。对于我们的示例查询forT,L,T { ?0: Foo?1, ?2 }替换 S 可能是S [?A, ?B, ?C]然后用这些推理变量替换被绑定的规范变量?0等得到完全实例化后的查询?A: Foo?B, ?C请记住替换 S——后面还会用到。接下来在新鲜推理上下文与实例化查询就绪后即可真正求解。trait 求解器的细节在另一章节中有更完整的介绍这里只需知道求解器会计算一个确定性值certainty valueProven或Ambiguous并对我们创建的推理变量产生副作用side-effects。例如如果Foo只有一个 implimpla, X Fooa, X for VecX where X: a { ... }那么求解结果可能是一个Proven的确定性值同时求解器创建了新的推理变量?D和?E代表 impl 上的参数并做了如下统一unification?B ?D?A Vec?E?C ?E同时由于 where 子句X: a还会累积区域约束region constraint?E: ?D。为了构造最终的查询结果必须把这些值从查询自己的推理上下文中提升lift出来转换成可以在原推理上下文中重新应用的形式。做法就是对查询结果再次应用 canonicalization。源码印证响应规范化的对称实现canonicalize_response 的源码注释给出了与查询规范化形成鲜明对比的规则规范化查询响应时只规范化未绑定的推理变量。也就是说查询输入与查询响应的规范化策略并不对称——输入要处理所有自由区域而输出不做特殊的自由生命周期处理。这个差异直接支撑了下一节的微妙点讨论。规范化查询结果Canonicalizing the query result如父章节所述大多数 trait 查询最终会得到一个把三部分信息组合在一起的结果确定性值certainty、结果替换var_values即ResultQueryResultT, NoSolution中的QueryResult主体以及若干区域约束region constraints。构造响应时我们复用最初实例化查询时创建的替换 S。回顾之前的查询forT,L,T { ?0: Foo?1, ?2 }对应的替换 SS [?A, ?B, ?C]经过求解中的统一工作后如果用最新结果刷新refreshS得到S [Vec?E, ?D, ?E]这三个值正是原查询中三个输入变量?A、static、?B的新值。注意它们引入了新的变量如?E。我们可以通过再次规范化让这些新变量消失。不过我们不只是规范化 S而是规范化整个查询响应 QRQR { certainty: Proven, // 或其他取值 var_values: [Vec?E, ?D, ?E] // 这就是 S region_constraints: [?E: ?D], // 来自 impl 的 where 子句 value: (), // 本示例中就是 ()某些场景下 // 可能携带类型或其他信息 }规范化后的结果为Canonical(QR) forT, L { certainty: Proven, var_values: [Vec?0, ?1, ?0] region_constraints: [?0: ?1], value: (), }这里有一个微妙点在规范化查询结果时我们不对自由生命周期做特殊处理。注意例如对?D的两处引用都被转换成了同一个规范变量?1。这与原始查询的规范化形成对比——在原始查询中每个自由生命周期都被规范化成一个全新的规范变量。对照新一代求解器文档中的表述响应中不唯一化uniquify区域也不规范化static。现在这个结果必须被重新应用reapply到每个需要它的上下文中。源码印证规范变量的种类信息规范化结果中的forT, L绑定信息在源码中对应CanonicalVarKind枚举。它区分了Ty { ui, sub_root }通用类型变量?T可与任意类型统一sub_root记录第一个与之做子统一sub-unify的类型变量索引若没有则指向自身Int整型类型变量?I只能与整型统一Float浮点类型变量?F只能与浮点统一PlaceholderTy表示任意类型的占位符Region(ui)区域变量?RPlaceholderRegion求解fora T: Fooa这类目标时为绑定区域a创建的占位符Const(ui)/PlaceholderConstconst 推理变量及其占位符。每个种类都携带universe()信息Int/Float恒为 ROOT 宇宙这正对应了文档中forT,L,T对变量种类的标注也是响应回填时创建正确种类推理变量的依据。而CanonicalI, V结构体 则由value规范化后的值、max_universe最大宇宙索引与var_kinds各规范变量的种类三部分组成注释明确说明一个规范化类型V是指其中所有自由推理变量都被重写为 canonical vars 的类型编号从 0 开始按首次出现顺序排列。处理规范化后的查询结果Processing the canonicalized query result现在需要把规范化的查询结果应用回原始上下文。回顾开头我们最初要证明的查询是?A: Foostatic, ?B它被规范化为forT,L,T { ?0: Foo?1, ?2 }而求解返回的规范响应是forT, L { certainty: Proven, var_values: [Vec?0, ?1, ?0] region_constraints: [?0: ?1], value: (), }应用该响应的概念流程分三步(a) 实例化为响应中的每个规范变量创建一个新鲜推理变量得到{ certainty: Proven, var_values: [Vec?C, ?D, ?C] ^^ ^^^ 新鲜的推理变量 region_constraints: [?C: ?D], value: (), }(b) 统一将结果中的值与原始值做统一?A 与 Vec?C 统一 static 与 ?D 统一 ?B 与 ?C 统一(c) 记录区域约束把区域约束?C: static记录下来留待后续验证在借用检查阶段统一处理生命周期关系。一个轻度优化变体文档还指出rustc 实际做的是一种轻度优化的变体并不急于把响应中的所有规范值都实例化为变量而是先遍历值向量寻找值恰好就是一个规范变量的情况。在示例中values[2]就是?C这意味着可以直接推出?C : ?B?D : static这样就得到部分赋值集合凡是推不出值的项再为它创建推理变量。这一优化减少了不必要的变量创建与统一开销但整体语义与朴素三步流程完全等价。与下一代 trait 求解器的呼应值得注意的是当前仓库中还包含对新一代 trait 求解器的设计文档下一代规范查询章节其中描述的整体流程与本篇完全同构规范化输入 → 在独立InferCtxt中实例化求解 → 规范化响应 → 回填并补充了本主题没有展开的细节输入规范化时推理变量inference映射为存在量化绑定变量existential bound var占位符placeholder映射为全称量化绑定变量universal bound var并考虑宇宙universe输入中的泛型参数被当作根宇宙root universe中的占位符处理输入中的所有区域一律映射为存在量化绑定变量并做唯一化uniquifya (): Traita会被规范化为exists0, 1 0 (): Trait1不关心它们的宇宙全部放入输入的最高宇宙输出中属于调用方宇宙的一切都被放入根宇宙只有在把var_values与调用方的原始值统一时才恢复正确宇宙求解目标除了约束推理变量还可能产生区域义务region obligations例如(): AOutlivesBa, b需要返回a: b必须成立这一事实——这是通过从InferCtxt中抽取额外的ExternalConstraints实现的。总结一次 canonicalization 的完整生命周期把全文串起来一条 trait 查询的完整生命周期是规范化输入?A: Foostatic, ?B→forT,L,T { ?0: Foo?1, ?2 }同时记录原始值向量[?A, static, ?B]即CanonicalVarValues自由生命周期全部被替换独立求解在全新InferCtxt中用替换 S 实例化规范查询如[?A, ?B, ?C]执行 trait 求解得到确定性值与统一/区域约束副作用规范化响应对{ certainty, var_values, region_constraints, value }整体重新规范化自由生命周期不再特殊处理得到可在任意上下文复用的forT, L { ... }标准形式回填结果调用方实例化响应中的规范变量为新鲜推理变量与原变量统一并暂存区域约束供后续验证或采用值即规范变量时直接推导的优化变体。通过这四步rustc 让 trait 查询的缓存与复用变得安全且高效相同规范形式的查询必然得到相同答案而答案中的规范变量又可以通过原始值向量精确映射回每个调用者的推理上下文。这份机制既是旧式 trait 求解器见 trait resolution 概览的基石也是新一代 trait solver 设计的基本盘理解它对深入 rustc 类型检查与 trait 系统实现至关重要。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表