这篇文章列出了一份路线图,旨在尝试在 Rust 2024 版本中将 Polonius 稳定化。它确定了一些高层次的里程碑,并总结了关键目标以及最近的进展。
Polonius 背景
Polonius 指的是一些事情。它是一种新的借用检查器公式。它也是一个特定项目,基于 Datalog 实现了该分析。我们目前的计划不使用基于 Datalog 的实现,而是利用我们从中获得的经验,专注于在 rustc 中重新实现 Polonius。
Polonius 的动机示例是所谓的“问题案例 #3:跨函数的条件控制流”:此处,从一个条件中,从一个函数返回一个引用。
fn get_default<'r, K: Hash + Eq + Copy, V: Default>(
map: &'r mut HashMap<K, V>,
key: K,
) -> &'r mut V {
match map.get_mut(&key) { // -------------+ 'r
Some(value) => value, // |
None => { // |
map.insert(key, V::default()); // |
// ^~~~~~ ERROR // |
map.get_mut(&key).unwrap() // |
} // |
} // |
} // v
在 Some
路径中返回可变引用 value
需要 map
上的可变借用到函数结束。这会阻止 None
路径中的可变操作,即使 map
上根本不存在可变借用。
修复此 borrowck 问题需要更精确的流程敏感性。它还暗示了我们对生命周期建模的局限性,在控制流稍微复杂的情况下会更明显,例如 issue #47680
struct Thing;
impl Thing {
fn maybe_next(&mut self) -> Option<&mut Self> { None }
}
fn main() {
let mut temp = &mut Thing;
loop {
match temp.maybe_next() {
Some(v) => { temp = v; }
None => { }
}
}
}
当前的借用检查器会拒绝此代码。它这样做是因为它看到存在对 temp
的借用以调用 temp.maybe_next()
。它还看到此借用可以循环流动——特别是,该借用被 v
引用,然后存储到 temp
中。因此,当我们尝试在循环顶部可变借用 temp
时,它会报告错误,因为可能存在先前迭代的借用。
然而,更仔细的阅读表明,虽然借用可能循环流动,但它只在 Some
路径上流动,并且在该路径上 temp
被覆盖。这意味着我们将在下一次迭代中借用的 temp
实际上与我们在上一次迭代中借用的位置不同。同时,在 None
路径上,借用结束。
这种“逐个案例”的区分——看到流程仅发生在一个路径上,并且在该路径上存在重新赋值,这需要比当前借用检查器能够达到的更高的精度。
因此,像“NLL 问题案例 #3”、issue #47680 以及其他问题都被推迟到 NLL 之后,作为未来的工作,Polonius。
关键思想是
- 从生命周期作为 CFG 中一组点的模型(具有外生存关系),切换到起源作为一组借用的模型(具有子集关系)。
- 计算并跟踪控制流图中每个点的子集关系(而现有的 borrowck 计算单个子类型关系)。
里程碑
这是一个粗略的路线图,我们对第一步最了解
- 每一步都有未知数,这些未知数将定义在后续步骤中需要完成的事情
- 因此,我们更多地谈论长期路线图的里程碑,以及短期内的适当任务。
以下是路线图的里程碑
1. 从主路径中分解出高阶问题
今天,trait 求解器产生高阶外生存约束,而借用检查器会解决它们。将来,我们希望使下一个 trait 求解器负责自行解决这些高阶约束,以便它仅生成 Polonius 中使用的更简单的 subset
约束。这将使我们能够解决诸如 for<T> { if (T: 'a, 'a: 'b) { T: 'b } }
之类的蕴涵谓词,而无需有效地再次重现相同的 trait 求解逻辑。这篇博客文章更详细地描述了该问题和可能的解决方案。
在短期内,我们正在探索重构借用检查器,以将高阶处理与普通处理分开。目标是对 Polonius “泄漏检查”中的外生存约束进行预处理,我们可以在其中计算高阶错误。一旦 trait 求解器可以解决这些约束,就可以删除它。
当前状态:⏳ 类型团队的成员将在未来几天开始处理此任务。
2. 范围内的位置不敏感借用
在 Polonius 和现有借用检查之间(区域作为“借用集”,并在 CFG 中的每个点计算子类型关系)的两个关键差异中,此步骤旨在解决第一个差异,而不是第二个差异,因此我们将其称为“位置不敏感的范围内的借用”(因为子类型化仅完成一次,而不是每个位置):这个想法可以描述为“使用 Polonius 模型的 NLL”。
请注意,现有借用检查器的其他方面仍然是流程敏感的。
在此步骤中,我们将仅通过外生存约束来计算实时借用集,而不是计算区域实时的 CFG 点(然后用于计算借用何时超出范围)。我们认为这在报告的错误方面等同于现有的借用检查。
重要的是,此更改为添加位置敏感性铺平了道路(借用集也是未来改进借用检查器(例如安全内部引用)的更好基础)。
当前状态:✅ 我们已经完成了原型,并且有一个开放的 PR 以在 -Z
标志下添加此功能,这应该会在不久的将来发生。
3. 验证完整测试套件是否通过位置不敏感的 Polonius
该 PR 确实通过了我们套件中的全部 15000 多个测试,但我们尚未在 crates.io 上发布的 crate 中使用 crater 运行进行检查。
与我们的内部测试套件相比,绝大多数已发布的 crate 预计会在没有错误的情况下构建。在这方面,应该不太可能在那里发现问题,但无论如何都会完成。
当前状态:⏳ 正在进行中,crater 运行本身将在 PR 提交之前完成。
4. 使用位置不敏感的 Polonius 替换借用检查器的部分
原型仅执行其他工作,并且不修改现有分析。
在此步骤中,我们将重构借用检查器,使其数据结构存储借用集,并进行更多性能工作:例如,删除冗余计算,调查最坏情况下的可伸缩性和常数因子。
预计性能将相似,然后我们可以想象在没有功能标志的情况下启用位置不敏感的传递,并删除一些旧代码。
为了保持许多贡献者多年工作的优质诊断,新分析可能会运行,如果检测到错误,则仅使用现有分析和诊断。
当前状态:我们已经对所需的数据结构更改,可以删除的一些冗余部分等进行了一些早期调查。
5. 夜间构建中的位置敏感传递
然后可以开始合并位置敏感性(难以有效地完成)的工作。此步骤将实现该分析的第一个版本。
此时,它仍然可能效率低下,并使用功能标志,但是当借用检查器应该接受比当前 NLL 更具表现力的代码时。
当前状态:我们正处于此处的设计阶段,以使我们的 Datalog 原型和算法适应 rustc,设想沿着 CFG 计算和传播子集约束的替代方法。
6. 在 a-mir-formality 中对借用检查和 Polonius 进行建模
类型团队正在构建一个名为a-mir-formality
的 Rust MIR 和 trait 系统的模型。一旦它达到足够完整的状态,其意图是该模型将在稳定化之前始终扩展以覆盖新的语言功能。因此,我们正在努力将 Polonius 添加到模型中。这实际上是第二次进行此类建模,因为我们已经将 Polonius 添加到 a-mir-formality 的先前版本中。实际上,该建模工作为我们提供了使位置不敏感的 Polonius 公式现在可以在夜间构建中着陆的见解。
有趣的是,这项工作完全独立于 rustc,并且理论上可以很快开始,并与其他工作并行完成。
7. 位置敏感传递稳定
在此里程碑中,我们期望在优化和产品化方面进行大量工作。
如果再次出现类似于 2018 版本中 NLL(非词法生命周期)的体验,那么还需要进行大量的工作和润色,以处理诊断差异和问题,确保错误和提示足够清晰,并完善文档。
目前,位置敏感传递过程有望足够高效,经过实践检验,在某种程度上进行了形式验证,并可以在 2024 版本中启用。
大约在这个时候,也可以重启库化工作,将树内的 Polonius 转变为库,也许可以使用 稳定的 MIR。这样做是为了可以在其他地方复用它,例如在 rust-analyzer、gccrs 中,或者供研究验证工具的研究人员使用(例如 kani、prusti 和 creusot)。
结论
我们非常高兴看到 Polonius 的计划逐渐清晰。目前,由于我们仍在进行基础性工作,我们不寻求志愿者或贡献者,除非他们精通编译器。我们预计,随着项目的推进,将会有越来越多的新贡献需求。请关注更新!