通用递归程序最弱前置条件在Coq中的形式化
1. 最弱前置条件相关定义与健康性条件
在对通用递归程序进行研究时,通过对参数
pt
进行抽象,得到了一个“谓词变换器的变换器”:
[
\lambda pt. wpc \left[ \sigma \left[ p \mapsto pt \right], c \right] : PT \to PT
]
这里,式 (18e) 中的 (\lambda pt. wpc \left[ \sigma \left[ p \mapsto pt \right], c \right]^n) 不再是式 (2) 中定义的展开操作,而是一个折叠操作,定义如下:
[
\begin{cases}
ptt^0 \triangleq \lambda P. F \
ptt^{n + 1} \triangleq ptt(ptt^n)
\end{cases}
]
用 (\exists n. \lambda pt. wpc \left[ \sigma \left[ p \mapsto pt \right], c \right]^n (P) (s)) 代替 (\bigvee_{n < \omega} wp \left( proc\ p \equiv c^n \right)),二者语义相同,但前者可以在Coq中表达。
式 (18f) 定义了
pcall p
的
wpc
,它是由 (\sigma) 分配给
p
的谓词变换器。
为了进行合理性检查,证明了三个健康性条件,即严格性、单调性和可结合性。这里仅列出本文中使用的广义单调性引理:
引理1(广义单调性引理)
:
[
\forall c. \left( \sigma_1 \preceq_{\sigma} \sigma_2 \Rightarrow mono(\sigma_1) \Rightarrow mono(\sigma_2) \Rightarrow wpc(\sigma_1, c) \preceq_{pt} wpc(\sigma_2, c) \right) \land \left( mono(\sigma) \Rightarrow mono(wpc(\sigma, c)) \right)
]
2. 操作语义
为了加强
wp
的合理性,定义了操作语义,并建立了操作语义与
wp
之间的形式化关系。操作语义以归纳定义的关系 (s \xrightarrow{c}_{cs} s’) 给出,意味着命令
c
的执行将程序存储从
s
转换为
s'
。
cs
是执行
c
时的“调用栈”,调用栈类型
CS
定义如下:
CS : Set CS form
ϑ : CS // 空调用栈
nil cs
p : ID
c : C
cs, cs' : CS
cs[p ⇝(c, cs')] : CS // 通过将 (p, c, cs') 压入 cs 得到的调用栈
cons cs
当执行递归过程
proc p ≡ c
时,操作
cs[p ⇝(c, cs)]
将过程体
c
连同调用栈
cs
压入调用栈,然后执行过程体
c
。当执行递归调用
pcall p
时,使用操作
lookup(cs, p)
查找被调用的过程体和执行它的调用栈。
lookup
的定义如下:
[
\begin{cases}
lookup(cs[p ⇝(c, cs’)], p) \triangleq (c, cs’) & \text{if } p = p’ \
lookup(cs[p ⇝(c, cs’)], p) \triangleq lookup(cs, p) & \text{if } p \neq p’ \
lookup(\vartheta, p) \triangleq \bot
\end{cases}
]
操作语义的规则如下:
|规则|条件|转换|
|----|----|----|
|
e asrt
|
[[e]]s = true
| (s \xrightarrow{assert(e)}
{cs} s) |
|
e asgn
|
[[e]]s = v
| (s \xrightarrow{i := e}
{cs} s \left[ v / i \right]) |
|
e ifs t
|
[[e]]s = true
且 (s \xrightarrow{c_1}
{cs} s’) | (s \xrightarrow{if\ e\ then\ c_1\ else\ c_2}
{cs} s’) |
|
e ifs f
|
[[e]]s = false
且 (s \xrightarrow{c_2}
{cs} s’) | (s \xrightarrow{if\ e\ then\ c_1\ else\ c_2}
{cs} s’) |
|
e seq
| (s \xrightarrow{c_1}
{cs} s’) 且 (s’ \xrightarrow{c_2}
{cs} s’‘) | (s \xrightarrow{c_1; c_2}
{cs} s’‘) |
|
e proc
| (s \xrightarrow{c}
{cs[p⇝(c,cs)]} s’) | (s \xrightarrow{proc\ p \equiv c}
{cs} s’) |
|
e pcall
|
lookup(cs, p) = (c, cs')
且 (s \xrightarrow{c}
{cs[p⇝(c,cs’)]} s’) | (s \xrightarrow{pcall\ p}_{cs} s’) |
3. 操作语义与wp的关系
操作语义与
wp
可以通过以下引理建立联系:
引理2(操作语义到wp)
:
[
s \xrightarrow{c}_{\vartheta} s’ \Rightarrow P(s’) \Rightarrow wp(c)(P)(s)
]
即如果在空调用栈下执行命令
c
得到程序存储
s'
,那么对于任何谓词
P
,若
P
在
s'
上成立,则谓词
wp(c)(P)
(由
wp(c)
变换后的谓词
P
)在初始程序存储
s
上成立。
为了证明引理2,首先证明了以下广义引理,引理2是引理3的推论:
引理3(操作语义到wp的广义引理)
:
[
\begin{cases}
s \xrightarrow{c}
{cs} s’ \
can(cs) \
\exists n. \left( \forall P. P(s’) \Rightarrow wpc({|cs|}^n, c)(P)(s) \right)
\end{cases}
]
其中,谓词
can
用于约束调用栈
cs
的形式,保证执行 (s \xrightarrow{c}
{cs} s’) 是某个顶层执行 (s \xrightarrow{c}_{\vartheta} s’) 的子执行。
can
是一个归纳定义的谓词:
[
\begin{cases}
can(\vartheta) \
can(cs) \Rightarrow can(cs[p ⇝(c, cs’)])
\end{cases}
]
操作 ({|cs|}^n) 用于将调用栈转换为环境,定义如下:
[
\begin{cases}
{|\vartheta|}^n \triangleq \varepsilon \
{|cs[p ⇝(c, cs’)]|}^n \triangleq {|\cs|}^n \left[ p \mapsto \lambda pt. wpc \left[ {|\cs|}^n \left[ p \mapsto pt \right], c \right]^n \right]
\end{cases}
]
下面是引理3的部分证明思路:
-
当执行使用
e proc
规则时
:
- 此时 (c = (proc\ p \equiv c)) 且 (s \xrightarrow{c}
{cs[p⇝(c,cs)]} s’)。根据归纳假设和条件 (23b),可以得到 (\exists n. \forall P. P(s’) \Rightarrow wpc({|cs[p ⇝(c, cs)]|}^n, c)(P)(s))。
- 将
n
赋值为
n
并展开
wpc
的定义,目标 (23c) 变为 (\forall P. P(s’) \Rightarrow \exists n. \lambda pt. wpc({|cs|}^n \left[ p \mapsto pt \right], c)^n (P)(s))。
- 再将
n + 1
赋值给
n
并展开 (\cdots^{n + 1}) 的定义,得到 (\forall P. P(s’) \Rightarrow wpc \left( {|cs|}^n \left[ p \mapsto \lambda pt. wpc \left( {|cs|}^n \left[ p \mapsto pt \right], c \right)^n \right], c \right) (P)(s)),这正是展开 ({| \cdots |}^n) 定义后的 (38) 式。
-
当执行使用
e pcall
规则时
:
- 此时 (c = (pcall\ p)) 且
lookup(cs, p) = (c, cs')
,(s \xrightarrow{c}
{cs[p⇝(c,cs)]} s’)。
- 通过应用引理7到 (23b) 和 (41a),可以推出
can(cs)
,进而推出
can(cs[p ⇝(c, cs')])
。根据归纳假设,可以推出 (\exists n. \forall P. P(s’) \Rightarrow wpc \left( {|cs|}^n \left[ p \mapsto \lambda pt. wpc \left( {|cs|}^n \left[ p \mapsto pt \right], c \right)^n \right], c \right) (P)(s))。
- 将
n + 1
赋值给
n
,目标 (23c) 变为 (P(s’) \Rightarrow {|cs|}^{n + 1}(p)(P)(s))。
- 通过应用引理8到 (41a) 并展开 (\cdots^{n + 1}) 的定义,可以推出 ({|cs|}^{n + 1}(p) = wpc \left( {|cs|}^{n + 1} \left[ p \mapsto \lambda pt. wpc \left( {|cs|}^{n + 1} \left[ p \mapsto pt \right], c \right)^n \right], c \right))。
- 应用 (42) 到 (45) 中的 (P(s’)),得到 (wpc \left( {|cs|}^n \left[ p \mapsto \lambda pt. wpc \left( {|cs|}^n \left[ p \mapsto pt \right], c \right)^n \right], c \right) (P)(s))。
- 通过应用引理9和引理10,可以推出 ({|cs|}^n \preceq_{\sigma} {|cs|}^{n + 1}) 和 (\lambda pt. wpc \left( {|cs|}^n \left[ p \mapsto pt \right], c \right)^n \preceq_{pt} \lambda pt. wpc \left( {|cs|}^{n + 1} \left[ p \mapsto pt \right], c \right)^n),进而推出 (wpc \left( {|cs|}^n \left[ p \mapsto \lambda pt. wpc \left( {|cs|}^n \left[ p \mapsto pt \right], c \right)^n \right], c \right) \preceq_{pt} wpc \left( {|cs|}^{n + 1} \left[ p \mapsto \lambda pt. wpc \left( {|cs|}^{n + 1} \left[ p \mapsto pt \right], c \right)^n \right], c \right)),从而证明目标 (45)。
下面是引理3证明的流程:
graph TD;
A[s c ->cs s'] --> B{执行规则};
B -->|e proc| C[推导归纳假设];
C --> D[展开wpc定义];
D --> E[赋值n并展开定义];
E --> F[完成证明];
B -->|e pcall| G[推导can(cs)和can(cs[p⇝(c,cs)])];
G --> H[推导归纳假设];
H --> I[赋值n+1并展开定义];
I --> J[应用引理推导关系];
J --> K[完成证明];
4. 引理3的非形式化解释
在式 (18e) 中,
n
由执行命令
c
期间对
p
的递归调用次数决定,
n
可以是大于该次数的任意自然数。展开 (wpc(\sigma, proc\ p \equiv c)(P)(s)) 中的
wpc
可得:
[
\exists n. \lambda pt. wpc \left[ \sigma \left[ p \mapsto pt \right], c \right]^n (P) (s)
]
若 (n = n + 1),展开 (\cdots^{n + 1}) 的定义后得到:
[
wpc \left( \sigma \left[ p \mapsto \lambda pt. wpc \left( \sigma \left[ p \mapsto pt \right], c \right)^n \right], c \right) (P)(s)
]
自然数
n
是从程序存储
s
开始执行命令
c
期间
p
的递归调用次数。
对操作语义的分析表明,如果执行 (s \xrightarrow{c}
{cs} s’) 是某个顶层执行 (s \xrightarrow{c}
{\vartheta} s’) 的子执行,那么
cs
必须具有以下形式:
[
\vartheta[p_0 \Rightarrow (c_0, cs_0)][p_1 \Rightarrow (c_1, cs_1)] \cdots [p_m \Rightarrow (c_m, cs_m)]
]
其中,([p_i \Rightarrow (c_i, cs_i)])((i \in {0, \cdots, m}))是通过执行
proc p_i ≡ c_i
压入
cs
的。在执行
c
期间,每个过程
p_i
都有一定数量的递归调用,设这些数量为 (n_{p_0}, n_{p_1}, \cdots, n_{p_m})。
在设计 ({|cs|}^n) 时,最初打算将
cs
转换为:
[
\varepsilon [p_0 \mapsto fd({|cs_0|}, n_{p_0}, p_0, c_0)] [p_1 \mapsto fd({|cs_1|}, n_{p_1}, p_1, c_1)] \cdots [p_m \mapsto fd({|cs_m|}, n_{p_m}, p_m, c_m)]
]
其中,(fd(\sigma, n, p, c) \triangleq \lambda pt. wpc \left[ \sigma \left[ p \mapsto pt \right], c \right]^n)。但要找到所有自然数 (n_{p_0}, n_{p_1}, \cdots, n_{p_m}) 的值很不方便。由于 (fd(\sigma, n, p, c)) 关于
n
是单调的,只需要处理它们的上界即可。因此,最终将
cs
转换为:
[
\varepsilon [p_0 \mapsto fd({|cs_0|}^{n_{max}}, n_{max}, p_0, c_0)] [p_1 \mapsto fd({|cs_1|}^{n_{max}}, n_{max}, p_1, c_1)] \cdots [p_m \mapsto fd({|cs_m|}^{n_{max}}, n_{max}, p_m, c_m)]
]
其中,(n_{max}) 是大于任何 (n_{p_i})((i \in {0, \cdots, m}))的自然数。引理3中的
n
实际上就是这里的 (n_{max}),这也解释了式 (25) 中 ({|cs|}^n) 的定义。
通用递归程序最弱前置条件在Coq中的形式化
5. wp与操作语义的关系
wp
与操作语义可以通过以下引理建立联系:
引理4(wp到操作语义)
:
[
wp(c)(P)(s) \Rightarrow \exists s’. \left( s \xrightarrow{c}_{\vartheta} s’ \land P(s’) \right)
]
即对于任何谓词
P
,如果
wp(c)
对谓词
P
的变换在
s
上成立,那么在空调用栈下执行命令
c
会终止,并将程序存储从
s
转换为
s'
,且
P
在
s'
上成立。
为了证明引理4,首先证明了以下广义引理,引理4是引理5的推论:
引理5(wp到操作语义的广义引理)
:
[
\begin{cases}
wpc(envof(ecs), c)(P)(s) \
ecan(ecs) \
\exists s’. \left( s \xrightarrow{c}_{csof(ecs)} s’ \land P(s’) \right)
\end{cases}
]
其中,
ecs
是一个“扩展调用栈”,其类型
ECS
定义如下:
ECS : Type ECS form
θ : ECS // 空扩展调用栈
nil ecs
p : ID
c : C
ecs, ecs' : ECS
pt : PT
ecs[p ⇝(c, ecs', pt)] : ECS // 通过将 (p, c, ecs', pt) 添加到 ecs 头部得到的扩展调用栈
cons ecs
通过遗忘三元组中的
pt
,可以得到一个正常的调用栈,这由操作
csof(ecs)
实现:
[
\begin{cases}
csof(\theta) \triangleq \vartheta \
csof(ecs[p ⇝(c, ecs’, pt)]) \triangleq csof(ecs)[p \Rightarrow (c, csof(ecs’))]
\end{cases}
]
通过遗忘三元组中的
c
和
ecs
,可以得到一个正常的环境,这由操作
envof(ecs)
实现:
[
\begin{cases}
envof(\theta) \triangleq \varepsilon \
envof(ecs[p ⇝(c, ecs’, pt)]) \triangleq envof(ecs) [p \mapsto pt]
\end{cases}
]
操作
lookup_ecs(p, ecs)
用于在
ecs
中查找映射到
p
的三元组
(c, ecs', pt)
:
[
\begin{cases}
lookup_ecs(p, ecs[p ⇝(c, ecs’, pt)]) \triangleq (p, c, ecs’, pt) & \text{if } p = p’ \
lookup_ecs(p, ecs[p ⇝(c, ecs’, pt)]) \triangleq lookup_ecs(p, ecs) & \text{if } p \neq p’ \
lookup_ecs(p, \theta) \triangleq \bot
\end{cases}
]
谓词
ecan(ecs)
用于约束扩展调用栈
ecs
的形式,使得在每个三元组
(c, ecs', pt)
中,
pt
与
(c, ecs')
有如下关系:
[
ecan(ecs) \triangleq lookup_ecs(p, ecs) = (c, ecs’, pt) \Rightarrow pt(P)(s) \Rightarrow \exists s’. \left( s \xrightarrow{proc\ p \equiv c}_{csof(ecs)} s’ \land P(s’) \right)
]
由于
lookup_ecs(p, \theta) = \bot
,显然
ecan(θ)
成立。因此,通过将
ecs
实例化为
θ
,引理4可以作为引理5的推论得到证明。
下面是引理5的部分证明思路:
-
当
c = (proc p ≡ c)
时
:
- 展开
wpc
的定义后,前提 (31a) 变为 (\exists n. \forall P. \lambda pt. wpc \left[ envof(ecs) \left[ p \mapsto pt \right], c \right]^n (P)(s))。
- 使用对
n
的嵌套归纳来证明目标,分为两种情况:
- 当
n = 0
时,这种情况可以被反驳。因为 (\lambda pt. wpc \left[ envof(ecs) \left[ p \mapsto pt \right], c \right]^n) 会简化为 (\lambda P. F),它不能在
P
和
s
上成立,这与 (50) 矛盾。
- 当
n = n + 1
时,展开 (\cdots^{n + 1}) 的定义后,(50) 变为 (wpc \left( envof(ecs) \left[ p \mapsto \lambda pt. wpc \left( envof(ecs) \left[ p \mapsto pt \right], c \right)^n \right], c \right) (P)(s)),这正是展开
envof(· · · )
定义后的 (wpc \left( envof(ecs[p \mapsto (c, ecs, \lambda pt. wpc \left( envof(ecs) \left[ p \mapsto pt \right], c \right)^n )]), c \right) (P)(s))。从对
n
的嵌套归纳假设可以证明 (ecan(ecs[p \mapsto (c, ecs, \lambda pt. wpc \left( envof(ecs) \left[ p \mapsto pt \right], c \right)^n )]))。通过对这个和 (52) 应用主归纳假设,展开
csof
的定义后,可以推出 (\exists s. s \xrightarrow{c}
{csof(ecs)[p⇝(c,csof(ecs))]} s \land P(s))。将
s
赋值为
s'
,目标 (31d) 可以直接从 (53) 中的 (P(s)) 得到证明,目标 (31c) 可以通过对 (53) 中的 (s \xrightarrow{c}
{csof(ecs)[p⇝(c,csof(ecs))]} s) 应用
e proc
规则得到证明。
-
当
c = (pcall p)
时
:
- 展开
wpc
的定义后,前提 (31a) 变为
envof(ecs)(p)(P)(s)
。通过应用引理11到这个式子,可以得到 (\exists c, ecs’. lookup_ecs(p, ecs) = (c, ecs’, envof(ecs)(p)))。
下面是引理5证明的流程:
graph TD;
A[wpc(envof(ecs), c)(P)(s)] --> B{命令类型};
B -->|proc p ≡ c| C[展开wpc定义];
C --> D{嵌套归纳n};
D -->|n = 0| E[反驳情况];
D -->|n = n + 1| F[展开定义并应用归纳假设];
F --> G[证明目标];
B -->|pcall p| H[展开wpc定义];
H --> I[应用引理推导];
I --> J[继续证明];
6. 结论
通过在Coq中对
wp
进行计算嵌入,并将其与操作语义建立联系,验证了该定义的合理性。这种嵌入方式结合了深度嵌入和浅度嵌入的优点,可以用于验证程序变换和具体程序。
与其他相关工作相比,本文的方法具有独特的优势。例如,Laibinis和Wright在HOL中处理一般递归,但采用的是浅度嵌入,且
wp
与操作语义之间没有联系;Fillitre通过扩展Coq从带注释的命令式程序生成证明义务,但由于采用浅度嵌入,无法验证元编程(如程序变换);Kleymann直接从操作语义推导Hoare逻辑,虽然可以验证程序变换,但由于操作语义是形式化为归纳关系,验证具体程序不太方便。而本文通过对
wp
进行计算处理,在验证具体程序时可以使用计算机制简化证明义务。
可以将Hoare三元组定义为:
[
{P_1} c {P_2} \triangleq P_1(s) \Rightarrow wp(c)(P_2)(s)
]
由此可以推导出结构语句的Hoare逻辑规则。例如,对于
if e then c1 else c2
的规则如下:
引理6(
if e then c1 else c2
的证明规则)
:
[
\begin{cases}
{\lambda s. ([[e]]s = true \land P_1(s))} c_1 {P_2} \
{\lambda s. ([[e]]s = false \land P_1(s))} c_2 {P_2} \
\Rightarrow {P_1} if\ e\ then\ c_1\ else\ c_2 {P_2}
\end{cases}
]
Hoare逻辑规则可以将证明义务从父语句传播到子语句。当传播过程到达原子语句(如
i := e
)时,通过展开
wp
的定义,可以使用计算机制简化证明义务。用户可以根据需要选择使用Hoare逻辑规则或直接使用
wp
的定义。
已经对插入排序程序进行了验证,虽然在本文的设置下验证具体程序比Fillitre的方法稍微复杂一些,但能够同时验证程序变换和具体程序的能力使本文的方法具有独特性。后续将在另一篇论文中详细介绍在本文设置下进行程序验证的处理方法。
超级会员免费看
54

被折叠的 条评论
为什么被折叠?



