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章 程序规约

本章引入了一种包含赋值, 条件式, 块和WHILE循环的简单编程语言. 然后用它来描述Hoare用于规约程序部分正确性的记号. Hoare的记号使用形式逻辑记号来表达关于程序变量值的条件. 该记号被非形式化地描述并通过示例加以刻画.

第1.1节 引论

为了从数学上证明一个程序的正确性, 我们必须首先刻画正确本身的含义. 本章我们描述了一种用于刻画命令式程序的预期行为的记号. 这种记号应该归功于C.A.R. Hoare.

执行命令式程序有着改变状态的作用, 直到第7章之前, 我们将状态取为程序变量的值. 为了使用这样的程序, 我们需要建立初始状态, 也就是将一些变量的值设置为有趣的值 (value of interest). 然后我们执行程序. 这将初始状态变换为最终状态. 然后我们检视最终状态里的变量的值来得到预期的结果. 例如, 为了计算y除以x, 我们可能会将x和y分别载入程序变量X和Y. 然后我们可能会执行一个合适的程序 (见第1.4节的例子7) 来将初始状态变换为最终状态以使得变量Q和R分别存放着商和余数.

这些笔记里要使用的编程语言将于下一节进行描述.

第1.2节 一种小语言

程序由诸如赋值和条件式等命令构建而成. 术语程序和命令的确可以算是近义词; 前者只会用于表示完整算法的命令. 这里的术语陈述 (statement)用于程序变量上的条件, 其出现于正确性规约之中 (见第1.3节). 这里存在混淆的风险, 因为一些作者将这个词用于命令 (如for-statement [14]).

现在我们描述我们的小语言里的各种命令的句法 (即形式) 和语义 (即意义). 以下约定将会采用:

  1. 符号V,V1,…,Vn代表任意的变量. 特定变量的例子有X,R,Q等等.
  2. 符号E,E1,…,En代表任意的表达式 (或者说项). 这些是像X+1,2之类的东西, 其指称着值 (通常是数字).
  3. 符号S,S1,…,Sn代表任意的陈述. 这些是像X<Y,X2=1之类的条件, 其要么为真要么为假.
  4. 符号C,C1,…,Cn代表我们编程语言里的任意命令; 这些会在本节的剩余部分进行描述.
项和陈述在第1.5节进行了更为细致的描述.

第1.2.1小节 赋值

句法: V≔E

语义: 状态的改变由将项E之值赋给变量V完成. 所有的变量都假定具有全局作用域.

例子: X:=X+1

这给变量X之值加上一.

第1.2.2小节 序列

句法: C1;…;Cn

语义: 命令C1,…,Cn按照顺序进行执行.

例子: R:=X; X:=Y; Y:=R

变量X和Y之值经由临时变量R进行了互换. 这个命令具有将变量R之值变为变量X之旧值的副作用. {译注: 这个还是这些?}

第1.2.3小节 条件式

句法: IFSTHENC1ELSEC2

语义: 如果陈述S在当前状态下为真, 那么C1被执行. 如果S为假, 那么C2被执行.

例子: IF X<Y THEN MAX:=Y ELSE MAX:=X

变量MAX之值被设为X和Y之值的最大值.

第1.2.4小节 WHILE命令

句法: WHILESDOC

语义: 如果陈述S在当前状态下为真, 那么C被执行, 然后WHILE命令重复. 如果S为假, 那么什么也不做. 因此, C反复执行, 直至S的值变为假. 如果S永远不变为假, 那么该命令的执行永远不终止.

例子: WHILE ¬(X=0) DO X:=X-2

如果X之值非零, 那么其值会减去2, 然后进程重复. 这个WHILE命令将会终止 (此时X有着值0), 如果X的值是一个非负的偶数. 其他状态下都不会终止.

第1.2.5小节 句法总结

我们的小语言的句法可以用BNF记号总结如下:

<command> ::= <variable> := <term>
           |  <command>; ...; <command>
           |  IF <statement> THEN <command> ELSE <command>
           |  WHILE <statement> DO <command>

请注意:

第1.2.6小节 历史注记

旧的Part II课程Specification and Verification I是基于一种与上述描述类似的语言的, 但具有额外的特性: 块 (带有局部变量), FOR命令和数组. 块和FOR命令并未添加根本性的新思想, 因此它们将不会被涵盖; 数组使用分离逻辑能够更好地处理 (见第7章). 在旧课程中我使用BEGIN和END来组合命令, 而在这里我只使用括号. 因此以前我会写BEGINC1;C2END而不是(C1;C2). 我提及此事是因为它可能有助于将旧的考试题目作为本课程的练习来重新使用.

第1.3节 Hoare记号

在一篇开创性论文 [13] 里C.A.R. Hoare引入了记号{P}C{Q}, 其有时被称为Hoare三元组, 用于刻画一个程序做了什么. 在这样一个Hoare三元组之中:

我们称{P}C{Q}为真, 如果每当C在某个满足P的状态下执行且C的执行终止, 那么C执行终止的状态满足Q.

例子: {X=1}X≔X+1{X=2}. 这里的P是X之值为1这一条件, Q是X之值为2这一条件, C是赋值命令X≔X+1. {X=1}X≔X+1{X=2}为真. {译注: 这个为真是从语义角度而言的.}

一个表达式{P}C{Q}被称为一个部分正确性规约; P被称为其前条件而Q被称为其后条件.

这样的规约之所以是部分的, 是因为即便{P}C{Q}为真, C的执行在满足P的状态下开始也不必终止. 其所要求的仅是如果C终止, 那么Q成立.

一种更强的规约是完全正确性规约. 这种规约没有标准的记号. 我们将使用[P]C[Q].

完全正确性规约[P]C[Q]为真当且仅当以下两个条件满足:

  1. 如果C在满足P状态下开始执行, 那么C终止.
  2. 在终止之后, Q成立.
部分正确性和完全正确性之间的关系可以非正式地表述为以下等式: 完全正确性= 终止性+部分正确性

完全正确性是我们最终感兴趣的, 但通常更容易通过分别建立部分正确性和终止性来证明它.

终止性通常是容易建立的, 但有一些著名的例子并非如此. 例如, 未解决的Collatz猜想与下面的程序是否对X的所有值都终止有关 (见下面的练习):

WHILE X>1 DO
  IF ODD(X) THEN X := (3×X)+1 ELSE X := X DIV 2
(表达式X DIV 2求值为X/2向下取整到一个整数的结果, 不过由于这里条件式的ELSE分支仅在X为偶数时才被执行, 实际上不需要取整.)

著名数学家Paul Erdős关于Collatz猜想说道: 数学对于这样的问题尚未准备好. 他为其解答提供500美元奖励.

第1.4节 一些例子

以下的例子刻画了部分正确性的各种方面.

在以下的例子5, 6, 7中, T是永远为真的条件. 在例子3, 4, 7中, ∧是联结词合取, 即如果P1和P2是条件, 那么P1∧P2是这样的条件, 其为真每当P1和P2均成立.

  1. {X=1}Y≔X{Y=1}
    这是说如果命令Y≔X在一个满足条件X=1的状态下执行, 那么如果执行终止 (在此情况下的确如此), 那么条件Y=1将会成立. 这个规约显然为真.
  2. {X=1}Y≔X{Y=2}
    这是说如果Y≔X的执行始于满足X=1的状态, 那么Y=2将会成立. 这显然为假.
  3. {X=x∧Y=y}R≔X;X≔Y;Y≔R{X=y∧Y=x}
    这是在说如果R≔X;X≔Y;Y≔R的执行终止 (的确如此), 那么X和Y之值就被交换了. 变量x和y没有出现在命令里, 其是用来对于程序变量X和Y的初值进行命名的, 这种变量被称为逻辑变量, 或者辅助变量, 或者幽灵变量.
  4. {X=x∧Y=y}X≔Y;Y≔X{X=y∧Y=x}
    这是在说X≔Y;Y≔X可以交换X和Y之值, 但是实际上这并不为真.
  5. {T}C{Q}
    这是在说每当C终止时, Q成立.
  6. {P}C{T}
    这种规约对于每个条件P和每个命令C为真, 这是因为T总是为真.
  7. {T}
    R:=X;
    Q:=0;
    WHILE Y≤R DO
      (R:=R-Y; Q:=Q+1)
    {R < Y ∧ X = R + (Y × Q)}
    这是{T}C{R<Y∧X=R+(Y×Q)}, 其中C是上述的命令. 这个规约为真, 如果每当C的执行终止时, 那么Q是商而R是X除以Y的余数. 这个规约的确为真, 即便X起始为负数!

这个例子里用到了程序变量Q, 读者不应将其与5中所用到的Q混淆. 程序变量Q (注意字体) 遍历数字, 而后条件Q (注意字体) 则遍历陈述. 一般而言, 我们使用typewriter font表示特定的程序变量, 而使用italic font表示遍历陈述的变量. 尽管这种对于字体的微妙运用乍看上去可能令人迷惑, 但是读者一旦理解之后, 两种Q之间的区分就非常显然了. (即便不看字体, 你也应该能够从上下文中区分它们.)

第1.5节 项和陈述

这里用于表达前条件和后条件的记号基于一阶逻辑. 我们只会简要回顾, 因为读者默认已经非常熟悉它了.

以下是原子陈述的例子: T,F, X=1 , R<Y , X= R+ ( Y×Q ) 陈述要么为真要么为假. 陈述T永远为真, 而陈述F永远为假. 陈述X=1为真, 如果X之值等于1. 陈述R<Y为真, 如果R之值小于Y之值. 陈述X=R+(Y×Q)为真, 如果X之值等于R之值与Y和Q之积的和.

陈述由项构建而成, 例如: X,1,R,Y, R+ ( Y×Q ) , Y×Q 项指称着值, 例如数字和字符串, 不像陈述要么为真要么为假. 一些项, 例如1和4+5, 指称着固定的值, 而其他一些项则包含变量, 例如X,Y,Z等等, 其值可能会变化. 我们对于项将会使用常规数学记号, 如以下例子所刻画的那样: X,Y,Z 1,2,325 −X , − ( X+1 ) , ( X×Y ) +Z ( 1+ X2 ) , X! , sin⁡ (X) , rem⁡ ( X,Y )

T和F是分别总是为真和为假的原子陈述. 其他的原子陈述则由项使用谓词构建而成. 以下是更多的一些例子: ODD⁡ (X) , PRIME⁡ (3) , X=1 , ( X+1 ) 2 ≥ X2 ODD和PRIME是谓词的例子, 而=和≥则是中缀谓词的例子. 表达式X,1,3,X+1,(X+1)2,X2是项的例子.

复合陈述由原子陈述使用以下逻辑联结词构建而成: ¬ 否定 ∧ 合取 ∨ 析取 ⇒ 推出 ⇔ 当且仅当 设P和Q是陈述, 那么:

使用联结词构建的陈述的例子有: ODD⁡ (X) ∨ EVEN⁡ (X) X是奇数或者偶数. ¬ ( PRIME⁡ (X) ⇒ ODD⁡ (X) ) 如果X为素数, 那么X为奇数, 并不成立. X≤Y ⇒ X≤ Y2 如果X小于等于Y, 那么X小于等于 Y2 . 为了减少括号的需要, 通常假定¬比∧和∨结合得更紧密, 而这两个又比⇒和⇔结合得更紧密, 例如: ¬P ∧Q 等价于 ( ¬P ) ∧Q P∧Q ⇒R 等价于 ( P∧Q ) ⇒R P∧Q ⇔ ¬R ∨S 等价于 ( P∧Q ) ⇔ ( ( ¬R ) ∨S )

第2章 Hoare逻辑

本章讨论了形式化证明的想法. 然后, 我们引入了Hoare逻辑 (或称Floyd-Hoare逻辑) 作为对于程序进行形式化推理的方法.

上一章我们引入了三种可能为真或假的表达式:

  1. 部分正确性规约{P}C{Q}.
  2. 完全正确性规约[P]C[Q].
  3. 数学陈述, 例如(X+1)2=X2+2×X+1.
我们假定读者已经知道如何证明简单的数学陈述, 例如以上的iii中的那条. 例如, 以下是该事实的一个证明. 1. ( X+1 ) 2 = ( X+1 ) × ( X+1 ) ( ) 2 的定义. 2. ( X+1 ) × ( X+1 ) = ( X+1 ) ×X + ( X+1 ) ×1 ×之于+的左分配律. 3. ( X+1 ) 2 = ( X+1 ) ×X + ( X+1 ) ×1 将第2行代入第1行. 4. ( X+1 ) ×1 = X+1 1的恒元律. 5. ( X+1 ) ×X = X×X + 1×X ×之于+的右分配律. 6. ( X+1 ) 2 = X×X + 1×X +X+1 将第4行和第5行代入第3行. 7. 1×X = X 1的恒元律. 8. ( X+1 ) 2 = X×X +X+X+1 将第7行代入第6行. 9. X×X = X2 ( ) 2 的定义. 10. X+X = 2×X 2= 1+1 ,分配律 11. ( X+1 ) 2 = X2 + 2×X +1 将第9行和第10行代入第8行.

这个证明由一系列行构成, 每行都是某个公理的实例或者根据之前的行由推理规则推出. 这个证明的最后一行是其所证明的陈述, 也就是(X+1)2=X2+2×X+1.

为了构造部分正确性规约的形式化证明, 需要公理和推理规则. 这正是Hoare逻辑所提供的. 该演绎系统的形式化表述归功于Hoare [13], 但其中一些基本思想源自Floyd [9].

Hoare逻辑中的证明是一系列行的序列, 其中每一行要么是该逻辑的一条公理, 要么是由前面的行通过该逻辑的一条推理规则推导而来.

构造形式化证明的原因在于试图确保只使用可靠的演绎方法. 有了可靠的公理和推理规则, 我们就可以确信所得出的结论是真的. 另一方面, 如果任何公理或推理规则是不可靠的, 那么就有可能推导出错误的结论; 例如: 1. −1 × −1 = −1 × −1 =的自反性. 2. −1 × −1 = ( −1 ) × ( −1 ) 之于×的分配律. 3. −1 × −1 = ( −1 ) 2 ( ) 2 的定义. 4. −1 × −1 = −1 的定义. 5. 1 = −1 鉴于 −1 × −1 =1 . 6. 1 = −1 鉴于 1 =1 .

形式化证明明确指出了为得出结论所使用的公理和推理规则. 人们很容易提出一些看似合理但实际上不可靠的程序推理规则. 计算机程序的正确性证明往往非常复杂, 需要形式化方法来确保其有效性. 因此, 将所使用的推理原则完全显式化是很重要的, 这样才能对其可靠性进行分析.

对于某些应用而言, 正确性尤为重要. 例子包括生命攸关的系统, 如核反应堆控制器, 汽车制动系统, 电传飞控飞机以及软件控制的医疗设备. 曾有一起法律诉讼源于一台癌症治疗设备因软件缺陷导致辐射过量, 造成数人死亡 [15]. 当穷举测试不可能实现时 (几乎总是如此), 形式化正确性证明提供了一种确认程序中不存在缺陷的方法.

本章将解释并举例说明用于程序推理的Hoare演绎系统. 该系统的可靠性和完备性的数学分析将在第4章中讨论.

第2.1节 Hoare逻辑的公理和规则

正如本章开头所讨论的, 一个命题的形式化证明是以该命题为最后一行的一个行序列, 其中每一行要么是一条公理的实例, 要么是由前面的行通过一条推理规则推导而来. 若S是一个命题 (无论是普通数学的还是Hoare逻辑的), 我们记⊢S来表示S有一个证明. 拥有证明的命题称为定理. 如前所述, 在本讲义中只描述Hoare逻辑的公理和推理规则; 因此, 若S是一个数学定理, 我们将直接断言⊢S而不给出任何形式化的论证. 当然, 为了达到完全的严格性, 这些断言必须被证明, 但如何做到这一点的细节假定读者已经了解 (例如通过逻辑与证明课程).

Hoare逻辑的公理在下面通过模式来指定, 这些模式可以被实例化以得到特定的部分正确性规约. Hoare逻辑的推理规则将用如下形式的记号来描述: ⊢ S1 ⋯ ⊢ Sn ⊢S 这是说结论⊢S可以从⊢S1,…,⊢Sn推导而来, 后者是该规则的假设. 这些假设可以全部是Hoare逻辑的定理 (如下面的顺序规则), 也可以是Hoare逻辑定理与数学定理的混合 (如第2.1.2小节中描述的前条件加强规则).

第2.1.1小节 赋值公理

赋值公理表达了这样一个事实: 在执行赋值命令V≔E之后, 变量V的值等于执行前表达式E的值. 为了将此形式化, 注意到如果一个命题P在赋值之后为真, 那么在执行赋值之前, 将P中的V替换为E所得到的命题必须为真. {译注: 这似乎和以下公理所表达的内容方向相反.}

为了形式化地表达这一点, 定义P[E/V]以表示将P中所有V的出现替换为E之结果. P[E/V]读作P with E for V. 例如, ( X+1 >X ) [ Y+Z /X ] = ( ( Y+Z ) +1 > Y+Z ) 记住这个记号的方法是消去律 V [ E/V ] =E 这类比于分式的消去性质 v× ( e/v ) =e

Hoare赋值公理
⊢ { P [ E/V ] } V≔E {P} 其中V是任意的变量, E是任意的表达式, P是任意的陈述, 而P[E/V]代表将陈述P中变量V的所有出现都替换为项E.

赋值公理的实例有:

  1. ⊢{Y=2}X≔2{Y=X}
  2. ⊢{X+1=n+1}X≔X+1{X=n+1}
  3. ⊢{E=E}X≔E{X=E}, 如果X在E中没有出现.

许多人会感到赋值公理和他们所期望的相反. 两种常见的错误直觉如下:

  1. ⊢{P}V≔E{P[V/E]}
    (未完待续)
  2. ⊢{P}V≔E{P[E/V]}
    如果取P为X=0, V为X, E为1, 就会得到⊢{X=0}X≔1{1=0}, 这显然是错误的. {译注: 也就是unsound.}

关于赋值公理容易产生错误直觉这一事实表明, 拥有严格的手段来确立公理和规则的有效性是很重要的. 我们将在后面的第4章中深入讨论这个主题, 届时我们将给出我们这个小型编程语言的形式化语义, 然后证明Hoare逻辑的公理和推理规则是可靠的. 当然, 这一过程只有在我们相信形式化语义本身的正确性的前提下, 才能增强我们对公理和规则的信心. 上述简单的赋值公理对于真实的编程语言是不成立的. 例如, G. Ligler [17] 的工作表明, 它在(现已过时的)语言Algol 60中以六种不同的方式失效.

赋值公理存在一种前向版本, 其有时称为Floyd赋值公理, 因为其对应于应该归功于Floyd [9] 的赋值的原始语义. 在以下规则之中, 存在量化的变量v是执行赋值之前的状态 (始状态) 中的V之值. 后条件断言了在赋值之后, V之值是E在初始状态下求得的值 (因而是E[v/V]), 并且在初始状态下评估的前条件 (因而是P[v/V]) 继续成立.

Floyd赋值公理
⊢ {P} V≔E { ∃v. ( V= E [ v/V ] ∧ P [ v/V ] ) } 其中v是一个新变量, 即既不等于V也没有出现在P或E中.

译者注记. 之所以P[v/V]继续成立, 是因为V≔E仅仅改变了V的值, 其他什么也没改动. 因此, P大致上应该仍然成立, 只不过V的值要换成旧值v.

一个实例是: ⊢ { X=1 } X≔ X+1 { ∃v. ( X= ( X+1 [ v/X ] ) ∧ ( X=1 [ v/X ] ) ) } 简化这个后条件: ⊢ { X=1 } X≔ X+1 { ∃v. ( X= ( X+1 [ v/X ] ) ∧ ( X=1 [ v/X ] ) ) } ⊢ { X=1 } X≔ X+1 { ∃v. ( X= v+1 ∧ v=1 ) } ⊢ { X=1 } X≔ X+1 { ∃v. ( X= 1+1 ∧ v=1 ) } ⊢ { X=1 } X≔ X+1 { X= 1+1 ∧ ∃v. v=1 } ⊢ { X=1 } X≔ X+1 { X=2 ∧T } ⊢ { X=1 } X≔ X+1 { X=2 } Floyd赋值公理等价于标准的赋值公理, 但是更难于运用, 这是由于其所引入的存在量词. 然而, 这是分离逻辑的重要部分.

Hoare赋值公理与最弱前条件有关 (见第4.3.3小节), 而Floyd赋值公理则与最强后条件有关 (见第4.4.1小节). 以下这些会在刚才提及的小节里得到解释: Hoare赋值公理: { wlp⁡ ( V≔E ,Q ) } V≔E {Q} Floyd赋值公理: {P} V≔E { sp⁡ ( V≔E ,P ) } 其中wlp⁡(C,Q)和sp⁡(C,P)分别代表最弱自由前条件和最强后条件.

我们的小编程语言和真实语言不同的一个方面在于对于赋值命令的右侧表达式的求值不会对于状态施加副作用. 赋值公理的有效性依赖于这个条件. 为了看出这一点, 设我们的语言得到了扩展, 其可以含有形如(C;E)的表达式, 其中C是一个命令, 而E是一个表达式. 这样一种表达式的求值是先执行C, 然后对于E求值, 最后返回该结果作为(C;E)的值. 因此, 对于这种表达式的求值可能会导致由执行C引起的副作用. 例如, (Y≔1;2)具有值2, 但是其求值也会对于变量Y产生副作用, 通过将1存储其中. 如果赋值公理应用于像(C;E)这样的表达式, 那么其可以用来推导出: ⊢ { Y=0 } X≔ ( Y≔1 ;2 ) { Y=0 } 这是由于X压根就没有出现在Y=0之中. 这个显然是错的, 因为在赋值之后Y应该具有值1.

第2.1.2小节 前条件强化

下一条Hoare逻辑规则允许第18页的i和ii的前条件得到简化. 回忆一下, ⊢ S1 ⋯ ⊢ Sn ⊢S 表示⊢S可由⊢S1,…,⊢Sn推导出来. {译注: 这是句法概念.}

使用该记号, 前条件强化规则为

前条件强化
⊢ P⇒ P′ ⊢ { P′ } C {Q} ⊢ {P} C {Q}

例子

  1. 根据算术事实⊢X=n⇒X+1=n+1, 以及第18页的2, 藉由前条件强化可以得到 ⊢ { X=n } X≔ X+1 { X= n+1 } 这里的变量n是辅助变量或者说幽灵变量的例子. 正如之前所描述的 (见第10页), 辅助变量是出现于部分正确性规约{P}C{Q}中但未出现在命令C中的变量. 这样的变量用于将C执行前后的状态中的值关联起来. 例如, 以上的规约里说的是如果X之值为n, 那么在执行赋值X≔X+1之后其值将会变成n+1.
  2. 根据逻辑事实⊢T⇒E=E, 以及第18页的3, 我们可以推导出如果X没有出现在E之中, 那么: ⊢ {T} X≔E { X=E }

第2.1.3小节 后条件弱化

正如之前的规则允许部分正确性规约里的前条件被强化, 以下的规则允许我们对于后条件进行弱化.

后条件弱化
⊢ {P} C { Q′ } ⊢ Q′ ⇒Q ⊢ {P} C {Q}

例子: 以下是一个小小的形式证明. 1. ⊢ { R=X ∧ 0=0 } Q≔0 { R=X ∧ Q=0 } 根据赋值公理. 2. ⊢ R=X ⇒ R=X ∧ 0=0 根据纯粹逻辑. 3. ⊢ { R=X } Q≔0 { R=X ∧ Q=0 } 根据前条件强化. 4. ⊢ R=X ∧ Q=0 ⇒ R= X+ ( Y×Q ) 根据算术律. 5. ⊢ { R=X } Q≔0 { R= X+ ( Y×Q ) } 根据后条件弱化.

前条件强化和后条件弱化规则有时被称为rules of consequence.

第2.1.4小节 规约合取与析取

以下两条规则提供了将关于相同命令的不同规约组合起来的方法.

规约合取
⊢ { P1 } C { Q1 } ⊢ { P2 } C { Q2 } ⊢ { P1 ∧ P2 } C { Q1 ∧ Q2 }
规约析取
⊢ { P1 } C { Q1 } ⊢ { P2 } C { Q2 } ⊢ { P1 ∨ P2 } C { Q1 ∨ Q2 }

这些规则对于将证明拆成独立的任务而言是有用的. 例如, 其允许通过单独证明⊢{P}C{Q1}和⊢{P}C{Q2}而证明⊢{P}C{Q1∧Q2}.

剩余的这些规则允许复合命令的性质由其分量的性质推导而来.

第2.1.5小节 顺序规则

接下来的规则允许我们由C1和C2的规约推导出序列C1;C2的部分正确性规约.

顺序规则
⊢ {P} C1 {Q} ⊢ {Q} C2 {R} ⊢ {P} C1 ; C2 {R}

例子: 根据赋值公理:

  1. ⊢{X=x∧Y=y}R≔X{R=x∧Y=y}
  2. ⊢{R=x∧Y=y}X≔Y{R=x∧X=y}
  3. ⊢{R=x∧X=y}Y≔R{Y=x∧X=y}
因此, 根据i, ii和顺序规则, 我们有
  1. ⊢{X=x∧Y=y}R≔X;X≔Y{R=x∧X=y}
因此, 根据iv和iii以及顺序规则, 我们有
  1. ⊢{X=x∧Y=y}R≔X;X≔Y;Y≔R{Y=x∧X=y}

第2.1.6小节 导出顺序规则

以下的规则可由顺序规则和consequence规则导出.

导出顺序规则

(未完待续)

第2.1.7小节 条件规则

条件规则
⊢ { P∧S } C1 {Q} ⊢ { P∧ ¬S } C2 {Q} ⊢ {P} IFSTHEN C1 ELSE C2 {Q}

例子: 假定我们有了

  1. ⊢X≥Y⇒max⁡(X,Y)=X
  2. ⊢Y≥X⇒max⁡(X,Y)=Y
那么, 根据条件式规则(和其他一些规则), 可以推出 ⊢ {T} IF X≥Y THEN MAX≔X ELSE MAX≔Y { MAX= max⁡ ( X,Y ) }

第2.1.8小节 WHILE规则

如果⊢{P∧S}C{P}, 那么我们称: 每当S成立, P是C的一个不变量. WHILE规则是说, 如果P每当测试条件成立时是WHILE命令之体的一个不变量, 那么P是整个WHILE命令的一个不变量. 换言之, 如果执行C一次能够保持P的为真, 那么执行C任意多次也能够保持P的为真.

WHILE命令也表达了这样的事实, 在一个WHILE命令终止之后, 测试条件必然为假 (否则的话, 之前就不会终止).

WHILE规则
⊢ { P∧S } C {P} ⊢ {P} WHILESDOC { P∧ ¬S }

例子: 根据之前的规则可得: ⊢ { X= R+ ( Y×Q ) } R≔ R−Y ; Q≔ Q+1 { X= R+ ( Y×Q ) } 因此, 根据前条件强化, 可知 ⊢ { X= R+ ( Y×Q ) ∧ Y≤R } R≔ R−Y ; Q≔ Q+1 { X= R+ ( Y×Q ) } 于是, 根据WHILE规则, 取P为X=R+(Y×Q), 可以得到 ⊢ { X= R+ ( Y×Q ) } WHILE Y≤R DO ( R≔ R−Y ; Q≔ Q+1 ) { X= R+ ( Y×Q ) ∧ ¬ ( Y≤R ) } 应用赋值公理两次 (以及顺序公理), 容易推导出 ⊢ {T} R≔X ; Q≔0 { X= R+ ( Y×Q ) } 根据以上两个结果, 以及顺序规则和后条件弱化, 可以推得 ⊢ {T} R≔X ; Q≔0 ; WHILE Y≤R DO ( R≔ R−Y ; Q≔ Q+1 ) { R<Y ∧ X= R+ ( Y×Q ) }

除了WHILE规则这个例外, 目前所有描述的公理和规则对于完全正确性和部分正确性都是可靠的. 这是因为我们的小语言中唯一可能不终止的命令就是WHILE命令. {译注: 并且, 根据精心(且脆弱)的安排, 如果该语言里的运算和谓词都是完全的, 那么该语言甚至不会抛出异常.} 现在考虑如下证明:

  1. ⊢{T}X≔0{T} (赋值公理)
  2. ⊢{T∧T}X≔0{T} (前条件强化)
  3. ⊢{T}WHILETDOX≔0{T∧¬T} (2和WHILE规则)
若是WHILE规则对于完全正确性也是适用的, 那么上述证明就会表明: ⊢ [T] WHILETDO X≔0 [ T∧ ¬T ] 但这显然不对, 因为WHILETDOX≔0并不终止, 但即便它终止了, T∧¬T也不可能在任何结果状态下成立.

第2.1.9小节 FOR规则

(未完待续)

第2.1.10小节 数组

在第2.1.1小节末我们表明了朴素的数组赋值公理 ⊢ { P [ E2 / A⁡ ( E1 ) ] } A⁡ ( E1 ) ≔ E2 {P} 并不成立, 这是因为对于A⁡(X)的改变也可能改变A⁡(Y),A⁡(Z),…, 鉴于X可能等于Y,Z,….

译者注记. 说明一下, 第2.1.1小节末没有讨论数组的事情.

这个问题的解法 (归功于Hoare) 是将数组赋值 A⁡ ( E1 ) ≔ E2 处理为通常赋值 A≔ A { E1 ← E2 } 其中项A{E1←E2}代表一个等同于A的数组, 除了E1分量被变为具有值E2.

因此, 数组赋值不过是通常变量赋值的特殊情形.

数组赋值公理
⊢ { P [ A { E1 ← E2 } /A ] } A⁡ ( E1 ) ≔ E2 {P} 其中A是一个数组变量, E1是一个整数值表达式, P是任意的陈述, 而记号A{E1←E2}代表一个等同于A的数组, 只是E1处的值为E2.

为了对于数组进行推理, 以下定义了记号A{E1←E2}意义的公理是必要的.

数组公理
⊢ A { E1 ← E2 } ⁡ ( E1 ) = E2 ⊢ E1 ≠ E3 ⇒ A { E1 ← E2 } ⁡ ( E3 ) = A⁡ ( E3 )

译者注记. 原文这里实际上写的是 E1 ≠ E3 ⇒ ⊢ A { E1 ← E2 } ⁡ ( E3 ) = A⁡ ( E3 ) 但是译者认为这样写并不好.

(未完待续)

第3章 机械化程序验证

本章描述了一个简单的程序验证器的架构. 其操作相对于Hoare逻辑的规则而澄清.

在摆弄了一些例子之后, 以下两件事情一目了然:

  1. 证明一般冗长且乏味 (即便要验证的程序相当简单).
  2. 存在大量琐碎的细节需要弄对, 其中许多极其平凡, 例如证明⊢(R=X∧Q=0)⇒(X=R+Y×Q).

人们已经进行了许多尝试(并且仍在继续), 试图通过设计系统来自动完成正确性证明, 让这些系统处理在Hoare逻辑中生成形式化证明时那些枯燥而繁琐的部分. 不幸的是, 逻辑学家已经证明, 从原理上讲, 不可能设计出一种判定过程来自动判定任意数学命题的真假 [10]. 然而, 这并不意味着我们无法拥有能够证明许多有用定理的过程. 一般判定过程的不存在仅仅表明, 我们不能期望自动证明一切. 在实践中, 构建一个能够将验证过程中许多枯燥和常规方面机械化的系统是完全可行的. 本章描述了一种常用的实现方法.

尽管不可能自动判定任意命题的真假, 但检验一个任意的形式化证明是否有效却是可能的. 这包括检查证明中每一行出现的结果是否确实是公理, 或者是由前面的行推导出的结论. 由于程序正确性的证明通常非常冗长且枯燥, 手动生成时往往会包含错误. 因此, 即使证明只能在人工辅助下生成, 对其进行机械化检验仍然是有价值的.

第3.1节 概览

前一章展示了如何证明{P}C{Q}, 方法是证明C的各部分之性质, 然后(以合适的证明规则)将它们拼在一起以获得关于C本身的预期性质. 例如, 为了证明⊢{P}C1;C2{Q}, 我们首先证明⊢{P}C1{R}和⊢{R}C2{Q} (对于某个合适的R而言), 然后根据顺序规则可以推出⊢{P}C1;C2{Q}.

这种过程被称为前向证明, 因为人们由公理通过规则向前推进到结论. 在实践中, 更为自然的是后向工作: 从表明{P}C{Q}的目标开始, 我们生成子目标, 子目标, 如此反复直至问题解决. 例如, 假定我们想要证明: ⊢ { X=x ∧ Y=y } R≔X ; X≔Y ; Y≔R { Y=x ∧ X=y } 那么根据赋值公理和顺序规则, 证明以下子目标就足够了: ⊢ { X=x ∧ Y=y } R≔X ; X≔Y { R=x ∧ X=y } 鉴于⊢{R=x∧X=y}Y≔R{Y=x∧X=y}. 根据类似的论证, 这个子目标可以被归约为 ⊢ { X=x ∧ Y=y } R≔X { R=x ∧ Y=y } 这显然可由赋值公理推出.

这一章描述了如何形式化这样一种面向目标的证明方法.

这里所描述的验证系统可以被视为一个证明检查器, 同时它也为证明的生成提供了一定的帮助. 下图给出了该系统的概览.

要证明的规约 →人类专家 注解了的规约 →vc生成器 逻辑陈述集合 →定理证明器 简化的验证条件集合 →人类专家 证明结束

该系统的输入是一个部分正确性规约, 其中标注了描述变量之间关系的数学陈述. 系统从标注后的规约中生成一组纯数学陈述, 称为验证条件 (即VC). 在3.5节中将会证明, 如果这些验证条件是可证明的, 那么原始规约就可以从Hoare逻辑的公理和规则中推导出来.

验证条件被传递给一个定理证明器程序, 该程序尝试自动地证明它们; 如果证明失败, 则向用户寻求建议. 我们将集中讨论与Hoare逻辑相关的方面, 而对定理证明本身着墨甚少.

当前大量研究的目标是构建能够将缓慢且昂贵的人类专家的参与降到最低的系统. 这可以通过以下途径实现:

下一节将解释验证条件的工作原理. 在3.5节中, 将依据Hoare逻辑的公理和规则来论证其使用的合理性. 除了作为机械化验证系统的基础之外, 验证条件也是手工进行证明的一种有用方式.

第3.2节 验证条件

以下各节描述了如何将面向目标的证明风格形式化. 为了证明一个目标{P}C{Q}, 需要完成三件事. 这些将在后文中详细解释, 但这里先给出一个简要概述:

  1. 通过在程序C中插入陈述 (通常称为断言) 来对其进行标注, 这些断言表达了在各个中间点上应当成立的条件. 这一步骤是棘手的, 需要智慧以及对程序工作方式的深刻理解. 将其自动化是一个人工智能问题.
  2. 然后从标注后的规约中生成一组称为验证条件 (简称VC) 的逻辑陈述. 这一过程是纯机械的, 很容易由程序完成.
  3. 证明这些验证条件. 将其自动化同样是一个人工智能问题.

我们将证明, 如果能够证明由{P}C{Q}生成的所有验证条件 (其中C已被适当注解), 则⊢{P}C{Q}.

由于验证条件只是数学陈述, 可以将上述第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}
注解P1和P2陈述了每当控制抵达它们时预期成立的条件.

第3.3节 注解

带注解的命令是指在其中嵌入了陈述 (称为断言) 的命令. 如果在以下位置插入了陈述, 则称该命令是正确注解的:

  1. 对于序列C1;C2;…;Cn而言, 每个不是赋值命令的命令Ci之前, 其中i>1.
  2. 在WHILE命令的DO之后.
从直觉上来说, 插入的断言应当表达的是: 每当控制抵达断言出现的点时, 我们预期成立的条件.

译者注记. 这个定义会遇到微妙的序列嵌套问题, 但并非不可解决. 不过, 本讲义呈现时, 似乎默认序列一定是摊平的.

正确注解的规约是一个规约{P}C{Q}, 其中C是正确注解的命令.

例子:

第3.4节 验证条件生成

本节描述了一个过程, 其为注解过了的部分正确性规约{P}C{Q}生成验证条件. 这个过程是C上的递归.

赋值命令
由 {P} V≔E {Q} 生成的单条验证条件为 P⇒ Q [ E/V ]

例子: { X=0 } X≔ X+1 { X=1 } 的验证条件为 X=0 ⇒ ( X+1 ) =1 这显然为真.

条件式
由 {P} IFSTHEN C1 ELSE C2 {Q} 生成的验证条件为
  1. { P∧S } C1 {Q} 所生成的验证条件
  2. { P∧ ¬S } C2 {Q} 所生成的验证条件

如果C1;…;Cn被正确注解了, 那么 (见第39页) 其必然具有以下两种形式之一:

  1. C1;…;Cn−1;{R}Cn, 或是
  2. C1;…;Cn−1;V≔E.
在这两种情形下, C1;…;Cn−1都是正确注解了的命令.

序列
  1. {P} C1 ;…; C n−1 ; {R} Cn {Q} (其中Cn不是一个赋值) 所生成的验证条件为:
    1. {P} C1 ;…; C n−1 {R} 所生成的验证条件
    2. {R} Cn {Q} 所生成的验证条件
  2. {P} C1 ;…; C n−1 ; V≔E {Q} 所生成的验证条件是由 {P} C1 ;…; C n−1 { Q [ E/V ] } 所生成的验证条件

例子: { X=x ∧ Y=y } R≔X ; X≔Y ; Y≔R { X=y ∧ Y=x } 所生成的验证条件是 { X=x ∧ Y=y } R≔X ; X≔Y { ( X=y ∧ Y=x ) [ R/Y ] } 所生成的验证条件, 这化简一下是 { X=x ∧ Y=y } R≔X ; X≔Y { X=y ∧ R=x } 由此生成的验证条件是 { X=x ∧ Y=y } R≔X { ( X=y ∧ R=x ) [ Y/X ] } 所生成的验证条件, 这又可以简化为 { X=x ∧ Y=y } R≔X { Y=y ∧ R=x } 这生成的唯一的验证条件是 X=x ∧ Y=y ⇒ ( Y=y ∧ R=x ) [ X/R ] 即可以化简为 X=x ∧ Y=y ⇒ Y=y ∧ X=x 这显然为真.

WHILE命令的正确注解规约具有形式 {P} WHILESDO {R} C {Q} 根据第24页的用法, 注解R被称为一个不变量.

WHILE命令
由 {P} WHILESDO {R} C {Q} 所生成的验证条件为
  1. P⇒R
  2. R∧¬S⇒Q
  3. 由{R∧S}C{R}所生成的验证条件
译者注记. 乍看上去, 对于没有注解过的WHILE命令的规约 {P} WHILESDOC {Q} 我们似乎也能够根据WHILE命令的规则生成以下验证条件:
  1. P∧¬S⇒Q
  2. 由{P∧S}C{P}所生成的验证条件
但问题在于, P往往太强了. 从直觉上来说, P描述的是WHILE循环执行之前的状态, 而命令的目的恰恰在于改变状态. 因此, 这里的注解几乎完全是必要的, 而且难以自动化.

例子:

{R=X ∧ Q=0}
WHILE Y≤R DO {X = R+Y×Q}
  (R:=R-Y; Q=Q+1)
{X = R+(Y×Q) ∧ R<Y}
的验证条件为:
  1. R=X∧Q=0⇒X=R+(Y×Q)
  2. X=R+(Y×Q)∧¬(Y≤R)⇒X=R+(Y×Q)∧R<Y
连带着 { X= R+ ( Y×Q ) ∧ Y≤R } R≔ R−Y ; Q≔ Q+1 { X= R+ ( Y×Q ) } 的验证条件, 而这个条件仅有一条, 即
  1. X= R+ ( Y×Q ) ∧ Y≤R ⇒ X= ( R−Y ) + ( Y× ( Q+1 ) )
因此, 若i, ii, iii成立, 那么此WHILE命令的规约为真, 即若 ⊢ R=X ∧ Q=0 ⇒ X= R+ ( Y×Q ) 且 ⊢ X= R+ ( Y×Q ) ∧ ¬ ( Y≤R ) ⇒ X= R+ ( Y×Q ) ∧ R<Y 且 ⊢ X= R+ ( Y×Q ) ∧ Y≤R ⇒ X= ( R−Y ) + ( Y× ( Q+1 ) ) 那么 ⊢ { R=X ∧ Q=0 } WHILE Y≤R DO { X= R+ ( Y×Q ) } R≔ R−Y ; Q≔ Q+1 { X= R+ ( Y×Q ) ∧ R<Y }

第3.5节 验证条件的澄清

本节将证明, 若一个带注解的规约{P}C{Q}所生成的验证条件是可证的, 则该规约在Hoare逻辑中也是可证的, 即⊢{P}C{Q}. 这说明验证条件是充分的, 但并不意味着它们是必要的. 事实上, 验证条件是最弱的充分条件, 但我们在此既不对此作更精确的表述, 也不深入讨论细节. 关于前条件的深入研究可参见Dijkstra的著作 [8].

容易证明验证条件不是必要的, 即{P}C{Q}的验证条件不可证并不意味着⊢{P}C{Q}不可推导. 例如, 带注解规约{T}WHILEFDO{F}X≔0{T}的验证条件是不可证的, 但该Hoare三元组在Hoare逻辑中是可证的. {译注: 当我们说其在Hoare逻辑中是可证时, 是直接将命令中的注解完全抛弃了的.}

验证条件充分性的论证将对C的结构进行归纳. 这类归纳论证包含两个部分. 首先, 证明结论对赋值命令成立. 其次, 证明当C不是赋值命令时, 若结论对C的各组成命令成立 (这称为归纳假设), 则结论对C也成立. 前者称为归纳基础, 后者称为归纳步骤. 由归纳基础和归纳步骤即可得出结论对所有命令成立.

第3.5.1小节 赋值

{P}V≔E{Q}唯一的验证条件是P⇒Q[E/V]. 如果这是可证的, 那么鉴于⊢{Q[E/V]}V≔E{Q} (赋值公理), 根据前条件强化可以推出⊢{P}V≔E{Q}.

第3.5.2小节 条件式

如果{P}IFSTHENC1ELSEC2{Q}的验证条件是可证的, 那么{P∧S}C1{Q}和{P∧¬S}C2{Q}的验证条件都是可证的. 根据归纳假设, 我们可以假定⊢{P∧S}C1{Q}和⊢{P∧¬S}C2{Q}. 因此, 根据条件式规则可得⊢{P}IFSTHENC1ELSEC2{Q}.

第3.5.3小节 序列

存在两种需要考虑的情形:

  1. 如果{P}C1;…;Cn−1;{R}Cn{Q}的验证条件是可证明的, 那么{P}C1;…;Cn−1{R}和{R}Cn{Q}的验证条件都必须是可证明的, 因而根据规则有⊢{P}C1;…;Cn−1{R}和⊢{R}Cn{Q}. 于是, 根据顺序规则, 我们可得⊢{P}C1;…;Cn−1;Cn{Q}.
  2. 如果{P}C1;…;Cn−1;V≔E{Q}的验证条件是可证明的, 那必然是{P}C1;…;Cn−1{Q[E/V]}的验证条件也是可证的这一情形, 因而根据归纳, 我们有⊢{P}C1;…;Cn−1{Q[E/V]}. 然后, 根据赋值公理可以得到⊢{Q[E/V]}V≔E{Q}, 于是由顺序规则得到⊢{P}C1;…;Cn−1;V≔E{Q}.

第3.5.4小节 WHILE命令

如果{P}WHILESDO{R}C{Q}生成的验证条件是可证的, 那么⊢P⇒R, ⊢(R∧¬S)⇒Q且由{R∧S}C{R}所生成的验证条件是可证的. 根据归纳可得⊢{R∧S}C{R}, 因而根据WHILE规则有⊢{R}WHILESDOC{R∧¬S}. 然后, 根据consequence规则可得⊢{P}WHILESDOC{Q}.

第4章 可靠性和完备性

本章研究了Hoare逻辑的公理和规则是否正确 (可靠性) 以及是否充分 (完备性) 的问题. 这要求明确地给出编程语言的意义 (语义), 从而能够严格地定义Hoare三元组的语义.

第4.1节 语义

一条命令C将一个初始状态转换为一个最终状态 (或者不终止). 对于目前所描述的语言, 从一个给定的初始状态出发, 至多只能到达一个终止状态——即命令是确定性的——但后面当我们向语言中加入存储分配时, 情况就不再如此了. 在数学上, 有几种本质上等价的方式来表示命令的意义. 我们将使用关系, 但部分函数也是常用的方式. 使用关系通常与操作语义相关联, 而部分函数则与指称语义相关联, 不过这并不是严格固定的: 指称语义可以使用关系作为指称, 操作语义也可以归纳地定义函数. 事实上, 在下面的第4.1.2节中, 我们给出了一种命令的指称语义, 其中指称就是关系.

各种风格的语义在很大程度上只是表示相同数学思想的不同方式. 某些数学表示更适合某些目的, 而另一些表示则更适合其他目的. 我在下面给出的语义可能与你在之前课程中见过的语义一致, 也可能不一致. 如果它看起来不同, 那么一个很好的练习就是思考它们之间有何关联.

我们将用状态集合上的一个二元关系来表示命令C的意义: 在这个关系中, s1与s2相关联, 当且仅当C在状态s1下执行后终止于状态s2.

数学上有多种方式来表示关系, 虽然选择哪种并不真正重要, 但在深入具体细节之前, 先简要介绍一下这些不同的表示方式, 可能有助于避免后续的混淆.

集合论的入门书籍通常将关系表示为序对的集合, 即x藉由关系R与y关联当且仅当(x,y)∈R. 因此, 集合X和Y之间的一个二元关系是X×Y的一个子集, 即R⊆X×Y或者R∈𝒫⁡(X×Y), 其中P是幂集算子. 如果S是任意的集合, 那么任何子集A⊆S都可以由一个函数fA:S→{true,false}刻画, 该函数的定义为 ∀ s∈S . fA ⁡ (s) =true ⇔ s∈A fA被称为A的特征函数. 因此, 一个关系R⊆X×Y可以由其特征函数fR刻画, 其被定义为 ∀ x∈X . ∀ y∈Y . fR ⁡ ( x,y ) =true ⇔ ( x,y ) ∈R 其中fR:X×Y→{true,false}. 如果从集合S到集合T的所有函数之集合记作S→T, 而Bool是集合{true,false}, 那么fR∈X×Y→Bool. 你或许可以从之前的课 (例如ML课) 上回忆一下, 接受两个参数或者更多参数的函数可以被curry化, 以使得这些函数每次接受一个参数. 如果我们对于fR进行curry化, 那么就会得到函数fRcurried, 其定义如下: fRcurried xy = fR ⁡ ( x,y ) 那么fRcurried:X→(Y→Bool), 或是fRcurried:X→Y→Bool, 如果我们采取→向右结合的标准约定. 注意到我们也有fRcurried∈X→𝒫⁡(Y). {译注: 原文写的是fRcurried∈𝒫⁡(X→Y→Bool), 这肯定不太对, 但是我不确定我修正之后的版本是作者的原意.}

总结一下, 一个关系R可以表示为一个序对的集合, 或者是一个将序对映射至布尔值的特征函数, 或者是curry化了的特征函数. 出于历史和风格方面的混合原因 (多少有些随意), 我们将使用curry化特征函数的表示方式来表示命令的语义. 具体而言, 我们将要定义CsemCs1s2来表达如果C始于状态s1, 那么其会终止于状态s2. 这里的Csem是用于表示C的语义的关系, 其被表示为curry化的特征函数. 我们语言中的命令集合将记作Com, 而状态集合则记作State. 因此, CsemC:State→State→Bool或是Csem:Com→State→State→Bool. 正如之前我们所提到的, 选择将CsemC表示为curry化的特征函数而非序对集合, 更多是风格上的考量, 与实质没有太多关系.

令Var是陈述, 表达式 (项), 命令中所允许的变量之集合, 令Val是变量可以取的值之集合. 不必具体描述变量和值实际上是什么: 例如, Var可以是 ASCII 字符的有限字符串的集合, Val可以是整数的集合. 一个状态确定每个变量的值, 并且此外可能包含其他信息. 对于我们的小型编程语言, 将状态取为从Var到Val的函数就足够了. 使用A→B的记法来表示定义域为A, 值域 (实则为陪域) 为B的函数的集合, 我们定义状态的集合State为: State= Var→Val 注意以下写法都是等价的: s∈State, s∈(Var→Val), 以及s:Var→Val. 对于状态s中与变量v关联的值, 我们有时记成s⁡(v), 有时记成sv, 此即应用表示状态s的函数于v. 尽管目前将状态表示为从变量到值的函数已经足够了, 第7章我们还要为状态加上另一组件, 用于表示指针的内容. 我们将在那一章中扩展State的定义. 特定的状态可以使用λ记号来定义. 例如, 将X映射为1, Y映射为2, 而其他一切变量映射为0的状态可以定义为 λv. if v=X then1else ( if v=Y then2else0 ) 如果s∈State,v∈Var,n∈Val, 那么s[n/v]表示这样的状态, 其和s一样, 除了变量v的值为n. 因此, s[n/v]由以下等式给出: s [ n/v ] = λ v′ . if v′ =v thennelse s⁡ ( v′ )

例子: ( λv. if v=X then1else ( if v=Y then2else0 ) ) [ 3/Z ] = λv. if v=X then1else ( if v=Y then2else ( if v=Z then3else0 ) ) {译注: 这是所谓的外延相等, 或者说指称相等.}

第4.1.1小节 表达式和陈述的语义

命令中可能包含表达式或陈述: 表达式出现在赋值的右侧, 而陈述出现在条件式和WHILE命令的测试条件中. Hoare三元组的前条件和后条件也是陈述. Hoare逻辑的经典处理建立在一阶逻辑之上, 表达式被视为逻辑中的项, 陈述被视为公式. 你应该在之前的课程中已经熟悉了一阶逻辑的语义, 我不想在这里重复那些内容. 此外, 在现代应用中, 用于书写前条件和后条件的语言有时比一阶逻辑更弱或更强, 例如无量词逻辑 (更弱) 或高阶逻辑 (更强).

为了避免涉及特定逻辑及其语义的细节, 我们假设已给定表达式集合Exp和陈述集合Sta, 以及定义它们语义的语义函数Esem和Ssem, 其中: Esem : Exp→State→Val Ssem : Sta→State→Bool 现在我们给出一些非形式化的讨论和例子来刻画Esem和Ssem是如何对于特定逻辑定义的. 我们希望从中可以清楚地看出, 更形式化的处理将如何进行. 按照通常的逻辑术语 (例如IB Tripos课程Logic and Proof中所使用的), 我们是在用状态来表示解释函数, 把Val作为论域或者说宇宙. 变量的解释通过在状态中查找来实现: EsemXs = s⁡ (X) {译注: 原文使用的是X, 对于这个字体我不敢苟同, 鉴于它应该是一个代表变量的元变量.}
常量获得其通常的数学或逻辑含义: Esem3s = 3 SsemTs = true {译注: 这里其实我有点问题, 对于数字我的译文一以贯之没有使用其他字体, 但是书中对于项/表达式中的数字是使用所谓的打字机字体的. 不过, 你会发现原文的排版也有一些疏忽, 例如角标的字体都没有顾及到这一点. 总之, 我只是希望字体不要带来困惑.}
复合表达式或陈述是自底而上进行解释的: (通过递归计算得到的)子表达式的值使用合适的数学或逻辑运算组合起来以得到整个表达式的解释. 例如: Esem ( −E ) s = − ( EsemEs ) Esem ( E1 + E2 ) s = ( Esem E1 s ) + ( Esem E2 s ) Ssem ( ¬S ) s = ¬ ( SsemSs ) Ssem ( E1 < E2 ) s = ( Esem E1 s ) < ( Esem E2 s ) {译注: 原文这里写的是 Ssem ( S1 < S2 ) s = ( Ssem S1 s ) < ( Ssem S2 s ) 个人认为并不合理, 因为<实际上是一个谓词而非逻辑联结词, S1<S2应该是一个原子陈述, 而S1和S2应该是项 (或者说表达式). 换言之, 我们需要用Esem来对于这些分量求值.}
其中这些等式左边的符号−, +, ¬, <是陈述句法的一部分 (也就是对象语言的一部分), 而右边的相应符号是非形式化的数学记号的一部分 (也就是我们的元语言的一部分). {译注: 更确切地说, 左边的那些符号本身就是句法, 而右边的符号则指称着某种函数. 而且, 最好我们也应该明确区分项和陈述, 不然就会犯下原文这里的错误.} 这是需要仔细琢磨的微妙之处! 量词 (或许会出现在前条件和后条件里, 但是不会出现在命令的测试里) 以标准方式进行解释: Ssem ( ∀v.S ) s = ∀n. SsemS ( s [ n/v ] ) Ssem ( ∃v.S ) s = ∃n. SsemS ( s [ n/v ] ) {译注: 右边的n取值于论域, 即Val.}

例子: Ssem ( Y< Z+3 ) ( λv. if v=X then1else ( if v=Y then2else0 ) ) = ( 2< 0+3 )

我们希望这足以解释之后要用到的Esem和Ssem了. 注意到对于任意的E∈Exp和S∈Sta, 我们有 EsemE : State→Val SsemS : State→Bool

第4.1.2小节 命令的语义

CsemCs1s2将自底而上递归定义. 唯一不含有子命令的命令是赋值. 在一次赋值之后, 最终状态s2等于初始状态, 不过其中赋值左侧的变量V被更新为右侧表达式E在初始状态下的值. Csem ( V≔E ) s1 s2 = ( s2 = s1 [ ( EsemE s1 ) /V ] )

从初始状态s1出发执行顺序组合C1;C2能够到达最终状态s2, 当且仅当存在一个中间状态s, 使得从s1执行C1可到达s, 并且从该中间状态s执行C2可到达s2. Csem ( C1 ; C2 ) s1 s2 = ∃s. Csem C1 s1 s ∧ Csem C2 s s2

如果S在状态s1中为真, 则从s1出发执行条件式IFSTHENC1ELSEC2能够到达状态s2, 当且仅当从s1出发执行THEN分支C1能够到达s2. 然而, 如果S在状态s1中为假, 则从s1出发执行该条件式能够到达状态s2, 当且仅当从s1出发执行ELSE分支C2能够到达s2. Csem ( IFSTHEN C1 ELSE C2 ) s1 s2 = if ( SsemS s1 ) then ( Csem C1 s1 s2 ) else ( Csem C2 s1 s2 )

如果从初始状态s1出发执行WHILESDOC能够到达最终状态s2, 那么必然存在有限次的C的迭代能够到达s2, 并且S在所有中间状态中为真, 而在s2中为假. 这通过定义一个迭代有限次的(谓词)函数Iter来形式化, 进而我们定义: Csem ( WHILESDOC ) s1 s2 = ∃n. Itern ( SsemS ) ( CsemC ) s1 s2 函数Iter是n上的递归, 定义如下: Iter0pc s1 s2 = ¬ ( p s1 ) ∧ ( s1 = s2 ) Iter ( n+1 ) pc s1 s2 = ( p s1 ) ∧ ( ∃s. ( c s1 s ) ∧ ( Iternpcs s2 ) ) Iter的第一个参数n是迭代的次数. 第二个参数p是状态上的谓词, 例如SsemS. 第三个参数c是一个curry化的特征函数, 例如CsemC. 第四个和第五个参数分别是初状态和终状态. 如果Num代表自然数集合{0,1,2,…}, 那么我们有 Iter: Num→ ( State→Bool ) → ( State→State→Bool ) →State→State→Bool

第4.2节 Hoare逻辑的可靠性

Hoare三元组{P}C{Q}的意义被定义为HsemPCQ, 其中: HsemPCQ = ∀ s1 , s2 . SsemP s1 ∧ CsemC s1 s2 ⇒ SsemQ s2 这个定义可以用来表述Hoare逻辑的可靠性. 为了表明可靠性, 我们需要证明赋值公理的所有实例均为真, 并且如果假设为真则由推理规则得到的所有结论为真. 回忆一下赋值公理:

Hoare赋值公理
⊢ { P [ E/V ] } V≔E {P} 其中V是任意的变量, E是任意的表达式, P是任意的陈述, 而P[E/V]代表将陈述P中变量V的所有出现都替换为项E.

为了证明这是可靠的, 我们必须表明对于每个V,E,P: Hsem ( P [ E/V ] ) ( V≔E ) P 展开Hsem的定义会将其转换为: ∀ s1 , s2 . Ssem ( P [ E/V ] ) s1 ∧ Csem ( V≔E ) s1 s2 ⇒ SsemP s2 展开Csem的定义会将其转换为: ∀ s1 , s2 . Ssem ( P [ E/V ] ) s1 ∧ ( s2 = s1 [ ( EsemE s1 ) /V ] ) ⇒ SsemP s2 这可以化简为: ∀ s1 . Ssem ( P [ E/V ] ) s1 ⇒ SsemP ( s1 [ ( EsemE s1 ) /V ] ) 这乍看上去可能有点令人困惑, 因为前件和后件里的记号[⋯/⋯]有着不同的含义. 前件里的P[E/V]指称着将陈述P中的变量V替换为表达式E的结果. 后件里的s1[(EsemEs1)/V]指称着状态, 其由更新s1以使得V之值为E在s1中的值得到 (所有其他的变量之值保持不变).

关于替换的离题.我们避免了细致刻画表达式和陈述的句法到底是什么, 所以说也不可能证明关于它们的一般性质. 然而, 对于任何理想的定义, 我们会期望: Ssem ( P [ E/V ] ) s = SsemP ( s [ ( EsemEs ) /V ] ) 例如, 取P为X+Y>Z, E为X+1, V为Y, 那么这个等式就变成了: Ssem ( ( X+Y >Z ) [ ( X+1 ) /Y ] ) s = Ssem ( X+Y >Z ) ( s [ ( Esem ( X+1 ) s ) /Y ] ) 既然Esem(X+1)s=s⁡(X)+1, 于是以上等式变成了: Ssem ( ( X+Y >Z ) [ ( X+1 ) /Y ] ) s = Ssem ( X+Y >Z ) ( s [ ( s⁡ (X) +1 ) /Y ] ) 对于左边的替换求值就将其归约为了: Ssem ( X+ ( X+1 ) >Z ) s = Ssem ( X+Y >Z ) ( s [ ( s⁡ (X) +1 ) /Y ] ) 对于Ssem求值给出: ( s⁡ (X) + ( s⁡ (X) +1 ) > s⁡ (Z) ) = ( ( s [ ( s⁡ (X) +1 ) /Y ] ) ⁡ (X) + ( s [ ( s⁡ (X) +1 ) /Y ] ) ⁡ (Y) > ( s [ ( s⁡ (X) +1 ) /Y ] ) ⁡ (Z) ) 使用s[n/v], 右边的等式可以化简, 这就给出 {译注: 原文说X,Y,Z要不同, 这不大对, 因为它们不是元变量, 而是对象语言中的变量}: ( s⁡ (X) + ( s⁡ (X) +1 ) > s⁡ (Z) ) = ( s⁡ (X) + ( s⁡ (X) +1 ) > s⁡ (Z) ) 这显然为真, 因为左边和右边是等同的.
尽管这只是一个例子, 其刻画了为什么对于所有的S,E,V,s都有 Ssem ( S [ E/V ] ) s = SsemS ( s [ ( EsemEs ) /V ] ) 实际上, 如果这个等式并不成立的话, 那么这个替换的定义就很糟糕了——这个等式的确应该当作替换的语义定义!
离题结束.

回到赋值公理的可靠性上来, 回忆一下其等价于对于所有的P,E,V, 以下公式成立: ∀ s1 . Ssem ( P [ E/V ] ) s1 ⇒ SsemP ( s1 [ ( EsemE s1 ) /V ] ) 如果之前离题讨论的替换等式成立, 那么这个推出当然也成立. 这是因为, 对于任意的陈述P和Q, 如果P=Q, 那么可以推出P⇒Q.

除了WHILE规则之外的其他Hoare逻辑规则的可靠性几乎都是平凡的, 即便是WHILE规则的可靠性也是相当直接的. 我们将会重述规则, 然后给出可靠性证明的提纲.

前条件强化
⊢ P⇒ P′ ⊢ { P′ } C {Q} ⊢ {P} C {Q}

如果以下公式对于所有的P,P′,C,Q均成立: ( ∀s. SsemPs ⇒ Ssem P′ s ) ∧ Hsem P′ CQ ⇒ HsemPCQ 那么这条规则就是可靠的.
这在以Hsem的定义扩展之后就变成了: ( ∀s. SsemPs ⇒ Ssem P′ s ) ∧ ( ∀ s1 , s2 . Ssem P′ s1 ∧ CsemC s1 s2 ⇒ SsemQ s2 ) ⇒ ( ∀ s1 , s2 . SsemP s1 ∧ CsemC s1 s2 ⇒ SsemQ s2 ) 这是以下陈述的一个实例, 如果我们取p,p′,q,c分别为SsemP,SsemP′,SsemQ,CsemC. ( ∀s. ps ⇒ p′ s ) ∧ ( ∀ s1 , s2 . p′ s1 ∧ c s1 s2 ⇒ q s2 ) ⇒ ( ∀ s1 , s2 . p s1 ∧ c s1 s2 ⇒ q s2 ) 这显然为真.

后条件弱化
⊢ {P} C { Q′ } ⊢ Q′ ⇒Q ⊢ {P} C {Q}

以类似的论证, 这是可靠的.

规约合取
⊢ { P1 } C { Q1 } ⊢ { P2 } C { Q2 } ⊢ { P1 ∧ P2 } C { Q1 ∧ Q2 }
规约析取
⊢ { P1 } C { Q1 } ⊢ { P2 } C { Q2 } ⊢ { P1 ∨ P2 } C { Q1 ∨ Q2 }

以类似的论证, 这是可靠的.

顺序规则
⊢ {P} C1 {Q} ⊢ {Q} C2 {R} ⊢ {P} C1 ; C2 {R}

如果以下公式对于所有的P,Q,R,C1,C2为真: HsemP C1 Q ∧ HsemQ C2 R ⇒ HsemP ( C1 ; C2 ) R 那么这条规则就是可靠的.
这在以Hsem的定义扩展之后就变成了: ( ∀ s1 , s2 . SsemP s1 ∧ Csem C1 s1 s2 ⇒ SsemQ s2 ) ∧ ( ∀ s1 , s2 . SsemQ s1 ∧ Csem C2 s1 s2 ⇒ SsemR s2 ) ⇒ ( ∀ s1 , s2 . SsemP s1 ∧ Csem ( C1 ; C2 ) s1 s2 ⇒ SsemR s2 ) 这是以下陈述的一个实例, 如果我们展开Csem(C1;C2)的定义并取p,q,r,c1,c2分别为SsemP,SsemQ,SsemR,CsemC1,CsemC2. ( ∀ s1 , s2 . p s1 ∧ c1 s1 s2 ⇒ q s2 ) ∧ ( ∀ s1 , s2 . q s1 ∧ c2 s1 s2 ⇒ r s2 ) ⇒ ( ∀ s1 , s2 . p s1 ∧ ( ∃s. c1 s1 s ∧ c2 s s2 ) ⇒ r s2 ) 这显然为真.

条件规则
⊢ { P∧S } C1 {Q} ⊢ { P∧ ¬S } C2 {Q} ⊢ {P} IFSTHEN C1 ELSE C2 {Q}

和顺序规则类似的论证表明条件规则是可靠的.

WHILE规则
⊢ { P∧S } C {P} ⊢ {P} WHILESDOC { P∧ ¬S }

如果以下公式对于所有的P,S,C均为真: Hsem ( P∧S ) CP ⇒ HsemP ( WHILESDOC ) ( P∧ ¬S ) 那么这条规则就是可靠的.
在以Hsem的定义扩展之后, 这就变成了: ( ∀ s1 , s2 . Ssem ( P∧S ) s1 ∧ CsemC s1 s2 ⇒ SsemP s2 ) ⇒ ( ∀ s1 , s2 . SsemP s1 ∧ Csem ( WHILESDOC ) s1 s2 ⇒ Ssem ( P∧ ¬S ) s2 ) 使用等式Ssem(P∧Q)s1=(SsemPs1∧SsemQs1)和Ssem(P∧¬Q)s2=(SsemPs2∧¬SsemQs2)并扩展Csem(WHILESDOC)的定义可以将其转换为:

第4.3节 可判定性和完备性

{T}C{F}为真当且仅当C并不终止, 因而既然停机问题是不可判定的, 于是Hoare逻辑也是不可判定的.

可靠性是指, 凡是能用Hoare逻辑的公理和推理规则推导出的Hoare三元组都是真的. 其逆命题, 即完备性, 则是说任何真的Hoare三元组都能用Hoare逻辑的公理和推理规则推导出来. 遗憾的是, 这在一般情况下并不成立.

考虑{T}X≔X{P}. 根据之前的语义, 此为真当且仅当HsemT(X≔X)P为真, 即: ∀ s1 , s2 . SsemT s1 ∧ Csem ( X≔X ) s1 s2 ⇒ SsemP s2 既然Csem(X≔X)s1s2=(s2=s1)且SsemTs1=true, 这可以归约为 ∀ s1 , s2 . ( s2 = s1 ) ⇒ SsemP s2 通过特化s1和s2为s, 这又化简为了∀s.SsemPs, 即P为真. 因此, 如果我们能够使用Hoare逻辑推导出任意为真的Hoare三元组, 那么我们就能使用Hoare逻辑推导出断言语言的任何为真的陈述. 绝大多数适用于刻画程序的逻辑都是不完备的 (例如一阶算术), 于是Hoare逻辑也不可能是完备的.

译者注记. ∀ s1 , s2 . ( s2 = s1 ) ⇒ SsemP s2 通过特化推出 ∀s. SsemPs 是一个单向行为, 但是因为反方向过于明显, 所以说这的确是一个等价. 换言之, 对于断言所使用的逻辑的真陈述P, 在Hoare逻辑里{T}X≔X{P}也是有效的Hoare三元组. 假如Hoare逻辑是完备的, 那么就存在Hoare逻辑的证明可以推出{T}X≔X{P}, 那么变相就推出了P. 对于较强的逻辑而言, 这都是不可能的, 因其是不完备的.

然而, 上述那种完备性之所以不可能实现, 仅仅是因为用于描述前条件和后条件的规约语言本身的不完备性. 如果我们将程序逻辑与规约逻辑分离开来, 那么就有可能构造出一种称为相对完备性 [6] 的概念, 它在一定程度上让我们确信, Hoare逻辑对于我们所讨论的这一小组简单命令的推理是充分的——也就是说, 不存在缺失的公理或规则. 然而事实证明, 即便是这种有限的完备性, 对于许多实际编程语言中存在的构造 (但不包括我们的玩具语言) 而言也可能是不可能的 [5].

我们不打算详细解释Cook等人在相对完备性方面的工作, 因为其中涉及的技术逻辑问题及其直觉解释都相当微妙 [1, 16]. 此外, 这样做将要求我们对表达前条件和后条件的规约语言的句法, 语义和证明理论进行比我们所期望的更为精确的刻画. 不过, 我们将概述其中的关键思想. 在证明相对完备性时所使用的一个概念是最弱前条件. 这一概念不仅在证明相对完备性方面发挥着作用, 它还具有实际应用价值, 包括提供一种改进的验证条件方法 (我们将在后文讨论), 以及作为程序精化理论的基础.

第4.3.1小节 相对完备性

我们将通过使用最弱自由前条件来解释相对完备性的思想, 并证明它对我们的小型程序语言成立. 然而, 我们将把其详细定义和分析推迟到第4.3.2节. 在本节中, 我们仅就最弱自由前条件是什么以及它们具有哪些性质 (下文的P1,P2,P3) 做出足够的说明, 以便我们能够解释相对完备性.

对于每个命令C和陈述Q, 我们假定存在一个陈述wlp⁡(C,Q)——直觉上它是确保在执行C之后Q成立的最弱前条件——并且它具有以下性质: ( P1 ) ⊢ { wlp⁡ ( C,Q ) } C {Q} ( P1 ) 陈述wlp⁡(C,Q)在规约语言中的存在性取决于规约语言是否足够强. 一个强到足以定义wlp⁡(C,Q)的语言被称为是有表达力的 (expressive).

算子wlp从一个命令和一个陈述构造出另一个陈述, 而陈述是形式语言中的一个句子. 因此wlp是由两个句法实体 (一个命令和一个陈述) 构造出一个句法实体 (一个陈述). 我们还假定存在wlp的一个语义对应物, 称为Wlp, 它作用于命令的意义 (表示状态上的二元关系的函数) 和陈述的意义 (状态上的谓词). Wlp是一个curry化的函数: Wlp: ( State→State→Bool ) → ( State→Bool ) →State→Bool 我们假定以下性质将wlp和Wlp联系起来, 其对所有命令C和陈述Q成立: ( P2 ) Ssem wlp⁡ ( C,Q ) = Wlp ( CsemC ) ( SsemQ ) ( P2 ) 注意这是一个谓词之间的等式. 我们还假定: ( P3 ) HsemPCQ = ∀s. SsemPs ⇒ Wlp ( CsemC ) ( SsemQ ) s ( P3 )

相对完备性证明的轮廓现在可以概述如下. 假定{P}C{Q}为真, 即HsemPCQ为真. 我们将证明陈述P⇒wlp⁡(C,Q)也必定为真——暂时先假定这一点. 如果我们能够证明这个真陈述, 即有⊢P⇒wlp⁡(C,Q), 那么通过前条件加强规则和性质P1, 就可以由Hoare逻辑推出⊢{P}C{Q}. 因此, Hoare逻辑的完备性是相对于一个能够证明所有形如P⇒wlp⁡(C,Q)的真陈述的神谕机而言的.

译者注记. 大致上, P1是说wlp在句法上是前条件, P2是说wlp和Wlp是一致的, P3是说Wlp在语义上是最弱前条件, 我是这样理解的.
译者注记. 神谕机是否会影响soundness? 这似乎是一个微妙的问题.

总结一下: 相对完备性表明, 如果wlp⁡(C,Q)在规约语言中是可表达的, 并且存在一个神谕机能够证明形如P⇒wlp⁡(C,Q)的真陈述, 那么任何为真的Hoare三元组{P}C{Q}都可以使用Hoare逻辑来证明.

我们现在再次梳理相对完备性的论证, 但更为详细, 展示假定P2和P3是如何使用的. 回忆一下: ( P2 ) Ssem wlp⁡ ( C,Q ) = Wlp ( CsemC ) ( SsemQ ) ( P2 ) ( P3 ) HsemPCQ = ∀s. SsemPs ⇒ Wlp ( CsemC ) ( SsemQ ) s ( P3 ) 假定对于任意C和Q, wlp⁡(C,Q)在规约语言中是可表达的, 并且假定对于任意P,C,Q, 存在一个神谕机能够证明形如P⇒wlp⁡(C,Q)的真陈述——即如果∀s.Ssem(P⇒wlp⁡(C,Q))s (陈述为真), 则神谕机给出⊢P⇒wlp⁡(C,Q) (陈述得证).

Hoare三元组{P}C{Q}为真, 按照我们的语义, 意味着HsemPCQ为真. 如果这成立, 则由上面假定的P3: ∀s. SsemPs ⇒ Wlp ( CsemC ) ( SsemQ ) s 然后由假定的性质P2: ∀s. SsemPs ⇒ Ssem wlp⁡ ( C,Q ) s 虽然我们没有完全定义规约语言, 但我们至少假定它包含一个中缀符号⇒, 其含义是逻辑后承, 因此从上面的陈述我们可以推出: ∀s. Ssem ( P⇒ wlp⁡ ( C,Q ) ) s 即陈述P⇒wlp⁡(C,Q)为真. 现在我们使用为这种形式的公式所假设的神谕机来证明⊢P⇒wlp⁡(C,Q), 因此由假设的性质P1和前条件加强规则, 我们可以证明⊢{P}C{Q}.

为了完成上述概要, 我们必须定义wlp和Wlp并证明性质P1,P2,P3. Hoare逻辑的公理和规则将被用于证明P1, 而它们能够证明P1这一事实, 正是其完备性的本质所在.

第4.3.2小节 句法和语义最弱前条件

译者注记. 这一小节的主要内容是Wlp的定义, 以及表述和证明了Wlp的一些性质, 这些下一小节要用到. 并且, 你会发现这些性质实际上正是为了配合下一小节而如此表述的.

如果P⇒Q, 我们说P比Q强, 对偶地, Q比P弱. 命令C相对于后条件Q的最弱前条件记作wp⁡(C,Q), 其是满足[wp⁡(C,Q)]C[Q]的最弱谓词. 注意到这和完全正确性相关联. 部分正确性的相应概念是最弱自由前条件, 记作wlp⁡(C,Q): 陈述wlp⁡(C,Q)是满足{wlp⁡(C,Q)}C{Q}的最弱谓词. 本章我们只会使用最弱自由前条件. 其关键性质是P1, 即⊢{wlp⁡(C,Q)}C{Q}, 以及对于所有的P都有 {P} C {Q} ⇒ ( P⇒ wlp⁡ ( C,Q ) ) 这些性质可以更为精确地表述为单一等式: {P} C {Q} = ( P⇒ wlp⁡ ( C,Q ) ) 我们可以很容易地看出来这个等式与刚才提及的关键性质是等价的, 只需使用前条件加强规则和⇒的自反性.

译者注记. 这里没有看上去这么简单, 我不确定作者想表达什么. 但是这个等价需要神谕机, 或者说Hoare逻辑的(相对)完备性.

如果规约语言 (即前条件和后条件的语言) 足够强以表达所有命令C和后条件Q的最弱自由前条件, 那么其就被称为有表达力的. 既然我们还没有说规约语言是什么, 那么我们对于表达力也就不好说什么.

我们可以藉由我们的语义定义谓词上的语义算子Wlp: 这是谓词转换器 [7] 的一个例子. Wlpcq = λs. ∀ s′ . cs s′ ⇒ q s′ 回忆一下Hsem的定义: HsemPCQ = ∀ s1 , s2 . SsemP s1 ∧ CsemC s1 s2 ⇒ SsemQ s2 我们可以轻易证明性质P3, 即 HsemPCQ = ∀s. SsemPs ⇒ Wlp ( CsemC ) ( SsemQ ) s P3可由Hsem和Wlp的定义推出, 通过在以下逻辑事实中取p,c,q分别为SsemP,CsemC,SsemQ得到. ( ∀ s1 , s2 . p s1 ∧ c s1 s2 ⇒ q s2 ) = ( ∀s. ps ⇒ ( λs. ∀ s′ . cs s′ ⇒ q s′ ) s ) 为了证明P1和P2, 我们需要下列等式, 其可由Wlp的定义推出. Wlp ( Csem ( V≔E ) ) q = λs. q⁡ ( s [ ( EsemEs ) /V ] ) Wlp ( Csem ( C1 ; C2 ) ) q = λs. Wlp ( Csem C1 ) ( Wlp ( Csem C2 ) q ) s Wlp ( Csem ( IFSTHEN C1 ELSE C2 ) ) q = λs. if ( SsemSs ) then ( Wlp ( Csem C1 ) qs ) else ( Wlp ( Csem C2 ) qs ) Wlp ( Csem ( WHILESDOC ) ) q = λs. ∀n. IterWlpn ( SsemS ) ( CsemC ) qs IterWlp0pcqs = ¬ ( ps ) ⇒ ( qs ) IterWlp ( n+1 ) pcqs = ( ps ) ⇒ ( Wlpc ( IterWlpnpcq ) s ) {译注: (IterWlpnpcqs)为真的含义应该是从状态s出发, 如果恰好执行n次循环体并退出, 那么qs为真, 其中p是循环条件S的语义, c是循环体命令C的语义. 注意一下, 提前或者延迟退出循环体都会导致这个谓词自动为真.}

(未完待续)

第4.3.3小节 句法前条件和可表达性

译者注记. 这一小节递归定义了wlp, 并归纳证明了wlp的性质, 也就是P1, 以及wlp和Wlp的协调, 也就是P2. 可能用到前一小节的结果, 当然前一小节的定义肯定是要使用的.

现在我们讨论如何以性质P1和P2定义陈述wlp⁡(C,Q), 即 ( P1 ) ⊢ { wlp⁡ ( C,Q ) } C {Q} ( P1 ) ( P2 ) Ssem wlp⁡ ( C,Q ) = Wlp ( CsemC ) ( SsemQ ) ( P2 ) 请注意, wlp是在句法东西 (命令和陈述) 上操作的, 而Wlp则是在语义东西 (状态上的数学函数, 代表命令和陈述的意义) 上操作的.

我们将在C上递归定义wlp⁡(C,Q), 然后藉由C上的结构归纳澄清P1和P2. C为赋值, 序列或是条件式的情形是直接的. wlp⁡ ( V≔E ,Q ) = Q [ E/V ] 如果C为V≔E, 那么P1直接就是赋值公理, 而根据第61页所讨论的等式 Wlp ( Csem ( V≔E ) ) q = λs. q⁡ ( s [ ( EsemEs ) /V ] ) P2为等式 Ssem ( Q [ E/V ] ) s = SsemQ ( s [ ( EsemEs ) /V ] ) 这不过就是第53页的关于替换的离题所澄清的. wlp⁡ ( C1 ; C2 ,Q ) = wlp⁡ ( C1 , wlp⁡ ( C2 ,Q ) ) 假定对于任意的Q, P1和P2对于C1和C2均成立, 那么 ⊢ { wlp⁡ ( C2 ,Q ) } C2 {Q} ⊢ { wlp⁡ ( C1 , wlp⁡ ( C2 ,Q ) ) } C1 { wlp⁡ ( C2 ,Q ) } 于是根据顺序规则可以得到P1, 即 ⊢ { wlp⁡ ( C1 , wlp⁡ ( C2 ,Q ) ) } C1 ; C2 {Q} 为了证明P2, 当C为C1;C2时, 注意到这种情况下的P2为 Ssem wlp⁡ ( C1 ; C2 ,Q ) = Ssem wlp⁡ ( C1 , wlp⁡ ( C2 ,Q ) ) = Wlp ( Csem ( C1 ; C2 ) ) ( SsemQ ) = Wlp ( λ s1 , s2 . ∃s. ( Csem C1 s1 s ) ∧ ( Csem C2 s s2 ) ) ( SsemQ ) = λ s1 . ∀ s2 . ( ∃s. ( Csem C1 s1 s ) ∧ ( Csem C2 s s2 ) ) ⇒ ( SsemQ s2 ) = λ s1 . ∀ s, s2 . ( Csem C1 s1 s ) ∧ ( Csem C2 s s2 ) ⇒ ( SsemQ s2 ) (未完待续) wlp⁡ ( IFSTHEN C1 ELSE C2 ,Q ) = ( S∧ wlp⁡ ( C1 ,Q ) ) ∨ ( ¬S ∧ wlp⁡ ( C2 ,Q ) ) 注意到(S∧S1)∨(¬S∧S2)的含义是ifSthenS1elseS2. 前者是为了强调我们对于规约语言的假定仅是布尔运算符¬,∧,∨的存在. 注意到根据布尔代数以及wlp⁡(IFSTHENC1ELSEC2,Q)的定义, 我们有 S∧ wlp⁡ ( IFSTHEN C1 ELSE C2 ,Q ) = S∧ wlp⁡ ( C1 ,Q ) ¬S ∧ wlp⁡ ( IFSTHEN C1 ELSE C2 ,Q ) = ¬S ∧ wlp⁡ ( C2 ,Q ) 根据归纳假设和前条件强化, 可以得到 ⊢ { S∧ wlp⁡ ( C1 ,Q ) } C1 {Q} ⊢ { ¬S ∧ wlp⁡ ( C2 ,Q ) } C2 {Q} {译注: 原文忘了加⊢, 译者将其补上.} 代入之前的等式, 并利用条件规则, 可以获得 ⊢ { wlp⁡ ( IFSTHEN C1 ELSE C2 ,Q ) } IFSTHEN C1 ELSE C2 {Q} 这就是条件式的性质P1. {译注: 这里作者的措辞存在疏漏, 作者说是利用布尔代数得到了等式, 然后代入. 严格说来, 这并不正确, 因为布尔代数是语义层面的事情. 不过, 句法层面我们也有分配律等手段可用, 这和语义层面是一样的, 所以论证可以修补成正确的形式.} 性质P2为 Ssem ( ( S∧ wlp⁡ ( C1 ,Q ) ) ∨ ( ¬S ∧ wlp⁡ ( C2 ,Q ) ) ) = Wlp ( Csem ( IFSTHEN C1 ELSE C2 ) ) ( SsemQ )

(未完待续)

第4.4节 藉由wlp生成验证条件

最弱前条件提供了一种理解和改进验证条件的方式. 回忆一下性质P1, 即⊢{wlp⁡(C,Q)}C{Q}. 为了证明{P}C{Q}为真, (根据前条件强化)证明⊢P⇒wlp⁡(C,Q)就足够了. 因此, 我们可以将P⇒wlp⁡(C,Q)视为对于目标{P}C{Q}而言的单独一条超级验证条件, 其无需对于C进行注解即可生成! 如果C不含有循环, 即不含有WHILE命令, 那么这就能很好地运转. 如果C的确含有WHILE命令, 那么wlp⁡(C,Q)将会是无限的陈述. 证明这样的陈述通常会涉及用归纳法证明本质上就是某个不变式的验证条件的东西. 因此找不变式是无法回避的! 然而, 可以用最弱前条件的思想来既解释又改进验证条件方法. 为了看出它如何解释验证条件, 回忆第41页, 由{P}C1;…;Cn−1;V≔E{Q}生成的验证条件是: {P}C1;…;Cn−1{Q[E/V]}(的验证条件), 也就是{P}C1;…;Cn−1{wlp⁡(V≔E,Q)}(的验证条件). 我们可以推广这一观察, 以减少序列中所需的标注数量, 做法是只要求在不是无循环的 (即包含WHILE命令的) 命令之前加标注, 然后修改序列的验证条件:

序列
  1. {P} C1 ;…; C n−1 ; {R} Cn {Q} 其中Cn包含一个WHILE命令, 由其所生成的验证条件为:
    1. {P} C1 ;…; C n−1 {R} 所生成的验证条件
    2. {R} Cn {Q} 所生成的验证条件
  2. {P} C1 ;…; C n−1 ; Cn {Q} 其中Cn无循环, 由其所生成的验证条件为 {P} C1 ;…; C n−1 { wlp⁡ ( Cn ,Q ) } 所生成的验证条件.
这些改进验证条件的澄清本质上和原本的一样, 只不过要使用性质P1而非赋值公理. {译注: 只有赋值公理吗?} 然而, 使用wlp的想法我们甚至可以做的更好, 将注解的需求减少为仅需对于WHILE命令的不变量仅需标注. 这种方法的大致框架如下: 由awp和wvc的定义可以推出wvc⁡(C,Q)中的陈述之合取蕴涵{awp⁡(C,Q)}C{Q}. 如果我们定义⋀S为S中的所有陈述之合取, 那么这可以写成: ⊢ ⋀ wvc⁡ ( C,Q ) ⇒ { awp⁡ ( C,Q ) } C {Q} 因此, 根据Modus Ponens和前条件强化, 为了证明{P}C{Q}(为真), 证明⊢⋀wvc⁡(C,Q)和⊢P⇒awp⁡(C,Q)就足够了. 如果C是无循环的, 那么实际上有awp⁡(C,Q)=wlp⁡(C,Q)且wvc⁡(C,Q)=∅, 于是这种方法就会坍缩为仅是证明⊢P⇒wlp⁡(C,Q). awp⁡(C,Q)和wvc⁡(C,Q)的定义都是C上的递归, 给出如下. 假定所有的WHILE命令都得到了注解: WHILESDO{R}C. awp⁡ ( V≔E ,Q ) = Q [ E/V ] awp⁡ ( C1 ; C2 ,Q ) = awp⁡ ( C1 , awp⁡ ( C2 ,Q ) ) awp⁡ ( IFSTHEN C1 ELSE C2 ,Q ) = ( S∧ awp⁡ ( C1 ,Q ) ) ∨ ( ¬S ∧ awp⁡ ( C2 ,Q ) ) awp⁡ ( WHILESDO {R} C ,Q ) = R wvc⁡ ( V≔E ,Q ) = ∅ wvc⁡ ( C1 ; C2 ,Q ) = wvc⁡ ( C1 , awp⁡ ( C2 ,Q ) ) ∪ wvc⁡ ( C2 ,Q ) wvc⁡ ( IFSTHEN C1 ELSE C2 ,Q ) = wvc⁡ ( C1 ,Q ) ∪ wvc⁡ ( C2 ,Q ) wvc⁡ ( WHILESDO {R} C ,Q ) = { R∧ ¬S ⇒Q , R∧S ⇒ awp⁡ ( C,R ) } ∪ wvc⁡ ( C,R )

定理. ⋀wvc⁡(C,Q)⇒{awp⁡(C,Q)}C{Q}.
证明. 对于C进行归纳.
如果C=(V≔E).
◻

第4.4.1小节 最强后条件

最弱前条件是由后条件开始反向计算的. 存在一个最强后条件的对偶理论, 其是自前条件正向计算的. 最强后条件具有性质⊢{P}C{sp⁡(C,P)}, 并且是最强的, 意即对于任意的Q, 如果⊢{P}C{Q}, 那么⊢sp⁡(C,P)⇒Q. 从直觉上说, sp⁡(C,P)是在由P所描述的初始状态下对于执行C之后的状态的符号表示. 对于赋值而言: sp⁡ ( V≔E ,P ) = ∃v. ( V= E [ v/V ] ∧ P [ v/V ] ) 存在量化变量v是V在执行赋值之前的初始状态中的值. 最强后条件表达了在赋值之后, V之值是E在初始状态下求得的值 (因而是E[v/V]), 而前条件在初始状态下评估 (因而是P[v/V]) 继续成立. 因此, 如果初始状态由陈述(V=v)∧P符号地表示, 那么执行V≔E之后的状态可由(V=E[v/V])∧P[v/V]符号地表示.

对于无循环命令C, sp⁡(C,P)相当于从符号状态P开始对于C进行符号执行. 符号执行的一个优点在于其允许对于到目前为止的状态的表示进行on-the-fly式的化简,

第5章 完全 (total) 正确性

本章Hoare逻辑的公理和规则被扩展到了完全正确性, 其给出了完全正确性规约的验证条件.

在1.3节中, 我们引入了记号[P]C[Q]来表示完全正确性规约, 即每当C在满足P的状态下执行时, 它将终止于一个满足Q的状态. 在描述WHILE规则的那一节 (第2.1.8节) 末尾, 我们已经表明该规则对于完全正确性规约不是可靠的. 这是因为WHILE命令可能引入非终止性. 其他命令都不会引入非终止性, 因此Hoare逻辑的规则可以继续使用.

第5.1节 非循环命令

将花括号替换为方括号, 我们就得到了以下公理和规则.

完全正确性的赋值公理 ⊢ [ P [ E/V ] ] V≔E [P]

完全正确性的前条件加强 ⊢ P⇒ P′ ⊢ [ P′ ] C [Q] ⊢ [P] C [Q]

完全正确性的后条件弱化 ⊢ [P] C [ Q′ ] ⊢ Q′ ⇒Q ⊢ [P] C [Q]

完全正确性的规约合取 ⊢ [ P1 ] C [ Q1 ] ⊢ [ P2 ] C [ Q2 ] ⊢ [ P1 ∧ P2 ] C [ Q1 ∧ Q2 ]

完全正确性的规约析取 ⊢ [ P1 ] C [ Q1 ] ⊢ [ P2 ] C [ Q2 ] ⊢ [ P1 ∨ P2 ] C [ Q1 ∨ Q2 ]

完全正确性的顺序规则 ⊢ [P] C1 [Q] ⊢ [Q] C2 [R] ⊢ [P] C1 ; C2 [R]

完全正确性的导出顺序规则

完全正确性的条件规则 ⊢ [ P∧S ] C1 [Q] ⊢ [ P∧ ¬S ] C2 [Q] ⊢ [P] IFSTHEN C1 ELSE C2 [Q]

这些规则从形式上来说和相应的Hoare逻辑规则等同, 除了使用方括号而非花括号. 因此, 显然以下是可靠 (valid) 的导出规则. ⊢ {P} C {Q} ⊢ [P] C [Q] , 如果C不含有WHILE命令 {译注: 以我之见, 这与其说是导出规则, 不如说是兼容规则 (admissible rule).}

第5.2节 赋值的终止性

注意到完全正确性的赋值公理陈述了赋值命令总是终止, 这隐式地假定了表达式中的所有函数应用都会终止. 如果函数可以递归定义, 那么或许并非如此. 例如, 考虑赋值X≔fact⁡(−1), 其中fact⁡(n)被递归定义为 fact⁡ (n) = IF n=0 THEN1ELSE n× fact⁡ ( n−1 ) 通常也假定诸如1/0这样错误的表达式也不会造成任何问题. 绝大多数编程语言遇到除以零时都会导致错误停下. 然而, 在我们的逻辑里, 可以推出 ⊢ [T] X≔ 1/0 [ X= 1/0 ] 即赋值X≔1/0总是能终止于一个条件X=1/0成立的状态. 这假定1/0指称着某个X可以具有的值. 存在两种可能性:

  1. 1/0指称着某个数字;
  2. 1/0指称着某种错误值.
乍看上去似乎采用ii是最为自然的选择. 然而, 这会使得理解算术定律是否应该成立变得棘手. 例如, (1/0)×0应该等于0还是某种错误值? 如果是后者, 那么就会导致n×0=0不再是有效的算术定律了. {译注: 含义是不能用其进行项重写化简.} 使用未定义或者 (and/or) 错误值或许也能让一切运转起来, 只不过其带来的理论有点混乱. 我们将会在这里假定算术表达式总是指称着数字, 只不过在某些情形下这个数字确切是什么未加刻画. 例如, 我们将会假定对于任意的m和n, m/n总会指称着某个数字, 但是其唯一假定的性质为 ¬ ( n=0 ) ⇒ ( m/n ) ×n =m 由此关于m/0并不能推出任何东西.

另一种处理错误的方法在于扩展命令的语义, 允许错误和状态一样作为结果. 这种方法用在第7章以处理内存错误, 但是类似的想法也能用来处理表达式求值错误 (尽管代价是语义更为复杂).

第5.3节 用于完全正确性的WHILE规则

WHILE命令是我们这个小语言中唯一可能导致不终止的命令, 因此也是唯一具有非平凡终止规则的命令. WHILE规则用于完全正确性的核心思想是: 要证明WHILESDOC能够终止, 必须证明某个非负量在C的每次迭代中都会减少. 这个递减的量称为变式量 (variant). {译注: 我感觉很多人也将其称为终止度量.} 在下面的规则中, 变式量为E, 而它递减的事实通过一个辅助变量n来刻画. 一个额外的假设⊢P∧S⇒E≥0确保了变式量是非负的.

完全正确性的WHILE规则

第5.4节 终止规约

正如第1.3节已经讨论过了的, 部分正确性和完全正确性之间的关系可以非正式地表述为以下等式: 完全正确性= 终止性+部分正确性

第5.5节 终止性的验证条件

第5.6节 验证条件生成

第6章 程序精化

Floyd-Hoare逻辑是一种证明现有程序满足其规约的方法. 它也可以作为将规约精化为程序的基础——即作为一种程序设计方法论的基础.

第6.1节 引论

程序员的任务可以被看作: 给定一个由前条件P和后条件Q组成的规约, 然后构造出一个命令C, 使得[P]C[Q]成立.

精化理论提出了从规约P和Q计算出程序C的规则. 一个关键思想来自芬兰的Ralph Back (后来被Joseph Morris和Carroll Morgan各自独立地重新发现), 即引入一类新的程序构造, 称为规约. 这些构造在句法上扮演与命令相同的角色, 但它们不可直接执行, 不过它们能保证在给定的前条件下达成给定的后条件. 由此产生的广义编程语言包含纯规约, 纯代码以及二者的混合体. 这样的语言被称为宽谱语言.

本章采用的方法遵循Morgan所发展的精化风格, 但其基础是Floyd-Hoare逻辑, 而非Dijkstra的最弱前条件理论 (见4.3.3节). 与传统方法相比, 这一基础更加具体和句法化: 规约被等同于其所有可能实现的集合, 精化则被表示为对普通命令集合的操作. 这种方法旨在利用前面各章所引入的记号和概念工具, 传达 (Morgan风格的) 精化的外观和感觉.

记号[P,Q]将用于表示规约, 因此: [ P,Q ] = { C| ⊢ [P] C [Q] } 精化过程将由一系列步骤组成, 这些步骤通过系统性的设计决策逐步缩小可能实现的集合, 直到达到唯一的实现. 因此, 从规约S到实现C的精化具有如下形式: S⊇ S1 ⊇ S2 ⊇⋯⊇ Sn ⊇ {C}

初始规约S具有[P,Q]的形式, 每个中间规约Si都是通过对其前驱Si−1应用一条精化律而得到的.

在文献中, S⊇S′通常写作S⊑S′. 这里使用⊇而非更抽象的⊑, 反映了精化作为逐步缩小实现集合这一具体解释.

第6.2节 精化律

第6.3节 一个例子

第6.4节 一般评注

第7章 指针和局部推理

第7.1节 指针操纵构造

第7.2节 例子: 反转链表

第7.3节 分离逻辑断言

第7.3.1小节 指向关系: E↦F

第7.4节 语义和分离逻辑