Hoare逻辑
CST Part II 课程的学习指南. 本文档旨在提供背景阅读材料以支持讲座——可以将其视为一本免费的可下载教科书. 第1到5章介绍了Floyd和Hoare提出的程序规约与证明的经典思想. 尽管大部分材料已经很老——请看一些被引用参考文献上的日期——它仍然是当前研究的基础. 第6章是对程序精化的一个非常简要的介绍; 它提供了从Hoare风格的规约计算
出一个实现的规则. 第7章是对分离逻辑思想的介绍, 分离逻辑是Hoare逻辑的一个扩展, 用于规约和验证操纵指针的程序. 分离逻辑建立在Burstall的早期思想之上, 但其现代形式归功于O'Hearn和Reynolds.
请注意, 讲座中呈现的一些主题可能未在本文档中涵盖, 本文档中的一些材料也可能与讲座中涵盖的主题无关. 例如, 程序精化和分离逻辑的主题可能只会被非常粗略地描述, 如果有涉及的话. 考试题目将基于讲座中呈现的材料.
Part II课程Hoare Logic是从一门更早的Part II课程演变而来的, 该课程的网页可以在我的主页上找到 (www.cl.cam.ac.uk/~mjcg). 那门课程的一些考试题目可能是很好的练习 (但请注意其中一些是基于本课程未涵盖的材料). 一份包含当前课程练习题的单独文档可从网页获取.
警告. 这里的材料由过去课程讲义的重新组织的摘录以及新材料组成. 很有可能存在符号不一致, 遗漏和错误. 如果您发现此类缺陷, 请将详细信息发送至Mike.Gordon@cl.cam.ac.uk.
致谢. 感谢Martin Vechev和John Wickerson在这些笔记的先前草稿中发现了许多错误 (其中一些是严重的), 同时也感谢他们对改进文本提出的建议.
第1章 程序规约
本章引入了一种包含赋值, 条件式, 块和循环的简单编程语言. 然后用它来描述Hoare用于规约程序部分正确性的记号. Hoare的记号使用形式逻辑记号来表达关于程序变量值的条件. 该记号被非形式化地描述并通过示例加以刻画.
第1.1节 引论
为了从数学上证明一个程序的正确性, 我们必须首先刻画正确本身的含义. 本章我们描述了一种用于刻画命令式程序的预期行为的记号. 这种记号应该归功于C.A.R. Hoare.
执行命令式程序有着改变状态的作用, 直到第7章之前, 我们将状态取为程序变量的值. 为了使用这样的程序, 我们需要建立初始状态, 也就是将一些变量的值设置为有趣的值 (value of interest). 然后我们执行程序. 这将初始状态变换为最终状态. 然后我们检视最终状态里的变量的值来得到预期的结果. 例如, 为了计算除以, 我们可能会将和分别载入程序变量和. 然后我们可能会执行一个合适的程序 (见第1.4节的例子7) 来将初始状态变换为最终状态以使得变量和分别存放着商和余数.
这些笔记里要使用的编程语言将于下一节进行描述.
第1.2节 一种小语言
程序由诸如赋值和条件式等命令构建而成. 术语程序
和命令
的确可以算是近义词; 前者只会用于表示完整算法的命令. 这里的术语陈述 (statement)
用于程序变量上的条件, 其出现于正确性规约之中 (见第1.3节). 这里存在混淆的风险, 因为一些作者将这个词用于命令 (如for-statement
[14]).
现在我们描述我们的小语言里的各种命令的句法 (即形式) 和语义 (即意义). 以下约定将会采用:
- 符号代表任意的变量. 特定变量的例子有等等.
- 符号代表任意的表达式 (或者说项). 这些是像之类的东西, 其指称着值 (通常是数字).
- 符号代表任意的陈述. 这些是像之类的条件, 其要么为真要么为假.
- 符号代表我们编程语言里的任意命令; 这些会在本节的剩余部分进行描述.
项和陈述在第1.5节进行了更为细致的描述.
第1.2.1小节 赋值
句法:
语义: 状态的改变由将项之值赋给变量完成. 所有的变量都假定具有全局作用域.
例子: X:=X+1
这给变量之值加上一.
第1.2.2小节 序列
句法:
语义: 命令按照顺序进行执行.
例子: R:=X; X:=Y; Y:=R
变量和之值经由临时变量进行了互换. 这个命令具有将变量之值变为变量之旧值的副作用. {译注: 这个还是这些?}
第1.2.3小节 条件式
句法:
语义: 如果陈述在当前状态下为真, 那么被执行. 如果为假, 那么被执行.
例子: IF X<Y THEN MAX:=Y ELSE MAX:=X
变量之值被设为和之值的最大值.
第1.2.4小节 命令
句法:
语义: 如果陈述在当前状态下为真, 那么被执行, 然后命令重复. 如果为假, 那么什么也不做. 因此, 反复执行, 直至的值变为假. 如果永远不变为假, 那么该命令的执行永远不终止.
例子: WHILE ¬(X=0) DO X:=X-2
如果之值非零, 那么其值会减去, 然后进程重复. 这个命令将会终止 (此时有着值), 如果的值是一个非负的偶数. 其他状态下都不会终止.
第1.2.5小节 句法总结
我们的小语言的句法可以用BNF记号总结如下:
<command> ::= <variable> := <term>
| <command>; ...; <command>
| IF <statement> THEN <command> ELSE <command>
| WHILE <statement> DO <command>
请注意:
- 变量, 项, 陈述描述于第1.5节.
- 这个BNF句法是有歧义的: 例如, 可能指的是, 也可能指的是. 每当有必要时, 我们会用括号进行澄清.
第1.2.6小节 历史注记
旧的Part II课程Specification and Verification I是基于一种与上述描述类似的语言的, 但具有额外的特性: 块 (带有局部变量), 命令和数组. 块和命令并未添加根本性的新思想, 因此它们将不会被涵盖; 数组使用分离逻辑能够更好地处理 (见第7章). 在旧课程中我使用和来组合命令, 而在这里我只使用括号. 因此以前我会写而不是. 我提及此事是因为它可能有助于将旧的考试题目作为本课程的练习来重新使用.
第1.3节 Hoare记号
在一篇开创性论文 [13] 里C.A.R. Hoare引入了记号, 其有时被称为Hoare三元组, 用于刻画一个程序做了什么. 在这样一个Hoare三元组之中:
- 是一个程序.
- 和是中用到的程序变量上的条件. 程序变量上的条件以标准的数学记号连带着诸如这样的逻辑联结词书写. 这些在第1.5节有进一步描述.
我们称为真, 如果每当在某个满足的状态下执行且的执行终止, 那么执行终止的状态满足.
例子: . 这里的是之值为这一条件, 是之值为这一条件, 是赋值命令. 为真. {译注: 这个为真是从语义角度而言的.}
一个表达式被称为一个部分正确性规约; 被称为其前条件而被称为其后条件.
这样的规约之所以是部分的
, 是因为即便为真, 的执行在满足的状态下开始也不必终止. 其所要求的仅是如果终止, 那么成立.
一种更强的规约是完全正确性规约. 这种规约没有标准的记号. 我们将使用.
完全正确性规约为真当且仅当以下两个条件满足:
- 如果在满足状态下开始执行, 那么终止.
- 在终止之后, 成立.
部分正确性和完全正确性之间的关系可以非正式地表述为以下等式:
完全正确性是我们最终感兴趣的, 但通常更容易通过分别建立部分正确性和终止性来证明它.
终止性通常是容易建立的, 但有一些著名的例子并非如此. 例如, 未解决的Collatz猜想与下面的程序是否对的所有值都终止有关 (见下面的练习):
WHILE X>1 DO
IF ODD(X) THEN X := (3×X)+1 ELSE X := X DIV 2
(表达式X DIV 2求值为向下取整到一个整数的结果, 不过由于这里条件式的分支仅在为偶数时才被执行, 实际上不需要取整.)
著名数学家Paul Erdős关于Collatz猜想说道: 数学对于这样的问题尚未准备好.
他为其解答提供500美元奖励.
第1.4节 一些例子
以下的例子刻画了部分正确性的各种方面.
在以下的例子5, 6, 7中, 是永远为真的条件. 在例子3, 4, 7中, 是联结词合取, 即如果和是条件, 那么是这样的条件, 其为真每当和均成立.
这是说如果命令在一个满足条件的状态下执行, 那么如果执行终止 (在此情况下的确如此), 那么条件将会成立. 这个规约显然为真.
这是说如果的执行始于满足的状态, 那么将会成立. 这显然为假.
这是在说如果的执行终止 (的确如此), 那么和之值就被交换了. 变量和没有出现在命令里, 其是用来对于程序变量和的初值进行命名的, 这种变量被称为逻辑变量, 或者辅助变量, 或者幽灵变量.
这是在说可以交换和之值, 但是实际上这并不为真.
这是在说每当终止时, 成立.
这种规约对于每个条件和每个命令为真, 这是因为总是为真.
{T}
R:=X;
Q:=0;
WHILE Y≤R DO
(R:=R-Y; Q:=Q+1)
{R < Y ∧ X = R + (Y × Q)}
这是, 其中是上述的命令. 这个规约为真, 如果每当的执行终止时, 那么是商而是除以的余数. 这个规约的确为真, 即便起始为负数!
这个例子里用到了程序变量, 读者不应将其与5中所用到的混淆. 程序变量 (注意字体) 遍历数字, 而后条件 (注意字体) 则遍历陈述. 一般而言, 我们使用表示特定的程序变量, 而使用表示遍历陈述的变量. 尽管这种对于字体的微妙运用乍看上去可能令人迷惑, 但是读者一旦理解之后, 两种Q
之间的区分就非常显然了. (即便不看字体, 你也应该能够从上下文中区分它们.)
第1.5节 项和陈述
这里用于表达前条件和后条件的记号基于一阶逻辑. 我们只会简要回顾, 因为读者默认已经非常熟悉它了.
以下是原子陈述的例子:
陈述要么为真要么为假. 陈述永远为真, 而陈述永远为假. 陈述为真, 如果之值等于. 陈述为真, 如果之值小于之值. 陈述为真, 如果之值等于之值与和之积的和.
陈述由项构建而成, 例如:
项指称着值, 例如数字和字符串, 不像陈述要么为真要么为假. 一些项, 例如和, 指称着固定的值, 而其他一些项则包含变量, 例如等等, 其值可能会变化. 我们对于项将会使用常规数学记号, 如以下例子所刻画的那样:
和是分别总是为真和为假的原子陈述. 其他的原子陈述则由项使用谓词构建而成. 以下是更多的一些例子:
和是谓词的例子, 而和则是中缀谓词的例子. 表达式是项的例子.
复合陈述由原子陈述使用以下逻辑联结词构建而成:
设和是陈述, 那么:
- 为真如果为假, 为假如果为真.
- 为真每当和均为真.
- 为真如果或为真, 可以同时为真.
- 为真如果每当为真则也为真. 根据约定, 如果为假, 那么为真. 实际上, 通常将视为等价于; 然而, 一些称为直觉主义者的哲学家并不赞同这一对于推出的处理方式. {译注: 显然, 不仅是哲学家, 绝大部分编程语言学家也并不赞同.}
使用联结词构建的陈述的例子有:
为了减少括号的需要, 通常假定比和结合得更紧密, 而这两个又比和结合得更紧密, 例如:
第2章 Hoare逻辑
本章讨论了形式化证明的想法. 然后, 我们引入了Hoare逻辑 (或称Floyd-Hoare逻辑) 作为对于程序进行形式化推理的方法.
上一章我们引入了三种可能为真或假的表达式:
- 部分正确性规约.
- 完全正确性规约.
- 数学陈述, 例如.
我们假定读者已经知道如何证明简单的数学陈述, 例如以上的iii中的那条. 例如, 以下是该事实的一个证明.
这个证明由一系列行构成, 每行都是某个公理的实例或者根据之前的行由推理规则推出. 这个证明的最后一行是其所证明的陈述, 也就是.
为了构造部分正确性规约的形式化证明, 需要公理和推理规则. 这正是Hoare逻辑所提供的. 该演绎系统的形式化表述归功于Hoare [13], 但其中一些基本思想源自Floyd [9].
Hoare逻辑中的证明是一系列行的序列, 其中每一行要么是该逻辑的一条公理, 要么是由前面的行通过该逻辑的一条推理规则推导而来.
构造形式化证明的原因在于试图确保只使用可靠的演绎方法. 有了可靠的公理和推理规则, 我们就可以确信所得出的结论是真的. 另一方面, 如果任何公理或推理规则是不可靠的, 那么就有可能推导出错误的结论; 例如:
形式化证明明确指出了为得出结论所使用的公理和推理规则. 人们很容易提出一些看似合理但实际上不可靠的程序推理规则. 计算机程序的正确性证明往往非常复杂, 需要形式化方法来确保其有效性. 因此, 将所使用的推理原则完全显式化是很重要的, 这样才能对其可靠性进行分析.
对于某些应用而言, 正确性尤为重要. 例子包括生命攸关的系统, 如核反应堆控制器, 汽车制动系统, 电传飞控飞机以及软件控制的医疗设备. 曾有一起法律诉讼源于一台癌症治疗设备因软件缺陷导致辐射过量, 造成数人死亡 [15]. 当穷举测试不可能实现时 (几乎总是如此), 形式化正确性证明提供了一种确认程序中不存在缺陷的方法.
本章将解释并举例说明用于程序推理的Hoare演绎系统. 该系统的可靠性和完备性的数学分析将在第4章中讨论.
第2.1节 Hoare逻辑的公理和规则
正如本章开头所讨论的, 一个命题的形式化证明是以该命题为最后一行的一个行序列, 其中每一行要么是一条公理的实例, 要么是由前面的行通过一条推理规则推导而来. 若是一个命题 (无论是普通数学的还是Hoare逻辑的), 我们记来表示有一个证明. 拥有证明的命题称为定理. 如前所述, 在本讲义中只描述Hoare逻辑的公理和推理规则; 因此, 若是一个数学定理, 我们将直接断言而不给出任何形式化的论证. 当然, 为了达到完全的严格性, 这些断言必须被证明, 但如何做到这一点的细节假定读者已经了解 (例如通过逻辑与证明课程).
Hoare逻辑的公理在下面通过模式来指定, 这些模式可以被实例化以得到特定的部分正确性规约. Hoare逻辑的推理规则将用如下形式的记号来描述:
这是说结论可以从推导而来, 后者是该规则的假设. 这些假设可以全部是Hoare逻辑的定理 (如下面的顺序规则), 也可以是Hoare逻辑定理与数学定理的混合 (如第2.1.2小节中描述的前条件加强规则).
第2.1.1小节 赋值公理
赋值公理表达了这样一个事实: 在执行赋值命令之后, 变量的值等于执行前表达式的值. 为了将此形式化, 注意到如果一个命题在赋值之后为真, 那么在执行赋值之前, 将中的替换为所得到的命题必须为真. {译注: 这似乎和以下公理所表达的内容方向相反.}
为了形式化地表达这一点, 定义以表示将中所有的出现替换为之结果. 读作 with for
. 例如,
记住这个记号的方法是消去律
这类比于分式的消去性质
Hoare赋值公理
其中
是任意的变量,
是任意的表达式,
是任意的陈述, 而
代表将陈述
中变量
的所有出现都替换为项
.
赋值公理的实例有:
- , 如果在中没有出现.
许多人会感到赋值公理和他们所期望的相反. 两种常见的错误直觉如下:
(未完待续)
如果取为, 为, 为, 就会得到, 这显然是错误的. {译注: 也就是unsound.}
关于赋值公理容易产生错误直觉这一事实表明, 拥有严格的手段来确立公理和规则的有效性是很重要的. 我们将在后面的第4章中深入讨论这个主题, 届时我们将给出我们这个小型编程语言的形式化语义, 然后证明Hoare逻辑的公理和推理规则是可靠的. 当然, 这一过程只有在我们相信形式化语义本身的正确性的前提下, 才能增强我们对公理和规则的信心. 上述简单的赋值公理对于真实的
编程语言是不成立的. 例如, G. Ligler [17] 的工作表明, 它在(现已过时的)语言Algol 60中以六种不同的方式失效.
赋值公理存在一种前向
版本, 其有时称为Floyd赋值公理, 因为其对应于应该归功于Floyd [9] 的赋值的原始语义. 在以下规则之中, 存在量化的变量是执行赋值之前的状态 (始状态) 中的之值. 后条件断言了在赋值之后, 之值是在初始状态下求得的值 (因而是), 并且在初始状态下评估的前条件 (因而是) 继续成立.
Floyd赋值公理
其中
是一个新变量, 即既不等于
也没有出现在
或
中.
一个实例是:
简化这个后条件:
Floyd赋值公理等价于标准的赋值公理, 但是更难于运用, 这是由于其所引入的存在量词. 然而, 这是分离逻辑的重要部分.
Hoare赋值公理与最弱前条件有关 (见第4.3.3小节), 而Floyd赋值公理则与最强后条件有关 (见第4.4.1小节). 以下这些会在刚才提及的小节里得到解释:
其中和分别代表最弱自由前条件和最强后条件.
我们的小编程语言和真实语言不同的一个方面在于对于赋值命令的右侧表达式的求值不会对于状态施加副作用
. 赋值公理的有效性依赖于这个条件. 为了看出这一点, 设我们的语言得到了扩展, 其可以含有形如的表达式, 其中是一个命令, 而是一个表达式. 这样一种表达式的求值是先执行, 然后对于求值, 最后返回该结果作为的值. 因此, 对于这种表达式的求值可能会导致由执行引起的副作用
. 例如, 具有值, 但是其求值也会对于变量产生副作用
, 通过将存储其中. 如果赋值公理应用于像这样的表达式, 那么其可以用来推导出:
这是由于压根就没有出现在之中. 这个显然是错的, 因为在赋值之后应该具有值.
第2.1.2小节 前条件强化
下一条Hoare逻辑规则允许第18页的i和ii的前条件得到简化. 回忆一下,
表示可由推导出来. {译注: 这是句法概念.}
使用该记号, 前条件强化规则为
前条件强化
例子
- 根据算术事实, 以及第18页的2, 藉由前条件强化可以得到
这里的变量是辅助变量或者说幽灵变量的例子. 正如之前所描述的 (见第10页), 辅助变量是出现于部分正确性规约中但未出现在命令中的变量. 这样的变量用于将执行前后的状态中的值关联起来. 例如, 以上的规约里说的是如果之值为, 那么在执行赋值之后其值将会变成.
- 根据逻辑事实, 以及第18页的3, 我们可以推导出如果没有出现在之中, 那么:
第2.1.3小节 后条件弱化
正如之前的规则允许部分正确性规约里的前条件被强化, 以下的规则允许我们对于后条件进行弱化.
后条件弱化
例子: 以下是一个小小的形式证明.
前条件强化和后条件弱化规则有时被称为rules of consequence.
第2.1.4小节 规约合取与析取
以下两条规则提供了将关于相同命令的不同规约组合起来的方法.
规约合取
规约析取
这些规则对于将证明拆成独立的任务而言是有用的. 例如, 其允许通过单独证明和而证明.
剩余的这些规则允许复合命令的性质由其分量的性质推导而来.
第2.1.5小节 顺序规则
接下来的规则允许我们由和的规约推导出序列的部分正确性规约.
顺序规则
例子: 根据赋值公理:
因此, 根据i, ii和顺序规则, 我们有
因此, 根据iv和iii以及顺序规则, 我们有
第2.1.6小节 导出顺序规则
以下的规则可由顺序规则和consequence规则导出.
(未完待续)
第2.1.7小节 条件规则
条件规则
例子: 假定我们有了
那么, 根据条件式规则(和其他一些规则), 可以推出
第2.1.8小节 规则
如果, 那么我们称: 每当成立, 是的一个不变量. 规则是说, 如果每当测试条件成立时是命令之体的一个不变量, 那么是整个命令的一个不变量. 换言之, 如果执行一次能够保持的为真, 那么执行任意多次也能够保持的为真.
命令也表达了这样的事实, 在一个命令终止之后, 测试条件必然为假 (否则的话, 之前就不会终止).
规则
例子: 根据之前的规则可得:
因此, 根据前条件强化, 可知
于是, 根据规则, 取为, 可以得到
应用赋值公理两次 (以及顺序公理), 容易推导出
根据以上两个结果, 以及顺序规则和后条件弱化, 可以推得
除了规则这个例外, 目前所有描述的公理和规则对于完全正确性和部分正确性都是可靠的. 这是因为我们的小语言中唯一可能不终止的命令就是命令. {译注: 并且, 根据精心(且脆弱)的安排, 如果该语言里的运算和谓词都是完全的, 那么该语言甚至不会抛出异常.} 现在考虑如下证明:
- (赋值公理)
- (前条件强化)
- (2和规则)
若是规则对于完全正确性也是适用的, 那么上述证明就会表明:
但这显然不对, 因为并不终止, 但即便它终止了, 也不可能在任何结果状态下成立.
第2.1.9小节 规则
(未完待续)
第2.1.10小节 数组
在第2.1.1小节末我们表明了朴素的数组赋值公理
并不成立, 这是因为对于的改变也可能改变, 鉴于可能等于.
这个问题的解法 (归功于Hoare) 是将数组赋值
处理为通常赋值
其中项代表一个等同于的数组, 除了分量被变为具有值.
因此, 数组赋值不过是通常变量赋值的特殊情形.
数组赋值公理
其中
是一个数组变量,
是一个整数值表达式,
是任意的陈述, 而记号
代表一个等同于
的数组, 只是
处的值为
.
为了对于数组进行推理, 以下定义了记号意义的公理是必要的.
数组公理
(未完待续)
第3章 机械化程序验证
本章描述了一个简单的程序验证器的架构. 其操作相对于Hoare逻辑的规则而澄清.
在摆弄了一些例子之后, 以下两件事情一目了然:
- 证明一般冗长且乏味 (即便要验证的程序相当简单).
- 存在大量琐碎的细节需要弄对, 其中许多极其平凡, 例如证明.
人们已经进行了许多尝试(并且仍在继续), 试图通过设计系统来自动完成正确性证明, 让这些系统处理在Hoare逻辑中生成形式化证明时那些枯燥而繁琐的部分. 不幸的是, 逻辑学家已经证明, 从原理上讲, 不可能设计出一种判定过程来自动判定任意数学命题的真假 [10]. 然而, 这并不意味着我们无法拥有能够证明许多有用定理的过程. 一般判定过程的不存在仅仅表明, 我们不能期望自动证明一切. 在实践中, 构建一个能够将验证过程中许多枯燥和常规方面机械化的系统是完全可行的. 本章描述了一种常用的实现方法.
尽管不可能自动判定任意命题的真假, 但检验一个任意的形式化证明是否有效却是可能的. 这包括检查证明中每一行出现的结果是否确实是公理, 或者是由前面的行推导出的结论. 由于程序正确性的证明通常非常冗长且枯燥, 手动生成时往往会包含错误. 因此, 即使证明只能在人工辅助下生成, 对其进行机械化检验仍然是有价值的.
第3.1节 概览
前一章展示了如何证明, 方法是证明的各部分之性质, 然后(以合适的证明规则)将它们拼在一起以获得关于本身的预期性质. 例如, 为了证明, 我们首先证明和 (对于某个合适的而言), 然后根据顺序规则可以推出.
这种过程被称为前向证明, 因为人们由公理通过规则向前推进到结论. 在实践中, 更为自然的是后向工作: 从表明的目标开始, 我们生成子目标, 子目标, 如此反复直至问题解决. 例如, 假定我们想要证明:
那么根据赋值公理和顺序规则, 证明以下子目标就足够了:
鉴于. 根据类似的论证, 这个子目标可以被归约为
这显然可由赋值公理推出.
这一章描述了如何形式化这样一种面向目标的证明方法.
这里所描述的验证系统可以被视为一个证明检查器, 同时它也为证明的生成提供了一定的帮助. 下图给出了该系统的概览.
该系统的输入是一个部分正确性规约, 其中标注了描述变量之间关系的数学陈述. 系统从标注后的规约中生成一组纯数学陈述, 称为验证条件 (即VC). 在3.5节中将会证明, 如果这些验证条件是可证明的, 那么原始规约就可以从Hoare逻辑的公理和规则中推导出来.
验证条件被传递给一个定理证明器程序, 该程序尝试自动地证明它们; 如果证明失败, 则向用户寻求建议. 我们将集中讨论与Hoare逻辑相关的方面, 而对定理证明本身着墨甚少.
当前大量研究的目标是构建能够将缓慢且昂贵的人类专家的参与降到最低的系统. 这可以通过以下途径实现:
- 减少所需标注的数量和复杂度, 以及
- 增强定理证明器的能力.
下一节将解释验证条件的工作原理. 在3.5节中, 将依据Hoare逻辑的公理和规则来论证其使用的合理性. 除了作为机械化验证系统的基础之外, 验证条件也是手工进行证明的一种有用方式.
第3.2节 验证条件
以下各节描述了如何将面向目标的证明风格形式化. 为了证明一个目标, 需要完成三件事. 这些将在后文中详细解释, 但这里先给出一个简要概述:
- 通过在程序中插入陈述 (通常称为断言) 来对其进行标注, 这些断言表达了在各个中间点上应当成立的条件. 这一步骤是棘手的, 需要智慧以及对程序工作方式的深刻理解. 将其自动化是一个人工智能问题.
- 然后从标注后的规约中生成一组称为验证条件 (简称VC) 的逻辑陈述. 这一过程是纯机械的, 很容易由程序完成.
- 证明这些验证条件. 将其自动化同样是一个人工智能问题.
我们将证明, 如果能够证明由生成的所有验证条件 (其中已被适当注解), 则.
由于验证条件只是数学陈述, 可以将上述第2步视为将验证问题编译
或翻译为传统数学问题的过程.
下面的例子将给出关于验证条件用法的初步感受.
假设目标是证明 (参见第24页的例子)
{T}
R:=X;
Q:=0;
WHILE Y≤R DO (R:=R-Y; Q:=Q+1)
{X = R+Y×Q ∧ R<Y}
第一步 (上述的i) 是插入注解. 一个合适的注解过了的规约如下:{T}
R:=X;
Q:=0; {R=X ∧ Q=0} ← P1
WHILE Y≤R DO {X = R+Y×Q} ← P2
(R:=R-Y; Q:=Q+1)
{X = R+Y×Q ∧ R<Y}
注解和陈述了每当控制抵达它们时预期成立的条件.
第3.3节 注解
带注解的命令是指在其中嵌入了陈述 (称为断言) 的命令. 如果在以下位置插入了陈述, 则称该命令是正确注解的:
- 对于序列而言, 每个不是赋值命令的命令之前, 其中.
- 在命令的之后.
从直觉上来说, 插入的断言应当表达的是: 每当控制抵达断言出现的点时, 我们预期成立的条件.
正确注解的规约是一个规约, 其中是正确注解的命令.
例子:
第3.4节 验证条件生成
本节描述了一个过程, 其为注解过了的部分正确性规约生成验证条件. 这个过程是上的递归.
赋值命令
由
生成的单条验证条件为
例子:
的验证条件为
这显然为真.
条件式
由
生成的验证条件为
-
所生成的验证条件
-
所生成的验证条件
如果被正确注解了, 那么 (见第39页) 其必然具有以下两种形式之一:
- , 或是
- .
在这两种情形下, 都是正确注解了的命令.
序列
-
(其中不是一个赋值) 所生成的验证条件为:
-
所生成的验证条件
-
所生成的验证条件
-
所生成的验证条件是由
所生成的验证条件
例子:
所生成的验证条件是
所生成的验证条件, 这化简一下是
由此生成的验证条件是
所生成的验证条件, 这又可以简化为
这生成的唯一的验证条件是
即可以化简为
这显然为真.
命令的正确注解规约具有形式
根据第24页的用法, 注解被称为一个不变量.
命令
由
所生成的验证条件为
- 由所生成的验证条件
例子:
{R=X ∧ Q=0}
WHILE Y≤R DO {X = R+Y×Q}
(R:=R-Y; Q=Q+1)
{X = R+(Y×Q) ∧ R<Y}
的验证条件为:
连带着
的验证条件, 而这个条件仅有一条, 即
-
因此, 若i, ii, iii成立, 那么此命令的规约为真, 即若
且
且
那么
第3.5节 验证条件的澄清
本节将证明, 若一个带注解的规约所生成的验证条件是可证的, 则该规约在Hoare逻辑中也是可证的, 即. 这说明验证条件是充分的, 但并不意味着它们是必要的. 事实上, 验证条件是最弱的充分条件, 但我们在此既不对此作更精确的表述, 也不深入讨论细节. 关于前条件的深入研究可参见Dijkstra的著作 [8].
容易证明验证条件不是必要的, 即的验证条件不可证并不意味着不可推导. 例如, 带注解规约的验证条件是不可证的, 但该Hoare三元组在Hoare逻辑中是可证的. {译注: 当我们说其在Hoare逻辑中是可证时, 是直接将命令中的注解完全抛弃了的.}
验证条件充分性的论证将对的结构进行归纳. 这类归纳论证包含两个部分. 首先, 证明结论对赋值命令成立. 其次, 证明当不是赋值命令时, 若结论对的各组成命令成立 (这称为归纳假设), 则结论对也成立. 前者称为归纳基础, 后者称为归纳步骤. 由归纳基础和归纳步骤即可得出结论对所有命令成立.
第3.5.1小节 赋值
唯一的验证条件是. 如果这是可证的, 那么鉴于 (赋值公理), 根据前条件强化可以推出.
第3.5.2小节 条件式
如果的验证条件是可证的, 那么和的验证条件都是可证的. 根据归纳假设, 我们可以假定和. 因此, 根据条件式规则可得.
第3.5.3小节 序列
存在两种需要考虑的情形:
- 如果的验证条件是可证明的, 那么和的验证条件都必须是可证明的, 因而根据规则有和. 于是, 根据顺序规则, 我们可得.
- 如果的验证条件是可证明的, 那必然是的验证条件也是可证的这一情形, 因而根据归纳, 我们有. 然后, 根据赋值公理可以得到, 于是由顺序规则得到.
第3.5.4小节 命令
如果生成的验证条件是可证的, 那么, 且由所生成的验证条件是可证的. 根据归纳可得, 因而根据规则有. 然后, 根据consequence规则可得.
第4章 可靠性和完备性
本章研究了Hoare逻辑的公理和规则是否正确 (可靠性) 以及是否充分 (完备性) 的问题. 这要求明确地给出编程语言的意义 (语义), 从而能够严格地定义Hoare三元组的语义.
第4.1节 语义
一条命令将一个初始状态转换为一个最终状态 (或者不终止). 对于目前所描述的语言, 从一个给定的初始状态出发, 至多只能到达一个终止状态——即命令是确定性的——但后面当我们向语言中加入存储分配时, 情况就不再如此了. 在数学上, 有几种本质上等价的方式来表示命令的意义. 我们将使用关系, 但部分函数也是常用的方式. 使用关系通常与操作语义相关联, 而部分函数则与指称语义相关联, 不过这并不是严格固定的: 指称语义可以使用关系作为指称, 操作语义也可以归纳地定义函数. 事实上, 在下面的第4.1.2节中, 我们给出了一种命令的指称语义, 其中指称就是关系.
各种风格的语义在很大程度上只是表示相同数学思想的不同方式. 某些数学表示更适合某些目的, 而另一些表示则更适合其他目的. 我在下面给出的语义可能与你在之前课程中见过的语义一致, 也可能不一致. 如果它看起来不同, 那么一个很好的练习就是思考它们之间有何关联.
我们将用状态集合上的一个二元关系来表示命令的意义: 在这个关系中, 与相关联, 当且仅当在状态下执行后终止于状态.
数学上有多种方式来表示关系, 虽然选择哪种并不真正重要, 但在深入具体细节之前, 先简要介绍一下这些不同的表示方式, 可能有助于避免后续的混淆.
集合论的入门书籍通常将关系表示为序对的集合, 即藉由关系与关联当且仅当. 因此, 集合和之间的一个二元关系是的一个子集, 即或者, 其中是幂集算子. 如果是任意的集合, 那么任何子集都可以由一个函数刻画, 该函数的定义为
被称为的特征函数. 因此, 一个关系可以由其特征函数刻画, 其被定义为
其中. 如果从集合到集合的所有函数之集合记作, 而是集合, 那么. 你或许可以从之前的课 (例如ML课) 上回忆一下, 接受两个参数或者更多参数的函数可以被curry化, 以使得这些函数每次接受一个参数. 如果我们对于进行curry化, 那么就会得到函数, 其定义如下:
那么, 或是, 如果我们采取向右结合的标准约定. 注意到我们也有. {译注: 原文写的是, 这肯定不太对, 但是我不确定我修正之后的版本是作者的原意.}
总结一下, 一个关系可以表示为一个序对的集合, 或者是一个将序对映射至布尔值的特征函数, 或者是curry化了的特征函数. 出于历史和风格方面的混合原因 (多少有些随意), 我们将使用curry化特征函数的表示方式来表示命令的语义. 具体而言, 我们将要定义来表达如果始于状态, 那么其会终止于状态. 这里的是用于表示的语义的关系, 其被表示为curry化的特征函数. 我们语言中的命令集合将记作, 而状态集合则记作. 因此, 或是. 正如之前我们所提到的, 选择将表示为curry化的特征函数而非序对集合, 更多是风格上的考量, 与实质没有太多关系.
令是陈述, 表达式 (项), 命令中所允许的变量之集合, 令是变量可以取的值之集合. 不必具体描述变量和值实际上是什么: 例如, 可以是 ASCII 字符的有限字符串的集合, 可以是整数的集合. 一个状态确定每个变量的值, 并且此外可能包含其他信息. 对于我们的小型编程语言, 将状态取为从到的函数就足够了. 使用的记法来表示定义域为, 值域 (实则为陪域) 为的函数的集合, 我们定义状态的集合为:
注意以下写法都是等价的: , , 以及. 对于状态中与变量关联的值, 我们有时记成, 有时记成, 此即应用表示状态的函数于. 尽管目前将状态表示为从变量到值的函数已经足够了, 第7章我们还要为状态加上另一组件, 用于表示指针的内容. 我们将在那一章中扩展的定义. 特定的状态可以使用记号来定义. 例如, 将映射为, 映射为, 而其他一切变量映射为的状态可以定义为
如果, 那么表示这样的状态, 其和一样, 除了变量的值为. 因此, 由以下等式给出:
例子:
{译注: 这是所谓的外延相等, 或者说指称相等.}
第4.1.1小节 表达式和陈述的语义
命令中可能包含表达式或陈述: 表达式出现在赋值的右侧, 而陈述出现在条件式和命令的测试条件中. Hoare三元组的前条件和后条件也是陈述. Hoare逻辑的经典处理建立在一阶逻辑之上, 表达式被视为逻辑中的项, 陈述被视为公式. 你应该在之前的课程中已经熟悉了一阶逻辑的语义, 我不想在这里重复那些内容. 此外, 在现代应用中, 用于书写前条件和后条件的语言有时比一阶逻辑更弱或更强, 例如无量词逻辑 (更弱) 或高阶逻辑 (更强).
为了避免涉及特定逻辑及其语义的细节, 我们假设已给定表达式集合和陈述集合, 以及定义它们语义的语义函数和, 其中:
现在我们给出一些非形式化的讨论和例子来刻画和是如何对于特定逻辑定义的. 我们希望从中可以清楚地看出, 更形式化的处理将如何进行. 按照通常的逻辑术语 (例如IB Tripos课程Logic and Proof中所使用的), 我们是在用状态来表示解释函数, 把作为论域或者说宇宙. 变量的解释通过在状态中查找来实现:
{译注: 原文使用的是, 对于这个字体我不敢苟同, 鉴于它应该是一个代表变量的元变量.}
常量获得其通常的数学或逻辑含义:
{译注: 这里其实我有点问题, 对于数字我的译文一以贯之没有使用其他字体, 但是书中对于项/表达式中的数字是使用所谓的打字机字体的. 不过, 你会发现原文的排版也有一些疏忽, 例如角标的字体都没有顾及到这一点. 总之, 我只是希望字体不要带来困惑.}
复合表达式或陈述是自底而上进行解释的: (通过递归计算得到的)子表达式的值使用合适的数学或逻辑运算组合起来以得到整个表达式的解释. 例如:
{译注: 原文这里写的是
个人认为并不合理, 因为实际上是一个谓词而非逻辑联结词, 应该是一个原子陈述, 而和应该是项 (或者说表达式). 换言之, 我们需要用来对于这些分量求值.}
其中这些等式左边的符号, , , 是陈述句法的一部分 (也就是对象语言的一部分), 而右边的相应符号是非形式化的数学记号的一部分 (也就是我们的元语言的一部分). {译注: 更确切地说, 左边的那些符号本身就是句法, 而右边的符号则指称着某种函数. 而且, 最好我们也应该明确区分项和陈述, 不然就会犯下原文这里的错误.} 这是需要仔细琢磨的微妙之处! 量词 (或许会出现在前条件和后条件里, 但是不会出现在命令的测试里) 以标准方式进行解释:
{译注: 右边的取值于论域, 即.}
例子:
我们希望这足以解释之后要用到的和了. 注意到对于任意的和, 我们有
第4.1.2小节 命令的语义
将自底而上递归定义. 唯一不含有子命令的命令是赋值. 在一次赋值之后, 最终状态等于初始状态, 不过其中赋值左侧的变量被更新为右侧表达式在初始状态下的值.
从初始状态出发执行顺序组合能够到达最终状态, 当且仅当存在一个中间状态, 使得从执行可到达, 并且从该中间状态执行可到达.
如果在状态中为真, 则从出发执行条件式能够到达状态, 当且仅当从出发执行分支能够到达. 然而, 如果在状态中为假, 则从出发执行该条件式能够到达状态, 当且仅当从出发执行分支能够到达.
如果从初始状态出发执行能够到达最终状态, 那么必然存在有限次的的迭代能够到达, 并且在所有中间状态中为真, 而在中为假. 这通过定义一个迭代有限次的(谓词)函数来形式化, 进而我们定义:
函数是上的递归, 定义如下:
的第一个参数是迭代的次数. 第二个参数是状态上的谓词, 例如. 第三个参数是一个curry化的特征函数, 例如. 第四个和第五个参数分别是初状态和终状态. 如果代表自然数集合, 那么我们有
第4.2节 Hoare逻辑的可靠性
Hoare三元组的意义被定义为, 其中:
这个定义可以用来表述Hoare逻辑的可靠性. 为了表明可靠性, 我们需要证明赋值公理的所有实例均为真, 并且如果假设为真则由推理规则得到的所有结论为真. 回忆一下赋值公理:
Hoare赋值公理
其中
是任意的变量,
是任意的表达式,
是任意的陈述, 而
代表将陈述
中变量
的所有出现都替换为项
.
为了证明这是可靠的, 我们必须表明对于每个:
展开的定义会将其转换为:
展开的定义会将其转换为:
这可以化简为:
这乍看上去可能有点令人困惑, 因为前件和后件里的记号有着不同的含义. 前件里的指称着将陈述中的变量替换为表达式的结果. 后件里的指称着状态, 其由更新以使得之值为在中的值得到 (所有其他的变量之值保持不变).
关于替换的离题.我们避免了细致刻画表达式和陈述的句法到底是什么, 所以说也不可能证明关于它们的一般性质. 然而, 对于任何理想的定义, 我们会期望:
例如, 取为, 为, 为, 那么这个等式就变成了:
既然, 于是以上等式变成了:
对于左边的替换求值就将其归约为了:
对于求值给出:
使用, 右边的等式可以化简, 这就给出 {译注: 原文说要不同, 这不大对, 因为它们不是元变量, 而是对象语言中的变量}:
这显然为真, 因为左边和右边是等同的.
尽管这只是一个例子, 其刻画了为什么对于所有的都有
实际上, 如果这个等式并不成立的话, 那么这个替换的定义就很糟糕了——这个等式的确应该当作替换的语义定义!
离题结束.
回到赋值公理的可靠性上来, 回忆一下其等价于对于所有的, 以下公式成立:
如果之前离题讨论的替换等式成立, 那么这个推出当然也成立. 这是因为, 对于任意的陈述和, 如果, 那么可以推出.
除了规则之外的其他Hoare逻辑规则的可靠性几乎都是平凡的, 即便是规则的可靠性也是相当直接的. 我们将会重述规则, 然后给出可靠性证明的提纲.
前条件强化
如果以下公式对于所有的均成立:
那么这条规则就是可靠的.
这在以的定义扩展之后就变成了:
这是以下陈述的一个实例, 如果我们取分别为.
这显然为真.
后条件弱化
以类似的论证, 这是可靠的.
规约合取
规约析取
以类似的论证, 这是可靠的.
顺序规则
如果以下公式对于所有的为真:
那么这条规则就是可靠的.
这在以的定义扩展之后就变成了:
这是以下陈述的一个实例, 如果我们展开的定义并取分别为.
这显然为真.
条件规则
和顺序规则类似的论证表明条件规则是可靠的.
规则
如果以下公式对于所有的均为真:
那么这条规则就是可靠的.
在以的定义扩展之后, 这就变成了:
使用等式和并扩展的定义可以将其转换为:
第4.3节 可判定性和完备性
为真当且仅当并不终止, 因而既然停机问题是不可判定的, 于是Hoare逻辑也是不可判定的.
可靠性是指, 凡是能用Hoare逻辑的公理和推理规则推导出的Hoare三元组都是真的. 其逆命题, 即完备性, 则是说任何真的Hoare三元组都能用Hoare逻辑的公理和推理规则推导出来. 遗憾的是, 这在一般情况下并不成立.
考虑. 根据之前的语义, 此为真当且仅当为真, 即:
既然且, 这可以归约为
通过特化和为, 这又化简为了, 即为真. 因此, 如果我们能够使用Hoare逻辑推导出任意为真的Hoare三元组, 那么我们就能使用Hoare逻辑推导出断言语言的任何为真的陈述. 绝大多数适用于刻画程序的逻辑都是不完备的 (例如一阶算术), 于是Hoare逻辑也不可能是完备的.
然而, 上述那种完备性之所以不可能实现, 仅仅是因为用于描述前条件和后条件的规约语言本身的不完备性. 如果我们将程序逻辑
与规约逻辑
分离开来, 那么就有可能构造出一种称为相对完备性 [6] 的概念, 它在一定程度上让我们确信, Hoare逻辑对于我们所讨论的这一小组简单命令的推理是充分的——也就是说, 不存在缺失
的公理或规则. 然而事实证明, 即便是这种有限的完备性, 对于许多实际编程语言中存在的构造 (但不包括我们的玩具
语言) 而言也可能是不可能的 [5].
我们不打算详细解释Cook等人在相对完备性方面的工作, 因为其中涉及的技术逻辑问题及其直觉解释都相当微妙 [1, 16]. 此外, 这样做将要求我们对表达前条件和后条件的规约语言的句法, 语义和证明理论进行比我们所期望的更为精确的刻画. 不过, 我们将概述其中的关键思想. 在证明相对完备性时所使用的一个概念是最弱前条件. 这一概念不仅在证明相对完备性方面发挥着作用, 它还具有实际应用价值, 包括提供一种改进的验证条件方法 (我们将在后文讨论), 以及作为程序精化理论的基础.
第4.3.1小节 相对完备性
我们将通过使用最弱自由前条件来解释相对完备性的思想, 并证明它对我们的小型程序语言成立. 然而, 我们将把其详细定义和分析推迟到第4.3.2节. 在本节中, 我们仅就最弱自由前条件是什么以及它们具有哪些性质 (下文的) 做出足够的说明, 以便我们能够解释相对完备性.
对于每个命令和陈述, 我们假定存在一个陈述——直觉上它是确保在执行之后成立的最弱前条件——并且它具有以下性质:
陈述在规约语言中的存在性取决于规约语言是否足够强. 一个强到足以定义的语言被称为是有表达力的 (expressive).
算子从一个命令和一个陈述构造出另一个陈述, 而陈述是形式语言中的一个句子. 因此是由两个句法实体 (一个命令和一个陈述) 构造出一个句法实体 (一个陈述). 我们还假定存在的一个语义对应物, 称为, 它作用于命令的意义 (表示状态上的二元关系的函数) 和陈述的意义 (状态上的谓词). 是一个curry化的函数:
我们假定以下性质将和联系起来, 其对所有命令和陈述成立:
注意这是一个谓词之间的等式. 我们还假定:
相对完备性证明的轮廓现在可以概述如下. 假定为真, 即为真. 我们将证明陈述也必定为真——暂时先假定这一点. 如果我们能够证明这个真陈述, 即有, 那么通过前条件加强规则和性质, 就可以由Hoare逻辑推出. 因此, Hoare逻辑的完备性是相对于一个能够证明所有形如的真陈述的神谕机而言的.
总结一下: 相对完备性表明, 如果在规约语言中是可表达的, 并且存在一个神谕机能够证明形如的真陈述, 那么任何为真的Hoare三元组都可以使用Hoare逻辑来证明.
我们现在再次梳理相对完备性的论证, 但更为详细, 展示假定和是如何使用的. 回忆一下:
假定对于任意和, 在规约语言中是可表达的, 并且假定对于任意, 存在一个神谕机能够证明形如的真陈述——即如果 (陈述为真), 则神谕机给出 (陈述得证).
Hoare三元组为真, 按照我们的语义, 意味着为真. 如果这成立, 则由上面假定的:
然后由假定的性质:
虽然我们没有完全定义规约语言, 但我们至少假定它包含一个中缀符号, 其含义是逻辑后承, 因此从上面的陈述我们可以推出:
即陈述为真. 现在我们使用为这种形式的公式所假设的神谕机来证明, 因此由假设的性质和前条件加强规则, 我们可以证明.
为了完成上述概要, 我们必须定义和并证明性质. Hoare逻辑的公理和规则将被用于证明, 而它们能够证明这一事实, 正是其完备性的本质所在.
第4.3.2小节 句法和语义最弱前条件
如果, 我们说比强, 对偶地, 比弱. 命令相对于后条件的最弱前条件记作, 其是满足的最弱谓词. 注意到这和完全正确性相关联. 部分正确性的相应概念是最弱自由前条件, 记作: 陈述是满足的最弱谓词. 本章我们只会使用最弱自由前条件. 其关键性质是, 即, 以及对于所有的都有
这些性质可以更为精确地表述为单一等式:
我们可以很容易地看出来这个等式与刚才提及的关键性质是等价的, 只需使用前条件加强规则和的自反性.
如果规约语言 (即前条件和后条件的语言) 足够强以表达所有命令和后条件的最弱自由前条件, 那么其就被称为有表达力的. 既然我们还没有说规约语言是什么, 那么我们对于表达力也就不好说什么.
我们可以藉由我们的语义定义谓词上的语义算子: 这是谓词转换器 [7] 的一个例子.
回忆一下的定义:
我们可以轻易证明性质, 即
可由和的定义推出, 通过在以下逻辑事实中取分别为得到.
为了证明和, 我们需要下列等式, 其可由的定义推出.
{译注: 为真的含义应该是从状态出发, 如果恰好执行次循环体并退出, 那么为真, 其中是循环条件的语义, 是循环体命令的语义. 注意一下, 提前或者延迟退出循环体都会导致这个谓词自动为真.}
(未完待续)
第4.3.3小节 句法前条件和可表达性
现在我们讨论如何以性质和定义陈述, 即
请注意, 是在句法东西 (命令和陈述) 上操作的, 而则是在语义东西 (状态上的数学函数, 代表命令和陈述的意义) 上操作的.
我们将在上递归定义, 然后藉由上的结构归纳澄清和. 为赋值, 序列或是条件式的情形是直接的.
如果为, 那么直接就是赋值公理, 而根据第61页所讨论的等式
为等式
这不过就是第53页的关于替换的离题
所澄清的.
假定对于任意的, 和对于和均成立, 那么
于是根据顺序规则可以得到, 即
为了证明, 当为时, 注意到这种情况下的为
(未完待续)
注意到的含义是. 前者是为了强调我们对于规约语言的假定仅是布尔运算符的存在. 注意到根据布尔代数以及的定义, 我们有
根据归纳假设和前条件强化, 可以得到
{译注: 原文忘了加, 译者将其补上.} 代入之前的等式, 并利用条件规则, 可以获得
这就是条件式的性质. {译注: 这里作者的措辞存在疏漏, 作者说是利用布尔代数得到了等式, 然后代入. 严格说来, 这并不正确, 因为布尔代数是语义层面的事情. 不过, 句法层面我们也有分配律等手段可用, 这和语义层面是一样的, 所以论证可以修补成正确的形式.} 性质为
(未完待续)
第4.4节 藉由生成验证条件
最弱前条件提供了一种理解和改进验证条件的方式. 回忆一下性质, 即. 为了证明为真, (根据前条件强化)证明就足够了. 因此, 我们可以将视为对于目标而言的单独一条超级验证条件
, 其无需对于进行注解即可生成! 如果不含有循环, 即不含有命令, 那么这就能很好地运转. 如果的确含有命令, 那么将会是无限的陈述. 证明这样的陈述通常会涉及用归纳法证明本质上就是某个不变式的验证条件的东西. 因此找不变式是无法回避的! 然而, 可以用最弱前条件的思想来既解释又改进验证条件方法. 为了看出它如何解释验证条件, 回忆第41页, 由生成的验证条件是: (的验证条件), 也就是(的验证条件). 我们可以推广这一观察, 以减少序列中所需的标注数量, 做法是只要求在不是无循环的 (即包含命令的) 命令之前加标注, 然后修改序列的验证条件:
序列
-
其中包含一个命令, 由其所生成的验证条件为:
-
所生成的验证条件
-
所生成的验证条件
-
其中无循环, 由其所生成的验证条件为
所生成的验证条件.
这些改进验证条件的澄清本质上和原本的一样, 只不过要使用性质而非赋值公理. {译注: 只有赋值公理吗?} 然而, 使用的想法我们甚至可以做的更好, 将注解的需求减少为仅需对于命令的不变量仅需标注. 这种方法的大致框架如下:
- 定义, 其和类似, 除了命令必须要注解;
- 定义陈述集合, 其给出了用于验证中所有循环的用户注解不变量的确是不变量的条件.
由和的定义可以推出中的陈述之合取蕴涵. 如果我们定义为中的所有陈述之合取, 那么这可以写成:
因此, 根据Modus Ponens和前条件强化, 为了证明(为真), 证明和就足够了. 如果是无循环的, 那么实际上有且, 于是这种方法就会坍缩为仅是证明. 和的定义都是上的递归, 给出如下. 假定所有的命令都得到了注解: .
定理. .
证明. 对于
进行归纳.
如果
.
第4.4.1小节 最强后条件
最弱前条件是由后条件开始反向计算的. 存在一个最强后条件的对偶理论, 其是自前条件正向计算的. 最强后条件具有性质, 并且是最强的, 意即对于任意的, 如果, 那么. 从直觉上说, 是在由所描述的初始状态下对于执行之后的状态的符号表示. 对于赋值而言:
存在量化变量是在执行赋值之前的初始状态中的值. 最强后条件表达了在赋值之后, 之值是在初始状态下求得的值 (因而是), 而前条件在初始状态下评估 (因而是) 继续成立. 因此, 如果初始状态由陈述符号地表示, 那么执行之后的状态可由符号地表示.
对于无循环命令, 相当于从符号状态开始对于进行符号执行
. 符号执行的一个优点在于其允许对于到目前为止的状态
的表示进行on-the-fly
式的化简,
第5章 完全 (total) 正确性
本章Hoare逻辑的公理和规则被扩展到了完全正确性, 其给出了完全正确性规约的验证条件.
在1.3节中, 我们引入了记号来表示完全正确性规约, 即每当在满足的状态下执行时, 它将终止于一个满足的状态. 在描述规则的那一节 (第2.1.8节) 末尾, 我们已经表明该规则对于完全正确性规约不是可靠的. 这是因为命令可能引入非终止性. 其他命令都不会引入非终止性, 因此Hoare逻辑的规则可以继续使用.
第5.1节 非循环命令
将花括号替换为方括号, 我们就得到了以下公理和规则.
完全正确性的赋值公理
完全正确性的前条件加强
完全正确性的后条件弱化
完全正确性的规约合取
完全正确性的规约析取
完全正确性的顺序规则
完全正确性的导出顺序规则
完全正确性的条件规则
这些规则从形式上来说和相应的Hoare逻辑规则等同, 除了使用方括号而非花括号. 因此, 显然以下是可靠 (valid) 的导出规则.
{译注: 以我之见, 这与其说是导出规则, 不如说是兼容规则 (admissible rule).}
第5.2节 赋值的终止性
注意到完全正确性的赋值公理陈述了赋值命令总是终止, 这隐式地假定了表达式中的所有函数应用都会终止. 如果函数可以递归定义, 那么或许并非如此. 例如, 考虑赋值, 其中被递归定义为
通常也假定诸如这样错误的表达式也不会造成任何问题. 绝大多数编程语言遇到除以零时都会导致错误停下. 然而, 在我们的逻辑里, 可以推出
即赋值总是能终止于一个条件成立的状态. 这假定指称着某个可以具有的值. 存在两种可能性:
- 指称着某个数字;
- 指称着某种
错误值
.
乍看上去似乎采用ii是最为自然的选择. 然而, 这会使得理解算术定律是否应该成立变得棘手. 例如, 应该等于还是某种错误值
? 如果是后者, 那么就会导致不再是有效的算术定律了. {译注: 含义是不能用其进行项重写化简.} 使用未定义或者 (and/or) 错误值或许也能让一切运转起来, 只不过其带来的理论有点混乱. 我们将会在这里假定算术表达式总是指称着数字, 只不过在某些情形下这个数字确切是什么未加刻画. 例如, 我们将会假定对于任意的和, 总会指称着某个数字, 但是其唯一假定的性质为
由此关于并不能推出任何东西.
另一种处理错误的方法在于扩展命令的语义, 允许错误和状态一样作为结果. 这种方法用在第7章以处理内存错误, 但是类似的想法也能用来处理表达式求值错误 (尽管代价是语义更为复杂).
第5.3节 用于完全正确性的规则
命令是我们这个小语言中唯一可能导致不终止的命令, 因此也是唯一具有非平凡终止规则的命令. 规则用于完全正确性的核心思想是: 要证明能够终止, 必须证明某个非负量在的每次迭代中都会减少. 这个递减的量称为变式量 (variant). {译注: 我感觉很多人也将其称为终止度量.} 在下面的规则中, 变式量为, 而它递减的事实通过一个辅助变量来刻画. 一个额外的假设确保了变式量是非负的.
第5.4节 终止规约
正如第1.3节已经讨论过了的, 部分正确性和完全正确性之间的关系可以非正式地表述为以下等式:
第5.5节 终止性的验证条件
第5.6节 验证条件生成
第6章 程序精化
Floyd-Hoare逻辑是一种证明现有程序满足其规约的方法. 它也可以作为将规约精化
为程序的基础——即作为一种程序设计方法论的基础.
第6.1节 引论
程序员的任务可以被看作: 给定一个由前条件和后条件组成的规约, 然后构造出一个命令, 使得成立.
精化理论提出了从规约和计算
出程序的规则. 一个关键思想来自芬兰的Ralph Back (后来被Joseph Morris和Carroll Morgan各自独立地重新发现), 即引入一类新的程序构造, 称为规约. 这些构造在句法上扮演与命令相同的角色, 但它们不可直接执行, 不过它们能保证在给定的前条件下达成给定的后条件. 由此产生的广义编程语言包含纯规约, 纯代码以及二者的混合体. 这样的语言被称为宽谱语言.
本章采用的方法遵循Morgan所发展的精化风格, 但其基础是Floyd-Hoare逻辑, 而非Dijkstra的最弱前条件理论 (见4.3.3节). 与传统方法相比, 这一基础更加具体和句法化: 规约被等同于其所有可能实现的集合, 精化则被表示为对普通命令集合的操作. 这种方法旨在利用前面各章所引入的记号和概念工具, 传达 (Morgan风格的) 精化的外观和感觉
.
记号将用于表示规约, 因此:
精化过程将由一系列步骤组成, 这些步骤通过系统性的设计决策逐步缩小可能实现的集合, 直到达到唯一的实现. 因此, 从规约到实现的精化具有如下形式:
初始规约具有的形式, 每个中间规约都是通过对其前驱应用一条精化律而得到的.
在文献中, 通常写作. 这里使用而非更抽象的, 反映了精化作为逐步缩小实现集合这一具体解释.
第6.2节 精化律
第6.3节 一个例子
第6.4节 一般评注
第7章 指针和局部推理
第7.1节 指针操纵构造
第7.2节 例子: 反转链表
第7.3节 分离逻辑断言
第7.3.1小节 指向关系:
第7.4节 语义和分离逻辑