一段只有 1919 个控制状态的图灵机程序,折算成 Python 也就 100 到 200 行,人类现有的数学基石(ZFC 公理系统)就已经永远无法证明它会不会停机。软件工程师 Ron Pressler 在尝试为新版 Galaxy 做形式化规范与验证时,被网上那种只要换个好工具就能轻松写出正确软件的论调惹毛了。他在 2016 年 7 月 23 日发出了一篇阅读时长约 45 分钟的硬核长文,把计算机科学的祖师爷们请出来挨个点名,核心判断只有一个,软件正确性的高昂代价就像热力学里降低熵增必须消耗能量一样,属于无法逃避的计算自然法则。

这盆冷水泼得够狠,但只泼对了一半。Pressler 把理论计算机科学里的最坏情况算得明明白白,却在程序语言理论(PLT)的逃生通道上滑了坡,也低估了工业界在不可判定性夹缝里套利的手艺。就在他写下这篇长文的前一年,AWS 团队已经在《Communications of the ACM》(58 卷第 4 期,66-73 页)上晒出了自 2011 年起的实战账本,7 个团队在 10 个复杂系统里用 TLA+ 和 PlusCal 配合 TLC 模型检验器捉虫,其中仅用 939 行 TLA+ 给 DynamoDB 建模,就揪出了一个潜伏 35 个高级步骤的致命数据丢失 Bug。

墓碑上的豪言与计算的热力学铁律

1930 年,数学家希尔伯特在哥尼斯堡发表退休演讲,喊出了后来刻在他墓碑上的名言,我们必须知道,我们必将知道。他想把所有数学形式化,做到一致、完备且可判定,也就是找到一个通用算法来判定任何形式化命题的真伪。讽刺的是,就在演讲前一天,哥德尔宣布了第一不完备定理,直接端掉了希尔伯特纲领的前两根柱子;到了 1936 年,邱奇和图灵各自证明了停机问题,把第三根柱子(可判定性)也彻底砸碎。

Pressler 顺着这条脉络,给指望全自动验证工具的程序员列出了一张绝望清单:

  • 停机问题与可达性.不存在通用算法能判定任意程序是否停机,进而也无法判定任意程序能否到达某个特定状态。
  • 莱斯定理(Rice's theorem):对于部分函数的任何非平凡语义属性,都不存在通用算法能判定任意程序是否具备该属性。哪怕只是想写个工具自动判断一段代码输出的是不是输入的平方,理论上都做不到。
  • 忙碌海狸函数(Busy Beaver):一个只有 6 个控制状态的双符号图灵机,停机前最多能跑超过 $7.4 \times 10^{36534}$ 步。无论动态运行还是静态推导,证明程序停机的最小证明长度函数,增长速度都超过任何可计算函数。
  • Cook-Reckhow 定理:当且仅当 NP=coNP 时,才存在对所有重言式都有多项式有界证明的命题证明系统。换句话说,只要 NP 不等于 coNP,哪怕你只是验证一个有限状态的程序属性,演绎证明的长度在最坏情况下也会发生指数级爆炸。

基於这些铁律,Pressler 抛出了两个极度悲观的推论,一个是语言抽象通常无法在验证中被利用,因为几行紧凑的高级代码背后照样是天文数字的状态空间;另一个是正确性无法分解,把代码拆成模块能让人类好读,但组件交互的验证依然是算力黑洞。

从数学梦碎到验证死胡同:理论极限的四道封锁 1930 希尔伯特纲领 完备与可判定 妄图找到判定一切 真伪的通用算法 哥德尔前一日爆破 1936 邱奇与图灵 停机问题 自我指涉对角线法 封死可达性判定 6状态机超10^36534步 语义属性推广 莱斯定理 非平凡语义属性 全自动判定归零 1919状态独立于ZFC Cook-Reckhow 证明长度爆炸 若 NP ≠ coNP 无多项式有界证明 通用银弹彻底破产

拿着莱斯定理当大锤,砸歪了哪颗钉子

Pressler 的理论科普写得极扎实,但他把可计算性理论当成大锤砸向现代强类型系统(比如依赖类型)和组合式验证时,却砸偏了准星。如果莱斯定理真的封死了一切,那今天的 Rust 借用检查器、Haskell 类型系统乃至 Idris、Lean 这些定理证明器,难道都是违背热力学的永动机?

问题出在他把对任意程序既可靠又完备的自动判定,直接等同于了工程上的软件验证。程序语言理论学者看这篇长文,一眼就能挑出三个逻辑滑坡。

第一个滑坡是忽略了不完备性逃生通道。莱斯定理只禁止同时做到可靠(Sound,没报错就一定安全)和完备(Complete,安全的程序一定不报错)。所有的静态类型检查器干的都是同一笔交易,死保可靠性,主动放弃完备性。编译器只放行一个受限的程序子集,宁可错杀一万个实际上安全但写法刁钻的合法程序,也绝不放过一个类型错误的程序。只要肯拒绝好程序,莱斯定理的封锁线当场失效。

第二个滑坡是混淆了证明搜索与类型检查。Pressler 看到依赖类型可能产生无穷的具体类型,就推导出无限复杂性和不可判定。这在逻辑上根本站不住脚,自然数有无穷多个,但判定两个自然数相不相等完全是可判定的。在依赖类型系统里,让机器自己去大海捞针找证明(证明搜索)或者不写注解硬猜(完全类型推导)确实不可判定;但一旦程序员老老实实写上类型注解,采用双向类型检查(Bidirectional type-checking),机器干的活就从自己想证明降维成了拿着答案对卷子,这在计算上完全是可判定的。

第三个滑坡是把特定参数化模型检验的困难,夸大成了正确性无法分解。早在几十年前,霍尔逻辑(Hoare logic)的序列规则就已经证明了正确性完全可以组合推理。模块化真正的痛点从来不是数学上不能组合,而是连接两个模块的那条契约(中间断言与循环不变量)无法靠机器廉价地自动生成,必须消耗人类工程师的脑力去构造。

最坏理论 vs 工程现实:形式化方法的三条逃生通道 Pressler 的理论绝对化误判 ● 莱斯定理封死一切语义属性判定 ● 无穷具体类型必然导致不可判定 ● 模块化无用,正确性根本无法分解 PLT 与工业界的降维解法 ● 保可靠弃完备:拒绝部分合法程序 ● 人工写注解:把搜索降维成双向检查 ● 霍尔逻辑组合:难在人工写循环不变量
  • 结论.计算理论证明的是全自动、零成本、通用银弹的破产,而现代类型系统与形式化工具的本质,正是用程序员手工写注解和契约的确定性脑力,去置换机器状态空间爆炸的指数级算力。

AWS 的 TLA+ 账本:怎么在自然法则的夹缝里套利

既然理论允许通过受限模型和人工介入来绕开死胡同,那在真金白银的工业界,这笔账到底划不划算?

《孙子兵法》讲,水因地而制流,兵因敌而制胜。AWS 的工程师没有愚蠢到拿形式化工具去死磕几百万行底层 C 或 Java 生产代码,而是把火力全部集中在最容易出人命、测试最难覆盖的顶层分布式算法与并发状态机设计上。看看 AWS 披露的这本投入产出账:

核心系统模块建模工具与代码量发现隐藏缺陷与验证成果关键细节与深度
DynamoDB 复制与组成员939 行 TLA+抓出 3 个 Bug最深数据丢失反例需 35 个高级步骤
S3 容错网络算法804 行 PlusCal抓出 2 个 Bug 及优化方案缺陷覆盖此前人工评审盲区
S3 后台数据重分布645 行 PlusCal抓出 1 个 Bug 及修复方案中 1 个新 Bug拦截连环引入的二次故障
EBS 卷管理102 行 PlusCal抓出 3 个 Bug极小建模体量撬动核心存储安全
内部锁管理器318 行 TLA+ / 223 行 PlusCal抓出 1 个 Bug 并验证激进优化223 行无锁建模因未检活性漏掉 1 个活性 Bug

这组数字直接把形式化验证从象牙塔拉进了工程算盘。一个逃过了广泛代码审查和压力测试的 DynamoDB 数据丢失隐患,触发路径长达 35 步,靠写集成测试去撞大运,概率跟中彩票差不多;而用 939 行 TLA+ 把高层状态机画出来,TLC 模型检验器暴力穷举状态空间,直接把最短反例拍在工程师脸上。更有说服力的是学习成本,AWS 工程师反馈,从零上手学习并应用 TLA+,通常只需要 2 到 3 周就能在项目里产出有用结果。

AWS 形式化验证实战:小代码量撬动深层并发缺陷 35 步 DynamoDB 最深反例 仅用 939 行 TLA+ 建模 抓出逃过测试的数据丢失 Bug 102 行 EBS 卷管理 PlusCal 百行级高层状态机规范 一举揪出 3 个隐藏架构缺陷 2-3 周 一线工程师上手周期 覆盖 7 个团队 10 个核心系统 漏检活性曾放过无锁结构 Bug

但 AWS 的账本同样验证了 Pressler 警告的另一面,天下没有白吃的午餐。在内部锁管理器的 223 行 PlusCal 无锁结构建模中,工程师只检查了安全性(Safety,即不会发生坏事),没有检查活性(Liveness,即好事最终一定会发生),结果就老老实实漏掉了一个活性 Bug。而且 TLA+ 验证的只是高层抽象设计,至于程序员把设计翻译成几万行 C++ 或 Java 代码时写出了指针越界或逻辑手滑,模型检验器根本管不着。

算力穷举不了无限的图灵机,但足够在百行状态机里堵死致命漏洞。
  • 风险.把高层设计验证当成底层代码免死金牌,或者在建模时漏写活性与公平性约束,会让团队在虚假的安全感中放过线上死锁与饥饿故障。

回看希尔伯特墓碑上那句我们必将知道,还有那个只有 1919 个控制状态却连 ZFC 公理系统都束手无策的微型图灵机,Pressler 提醒得对,程序员确实该对计算的自然法则保持敬畏。指望靠一个新语言或新工具就躺着写出零 Bug 的软件,纯属痴人说梦。但敬畏自然法则,不等于在工程上躺平。造桥的工程师不会因为热力学第二定律说万物终将熵增,就不去计算钢梁的承重极限;写基础架构的程序员,也不该拿停机问题和莱斯定理当挡箭牌,拒绝在核心分布式状态机上花两三周写那几百行救命的 TLA+。