ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Rust 类型系统不变量完全指南:从 rustc 源码看 15 条关键保证与真实破坏现状

2026/9/12 15:39:43 拓冰建站 浏览量
Rust 类型系统不变量完全指南:从 rustc 源码看 15 条关键保证与真实破坏现状 Rust 类型系统不变量完全指南从 rustc 源码看 15 条关键保证与真实破坏现状【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust导读Rust 的类型系统并不是一纸规格书而是一套运行在编译器内部的、由大量隐式约定支撑的复杂引擎。本指南以 rustc-dev-guide 中 Invariants of the type system 一章为骨架系统梳理 Rust 类型系统承诺始终为真的 15 条不变量invariant逐条解释其含义、为什么需要它、当前是否成立并结合本仓库源码compiler/rustc_next_trait_solver、compiler/rustc_infer、compiler/rustc_middle等给出可验证的实现证据。读完本文你将掌握在 HIR typeck、MIR borrowck、coherence 检查、codegen 各阶段能安全依赖哪些保证、哪些保证已被破坏且必须小心对待以及如何从源码层面定位这些不变量对应的代码位置。一、认识不变量什么是类型系统的承诺所谓不变量是指类型系统在所有时刻都保证为真的性质。其他语言或类型系统的使用者往往把这些性质视为理所当然但遗憾的是Rust 中相当一部分不变量目前并不成立——有些是设计使然fundamental to its design有些则是 bug 导致且未来可能修复。在 rustc-dev-guide 中这份清单被明确标注为不完整的、非官方的核心类型系统不变量清单incomplete and unofficial list使用两种记号✅该不变量基本成立但存在一些奇怪的例外或当前已知 bug❌该不变量不成立且未来也不太可能成立不要为了 soundness 依赖它即使要依赖也必须极其小心。这些记号直接决定你在写编译器代码、lint 或类型级 hack 时可以把哪些性质当作前提。下面逐条展开。二、规范化与良构性wf(X)蕴含wf(normalize(X))✅不变量内容如果一个包含别名alias的类型是良构的well-formed即wf那么在对这些别名做规范化normalize之后得到的类型也应当是良构的。为什么需要编译器依赖这条性质来避免对规范化后的类型重新进行良构性检查。如果规范化可能制造出非良构的类型编译器就必须处处防御性重查代价极高。当前状态✅ 但实际上被打破。该文档明确指出这条不变量当前因一个类型系统 unsoundness 而失效对应历史 issue #84533。也就是说理论上存在规范化前良构、规范化后非良构的边界情形。源码层面的关联规范化逻辑集中在新求解器的 compiler/rustc_next_trait_solver/src/solve/normalize.rs它在求解目标时对关联类型、AliasTy等进行展开。你可以在 compiler/rustc_trait_selection/src/solve/normalize.rs 看到 trait selection 侧的规范化封装——两处共享同一套规范化后必须重新检查良构性的推理前提。三、结构性相等模 region蕴含语义相等 ✅不变量内容取某个类型把左右两侧的 region生命周期全部替换为新鲜的推理变量unique inference variables再将其与自身做相等判定——此时虽然结构上可能已经不同但两者仍然必须相等。为什么需要这条不变量用于防止目标在 HIR typeck 阶段成功、却在 MIR borrowck 阶段失败。如果它被破坏MIR typeck 最终会以 ICEInternal Compiler Error崩溃文档明确给出该结论。当前状态✅ 基本成立但依赖上述前提不越界。实践含义HIR typeck 与 MIR borrowck 是两个独立阶段region 信息会丢失或重造。任何依赖具体 region 身份才能成功的类型判定都会在阶段切换时翻车。这也是 compiler/rustc_hir_typeck 与 compiler/rustc_mir_build 之间长期要维护的隐性契约。四、应用推理结果不应改变目标结果 ❌不变量内容如果我们证明某个目标goal/完成某组类型相等判定把产生的推理约束inference constraints应用回去再重做原来的动作——结果应当保持一致。为什么需要这条性质保证求解的幂等性避免出现先成功、应用约束后反而失败的自相矛盾。当前状态❌不成立至少在下一代求解器next-generation trait solver中不成立。文档还留了一个 TODO希望这个检查只在出现求解器 bug 时失败并计划重新加回该断言We should readd this check and see where it breaks :3。源码证据这份 TODO 的落点已经可以从当前仓库中直接看到。在 compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rs#L802-L810 处evaluate_goal在instantiate_and_apply_query_response应用完查询响应后有一段 FIXME 注释注释明确记录此前这里有一个断言用于检查对目标应用其约束后重新计算响应不应改变该断言被移除原因是它对把推理变量约束为递归别名recursive alias的目标不成立复现用例指向测试 tests/ui/traits/next-solver/overflow/recursive-self-normalization.rs结论是待trait-system-refactor-initiative相关议题定案后应重新加回断言。这正是文档所述应用推理结果不改变结果不变量在源码中的第一现场也是研究求解器稳定性最值得关注的断点之一。五、trait 求解器必须局部可靠✅不变量内容求解器绝不能对不存在任何impl的目标返回成功。否则就等于假设某个 trait 被实现了而实际上没有——这极可能导致真正的 unsoundness。当使用where约束证明目标时impl由该 item 的调用者提供。当前状态✅ 基本成立但有一个已知例外coherence 中的隐式负重叠检查implicit negative overlap check阶段不检查 region 约束因此该不变量在那里被打破。原因在于该检查依赖 trait 求解器的完全性completeness而完全性要求无法使用当前的 region 约束检查——InferCtxt::resolve_regions——因为它对类型 outlives 目标type outlives goals的处理不完整。源码证据resolve_regions的实现位于 compiler/rustc_infer/src/infer/mod.rs其 outlives 相关逻辑在 compiler/rustc_infer/src/infer/outlives/mod.rs隐式负重叠检查的整体背景可参考 Coherence 章节coherence 检查负责检测 trait impl 与固有 impl 之间的重叠其中隐式负 impl 检查impl_intersection_has_impossible_obligation不依赖负 impl 即可判定不可能重叠是稳定可用的机制。实践含义在写 trait 求解逻辑时返回成功是一个需要反复掂量的动作凡是依赖 region 约束才能成立的证明在 coherence 的隐式负检查路径上都不可信。六、空环境下语义相等的别名规范化结果唯一 ✅不变量内容别名类型/常量的规范化normalization必须有唯一结果。否则我们很容易在 safe 代码中实现transmute。给定下面的函数必须保证输入类型与输出类型总是被规范化到同一个具体类型fn fooT: Trait( x: T as Trait::Assoc ) - T as Trait::Assoc { x }当前状态✅ 视为必要但文档直言许多已知的 unsound 问题最终都依赖这条不变量被打破。同时文档强调很难想象一个没有这条不变量的健全类型系统所以问题在于不变量被打破而不是我们错误地依赖它。实践含义这是对别名规范化最严苛的一条要求。foo这种对称签名如果两侧规范化结果不同safe 代码就能借类型别名搬运内存布局形成 transmute 漏洞。新求解器在 compiler/rustc_next_trait_solver/src/solve/normalize.rs 中维护规范化缓存其正确性直接承载这条不变量。七、类型系统不是完全的 ❌不变量内容类型系统是完全的——即只要目标在逻辑上可证求解器就一定能证出来。当前状态❌不成立。求解器经常添加不必要的推理约束甚至在目标本可成立时报错。文档列举了主要的不完全性来源方法选择method selection不透明类型推断opaque type inference类型 outlives 约束的处理在 trait 求解器的候选项选择中ParamEnv候选项优先于Impl候选项实践含义这条 ❌ 不变量解释了 Rust 中大量编译器报错但其实逻辑上说得通的现象。它也是第 12 条消除歧义让更多代码可编译不成立的直接原因见下文。八、目标在 HIR typeck 之后保持其结果 ✅不变量内容一个目标如果在 HIR typeck 期间成功那么若在 MIR borrowck 重新求值时失败 → 触发 ICE文档给出 issue #140211 作为例子若实例化instantiate之后失败 → unsoundness文档给出 issue #140212 作为例子。当前状态✅ 基本成立。文档特别指出有意思的是我们允许 trait 求解器存在一定的不完全性却仍然维持这条限制。理想情况是能清晰地区分被允许的不完全性与会破坏该不变量的行为。子条款一规范化不得改变结果该不变量被依赖来允许泛型别名的规范化。破坏它很容易导致 unsoundness对应 issue #57893。子条款二实例化后目标仍可能溢出当目标开始触及递归深度限制recursion limit时就会发生溢出。文档还提到存在发散别名diverging aliases这类棘手情形并直言目前不清楚应如何处理这些情况。源码证据递归深度限制相关的求解器防御逻辑位于 compiler/rustc_next_trait_solver/src/solve/eval_ctxt/mod.rsevaluate_goal的递归入口与深度追踪溢出场景的测试可参考上文提到的 tests/ui/traits/next-solver/overflow/recursive-self-normalization.rs。九、空环境下 trait 目标由唯一 impl 证明 ✅不变量内容如果一个 trait 目标在空环境下成立那么应当有唯一的impl用户自定义或内建用来证明该目标。这是选择唯一方法method和关联项associated item的必要条件。当前状态✅ 基本成立但存在几种已知的打破情况有些是 bug有些是设计使然marker traits允许重叠因为它们没有关联项specialization特化允许特化 impl 与其父 impl 重叠内建的 trait object trait 实现可能与用户自定义 impl 重叠对应 issue #57893。实践含义这条不变量与方法解析唯一性直接挂钩。空环境下如果出现两个可用的 impl编译器就无法唯一确定方法语义marker trait 与特化是刻意的例外而 trait object 的内建实现与用户 impl 重叠则属于需要警惕的 bug 面。十、非空环境中可证的目标在单态化时仍成立 ✅不变量内容如果一个目标在泛型环境generic environment中可证那么在把它实例化为完全具体类型、且作用域内没有任何 where 子句之后该目标仍然应当成立。为什么需要codegen 直接假设这一点——它在遇到非溢出的歧义non-overflow ambiguity时会直接 ICE。当前状态✅ 基本成立但目前被两个因素打破specialization对应 issue #147507marker traits对应 issue #149502。子条款coherence 隐式负重叠检查期间类型系统必须完全 ✅关于重叠检查的完整背景请参阅 Coherence 章节。不变量内容在 coherence 的隐式负重叠检查期间对可以证明的目标绝不允许返回 error。否则会允许带有潜在不同关联项的重叠 impl进而破坏一系列其他不变量。当前状态✅ 名义上成立但文档直言这条不变量在许多方面实际上已被打破而它恰恰是我们依赖的东西并提醒它非常容易被破坏例如别名的泛化generalization of aliases子类型化绑定器subtyping binders期间的泛化好在 coherence 中不可利用。实践含义coherence 检查是整个类型系统里完全性要求最高的环节。隐式负重叠检查必须乐观——只要目标可能成立就不能断定两个 impl 不重叠。对比如 coherence.md 中的例子Boxdyn Error: FromMyLocalType与Boxdyn Error: From?EE: Error能否共存取决于MyLocalType: Error是否可证由于孤儿规则保证下游 crate 无法为本地类型实现远程 trait这个目标被判定为不可能从而允许两个 impl 并存。十一、trait 求解不得依赖生命周期不同 ✅不变量内容如果一个目标在生命周期互不相同时成立那么在把这些生命周期视为相同时也必须成立。否则会在 codegen 阶段产生 post-monomorphization 错误或由于无效的 vtable 导致 unsoundness还可能出现前后不一致的行为——先用不同的生命周期证明目标之后这些生命周期又被约束为相等。当前状态✅ 基本成立。实践含义这是对 region 处理单调性的要求region 的合并equating不应使已成立的目标失效。任何依赖具体 region 身份差异的证明都会在 region 擦除erase后的 codegen 阶段暴露问题。十二、函数体内求解不得依赖生命周期相同 ✅不变量内容与上一条互补——在函数体内trait 求解同样不能依赖 region 的相等性。对 codegen 来说这没问题所有擦除后的 region 都视为相等但从 HIR 到 MIR typeck 的过程中可能会丢失相等性信息。当前状态✅ 名义上成立但文档明确指出在新求解器中目前不成立对应 trait-system-refactor-initiative 的 issue #27。实践含义这是新求解器rustc_next_trait_solver当前已知的薄弱点之一。如果你在 HIR typeck 中依赖某两个 region 相等来证明目标MIR typeck 阶段可能不再拥有这条信息导致阶段间结果漂移。十三、消除歧义应让更多代码可编译 ❌不变量内容理想情况下我们不应该依赖歧义ambiguity来让代码通过编译——即消除歧义应当使更多代码可编译而非更少。为什么需要如果现有代码依赖歧义才能编译那么未来的改进如更精确的推断将变成破坏性变更breaking change。当前状态❌不成立。由于不完全性见第七条实际情况是改进推断可能导致推理结果变化从而破坏现有项目。实践含义这条 ❌ 不变量解释了为什么 Rust 编译器团队对推断改进如此谨慎——让更多代码通过编译的修复常常同时让依赖旧歧义行为的代码编译失败。它也是 Rust 类型系统演进中兼容性压力的根源之一。十四、语义相等蕴含结构相等 ✅不变量内容两个类型在类型系统中相等必须意味着它们在用具体实参实例化泛型参数后具有相同的TypeId。否则我们可以利用它们不同的TypeId影响 trait 选择。当前状态✅ 基本成立。文档补充说明codegen 阶段使用结构相等structural equality查找类型这本身不一定是 unsound——但可能导致冗余的方法 codegen 或后端类型检查错误CTFE编译期求值断言也依赖这条不变量。源码证据TypeId相关的哈希与比较逻辑位于 compiler/rustc_middle/src/ty/util.rs其中type_id_hash#L134负责生成类型的TypeId哈希结构相等查找则散布在 compiler/rustc_middle 的Ty比较基础设施中。十五、语义不同的类型必须有不同的TypeId✅不变量内容语义不同的static类型需要不同的TypeId以避免 transmute。例如fora fn(a str)与fn(static str)必须拥有不同的TypeId——尽管在擦除后它们的结构可能相同。当前状态✅ 基本成立。实践含义TypeId是Any::downcast、TypeId::of::T()等机制的安全根基。若两个语义不同的函数指针类型得到相同TypeIdsafe 代码即可实现跨类型强制转换构成 transmute 通道。这条不变量与第 14 条构成一对双向约束语义相等 ⇒ 结构相等同TypeId语义不同 ⇒TypeId不同。十六、const 项的求值是确定性的 ✅不变量内容const 项const items的值可以反馈进类型系统因此每个 crate 中 const 项的值必须始终相同。否则我们可能得到相等的关联类型带有相等的 const 实参却在不同 crate 的 codegen 规范化时变成不同的类型。重要边界这条不变量不适用于 const 函数const functions。因为类型系统只使用 const项的最终结果只要不影响到某个 const 项的最终值const 函数本身非确定性是可以接受的。实践含义const 求值const eval的结果是跨 crate 共享的类型级事实。如果你实现了一个看似确定性但实际依赖环境或未定义行为的 const 计算它可能在 A crate 与 B crate 中得到不同结果从而让相等的关联类型在链接后分道扬镳——这是典型的隐蔽 UB 来源。十七、实战启示在 rustc 开发与类型级编程中如何使用这份清单对编译器开发者区分 ✅ 与 ❌ 是写代码的第一前提标 ❌ 的不变量如应用推理结果不改变结果类型系统完全消除歧义使更多代码可编译在提交新求解器逻辑时不能作为健全性论证的前提关注 FIXME/TODO 现场文档中两条 TODO/FIXME重加evaluate_goal断言、澄清应用推理结果表述在源码中均有对应位置——eval_ctxt/mod.rs#L802-L810 是验证求解器幂等性的最佳埋点coherence 路径要乐观任何在隐式负重叠检查中过早返回 error的改动都会打破第 10 条的完全性子条款属于高危改动。对使用 nightly / 编写类型级代码的开发者不要依赖 region 身份差异第 11、12 条泛型代码中尽量让 trait 目标在 region 相同与不同两种情况下一致成立警惕别名规范化不一致第 6 条涉及T as Trait::Assoc的对称签名代码如果编译器行为异常可对照该不变量排查TypeId边界第 14、15 条static类型间TypeId的区分是安全代码的隐形护栏任何擦除后结构相同的类型混用都应视为危险信号。相关源码速查表不变量源码位置测试/文档佐证应用推理结果不改变结果❌eval_ctxt/mod.rs#L802-L810recursive-self-normalization.rs局部可靠性 / region 约束检查infer/mod.rs、infer/outlives/mod.rscoherence.md别名规范化next_solver/solve/normalize.rs、rustc_trait_selection/src/solve/normalize.rs本文第 2、6 条TypeId哈希rustc_middle/src/ty/util.rs#L134本文第 14、15 条隐式负重叠检查 / coherencecoherence.md本文第 5、10 条结语Rust 类型系统的这 15 条不变量既是编译器内部的工程契约也是理解为什么某些 Rust 代码能编译、另一些不能的底层透镜。标注 ✅ 的不变量值得信任但要知道其边界标注 ❌ 的不变量必须放弃依赖或在使用时极度谨慎。值得注意的是文档反复强调这份清单不完整且非官方而源码中的 FIXME、被移除的断言、以及 eval_ctxt/mod.rs 里等待重加的检查都说明这份清单正在随新求解器的演进持续变化——跟踪这些 FIXME 的走向就是在跟踪 Rust 类型系统未来的稳定性版图。【免费下载链接】rustEmpowering everyone to build reliable and efficient software.项目地址: https://gitcode.com/GitHub_Trending/ru/rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考