《面向软件工程师的 Lean 证明剖析》
Anatomy of a Lean proof for software engineers

原始链接: https://agostbiro.net/posts/2026-10-anatomy-of-a-lean-proof/

本文分步介绍了一个教材定理的 Lean 4 形式化证明:三行二进制位构成的语言中,若最下面一行等于上面两行之和,则该语言是正则语言。由于 DFA 从左向右读取符号,而二进制加法从最低有效位开始处理更为方便,因此证明构造了一个识别反转语言的 DFA。 该 DFA 有三个状态,分别跟踪进位为 0、进位为 1,以及一个死状态。进位为 0 的状态既是初始状态,也是接受状态,从而防止溢出。 核心证明建立了一个运行不变式,将自动机的执行与任意长度输入上的二进制加法联系起来。归纳过程使用若干辅助引理来拆分运行、将一次转移转化为加法器方程,并在最低有效位处分解整个算术方程。布尔情况通过 `decide` 或 `simp` 处理,线性算术则由 `omega` 求解。 最后,利用 Mathlib 中关于语言反转的封闭性定理,将正则性传递回原语言。本文还指出,形式化验证能够揭示规范中原本未明确写出的要求,同时警告不应盲目信任机器生成的证明,而应确保这些证明仍然易于理解。

讨论的核心在于,Lean 的证明实际上能够保证什么。支持者认为 Assuming a sound Lean kernel, reviewers need not inspect a huge proof body. The kernel verifies every intermediate inference, so humans mainly need to validate the theorem statement, its definitions, permitted axioms, and whether the statement expresses the intended claim. 批评者回应说,这并不能消除语义错误。LLM 生成的形式化证明可能证明了错误的定理,使用了空泛或不成立的假设,引入可疑的公理,或者以非预期的方式定义看似熟悉的概念,例如把“实数”别名定义为复数。这类问题可能出现在同样庞大的定理陈述或配套定义中,因此,成功通过编译并不等同于验证了现实世界中的主张。内核的可靠性漏洞以及建模方式的选择,也仍然是值得关注的问题。 另一个次要争议涉及如何学习 Lean:一些评论者建议先理解依赖类型论、构造逻辑和柯里–霍华德对应;另一些人则主张从实践练习和官方社区资源开始。总体而言,Lean 减轻了证明检查的负担,但并没有消除验证规格是否名副其实的必要性。
相关文章

原文
[Contents]

Intro

I recently worked through a problem from a theory of computation textbook that asked me to prove a property of a language using finite automata. The informal proof is a simple constructive proof where you build an automaton and show that it recognizes the language. This is kind of similar to program verification, so I thought it’d be interesting to see what it takes to formalize the proof. Lean is a good choice for this, because its Mathlib has all the theorems for the problem.

After finishing the formal proof, I decided to write it up, because I think it provides software engineers with good insight into what it takes to formally prove properties of a system.

I tried to make this post accessible. If you’re comfortable with a modern statically typed programming language (such as TypeScript or Rust), binary arithmetic, basic propositional logic, and inductive proofs, you should be able to follow along.

Background: DFAs & Regular Languages

Feel free to skip to the next section if you’re comfortable with DFAs and regular languages.

Finite automata provide a theoretical model of computation with fixed memory. Besides theory, finite automata also have important practical applications. For example, finite automata are relevant for parsers and regular expressions, where a bug once took a significant portion of the internet down.

Deterministic Finite Automaton (DFA)

A deterministic finite automaton (DFA) is a machine with a fixed, finite set of states that reads its input one symbol at a time, left to right. With each symbol, it updates its state using a deterministic transition function. After the last symbol, the machine either sits in an accepting state (input is accepted) or not (input is rejected).

If you’ve ever written a simple regular expression like -?[0-9]+, then you’ve constructed a DFA. This regex matches integer literals like 12 and -123 and the corresponding DFA looks like this (the arrows are annotated with the symbols that lead to the next state):

DFA for a decimal integer literal, including the dead statestartsigndigitsdead-0-90-90-9otherotherotherany

This DFA has four states:

  • Start: this is where we start before processing the first character. Since the start state is not an accepting state, we reject the empty string.
  • Sign: we move to the sign state when we encounter the - character in the start state. We can skip the sign state and jump directly to digits from start, since the sign character is optional (-?). If we’re in this state at the end of the string, then we reject the string.
  • Digits: we move from start or sign to digits when we encounter a digit character ([0-9]). If we’re in the digits state and encounter a digit character again, then we stay in the digits state. The digits state is the only accepting state of the DFA. If we’re in this state after we’ve processed the input string, then the DFA accepts the string.
  • Dead: we get into this state if we encounter any other character than a digit (unless it’s a negative sign at the start). If we’re in the dead state at the end of the string, then the DFA rejects the string. Once we’re in the dead state, we stay in it, so the dead state in this DFA is a sink.

The set of input symbols to the machine is defined by the set Σ\Sigma. In our regex example, Σ={−,0,1,2,…,9}\Sigma = \left\{-, 0, 1, 2, \ldots, 9\right\}

Regular Languages

A language is just a set of strings, also called words, and a language is called regular if some DFA accepts exactly the strings in it. Recognizing regular languages is the class of decision problems solvable with an amount of memory that does not grow with the input.

Regular languages have useful closure properties: the union and intersection of two regular languages are regular, and so are the complement and (important for us) the reversal of a regular language.

The standard way to prove that a language is regular is to build a DFA and show that it accepts exactly that language.

We can describe a language AA with set-builder notation:

A={ w∈Σ∗∣P(w) }A = \bigl\{\, w \in \Sigma^{*} \bigm| P(w) \,\bigr\}
联系我们 contact @ memedata.com