假设弱化性质
摘要
本文探讨了为什么在规范或测试中添加假设会从逻辑上弱化所得性质,使用了逻辑蕴含以及来自形式化方法和 Rust 的示例。此外,还讨论了尽管存在这种弱化,仍使用假设的实际原因。
<p>在<a href="https://buttondown.com/hillelwayne/archive/some-tests-are-stronger-than-others/" target="_blank">有些测试比其他测试更强</a>一文中,我将 <code>STRONG => WEAK</code> 定义为“任何通过测试 STRONG 的系统也保证能通过 WEAK”。这使用了逻辑蕴含运算符,定义为 <code>P => Q = !P || (P && Q)</code>。</p>
<p>蕴含可能是逻辑中被过度使用的运算符。除此之外,它还用于形式化规范,其中 <code>Spec => Prop</code> 表示“任何满足 <code>Spec</code> 的系统都具有属性 <code>Prop</code>”,而 <code>ASSUME => Spec</code> 表示“假设 <code>ASSUME</code> 必须成立,系统才能满足 <code>Spec</code>”。</p>
<p>现在我们把它们混在一起做些数学推导。首先,“系统具有属性 <code>Prop</code>”等同于“系统通过了检查 <code>Prop</code> 的测试”,所以测试强度也是属性强度。现在让 <code>ASSUME => Prop</code> 表示“假设 <code>ASSUME</code> 为真时系统通过 <code>Prop</code>”。在经典逻辑中,如果 <code>P</code> 为真,那么显然 <code>!Q || P</code> 为真。进一步,这等价于(只需画出真值表!)<code>!Q || (P && Q)</code>。因此对于<em>任何</em>命题 <code>P</code> 和 <code>Q</code>,有 <code>P => (Q => P)</code>。</p>
<p>换句话说,<code>Prop => (ASSUME => Prop)</code>。再换句话说,“系统通过 <code>Prop</code>” 是一个比“只要我们的假设成立,系统就通过 <code>Prop</code>”更强的性质。</p>
<p>再再换句话说,任何添加的假设都会使性质变弱。</p>
<p>这对我来说很直观。一个只经过 ASCII 字符串验证的 JSON 解析器具有的性质是“输入仅使用 ASCII && 是有效 JSON => 正确解析”。一个更好的、适用于所有 Unicode 的 JSON 解析器将具有“是有效 JSON => 正确解析”的性质,它假设更少,意味着保证在严格超集的情况下工作。</p>
<p>这也符合“假设越多,出错可能性越大”的直觉。当 <code>Prop</code> 为假时我们遇到 bug。<code>Spec => Prop</code> 为真而 <code>Prop</code> 为假的唯一情况是 <code>Spec</code> 为假,例如我们的系统没有满足我们打算实现的规范。另一方面,当 <code>Spec</code> <em>和/或</em> <code>ASSUME</code> 为假时,<code>Spec => (ASSUME => Prop) && !Prop</code> 为真,这意味着即使正确实现的系统,如果运行时假设为假,也可能显示 bug。</p>
<p>...回顾一下,最后两段有不少概念跳跃。你觉得有道理吗?对我来说一切都很自然,但这可能只是我熟悉这个话题的缘故。</p>
<p>无论如何,关于假设还有几点说明:</p>
<h2>我们为何使用假设</h2>
<p>为什么不在我们的系统中满足 <code>ASSUME => Prop</code>,而是“直接”满足 <code>Prop</code>?至少三个原因。</p>
<p><strong>第一</strong>,有时 <code>Prop</code> 根本无法满足,我们需要添加假设来使这一性质成立。我们在形式化方法中经常通过<a href="https://www.hillelwayne.com/post/fairness/" target="_blank">公平性</a>这样做。性质“<code>mergesort</code> 总是返回排序后的列表”是不可满足的,因为我们可以在它返回前把电脑踢飞。相反,我们必须验证一个较弱的性质,比如“<code>mergesort</code> 总是返回 => 它返回排序后的列表”或“<code>mergesort</code> 总是取得进展 => 它最终会返回排序后的列表”。</p>
<p>另一个例子是 Rust。Rust <em>并不</em>保证性质“程序是内存安全的”。它保证较弱的性质“所有 <code>unsafe</code> 块都是内存安全的 => 程序是内存安全的”。让语言满足更强的性质会排除 Rust 的太多用例。注意,如果你不使用 <code>unsafe</code>,你也会获得内存安全,但这仍然满足假设,因为所有<em>零</em>个块都是安全的!</p>
<p><strong>第二</strong>,有时强性质是可满足的,但额外成本不值当。比如让软件抗宇宙射线是可能的,但如果你的代码不是在太空运行,何必呢?只需说“没有宇宙位翻转 => 一切正常工作”。或者,如果你的插件适用于 Neovim <code>0.12</code> 但不适用于 0.11,你可能花功夫让它兼容旧版本,或者告诉大家需要升级才能使用你的插件。“Neovim 版本至少为 0.12 => 插件正常工作”。</p>
<p><strong>第三</strong>,有时强性质在系统中是可满足的,但不容易<em>验证</em>。比如你的算法做了很多 API 调用,测试时不想触发速率限制。如果你<a href="https://en.wikipedia.org/wiki/Mock_object" target="_blank">模拟 API</a>,你测试的是较弱的性质“模拟对 API 足够准确 => 算法正确”。</p>
<h2>假设是系统效应的第二层</h2>
<p>我注意到最后两节中的几乎所有例子都是外部程序因素:</p>
<ul>
<li>JSON 解析器的假设是关于用户输入的</li>
<li>公平性假设是关于操作系统/硬件/运行环境的</li>
<li>Unsafe 假设是关于 Rust 编译器无法验证的事情</li>
<li>宇宙射线假设取决于硬件的物理位置</li>
<li>插件假设是关于 Neovim 团队的发布计划和社会兼容性契约</li>
</ul>
<p>边缘情况是用模拟替换第三方调用。这个假设是程序内部的,因为程序在测试中可以直接调用 API。我们仍然有这个假设是因为一个外部程序约束。也许这就是为什么模拟被视为敏捷开发中的反模式。</p>
<p>一个后果是,检查假设是否成立与验证代码在假设下是否正确工作是不同的问题。例如,确保“所有 unsafe 块都是安全的”不能使用 Rust 编译器,你需要第二个工具如 <a href="https://github.com/rust-lang/miri" target="_blank">Miri</a>。我想知道在实践中检查假设是否通常比检查其他所有事情更困难。</p>
查看缓存全文
缓存时间: 2026/05/20 15:36
# 假设削弱属性
来源:https://buttondown.com/hillelwayne/archive/assumptions-weaken-properties
在[有些测试比其他的更强](https://buttondown.com/hillelwayne/archive/some-tests-are-stronger-than-others/)一文中,我将 `STRONG => WEAK` 定义为“任何通过 STRONG 测试的系统也保证通过 WEAK 测试”。这里使用了逻辑蕴含运算符,其定义为 `P => Q = !P || (P && Q)`。
蕴含可能是逻辑中被过度使用最多的运算符。在其他场合,它也用在形式规约中,其中 `Spec => Prop` 表示“任何满足 `Spec` 的系统都具有属性 `Prop`”,而 `ASSUME => Spec` 表示“为了让系统满足 `Spec`,必须满足假设 `ASSUME`”。
现在让我们把这些概念揉在一起,做点数学推导。首先,“系统具有属性 `Prop`”等同于“系统通过了检查 `Prop` 的测试”,所以测试强度也就是属性强度。现在让 “`ASSUME => Prop`” 表示“在假设 `ASSUME` 为真的前提下,系统通过 `Prop`”。在经典逻辑中,如果 `P` 为真,那么显然 `!Q || P` 也为真。更进一步,这等价于(画一下真值表就知道了!)`!Q || (P && Q)`。因此,对于**任何**命题 `P` 和 `Q`,都有 `P => (Q => P)`。
换句话说,`Prop => (ASSUME => Prop)`。再换句话说,“系统通过 `Prop`” 是一个比“只要我们的假设成立,系统就通过 `Prop`” 更强的属性。
再再换句话说,任何添加的假设都会使属性变弱。
这对我来说很直观。一个仅用 ASCII 字符串验证过的 JSON 解析器具有属性“输入仅使用 ASCII && 是有效 JSON => 正确解析”。一个更好的、支持全部 Unicode 的 JSON 解析器具有属性“是有效 JSON => 正确解析”,后者假设更少,因此保证在更广泛的情况下工作。
这也符合直觉:“假设越多,越容易出错”。每当 `Prop` 为假时,我们就遇到一个 bug。`Spec => Prop` 为真而 `Prop` 为假的唯一情况是 `Spec` 为假,例如我们的系统没有满足我们打算实现的规约。另一方面,只要 `Spec` **和/或** `ASSUME` 为假,`Spec => (ASSUME => Prop) && !Prop` 就为真,这意味着一个正确实现的系统依然可能因为运行时假设不成立而暴露 bug。
……回顾一下,最后两段有很多概念上的跳跃。你都能理解吗?对我来说这一切都很自然,但这可能只是因为我熟悉这个话题。
无论如何,关于假设还有几点补充:
## 为什么我们有假设
为什么不直接构建系统使其满足 `ASSUME => Prop`,而是“仅仅”满足 `Prop`?至少有三个原因。
**第一**,有时 `Prop` 根本无法满足,我们需要添加假设来使这个属性成立。我们在形式方法中经常使用[公平性](https://www.hillelwayne.com/post/fairness/)来做这件事。属性“`mergesort` 总是返回排序后的列表”是不可满足的,因为我们可以在它返回之前把电脑踢飞。相反,我们必须验证一个更弱的属性,比如“`mergesort` 总是返回 => 它返回排序后的列表”或“`mergesort` 总是在推进 => 它最终会返回排序后的列表”。
另一个例子是 Rust。Rust **并不**保证“程序是内存安全的”这一属性。它保证的是更弱的属性:“所有 `unsafe` 块都是内存安全的 => 程序是内存安全的”。让语言满足更强的属性会排除 Rust 太多用例。注意,如果你不使用 `unsafe`,你也能获得内存安全,但这仍然满足假设,因为零个块都是安全的!
**第二**,有时强属性是可满足的,但不值得付出额外代价。比如,让软件抵抗宇宙射线是可能的,但如果你的代码不跑在太空里,何必呢?就说“没有宇宙射线导致的比特翻转 => 一切正常”。或者,如果你的插件在 Neovim `0.12` 上运行但不在 `0.11` 上,你可以努力让它兼容旧版本,或者告诉大家需要升级才能使用你的插件:“Neovim 版本至少为 `0.12` => 插件正常工作”。
**第三**,有时强属性在系统中是可满足的,但不容易**验证**。假设你的算法调用了很多 API,而你不想在测试时触发速率限制。如果你[模拟 API](https://en.wikipedia.org/wiki/Mock_object),你测试的是更弱的属性:“模拟对象与 API 一致 => 算法正确”。
## 假设是系统效应的第二层级
我注意到,前两节中的几乎所有例子都是程序外因素:
- JSON 解析器的假设是关于用户输入的
- 公平性假设是关于操作系统/硬件/运行环境的
- Unsafe 假设是关于 Rust 编译器无法验证的事情
- 宇宙射线假设取决于硬件的物理位置
- 插件假设是关于 Neovim 团队的发布计划和社会兼容性契约的
边缘情况是用模拟替代第三方调用。这个假设是程序内的,因为程序在测试时可以直接调用 API。但我们仍然有假设,因为程序外约束。也许这就是模拟被视为敏捷反模式的原因。
这样做的一个后果是,检查假设是否成立,与验证你的代码在假设下是否工作,是两个不同的问题。比如,确保“所有 `unsafe` 块都是安全的”不能用 rust 编译器,你需要像 [Miri](https://github.com/rust-lang/miri) 这样的第二个工具。我想知道在实践中,检查假设是否通常比检查其他一切更困难?
相似文章
证明可能性
在形式化方法中解释可能性属性的概念,补充安全性和活性,并讨论它们在规范制定和模型检验中的使用。
里氏替换原则远超你的想象
本文探讨了里氏替换原则超越其常见解释的内涵,强调其基于子类型的形式化基础,涉及前置条件、后置条件、不变量和历史属性,并引用了原始研究论文。
规范并不存在
本文讨论了复杂软件系统中形式化规范的缺失,通过假设情景来强调计算机科学中形式化方法的挑战与重要性。
你必须修复你的断言
本文认为在生产环境中禁用断言是一种不好的实践,以Zig的断言机制为例,说明即使在生产版本中也应保持断言启用以捕获编程错误的好处。
LLM生成的SystemVerilog断言对语义保持RTL变换的鲁棒性
本文评估了LLM生成的SystemVerilog断言在语义保持RTL变换下的鲁棒性,发现点精度可能隐藏了显著的不稳定性,并倡导在AI辅助硬件验证中进行鲁棒性感知评估。