2023 年 1 月,Rust 官方博客发布了一篇公告,正式宣布 Types Team(类型团队) 的成立。
说"正式宣布",是因为这个团队其实早在 2022 年 5 月底就已经悄然运作了——只是一直没有对外说过。这次公告,是在团队完成了一次为期三天的线上线下混合会议之后,借着年初的时机,向社区汇报:我们是谁、做了什么、接下来要去哪里。
本文就是基于 Rust 官方博客 2023 年 1 月 20 日发布的《Officially announcing the types team》整理撰写,作者为 Jack Huey,代表 The Types Team。
两个数字,说明问题有多严重
公告开门见山地列出了两个数字,解释为什么需要成立这样一个团队。
220 个:Rust 目前有约 220 个已接受但尚未完整实现的语言、编译器或类型系统特性的追踪 Issue,其中约一半已经存在了至少三年,许多更是积压了更久。这些 Issue 之所以久拖不决,不单是人手不足的问题,更根本的原因是:在 Rust 这样复杂的语言里,想把一个特性的语义和整体语言上下文对齐,本身就极其困难。往往没有人能一眼看出"要把这件事做完,具体该做什么"。
62 个:目前有 62 个未解决的健全性漏洞(unsoundness issues)。听起来很吓人,但公告也做了说明:这些几乎都是在极端边界条件下才会触发的问题,是专门找 bug 的人刻意去戳才能发现的;正常写代码几乎不会踩到。尽管如此,这些漏洞不应该存在,而修复它们一直没有一个明确的归口。
这两个数字共同指向一个结论:Rust 的类型系统需要一个专职团队来系统性地治理。
类型系统到底管哪些东西?
公告把 Types Team 的职责范围划定在三个紧密相关的组件上:
- 类型检查器(Type Checker):负责确定变量的类型,以及类型之间的关系是否合法;
- Trait 求解器(Trait Solver):负责回答"类型 T 是否满足某个 trait 约束"这类问题;
- 借用检查器(Borrow Checker):负责验证 Rust 的所有权模型在任何情况下都成立。
这三者合在一起,就是 Rust 的"类型系统"。它们在实现上深度耦合,往往牵一发而动全身,独立处理任何一个都很难不影响其他两个。
这个边界由 RFC 3254 正式确立,Types Team 对这个范围内的设计和实现拥有完整的决策权。
两个父团队,一项特殊授权
Types Team 在 Rust 的治理结构中有一个独特的位置:它同时拥有两个父团队——语言团队(Lang Team)和编译器团队(Compiler Team),这在 Rust 的团队体系中是独一份的。
这两个父团队分别向 Types Team 做了不同的授权:
- 语言团队授权 Types Team 负责类型系统的设计——不是"这个特性用起来顺不顺手"这类体验层面的事,而是"这个机制在类型系统内部是怎么运作的"这类底层语义的决策;
- 编译器团队授权 Types Team 负责 trait 系统实现层面的定义和维护。
更重要的是一项额外授权:健全性漏洞的修复。过去,修复一个涉及类型系统的健全性 bug,需要同时协调语言团队和编译器团队,流程繁琐。现在,Types Team 在大多数情况下可以独立完成评估和修复,无需两个父团队同步参与。
即便某个修复在技术上会破坏向后兼容性(修复安全漏洞本不在 Rust 的兼容性承诺范围内),类型团队也可以经过内部签字确认,并用 Crater 工具评估对生态系统的影响后,自行推进——而不需要跨团队拉会。这让关闭健全性漏洞这件事变得切实可行。
Chalk 的终结,以及两条新路
Types Team 公告中最让人瞩目的技术决策,是对 Chalk 命运的宣判。
Chalk 是一个从 2015 年就开始开发的实验性 trait 求解器。它的思路是把 Rust trait 系统的语法和语义翻译成一套类似 Prolog 的逻辑规则,用逻辑求解器来处理 trait 查询,然后替换编译器内部的旧求解器。Rust-analyzer 今天仍在使用 Chalk。
但经过多年的开发,团队得出了一个结论:Chalk 很可能不是 Rust 的长期解决方案。原因有两点:
第一,trait 求解器只是整个类型系统的一部分。如果只对这一部分单独建模,就很难把它和类型检查器、借用检查器之间的关系描述清楚;而对整个类型系统统一建模,才能得到完整的图景。
第二,编译器和形式化的需求其实是两件不同的事。编译器需要高性能,还要能追踪足够多的信息来生成有用的错误提示;而一个好的形式化模型,需要的是完整、易读、易维护。Chalk 多年来试图同时满足这两者,结果两者都没有做好。
于是,团队走向了两条并行的新路:
a-mir-formality:这是一个对 Rust 整个类型系统进行形式化描述的项目。它的目标不是直接跑在生产编译器里,而是成为评估新特性设计和健全性漏洞的"参考实现"——在一个干净的模型上验证想法,然后再把结论同步给实际的编译器。项目最初用 PLT Redex 编写,Rust 移植版本正在进行中。
新的内置 Trait 求解器(New In-Tree Trait Solver):这是一个直接在 rustc 代码树内编写的新 trait 求解器,目标是最终替换掉现有的旧求解器。它的范围比 a-mir-formality 小,设计上会尽可能复用现有编译器的基础设施,以减少迁移代价。为了将来可能被独立拆出,它从一开始就被设计为尽可能模块化。
已经完成的健全性修复
在去年 5 月到公告发布的约七个月内,团队已经落地了一系列健全性修复:
- Rust 1.65 修复了隐含边界(implied bounds)在非规范化类型下的处理,未发现兼容性回归;
- Rust 1.66 修复了不透明类型(opaque type)上不应存在的隐式生命周期约束,未发现兼容性回归;
- Rust 1.68 将引入
IMPLIED_BOUNDS_ENTAILMENTlint,标记一类发现了大量历史代码回归的不健全模式,以"未来兼容性警告"的方式给出过渡期; - 另有若干修复正在推进中,部分因发现兼容性回归而需要额外工作。
这些数字说明了一件事:修复健全性问题并不总是无痛的。有时确实会让旧代码无法编译,即使那些代码从技术上讲本来就不应该通过编译。团队的做法是尽量给出过渡期,并用 Crater 提前评估影响范围。
正在推进的新特性
公告还列出了几个与类型系统深度绑定、类型团队深度参与的特性:
GATs(泛型关联类型):这是列表里唯一一个已经稳定的特性,于 Rust 1.65 稳定。GATs 早在类型团队成立之前就存在,但稳定化最后阶段的推进工作由团队主导完成。
TAITs(类型别名 impl Trait):正确实现这个特性需要对类型检查器有深入的理解。公告时点已接近稳定。
Trait 向上转型(Trait Upcasting):允许把一个 trait 对象向上转型为它的父 trait,改动不大但涉及类型系统交互。
否定 impl(Negative Impls):允许显式声明某个类型不实现某个 trait。还有若干 bug 和健全性问题未解决,距稳定还有距离。
RPITITs 和 AFITs:分别是"在 trait 方法中返回 impl Trait"和"在 trait 方法中使用 async fn"。这两个特性依赖 GATs 和 TAITs 的基础,目前一起追踪。
分四阶段的路线图
公告给出了一张具体的时间表,把目标切成了四段:
2023 年夏季(6 个月内)
- 新 trait 求解器达到可测试状态;
- a-mir-formality 能够针对 Rust 测试套件运行;
- TAITs 和 RPITITs/AFITs 稳定,或走上稳定路径。
2023 年底
- 新 trait 求解器替换旧求解器的部分功能(不是全部);
- 新求解器有完整的入门文档;
- a-mir-formality 正式纳入语言设计流程。
2024 年底
- rustc 和 rust-analyzer 共用同一个新 trait 求解器;
- 可扩展的 trait 错误 API 在内部可用;
- Polonius 借用检查器达到可用状态;
- 更高阶 trait 约束中的隐含边界问题得到解决;
impl Trait基本上可以出现在任何应该出现的地方。
2027 年底
- 类型系统相关的健全性问题全部解决;
- 大多数语言扩展变得容易实现,大型扩展变得可行;
- a-mir-formality 通过 Rust 测试套件的 99.9%。
一个团队的成立,意味着什么
这篇公告读下来,有一点很值得注意:Types Team 的成立,首先解决的不是技术问题,而是组织问题。
过去,类型系统相关的决策需要在语言团队和编译器团队之间反复协调。一个健全性 bug 的修复,往往因为归口不清而搁置。一个新特性的推进,往往因为没有明确的负责人而在追踪 Issue 里沉睡。
现在,这些问题有了一个明确的主人。设计、实现、健全性修复——在这个范围内,Types Team 说了算,不需要每次都拉两个团队协调。
这件事的重要性,可能不比任何一个具体的技术突破低。