21 mins read

软件基础:形式验证

一篇关于形式验证、Coq 安装和配置的入门指南,涵盖了 opam 和 VsCoq 的详细步骤。

软件基础:形式验证

形式验证是使用数学方法严格证明程序或系统正确性的技术。不同于测试(只能发现存在的错误),形式验证可以证明错误的不存在。

形式验证的目标是,将一段程序的功能用另一段程序表达,并且再用另一段程序来表示该程序符合给定的功能要求。

Coq安装h2

Coq 提供了Linux、Windows和MacOS 在内的多系统安装。但是由于博主电脑问题或者其他一些原因,并没有成功安装其Windows环境,只能想办法在WSL2中,即Linux中使用COQ。 也幸亏之前有过WSL2+VsCode经验,不然又少不了一番折腾。

安装opamh3

Linux 和MacOs 可以使用 opam 官网提供的脚本一键安装。

Shell Command
bash -c "sh <(curl -fsSL https://opam.ocaml.org/install.sh)"

注意: 脚本会将 opam 默认安装到 /usr/local/bin,如不修改请确保当前用户该路径有读写权限(sudo)

安装完成后可以使用下面命令确认是否安装成功。如果成功将返回 opam的版本号,为确保Coq 安装成功,请安装最新版 opam。

Shell Command
opam --version

安装成功后需要执行下面命令 初始化 opam 状态:

Shell Command
opam init

至此 opam 安装配置完成。

使用 opam 安装 Coqh3

本小节内容亦可访问 官方安装指南

首先确保 opam 安装成功并初始化,接下来可以使用下面命令自动安装,由于 Coq 需要从源码编译所以安装时间较长:

Shell Command
opam pin add coq 8.20.0

注意:Coq 最新版本可能已更新,如果想查看最近版本请访问 Coq官网 或者其 github仓库

以上述命令安装 Coq 不会自动更新,若更新请执行:

Shell Command
opam pin add coq <版本号>

使用下面命令可以查看 Coq 是否被正确安装:

Shell Command
coqc --version

配置 VsCoqh3

首先在 vscode 中安装扩展 VsCoq。接下来按照 插件说明页 的说明,依次运行两个下载命令:

Shell Command
opam pin add coq 8.18.0
opam install vscoq-language-server

这会在 ubuntu 上安装 language server,接下来为了让 vscode 找到这个 server,你需要运行

Shell Command
which vscoqtop
CAUTION

注意

如果此时没有输出(即失败),可能是没有刷新。运行

Shell Command
eval $(opam env)

此时你将得到一个类似于

Shell Command
`/home/$USERNAME$/.opam/default/bin/vscoqtop`

的输出,将其完整地复制下来,填入 VsCoq 的 Extension Setting 中,VsCoq: Path 的值。

有人在 Windows 下配环境变量配习惯了,把路径复制到 ../bin 就停了。但是此时必须要完整地复制全部内容。

COQ语法介绍h2

在浏览软件基础这本书时,发觉它对入门者的照顾并不是特别友好,因此,关于Coq的语法规则,我使用Cluade大模型来进行一步步的讲解。

1. 基本定义和声明h3

1.1 定义常量h4

使用 Definition 来定义常量或函数。

Definition pi := 3.14.
Check pi. (* 输出: pi : R *)

1.2 定义函数h4

以下是一个定义简单加法函数的例子:

Definition add (a b : nat) : nat := a + b.
Check add.
(* 输出: add : nat -> nat -> nat *)

2. 数据类型h3

2.1 自然数(nat)h4

Coq 内置了自然数类型 nat,并提供了相关操作。

Check 0. (* 输出: 0 : nat *)
Check 1. (* 输出: 1 : nat *)

2.2 布尔型(bool)h4

布尔型在 Coq 中也很常用。

Check true. (* 输出: true : bool *)
Check false. (* 输出: false : bool *)

3. 归纳类型h4

你可以定义自己的数据结构,例如列表。

Inductive my_list (A : Type) : Type :=
| nil : my_list A
| cons : A -> my_list A -> my_list A.

4. 函数定义与模式匹配h3

4.1 定义函数h4

使用 Fixpoint 定义递归函数。以下是一个计算列表长度的例子:

Fixpoint my_length {A : Type} (l : my_list A) : nat :=
match l with
| nil _ => 0
| cons _ _ l' => 1 + my_length l'
end.

5. 证明结构h3

5.1 声明定理h4

使用 Theorem 来声明一个新的定理。

Theorem zero_plus : forall n : nat, 0 + n = n.
Proof.
(* 证明将在这里进行 *)
Qed.

5.2 使用策略h4

以下是使用策略证明加法的性质:

Theorem zero_plus : forall n : nat, 0 + n = n.
Proof.
intro n. (* 引入变量 n *)
simpl. (* 简化表达式 *)
reflexivity. (* 证明两边相等 *)
Qed.

5.3 归纳法证明h4

归纳法证明的基本结构如下:

Theorem plus_0_r : forall n : nat, n + 0 = n.
Proof.
intro n.
(* 使用归纳法 *)
induction n as [| n' IHn'].
- simpl. reflexivity. (* 基础情况 *)
- simpl. rewrite -> IHn'. reflexivity. (* 归纳步骤 *)
Qed.

6. 条件语句和循环h3

Coq 使用模式匹配来定义条件和处理逻辑。

Definition is_zero (n : nat) : bool :=
match n with
| 0 => true
| _ => false
end.

7. 逻辑运算h3

Coq 提供了基本的逻辑运算,例如与(and)、或(or)、非(not)。

7.1 逻辑与h4

Definition and_example (p q : Prop) : Prop := p /\ q.

7.2 逻辑或h4

Definition or_example (p q : Prop) : Prop := p \/ q.

7.3 逻辑非h4

Definition not_example (p : Prop) : Prop := ~p.

8. 变量和类型h3

8.1 类型声明h4

你可以声明变量和其类型:

Definition x : nat := 5.
Check x. (* 输出: x : nat *)

9. 注释h4

Coq 允许使用 (* … *) 进行注释:

(* 这是一个注释 *)

一周七日h3

定义类型h4

one.v
10 collapsed lines
(*
这段Coq代码定义了一个新的归纳类型(Inductive Type),名为 day。
可以这样理解:
Inductive day : Type: 这部分声明了我们要创建一个名为 day 的新类型。Inductive 关键字表示这个类型是通过列举其所有可能的构造函数(constructor)来定义的。: Type 表示 day 本身是一个类型。
:=: 这是定义操作符,意思是“被定义为”。
| monday | tuesday ... | sunday: 这一长串由 | 分隔的列表是 day 类型的构造函数。每一个构造函数都是 day 类型的一个唯一、离散的值。
简单来说,这段代码是在创建一个枚举类型,就像在其他编程语言中一样。 它创建了一个名为 day 的新类型,这个类型的变量只能取 monday, tuesday, wednesday, thursday, friday, saturday, sunday 这七个值中的一个。
*)
Inductive day : Type :=
| monday
| tuesday
| wednesday
| thursday
| friday
| saturday
| sunday.
12 collapsed lines
(*
match d with ... end:
这是一个模式匹配表达式,是Coq(以及许多函数式编程语言)中非常核心和强大的功能。
它的作用是检查变量 d 的具体值,并根据不同的值执行不同的代码分支。
你可以把它看作是其他语言中 switch-case 语句的更强大版本。
| monday => tuesday:
这是一个匹配分支。| 分隔了不同的情况。
它表示:“如果 d 的值是 monday,
那么 => 整个表达式的结果就是 tuesday”。
*)
Definition next_weekday (d : day) : day :=
match d with
| monday => tuesday
| tuesday => wednesday
| wednesday => thursday
| thursday => friday
| friday => monday
| saturday => monday
| sunday => monday
end.
(* 测试:计算 next_weekday friday 的结果(应返回 monday) *)
Compute (next_weekday friday). (* 输出:monday : day *)
(*
定义一个测试用例命题:
"连续两次对 saturday 调用 next_weekday 的结果应等于 tuesday"
解释:
1. 第一次 next_weekday saturday → monday
2. 第二次 next_weekday monday → tuesday
*)
Example test_next_weekday:
(next_weekday (next_weekday saturday)) = tuesday.
(* 证明过程:
- simpl:化简表达式(计算具体值)
- reflexivity:检查等式两边是否相同
- Qed:完成证明
*)
Proof.
simpl. (* 展开计算:next_weekday saturday → monday
然后 next_weekday monday → tuesday *)
reflexivity. (* 验证 tuesday = tuesday *)
Qed.
(* 检查 test_next_weekday 的类型,确认它是一个已被证明的命题 *)
Check test_next_weekday. (* 输出:test_next_weekday : ... = tuesday *)

如上所述,有了上面这个例子,之后的一些内容就可以看懂了,此处不再赘述。

练习(✨) standard (nandb)h4

nandb.v
Definition nandb (b1:bool) (b2:bool) : bool :=
(*一开始不知道match可以匹配两个参数*)
match b1, b2 with
| true, true => false
| _, _ => true
end.
Example test_nandb1: (nandb true false) = true.
Proof. reflexivity. Qed.
Example test_nandb2: (nandb false false) = true.
Proof. reflexivity. Qed.
Example test_nandb3: (nandb false true) = true.
Proof. reflexivity. Qed.
Example test_nandb4: (nandb true true) = false.
Proof. reflexivity. Qed.
simpl 和 reflexivity 的简明总结
# Coq 中的 `simpl``reflexivity` 总结
## 1. 为什么例子不加 `simpl` 也能通过?
- `reflexivity` 会自动进行隐式计算
- 能直接处理像 `nandb` 这样的透明定义
- 自动验证化简后的等式(如 `true = true`
## 2. `simpl` 的作用
| 功能 | 说明 |
|------|------|
| 显式化简 | 展开函数定义、计算 match 分支 |
| 调试用途 | 观察中间计算步骤 |
| 复杂表达式 | 需要分步计算时使用 |
## 3. 策略对比
| 策略 | 功能 | 自动计算 |
|------|------|---------|
| `reflexivity` | 检查等式一致性 ||
| `simpl` | 仅进行表达式化简 ||
## 4. 必须使用 `simpl` 的情况
```coq
Example: (1 + 1) + (1 + 1) = 4.
Proof.
simpl. (* 必须显式化简 *)
reflexivity.
Qed.

练习(✨) standard (andb3)h4

and3.v
Definition andb3 (b1:bool) (b2:bool) (b3:bool) : bool :=
match b1,b2,b3 with
| true,true,true => true
| _,_,_ => false
end.
Example test_andb31: (andb3 true true true) = true.
Proof. simpl. reflexivity. Qed.
Example test_andb32: (andb3 false true true) = false.
Proof. simpl. reflexivity. Qed.
Example test_andb33: (andb3 true false true) = false.
Proof. simpl. reflexivity. Qed.
Example test_andb34: (andb3 true true false) = false.
Proof. simpl. reflexivity. Qed.

练习(✨) standard (factorial)h4

factorial.v
Require Import Coq.Arith.Arith.
Fixpoint plus (n m : nat) : nat :=
match n with
| O => m
| S n' => S (plus n' m)
end.
Module NatPlayground2.
Fixpoint mult (n m : nat) : nat :=
match n with
| O => O
| S n' => plus m (mult n' m) (* 正确的递归乘法 *)
end.
Example test_mult1: (mult 3 3) = 9.
Proof. simpl. reflexivity. Qed.
Fixpoint factorial ( n : nat) : nat :=
match n with
| O => 1
| S n' => mult n (factorial n')
end.
Example test_factorial1: (factorial 3) = 6.
Proof. simpl. reflexivity. Qed.
Example test_factorial2: (factorial 5) = (mult 10 12).
Proof. simpl. reflexivity. Qed.

练习(✨) standard (ltb)h4

ltb.v
(* 导入标准库(确保 eqb 和 leb 已定义) *)
Require Import Coq.Arith.Arith.
(* 方案1:直接模式匹配 leb 和 eqb 的结果 *)
Definition ltb_v1 (n m : nat) : bool :=
match (n <=? m), (n =? m) with
| true, false => true (* n ≤ m 且 n ≠ m 时返回 true *)
| _, _ => false (* 其他情况返回 false *)
end.
(* 方案2:嵌套 if 表达式 *)
Definition ltb_v2 (n m : nat) : bool :=
if (n <=? m) then (* 先检查 n ≤ m *)
if (n =? m) then (* 再检查是否不等于 *)
false (* n = m 时返回 false *)
else true (* n < m 时返回 true *)
else false. (* n > m 时返回 false *)
(* 方案3:独立递归实现(不依赖 leb/eqb) *)
Fixpoint ltb_v3 (n m : nat) : bool :=
match n, m with
| O, S _ => true (* 0 < 任何正数 *)
| S n', S m' => ltb_v3 n' m' (* 递归比较前驱 *)
| _, _ => false (* 其他情况(包括 n=O,m=O 和 n>S _,m=O) *)
end.
(* 方案4:利用 leb 的否定(最简洁数学定义) *)
Definition ltb_v4 (n m : nat) : bool :=
negb (m <=? n). (* n < m 当且仅当 ¬(m ≤ n) *)
(* 为所有方案声明统一的中缀记法 *)
Notation "x <? y" := (ltb_v1 x y) (at level 70). (* 可替换为 v2/v3/v4 *)
(* ===================== 测试用例 ===================== *)
Section Tests.
(* 测试用例验证所有方案等价 *)
Example test_all1: ltb_v1 2 2 = false /\ ltb_v2 2 2 = false /\
ltb_v3 2 2 = false /\ ltb_v4 2 2 = false.
Proof. simpl; auto. Qed.
Example test_all2: ltb_v1 2 4 = true /\ ltb_v2 2 4 = true /\
ltb_v3 2 4 = true /\ ltb_v4 2 4 = true.
Proof. simpl; auto. Qed.
Example test_all3: ltb_v1 4 2 = false /\ ltb_v2 4 2 = false /\
ltb_v3 4 2 = false /\ ltb_v4 4 2 = false.
Proof. simpl; auto. Qed.
(* 边界测试:0 的比较 *)
Example test_zero: ltb_v1 0 0 = false /\ ltb_v3 0 1 = true.
Proof. simpl; auto. Qed.
End Tests.
(* 方案选择建议 *)
(*
1. 需要清晰逻辑:选择 v1 或 v2
2. 需要独立实现:选择 v3
3. 需要数学简洁性:选择 v4
4. 实际开发推荐直接使用标准库的 Nat.ltb
*)
指令用途正式性是否可后续引用
Example测试用例非正式
Lemma辅助引理正式
Theorem重要结论最正式

Coq 策略 reflexivitysimpl 对比文档h3

核心差异对比h4

策略化简行为设计目的
reflexivity自动展开所有定义,直到等式两边完全一致或无法化简快速验证等式成立(不关心中间步骤)
simpl仅进行单步化简,保留用户可读的中间形式调试和观察化简过程

示例印证h4

Definition double n := n + n.
Lemma demo_diff : double 2 = 4.
Proof.
(* 使用 simpl 时: *)
simpl. (* 目标变为 2 + 2 = 4,保留可读形式 *)
(* 使用 reflexivity 时: *)
reflexivity. (* 直接展开所有定义完成证明,不显示中间状态 *)
Qed.

使用建议

  • 优先使用 reflexivity: 当等式显然成立时(如算术运算) 希望自动处理所有展开时

  • 优先使用 simpl: 需要观察中间化简状态时 避免过度展开导致表达式混乱

练习(✨) standard (plus_id_exercise)h4

Theorem plus_id_exercise : forall n m o : nat,
n = m m = o n + m = m + o.
Proof.
(* 引入所有前提 *)
intros n m o H1 H2.
(* H1 : n = m
H2 : m = o
目标:n + m = m + o *)
(* 先用 H1 将目标中的 n 替换为 m *)
rewrite -> H1.
(* 新目标:m + m = m + o *)
(* 再用 H2 将目标中的 m 替换为 o *)
rewrite -> H2.
(* 新目标:o + o = o + o *)
(* 两边已完全相同 *)
reflexivity.
Qed.

但是,此时存在疑问🤔?为什么假定H1就代表了n=m呢?这是COQ的规定吗?或者说我漏掉了什么东西? 经查阅,有以下结论: 在 Coq 中,H1 代表 n = m 是由 intros 策略的语法规则和逻辑结构决定的,这是 Coq 的基本设计约定,但并非随意规定,而是基于逻辑推理的自然形式化。

总结
  1. intros 策略的工作机制 当证明一个形如 ∀ n m, n = m → m = o → … 的定理时: intros n m o H1 H2 的执行过程: 按顺序引入 ∀ 量词变量:n, m, o 按顺序引入 → 的前提: 第一个前提 n = m 被命名为 H1 第二个前提 m = o 被命名为 H2 自动命名规则: 若用户不显式指定名称(如 intros n m o Hfoo Hbar),Coq 会按 H, H0, H1… 自动生成名称 名称本身可自定义,但位置顺序必须对应前提的顺序

  2. 为什么 H1 对应 n = m? 这是由定理的逻辑结构顺序决定的:

Theorem plus_id_exercise : n m o,
n = m (* 第一个前提 *)
m = o (* 第二个前提 *)
n + m = m + o.

intros 从左到右依次处理: 先处理 n = m,命名为 H1, 再处理 m = o,命名为 H2

  1. 底层逻辑依据 在类型论中,→ 对应函数类型: A → B 可理解为“给定一个 A 类型的值,可得到 B 类型的值” 在证明中,intros H 相当于: 假设你已有一个 A 的证明(即前提 H : A) 目标是构造 B 的证明

  2. 用户自定义示例 你可以自由重命名前提,但顺序不变:

Theorem plus_id_exercise : n m o,
n = m m = o n + m = m + o.
Proof.
intros x y z Eq_nm Eq_mo. (* 自定义名称 *)
rewrite -> Eq_nm, -> Eq_mo. (* 使用新名称 *)
reflexivity.
Qed.

Eq_nm 仍对应第一个前提 n = m(即 x = y) Eq_mo 仍对应第二个前提 m = o(即 y = z)

练习(✨✨) standard (andb_true_elim2)h4

andb_true_elim2.v
Theorem andb_true_elim2 : forall b c : bool,
andb b c = true -> c = true.
Proof.
intros b c H. (* 引入假设 H: andb b c = true *)
destruct b eqn:Eb. (* 分情况讨论b *)
- (* b = true *)
simpl in H. (* H变为 c = true *)
apply H. (* 直接使用假设 *)
- (* b = false *)
simpl in H. (* H变为 false = true *)
discriminate H. (* 排除不可能情况 *)
Qed.

当然,使用rewrite也是可以的,以下是rewrite版本:

version-2.0
Theorem andb_true_elim2 : b c, andb b c = true c = true.
Proof.
intros b c H.
destruct b eqn:Eb.
- (* b = true *)
simpl in H. (* H : c = true *)
rewrite H. (* 目标从 c=true 变为 true=true *)
reflexivity. (* 验证 *)
- (* b = false *)
simpl in H. (* H : false = true *)
discriminate H.
Qed.
特性applyrewrite
核心作用将假设/定理的结论匹配当前目标按等式替换表达式中的部分
底层逻辑反向推理(从结论反推前提)符号重写(表达式转换)
典型使用场景假设与目标完全一致时需要修改目标或假设中的子表达式时
是否需要额外步骤直接完成证明通常需要配合 reflexivity
语法示例apply H. (H: A → B,目标为B)rewrite H. (H: x = y)
执行结果用假设直接闭合目标替换后可能仍需验证
类比数学证明”根据定理T,结论成立""将式子中的x替换为y”

终极选择指南

能用 apply 时优先用 —— 当假设与目标形状完全匹配

必须用 rewrite 时 —— 当需要表达式转换或部分替换

复杂场景组合用 —— 例如先 rewrite 修改目标,再 apply 匹配定理