指称语义

已经学习了两种操作语义,这一节,我们学习新的语义模型–指称语义。指称语义意用数学函数表达程序在计算什么,以IMP为例子就是一个把store转换为另一个store的函数:给定一个store,程序输出一个最终的store。举个例子,可以把程序foo:=bar+1想成一个函数,此函数接受一个storeσ,输出另一个和σstoreσ,只不过σ另外包含了foo到σ(bar)+1的映射:σ=σ[fooσ(bar)+1]。程序就是store到store的函数。操作语义告诉我们程序如何运行,指称语义展示的是程序在计算什么。

IMP的指称语义

给定程序c,我们用C[c]表示c的指称(denotation),用数学函数表示就是:

C[c]:StoreStore

其中C[c]其实是个偏函数,因为既可能输入的store不是由程序的自由变量定义的,也可能程序不会终止。不终止的程序的C[c]是未定义的,因为没有对应的store作为结果。把C[c]应用到storeσ上则写成C[c]σ,如果用f表示C[c],那f(σ)C[c]意思是一样的。同时定义下面的两个函数,A[a]是算术表达式a的指称,B[b]是布尔表达式b的指称:

A[a]:StoreIntB[b]:Store{true,false}

现在我们能定义IMP的的指称语义了。首先定义算术表达式的布尔表达式的指称:

A[n]={(σ,n)}A[x]={(σ,σ(x))}A[a1+a2]={(σ,n)|(σ,n1)A[a1](σ,n2)A[a2]n=n1+n2}B[true]={(σ,true)}B[false]={(σ,false)}B[a1<a2]={(σ,true)|(σ,n1)A[a1](σ,n2)A[a2]n1<n2}{(σ,false)|(σ,n1)A[a1](σ,n2)A[a2]n1n2}

命令的指称如下:

C[skip]={(σ,σ)}C[x:=a]={(σ,σ[xn])|(σ,n)A[a])}C[c1;c2]={(σ,σ)|σ.((σ,σ)C[c1](σ,σC[c2])}

定义关系运算C[c1;c2]=C[c2]C[c1]是关系的复合运算,定义如下:

ifR1A×bandR2B×C,R2R1A×C,thenR2R1={(a,c)|bB.(a,b)R1(b,c)R2.}

如果C[c1]C[c2]是全函数,那么则是函数复合。下面是if命令和while命令的定义:

C[ifbthenc1elsec2]={(σ,σ)|(σ,true)B[b](σ,σ)C[c1]}{(σ,σ)|(σ,false)B[b](σ,σ)C[c2]}C[whilebdoc]={(σ,σ)|(σ,false)B[b]}{(σ,σ)|(σ,true)B[b]σ.((σ,σ)C[c](σ,σ)C[whilebdoc])}

但是只要你仔细一看,就会发现while的定义是不对的,它在自己的定义中用到了自己,这就不是一个定义了,而是一个递归等式,解决这个问题的方法就是不动点。

不动点(Fixed points)

为了解决while定义的问题,我们必须找出符合递归等式的函数。为了理解这个问题,我们先理解下面这个例子,给定函数f:NN:

f(x)={0,if x=0f(x1)+2x1otherwise

这不是f的定义,而是f满足的等式,而且这里似乎只有一个函数f满足这个等式:f(x)=x2,这个函数f就是不动点。通常来说,给定一个递归等式,不一定有解(g:NN,g(x)=g(x)+1就没解)。

利用逐步渐近法,我们可以计算出不动点。每次计算就越来越逼近最终的答案。为了求出满足递归等式的f,首先给定函数f0=f0是空关系,一个定义域为空的偏函数),然后用逐步渐进法逐步计算:

f0=f1={0,if x=0f0(x1)+2x1otherwise={(0,0)}f2={0,if x=0f1(x1)+2x1otherwise={(0,0),(1,1)}f3={0,if x=0f2(x1)+2x1otherwise={(0,0),(1,1),(2,4)}

fi逐步呈现出f(x)=x2

但是数学中没有‘显然’,接下来就是给逐步渐进法建立一个数学模型:一个高阶函数F,F接受一个fk,输出下一个fk+1:

F:(NN)(NN)

其中 (F(f))(x)={0,if x=0f(x1)+2x1otherwise

上述等式的一个解就是f=F(f)。通常来说,给定函数F:AA,如果aA,F(a)=a,那么a就是F的不动点。于是,F的所有不动点的集合就是递归等式最终的解:

f=fix(F)=f0f1f2f3=F()F(F())F(F(F()))=i0Fi()

Kleene不动点定理与IMP

让我们利用不动点重新定义IMP的while语义:

C[whilebdoc]=fix(F)F(f)={(σ,σ)|(σ,false)B[b]}{(σ,σ)|(σ,true)B[b]σ.(σ,σ)C[c](σ,σ)f}

但是认真的读者肯定心中已经有个疑问了:“你怎么确定不动点存在?”,是的,就像上一节所说的那样,递归等式不一定有解,不动点不一定存在。这一节就要利用Kleen不动点定理证明while命令的语义是存在不动点的。

Kleene不动点定理

Definition(Scott).f:Cl(X)Cl(Y)xCl(X),bf(x),x0finx,bf(x0) fabf(a)f(b)fScott Definition().Dx,xD,zD使xD,xDD

bf(x),意思是给定x,其中x可能是由无限的元素组成的,函数接受x输出一个b;函数是连续的意思就是,其实不需要所有的x,只需要x的一个有限的子集就能同样达到输出b的效果。这就是连续的本质。为了得到函数包含有限信息的输出,只要包含有限信息的输入。有限信息的输入的每个元素的有限渐进的总和组成了输出,其实输入从有限到无限时的输出,没有一个神奇的跳跃,你在无限得到的b,其实在有限的某个时候就已经得到了。如下是证明,首先根据上文的解释翻译一下Scott连续的定义,其中D是有向集合:

f(xDx)=xDf(x)

proof. 如要证明A=B,可以先证明AB,再证明BA

Theorem(Kleene).ScottffiFi()

proof. 设X=iFi(),首先证明XF的不动点–F(X)=X

F(X)=F(iFi())By definition of X=iF(Fi())By Scott continuity=iFi+1()=iFi+1()=F0()iFi+1()=iFi()=X

然后,我们证明XF的最小不动点。假设YF的另一个不动点,我们要证明的是,对所有得i,Fi()Y都成立。当i是0的时候,F0()=Y;对剩下的,我们先假设有Fi()Y,由单调性可知,所以F(Fi+1())F(Y),因为Y是一个不动点,所以F(Y)=Y,然后Fi+1()Y。最后,所有的元素都是Y的子集:

F0F1

所以所有元素的并集X也属于YX=iFi()Y,故XF的最小不动点。得证。

指称语义的论证

相比操作语义,指称语义的一大好处就是,在论证等价问题的时候,只需要看程序指称的计算结果就行了,而操作语义则必须把抽象机器底层变换、衍生出的都论证清楚。

比如,要证明skip;cc;skip是等价的,计算如下:

C[skip;c]={(σ,σ)|σ.(σ,σ)C[skip](σ,σ)C[c]}={(σ,σ)|(σ,σ)C[c]}={(σ,σ)|σ.(σ,σ)C[c](σ,σ)C[skip]}=C[c;skip]

利用偏函数,映射关系和集合,证明变得容易多了。再举个例子C[whilefalsedoc],根据定义,只需证fix(F),其中F为:

F(f)={(σ,σ)|(σ,false)B[b]}{(σ,σ)|σ.(σ,true)B[b](σ,σ)C[c](σ,σ)f}

由Kleene不动点定理可得fixF=iFi(),因为对所有的i,都有Fi()={(σ,σ)},所以fixF={(σ,σ)},即C[skip]

公理语义

什么是公理语义

公理语义用逻辑规则来定义程序(操作语义模型展示程序如何运行,指称语义模型展示程序计算什么)。公理语义最初由Floyd和Hoare提出,后来由Dijkstra和Gries进一步发展。

公理语义通常用前置条件和后置条件来描述程序规则:

{Pre}c{Post}

其中c是程序,Pre和Post是描述程序状态的公式,通常称之为断言;这种三元逻辑又称之为部分正确规则,或者霍尔三元组,其意思如下:如果在运行c前Pre成立,而且c是能终止的,那么Post在c终止后也成立。也可以这样说,给定满足Pre的store σ,运行c并终止后,Post在输出的store σ中也成立。

前置条件和后置条件可以看成程序和用户的接口、约定,用户只需关系程序运行的结果,而不用理解程序是怎么运行的。通常,为了使程序更好维护,程序员用写注释的方法给方法、函数添加文档,特别是对那些闭源的库来说,这就是库使用者和库开发者之间的约定。

但是,谁也不能保证写在注释里的前置条件和后置条件是正确的,注释描述的是开发者的意图,而不能保证程序的正确性。公理语义解决的就是这个问题。

公理语义展示了如何描述部分正确语句以及用论证证明程序的程序性。但是,部分正确规则不能保证程序会终止,这也是它叫‘部分’的原因,而完全正确规则保证了程序在满足前置条件时一定会终止,我们用方括号表示完全正确规则:

[Pre]c[Post]

其意思:如果在执行c之前满足Pre,那么c会终止并且终止后满足Post。

大体上,在霍尔三元组中,前置条件指明了在运行程序前的需求,后置条件指明了程序的期望结果(如果程序会终止)。举个例子:

{foo=0bar=i}baz:=0;whilefoobardo(baz:=baz2;foo:=foo+1){baz=2i}

表明了如果有个store中有foo和bar,分别映射到0和i,那么如果程序终止,那么最终的store中应该包含baz到-2i的映射。注意其中i只是个逻辑需要的变量,程序中并没有i,它的作用只是表述bar的初始值,通常称这种变量为‘伪变量’(ghost variables)。

例子中的部分正确语句是正确的:给定任何满足σ(foo)=0的storeσ,和

C[baz:=0;whilefoobardo(baz:=baz2;foo:=foo+1)]σ=σ

σ(baz)=2σ(bar)。注意这只是个部分正确语句,只有程序能终止时才成立,有些初始store不能让程序终止。但下面的完全正确语句是成立的:

[bar=0bar=ii0]baz:=0;whilefoobardo(baz:=baz2;foo:=foo+1)[baz=2i].

表明了如果有个包含foo到0、bar到正整数映射的storeσ,那么程序会终止,并输出最终storeσσ(baz)=2σ(bar)

我们所讲的公理语义会主要集中在部分正确断言。

断言

怎么表述前置条件或者后置条件?上面的例子中,我们已经用过了程序变量、逻辑相等、逻辑变量、逻辑合取()。能用什么直接影响到部分正确语句描述出的程序属性,对于IMP,我们用算术表达式之间的比较、逻辑操作符和量词(全部量词和部分量词)来表示断言:

i,jLVaraAxep::=x|i|n|a1+a2|a1P,QAssn::=true|false|a1<a2|P1P2|P11P2|P1P2|¬P|i.P|i.P

为了表示什么是“断言P在storeσ中成立”,我们还要定义一些新东西,因为除了store,我们还需要知道逻辑变量的值,首先我们定义一个逻辑变量到整数的映射I:

I:LVarInt

接着定义方法Ai[a],它是逻辑变量的指称语义:

Ai[n](σ,I)=nAi[x](σ,I)=σ(x)Ai[i](σ,I)=I(i)Ai[a1+a2](σ,I)=Ai[a1](σ,I)+Ai[a2](σ,I)

现在我们能表示断言的正确性了,σIP,意为,在I解释下,P在storeσ中成立:

σItrue(always)σIa1<a2ifAi[a1](σ,I)<Ai[a2](σ,I)σIP1P2ifσIP1andσIP2σIP1P2ifσIP1orσIP2σIP1P2ifsIP1orσIP2σI¬PifsIPσIi.PifkInt.σI[ik]PσIi.PifkInt.σI[ik]P

定义了断言的正确性,现在我们就可以定义一个部分正确语句的正确性了:

σ.ifσIPandC[c]σ=σthenσIQ

当一个霍尔三元组是合理的(写作{P}c{Q}),那它在所有的store和解释中都是正确的:σ,I.σI{P}c{Q}

现在我们知道当我们说断言P成立、部分正确语句{P}c{Q}成立是什么意思了。

霍尔逻辑

利用公理和推论,我们可以直接推导出合理的部分正确语句,而不用关心store或者程序,这些就叫做霍尔规则,由霍尔规则组成的证明系统就是霍尔逻辑:

SKIP{P}skip{P}ASSGN{P[a/x]}x:=a{P} SEQ{P}c1{R}{R}c2{Q}{P}c1;c2{Q}IF{Pb}c1{Q}{P¬b}c2{Q}{P}ifbthenc1elsec2{Q} WHILE{Pb}c{P}{P}whilebdoc{P¬b} CONSEQUENCE(PP){P}c{Q}(QQ){P}c{Q}

while中的断言P是个循环不变式,它既是前置条件又是后置条件,它在循环前后都成立,这一点在while规则的结论中也体现出来了。consequence规则加强了前置条件,弱化了后置条件。

这些霍尔逻辑组成了部分正确语句的归纳定义。当我们能为{P}c{Q}构建一个证明树,那么就说它是霍尔逻辑的一个公理,写成{P}c{Q}

soundness和completeness

现在我们已经有两种断言:

这两者之间有什么关系呢?首先,第一个问题,是不是每个霍尔逻辑公理都是合理的部分正确三元组呢({P}c{Q}{P}c{Q})?答案是肯定的,霍尔逻辑是sound的,这一点很重要,这意味着我们不能推导出不成立的部分正确三元组。

第二个问题,对每个合理的断言,我们都能构造出一个霍尔逻辑吗({P}c{Q}{P}c{Q})?这个答案也是肯定的,这就是霍尔逻辑相对完整性,由Cook在1974年证明。

例子:阶乘

{x=n ∧ n>0}
y:=1;
while x>0 do {
    y:=y*x;
    x:=x-1;
}
{y=n!}

我们会用霍尔逻辑证明上面是计算n的阶乘的程序。

要证明上面的程序,因为这是一个包含一个赋值语句和一个while语句的程序,就要利用SEQ规则,于是我们要证明下面的两个三元组:

{x=nn>0}y:=1{I}{I}whilex>0do{y:=yx;x:=x1}{y=n!}

但要想利用SEQ规则,要先满足SEQ规则的分子(前提条件),也就是要先找到满足上面两个三元组的I。首先I需要满足在循环前后都满足,我们已经说过,I需要是个循环不变量。只看循环体可知(不要看while的条件,因为我们要找的是循环前后都满足的断言),y的值是先乘x的初始值n,再乘下一个x,也就是n-1,然后一步步累乘的结果:

y=n(n1)(x+1)

两边同时乘x!,得到x!*y=n!,当然其中的x是正整数。于是我们得到I

I=x!y=n!x0

要证明I是循环不变量,即必须满足while规则的前提:

{Ix>0}y:=yx;x:=x1{I}

要证明上式,我们倒着走一遍:

{(x1)!y=n!(x1)0}x:=x1{I} {(x1)!yx=n!(x1)0}y:=yx{(x1)!y=n!(x1)0}

又因为Ix>0(x1)!yx=n!(x1)0,再由CONSEQUENCE规则,可得上式。现在满足了while的前提条件,终于可以使用while规则了,得到:

{I}whilex>0do{y:=yx;x:=x1}{Ix0}

剩下的只需证明Ix0y=n!

x!y=n!x0x0y=n!

由CONSEQUENCE规则可得第二个三元组。

下面,证前一个三元组。首先,利用不需要前提条件的赋值规则:{I[1/y]}y:=1{I},展开得到:

{x!1=n!x0}y:=1{x!y=n!x0}

又因为x=nn>0x!1=n!x0,再由CONSEQUENCE规则可得第一个三元组。

小结

到这里就告一段落了,写了不少,基本介绍了语义,以及证明语义属性的方法。