MathLabs

数学基础

形式化证明(Lean)

像Lean这样的证明助手检查数学证明的方式,与编译器检查程序的方式相同:把"这个证明对不对"归结为类型判断 Γ⊢t:P\Gamma \vdash t : P 是否成立,由一个很小的可信内核来验证。

直观为什么可以相信一份你读不完的证明?

Lean 4的数学库Mathlib现在已有数百万行证明——远超任何人能逐行读完的量。但数学家们仍然信任它,原因和我们信任电梯检验合格印章而不是自己重新焊接每条钢缆一样:一个很小、固定的检查程序(内核)已经验证了每一步,而这个内核本身短到可以手工审查。

交互式图,以节点表示引理,以有向边表示它们之间的逻辑依赖关系。
已验证证明的依赖图:每个节点是一个引理,每条边是对先前结果的一次使用;内核会遍历每一条边。

大学归纳构造演算

定义: 项、类型与类型判断

Lean的核心逻辑是归纳构造演算(CIC):一种依赖类型论,类型可以依赖于值(依赖函数类型 Π (x:A) B(x)\Pi\,(x:A)\,B(x),读作"对每个 x:Ax:A,都有一个类型为 B(x)B(x) 的值"),新类型由归纳声明构造(自然数、列表、命题相等类型)。每个对象都由类型判断 Γ⊢t:P\Gamma \vdash t : P 分类:"在上下文 Γ\Gamma 中,项 tt 具有类型 PP"。

Γ⊢t:P\Gamma \vdash t : P

按照柯里–霍华德对应,命题 PP 本身就是一个类型,而 PP 的证明就是该类型的一个项 tt——证明即编程。这正是为什么编译程序本就需要的类型检查算法,足以用来检查证明。

⊢t:P  ⟹  P\vdash t : P \;\Longrightarrow\; P
证明助手的两层
层任务可信度
细化器/策略从高层策略脚本中搜索证明项 tt不可信:此处的错误只会浪费时间
内核重新检查 tt 是否真的具有类型 PP可信:这就是de Bruijn准则

进阶de Bruijn准则与通过Eq.rec的相等

de Bruijn准则是指,当一个小型、可独立审查的内核检查每一个证明项时,证明助手才是可信的,因此 ∣kernel∣≪∣tactic engine∣|\text{kernel}| \ll |\text{tactic engine}|:可靠性依赖于几百行代码,而不依赖于生成该项的策略引擎。归纳相等 a=ba = b 本身由唯一构造子 refl:∀ a, a=a\mathrm{refl} : \forall\, a,\ a = a 定义,而它的消去子 Eq.rec\mathsf{Eq.rec}(J规则)是内核推导相等的其他一切性质所需的唯一原语。

Eq.rec:{C:∀ y, a=y→Sort}→C a (refl a)→∀ y (h:a=y), C y h\mathsf{Eq.rec} : \{C : \forall\, y,\ a=y \to \mathrm{Sort}\} \to C\,a\,(\mathrm{refl}\,a) \to \forall\, y\,(h:a=y),\, C\,y\,h

存在仅由 refla:a=a\mathrm{refl}_a : a = a 与 Eq.rec\mathsf{Eq.rec} 构造出的项 Eq.symm:a=b→b=a\mathrm{Eq.symm} : a = b \to b = a;对称性是CIC的一个定理,而非额外公理。

为什么成立?

如果相等需要为对称性、传递性、合同性等分别设立公理,可信内核就会随着关于 == 的每条新事实而膨胀。从一个消去子推出所有这些性质,才能让内核保持很小。

证明

固定 aa。我们想要一个函数,对任意 bb,把 h:a=bh : a = b 变成 b=ab = a 的证明。用动机(motive)C(y,h):=(y=a)C(y, h) := (y = a) 来使用 Eq.rec\mathsf{Eq.rec}——这是一族由点 yy 及连接 aa 到 yy 的证明 hh 索引的命题。

消去子要求提供基础情形 C(a,refl a)C(a, \mathrm{refl}\,a) 的证明,即 a=aa = a;我们提供 refl a\mathrm{refl}\,a 本身。这是合法的,因为基础情形总是在起点 aa 处由 refl\mathrm{refl} 索引。

于是 Eq.rec\mathsf{Eq.rec} 对每个 yy 与每个 h:a=yh : a = y 返回 C(y,h)=(y=a)C(y,h) = (y = a) 的证明。代入 y:=b, h:=hy := b,\ h := h:我们恰好得到 b=ab = a 的证明。

把它打包为 Eq.symm h:=Eq.rec (C:=λy h, y=a) (refl a) b h\mathrm{Eq.symm}\,h := \mathsf{Eq.rec}\,(C := \lambda y\, h,\, y = a)\,(\mathrm{refl}\,a)\,b\,h,就得到类型为 Eq.symm:a=b→b=a\mathrm{Eq.symm} : a = b \to b = a 的项。内核仅通过将这个应用与 Eq.rec\mathsf{Eq.rec} 的类型签名匹配就能接受它——在内核层面完全不需要对"对称性"这一概念进行推理。

存在仅由 Eq.rec\mathsf{Eq.rec} 构造出的项 Eq.trans:a=b→b=c→a=c\mathrm{Eq.trans} : a = b \to b = c \to a = c。

为什么成立?

把等式串联起来(a=b=ca=b=c 因而 a=ca=c)几乎在每个证明中都会用到;如果这是一条公理而不是一个可推导的项,内核就必须信任该公理,而不能通过计算来验证它。

证明

固定 a,ba,b 及 h1:a=bh_1 : a = b。给定任意 cc 上的 h2:b=ch_2 : b = c,我们想要 a=ca = c 的证明。用动机 C(y,h):=(a=y)C(y, h) := (a = y)(由点 yy 及连接 bb 到 yy 的证明 hh 索引)对 h2h_2 使用 Eq.rec\mathsf{Eq.rec}。

所需的基础情形是 C(b,refl b)C(b, \mathrm{refl}\,b),即 a=ba = b——恰好就是 h1h_1。所以 h1h_1 就是提供给 Eq.rec\mathsf{Eq.rec} 的基础情形证明。

于是 Eq.rec\mathsf{Eq.rec} 对每个 cc 与每个 h2:b=ch_2 : b = c 给出 C(c,h2)=(a=c)C(c, h_2) = (a = c) 的证明,这正是我们想要的。

所以 Eq.trans h1 h2:=Eq.rec (C:=λy h, a=y) h1 c h2\mathrm{Eq.trans}\,h_1\,h_2 := \mathsf{Eq.rec}\,(C := \lambda y\, h,\, a = y)\,h_1\,c\,h_2 的类型是 Eq.trans:a=b→b=c→a=c\mathrm{Eq.trans} : a = b \to b = c \to a = c。注意它与对称性共享的模式:两个证明都是通过选取合适的动机 CC,沿着一个相等关系"传输"一个事实,而真正的计算工作则交给内核对 refl\mathrm{refl} 上 Eq.rec\mathsf{Eq.rec} 的固定归约规则来完成。

大学实际应用:从编译器到数论

形式化证明已不再只是实验室里的珍品。CompCert是在Coq中经过形式验证的C语言编译器,它拥有机器检查过的证明,保证其生成的汇编代码总是与源程序行为一致——这是任何测试套件都给不出的保证。AWS和Signal使用经过形式验证的密码学代码(如AWS的s2n TLS库、HACL和F等工具)来彻底消除一整类内存安全和旁道攻击漏洞。在纯数学领域,Lean的液态张量实验(2020–2022)将Peter Scholze在凝聚数学中的一个困难定理形式化,而Terence Tao团队在2023年人类证明发表后仅数月内,就在Lean中形式化了多项式Freiman–Ruzsa(PFR)猜想的证明。

例题: 对 n+0=nn+0=n 证明的类型检查

在内核层面说明,为何用归纳法构造出的 ∀ n:N, n+0=n\forall\, n : \mathbb{N},\ n + 0 = n 的证明项能被接受。

解答

N\mathbb{N} 上的加法按第二个参数递归定义:n+0:=nn + 0 := n,n+succ m:=succ (n+m)n + \mathrm{succ}\,m := \mathrm{succ}\,(n+m)。因此目标 n+0=nn + 0 = n 的基础情形通过计算归约恰好化为 n=nn = n,即 refl n\mathrm{refl}\,n——内核不需要任何逻辑推理就能接受它,只需展开定义。

对于一般定理 ∀n, n+0=n\forall n,\ n + 0 = n,这个基础情形按上述定义是显然的,因此这个特定命题甚至不需要归纳法(与 ∀n, 0+n=n\forall n,\ 0 + n = n 不同,后者因递归发生在第二个参数上而确实需要归纳法)。

内核的作用仅仅是按递归定义展开项 n+0n+0 中的 ++,得到 nn,然后检查目标两边是否按定义相等——把"这个证明是否正确"归结为一个可终止的计算,这正是de Bruijn准则在起作用。

例题: 作为带类型相等的已验证编译

CompCert将其正确性定理表述为一个语义保持性质。解释"证明是该类型的一个项"相较于普通编译器测试,给工程师带来了什么。

解答

CompCert的定理形如"对于每一个编译为汇编 AA 的源程序 SS,AA 的每一个可观察行为都是 SS 的一个行为"——这是对所有源程序的全称量化陈述,而不仅是测试套件中的那些。

测试套件只能检查有限多个 (S,A)(S,A) 对,永远无法排除第4,000,001个程序上的编译错误。而全称量化陈述的Coq证明项 tt,则由内核对整个量词一次性完成类型检查——与用于CompCert其他每条引理的内核相同,因此不会为每个测试用例引入新的信任。

由于证明项是完全的,且定理谈论的是编译器真实的Coq源代码(提取为OCaml),任何破坏该保证的编译器改动都会直接类型检查失败,从而捕获有限回归测试套件完全会漏掉的回归——这正是为什么在GCC和Clang上发现数百个错误的模糊测试活动,在CompCert生成的代码中一个编译错误都没有发现的原因。

类型判断 Γ⊢t:P\Gamma \vdash t : P 断言了什么?

de Bruijn准则最恰当的概括是:

在 Eq.symm:a=b→b=a\mathrm{Eq.symm} : a = b \to b = a 的证明中,基础情形 C(a,refl a)C(a, \mathrm{refl}\,a) 提供的是哪个命题?

哪个实际系统依赖于一个机器检查过的证明,保证生成的汇编代码始终与源程序行为一致?

参考文献

  1. Leonardo de Moura, Sebastian Ullrich (2021). The Lean 4 Theorem Prover and Programming Language
  2. Xavier Leroy (2009). Formal verification of a realistic compiler
  3. Terence Tao, et al. (2023). A Proof of the Polynomial Freiman-Ruzsa Conjecture · arXiv:2311.05762