Dafny:让编译器用数学证明你的代码是对的

验证感知的编程语言 Dafny 全解析:边写代码边写规格(requires/ensures/invariant),编译器用 Z3 自动证明正确性;Dafny → Boogie → Z3 三层引擎、可编译到 Java/Go/Python 多目标、与 Coq/Lean 的自动 vs 交互之分,以及 AWS 用它重写每秒 10 亿次授权引擎的工业案例。

  • AI
  • Dafny
  • 形式化验证
  • 编程语言
  • Z3
  • 正确性
  • AWS

上篇聊了 AI-Native SDLC——让 agent 把代码写得越来越快。但写得快从来不等于写得对。 当越来越多代码由模型生成,一个老问题反而更尖锐:你怎么确信这段代码真的满足它该满足的东西?

测试能覆盖有限样本;而 Dafny 走得更远——它是一门「验证感知」的编程语言, 你边写代码边写规格(前置条件、后置条件、循环不变式),编译器用 SMT 求解器自动把「代码满足规格」证明出来。不是「在这些测试下通过了」,而是「对所有可能输入,数学上成立」。

一、先说它是什么:边写边证

Dafny 的官方定义很精确:一门内置规范构造、并配有一个静态程序验证器的验证感知语言。 它把复杂的自动推理和熟悉的编程习惯绑在一起,让开发者能写出「相对规格而言可证明正确」的代码。

关键区别就一句话:验证是编译的一部分,不是事后测试。你在写代码时,验证器实时查错、给反例、确认规格被满足。比如一个求最大值的函数,规格是这样写的:

method Max(a: int, b: int) returns (m: int)
  ensures m >= a && m >= b      // 返回值不小于两个输入
  ensures m == a || m == b     // 返回值必须是其中之一

你不用写测试去枚举输入,验证器直接证明:无论 a、b 取什么整数,返回值都满足这两条。 这就是它和「看起来对」的本质差距。

二、从微软研究院到 AWS 授权引擎

Dafny 由 Rustan Leino 在微软研究院于 2008 年启动,前身是 ESC/Modula-3、ESC/Java、Spec# 一脉的验证研究。 今天它由社区组织 dafny-lang 维护(MIT 许可,约 3.5k star,最新 v4.11.0),Leino 本人现在 AWS 工作,并在 2023 年出了 MIT Press 专著《Program Proofs》。

但真正让 Dafny 不只是学术玩具的,是 AWS 的旗舰案例:

  • 授权引擎每秒调用 10 亿次,原先用 Java 写,要演进又必须向后兼容;
  • AWS 选择用 Dafny 重写新引擎(而非去证明旧代码),再编译回 Java;
  • 耗时约 4 年(~2020–2024),2024 年无事故上线,性能提升 3 倍;
  • 全程用差异测试 + 影子测试,对 10¹⁵ 量级的真实样本做了比对;
  • 成果写成 ICSE 2025 论文《Formally Verified Cloud-Scale Authorization》。

核心经验就一句:对关键系统,重写一遍带证明的代码,比给旧代码补证明更划算。

三、编程模型:把规格写进代码

三个最常用的规范构造:

构造关键字含义
前置条件requires调用者必须保证成立(如「除数非零」)
后置条件ensures函数返回时必须成立(如「结果最大」)
循环不变式invariant每次循环前后都保持为真,用来证循环正确

还有 ghost 代码:只参与规范和证明、不进最终编译(幽灵变量、幽灵函数)。 看一个完整的二分查找,规格把「数组有序、返回下标对得上 key、找不到时确实没有」一次性写死:

method BinarySearch(a: array<int>, key: int) returns (result: int)
  requires forall i, j :: 0 <= i < j < a.Length ==> a[i] <= a[j]   // 数组已排序
  ensures -1 <= result < a.Length
  ensures 0 <= result ==> a[result] == key
  ensures result == -1 ==> forall i :: 0 <= i < a.Length ==> a[i] != key
{
  var low := 0;
  var high := a.Length;
  while (low < high)
    invariant 0 <= low <= high <= a.Length
    invariant forall i :: 0 <= i < low  ==> a[i] < key
    invariant forall i :: high <= i < a.Length ==> key < a[i]
  {
    var mid := low + (high - low) / 2;
    var midVal := a[mid];
    if (midVal < key) {
      low := mid + 1;
    } else if (key < midVal) {
      high := mid;
    } else {
      result := mid;
      return;
    }
  }
  result := -1;
}

注意不变式里那两行 forall:它们精确刻画了「low 左边都比 key 小、high 右边都比 key 大」。 验证器据此自动推出函数整体正确——你要做的不是补测试,而是把不变量想清楚。

四、验证引擎揭秘:Dafny → Boogie → Z3

你写的程序不会直接被「证明」,而是经过三层翻译:

层是什么职责
Dafny你写的程序(代码 + 规格)翻译成下面的中间语言
Boogie中间验证语言(IVL,微软研究院出品)用语义保持的翻译,再生成一阶逻辑「验证条件」
Z3SMT 求解器(同样出自微软研究院)自动判定验证条件是否可满足——绝大多数情况无需你手动介入

这正好对比出验证和测试的天壤之别:

维度测试验证
覆盖范围有限输入样本所有可能输入
保证级别「在这些测试下通过」「数学上证明正确」
失败反馈测试输入反例(最小的破坏情形)
开销发生地运行时编译时

补充一点:Z3 这类 SMT 求解器对「小改动导致证明突然失败」很敏感(证明不稳定)。 实战里靠多写不变式、用 assert 当路标、调 {:fuel} / {:trigger} 来控制展开与量化实例化来稳住它。

五、验证完还能跑:编译到多语言

Dafny 不是停在「证明完就完事」——它把验证过的代码编译成真实可部署的程序。 稳定支持的目标包括 C#、Java、JavaScript、Go、Python(C++ 仍实验性),命令就像:

dafny verify program.dfy          # 只验证
dafny build --target:java program.dfy   # 编译为 Java
dafny run program.dfy              # 验证并直接跑

工具链还包括 VSCode 扩展(边写边证、错误高亮、反例展示)、语言服务器(Emacs/Vim 也能用)、以及标准库与数学引理集。 一个坑要记牢:通过 {:extern} 调用目标语言函数(FFI)时,那部分代码不被验证—— 边界处需要额外的测试或人工保证。

六、Dafny vs 其他验证工具

常拿来比较的「证明系」语言,差异主要在「自动 vs 交互」:

工具类型交互性可编译目标
Dafny自动验证低(自动)多目标
Coq / Lean交互式证明高(手动推演)无
F*混合中无
Why3自动验证低无
SPARK自动验证低Ada

Dafny 的甜点在于:大多数情况自动证完,验证完还能编译成多种语言,且有 AWS 这种生产级案例。 它和 Rust 也不冲突——Rust 在编译期保证「不崩溃」(内存安全、无数据竞争),Dafny 证明「做对了事」(业务逻辑正确),二者互补。

七、何时用,何时不用

适合:

  • 安全关键系统——加密、授权、协议,错一次的代价极高;
  • 核心算法——排序、搜索、图算法,需要数学级正确性;
  • 长期维护的代码——规格即文档,重构时有验证兜底;
  • 有合规要求的行业(航空、金融)。

不适合:

  • 性能极度敏感、要手调汇编级优化的代码(生成码未必最优);
  • 快速原型——写规格本身要时间,迭代反而慢;
  • 与大量未验证代码交互(FFI 边界会漏);
  • 并发程序(Dafny 主要验证顺序程序,并发需额外工具如 Civl);
  • 简单 CRUD——测试就够,验证是杀鸡用牛刀。

小结

把这篇收成三句话:

Dafny 是一门验证感知语言:你边写代码边写规格,编译器用 Z3 把「代码满足规格」自动证明出来。 它不是测试,而是对所有输入的数学证明;并且证明完还能编译成 Java/Go/Python 等真实部署。 AWS 用它重写了每秒 10 亿次调用的授权引擎——验证早已走出象牙塔。

  1. 验证超越测试。测试是「在这些输入下正确」,验证是「在所有输入下正确」; 代价是你要先把不变式想清楚,而不是堆测试用例。
  2. 它不是学术玩具。AWS 用 Dafny 重写授权引擎,4 年、3 倍性能、零事故上线—— 对关键系统,「重写带证明」往往比「给旧代码补证明」更划算。
  3. 和 AI 写代码是同一道题的两面。Agent 让代码变多、变快,正确性反而更稀缺; 把规格写进语言的 Dafny,正好指向「机器生成、机器证明」那条路。

想动手:官网 dafny.org, 仓库 github.com/dafny-lang/dafny, 另有在线教程、参考手册与 Leino 的《Program Proofs》。