【Coq学习】Formal Reasoning About Programs 阅读笔记第六章

文章介绍了如何使用变迁系统和不变量来形式化证明程序的正确性,特别是针对无限状态系统。通过阶乘计算的例子展示了状态机的建模,并定义了不变量的概念,强调不变量在证明程序行为中的重要性。文章还探讨了并发程序的模型,以及如何处理并发中的状态不确定性,提出了用于并发程序正确性验证的不变量方法。
摘要由CSDN通过智能技术生成

第六章 变迁系统和不变量 Transition Systems and Invariants

对于程序总是终止的简单编程语言,使用解释器(interpreters)将它们形式化通常是最方便的,如第 4 章所述。然而,许多重要的语言不属于该类别,对于它们我们需要不同的技术。非终止并不总是一个错误。例如,我们希望网络服务器可以无限期地运行。我们仍然需要能够根据设计讨论永远运行的程序的正确行为。出于这个原因,在本章和本书其余大部分内容中,我们使用关系对程序进行建模,其方式与自动机理论(automata theory)中熟悉的方式大致相同。不过,一个重要的区别是,虽然本科自动机理论课程通常研究有限状态机(finite-state machines),但对于一般程序推理,我们希望允许无限状态集(infinite sets of states),或者称为无限状态系统(infinite-state systems)

让我们从一个几乎看起来太平凡而无法与这些术语相关联的例子开始。

6.1 阶乘作为状态机 Factorial as a State Machine

下面是我们熟悉的阶乘运算( factorial operation ),用带有循环的命令式程序实现的。

在接下来的分析中,考虑某个自然数常量n0,作为传递给该操作的输入。状态机隐藏在程序的表层语法中。在将上述程序建模为状态机时,我们有多种选择。下面是我们选择在这里使用的状态集:

有两种状态:一个AnswerIs(a) 状态对应于return 语句,它记录了阶乘运算的最终结果a。 WithAccumulator(n, a) 记录一个中间状态,它在循环迭代开始之前给出两个局部变量的值。


接下来为状态机定义一组初始状态(initial states),也就是从上面代码的第一行读取初始变量值。可以使用推理规则定义集合F0:

 也可以直接写成:


类似地,还要定义一组最终状态( final states),这个定义仅在程序完成时捕获,而不是在返回正确答案时捕获,因为它来自代码的最后的return语句。

 或者:


 接下来是定义状态机的变迁关系(transition relation),把状态s前进到状态s'记为:

 用推理规则表示的变迁关系定义如下:第一条规则对应程序结束的情况,如果当前状态的n为0,那么程序应该退出循环返回结果a的值;第二条规则对应于执行一次循环体中的代码,如果当前的两个局部变量分别为n+1和a,下一步状态的两个变量值应该分别为n和a*(n+1)。


由于 “状态机(state machine)”一词一般表示它的状态集必须是有限的,因此这本书后面都用“变迁系统(transition system)”这个术语来表示这种状态机。

定义6.1:一个变迁系统是由一个三元组<S, S0, ->>组成,S表示状态集合,S0包含于S是初始状态集合,变迁关系->包含于S到S的映射。

 对于任意的变迁关系->,而不仅仅是上面为阶乘定义的关系,我们使用两个推理规则定义它的传递自反闭包(transitive-reflexive closure)关系 ->*:

 也就是说,s ->* s' 表示从状态s开始可以到达状态s'。

补充:

Rel: 关系的性质


定义6.2:对于一个变迁系统,我们说一个状态s可达,当且仅当存在属于S0的一个初始状态s0,从s0出发可以到达状态s。

有了以上的符号定义,现在我们可以将阶乘的程序建模为:

定理6.3:对于F里面的任意可达状态s,如果s属于最终状态集Fw,那么状态s就等于最终状态Answerls(n0)(n0是输入变量的初始值)。也就是每当程序结束时,它都会返回正确的答案。

 目前我们可以用一种相对特殊的方式来证明这个定理。但是,让我们先去学习一下不变量(invariants)的一般机制。

6.2 不变量 Invariants

补充:

一个变迁系统的不变量是指在系统的所有状态转移中保持不变的性质或关系。不变量可以为我们提供系统行为的重要信息和洞见,因为它们定义了系统的一些基本特征,并且在系统变化时保持恒定。

在形式化方法中,我们通常使用不变量来检查系统的正确性。如果我们能够证明在所有状态转移中不变量都保持不变,则我们可以得出结论,系统在这些方面是正确的。

例如,对于一个简单的计数器转移系统,我们可能会定义一个表示计数器值长度的不变量,该值在所有状态转移中都保持不变。在 Coq 中,我们可以使用归纳证明来证明这种类型的不变量。具体而言,我们可以使用归纳基础步骤证明这个性质在初始状态下成立,并使用归纳步骤证明这个性质在每个状态转移后都保持不变

在形式化方法中,不变量是形式化证明的重要技术。它们可以用来证明系统的正确性,并发现系统行为的一些基本特征。

不变量是程序状态的一个属性,它开始为真,然后保持为真(starts true and stays true)。下面是它的定义:

定义6.4:变迁系统的不变量是在系统的所有可达状态下始终为真的属性。也就是说,对于变迁系统<S, S0, ->>,其中R是其所有可达状态的集合,一些包含于S的I是一个不变量当且仅当R也包含于I。

(注意,这里我们采用了状态的“属性”(“properties”of states)和状态的“集合”(“sets” of states)是同义词的数学惯例,因此在每种情况下我们都可以使用看起来最自然的术语。“属性”保持成立的状态正是那些属于“集合”的状态。The “property” holds of exactly those states that belong to the “set.” 也就是把不变量看作是不变量保持成立的状态的集合


通常更容易描述一个不完全精确的不变量,承认系统永远无法真正达到的某些状态。另外,用归纳法,也就是下一个关键定理的形式化方法,可以更容易地证明一个近似不变量的存在性。

定理6.5:考虑一个变迁系统<S,S0,->>和它的候选不变量I,当以下两个条件成立的时候,I就真的是不变量:(1)初始状态S0包含于I;(2)对于I里面的每一个状态s,如果有s'是s的后继状态,那么s'也属于I。


看具体的例子,接下来让我们为阶乘定义一个合适的不变量:

 

我们可以使用定理6.5证明上面说的I确实是一个不变量:

 补充:

我们需要的关键新方法是反演inversion,这是一种用于推断哪些推理规则可能被用于证明事实的原则。

例如,在证明中的某一点,我们需要从前提s属于F0中得出结论,这意味着s是一个初始状态。通过inversion,因为集合F0是由一个单一的推理规则定义的,该规则必须被用于从前提推导出结论,因此必须满足s = WithAccumulator(n0, 1)。

类似地,在证明的另一点上,我们必须对一个前提s->s'进行推理。变迁关系->是由两条推理规则定义的,因此inversion引导我们考虑两种情况。在第一种情况下,对应于第一条规则,s= WithAccumulator(0, a) 以及 s'=AnswerIs(a)。在第二种情况下,对应于第二条规则,s= WithAccumulator(n + 1, a) 以及 s'= WithAccumulator(n, a*(n+1))。这些s和s'的值是直接从规则中读取的。

虽然对inversion的完全正式和详尽的处理超出了本文的范围,但通常它遵循关于“逆向工程”的标准直觉:一组可以用来推导某些前提的规则。


不变量的另一个重要性质形式化了与削弱weakening一个归纳假设的联系。

定理6.6:如果I是一个变迁系统的一个不变量,那么I'包含I(I'是原始集合I的一个超集)也是这个系统的一个不变量。

注意,上面较大的I'可能不适用于定理6.5的归纳证明! 例如,对于阶乘,我们可以定义I' =  {AnswerIs(n0!)}∪{WithAccumulator(n, a) | n, a ∈ N},它显然是I的超集。然而,如果忘记了我们所知道的关于中间状态WithAccumulator的一切,我们将困在证明的归纳步骤。因此,我们在这里所说的不变量不一定也是归纳不变量(inductive invariants),可能与其他来源有轻微的术语不匹配。

结合定理6.5和6.6,现在容易证明定理6.3,建立我们特定的阶乘系统F的正确性。首先,我们使用定理6.5推断出I是F的一个不变量。然后,我们选择上面说的不是一个归纳不变量的I'作为I的超集。因此,由定理6.6,I'也是F的一个不变量;定理6.3可以直接从这一事实得出,因为I'本质上是定理6.3的重新表述。

6.3 应用的规则归纳 Rule Induction Applied

假设我们想证明某种关系P适用于所有的状态对,其中第一个状态可以达到第二个状态。也就是我们想证明:

那么我们首先推导出它的归纳法原理。我们修改关系->*的每条定义规则,将其结论替换为使用P,并为每个递归的前提添加P的归纳假设。

例如考虑证明可达状态的传递性:

我们对第一个前提trc R x y进行归纳证明。按照trc的定义进行归纳:

trc R x y分两种情况:一种是等于trc R x x,在此证明中相当于y=x,因此将y=x代入后得第一个证明子目标:trc R x z -> trc R x z。另一种情况是trc R x y等于trc R x z,在此证明中相当于y=z,那证明里原本存在的z就要更名为z0,因此将y=z代入后得第二个证明子目标:trc R z z0 -> trc R x z0

 对于第一个证明子目标trc R x z -> trc R x z,利用化简策略可将前提引入为H,子目标变成trc R x z

 此时利用assumption策略,可将目标与前提H进行匹配,第一个子目标得证。

对于第二个子目标trc R z z0 -> trc R x z0,根据结构归纳原理的第二条规则以及trc的定义,需要引入两个前提和一个归纳假设,即引入前提H:R x y、前提H0:trc R y z以及与H0相关的表示传递性的归纳假设IHtrc: trc R z z0 -> trc R y z0  。(我没看懂IHtrc为什么是trc R z z0 -> trc R y z0 )

 通过化简策略,将子目标中的前提引入为H1,因此子目标变成trc R x z0

 利用eapply策略应用trc定义的第二条规则TrcFront与目标trc R x z0进行匹配,匹配时z=z0,其余的未知变量用?y表示,因此匹配成功后产生两个子目标,分别对应于TrcFront的两个前提,一个是R x ?y,另一个是trc R ?y z0

 对于第一个子目标,应用eassumption策略可与前提H匹配,匹配时?y被实例化为y,因此第一个子目标得证,第二个子目标的?y也同步的替换为y,第二个子目标变成trc R y z0

 应用apply策略将目标于归纳假设IHtrc的结论进行匹配,证明目标变成IHtrc的前提,也就是trc R z z0,最后一步应用assumption策略将目标与前提H1完美匹配,整个证明得证。

6.4 一个并发程序的例子 An Example with a Concurrent Program

假设我们想要验证一个多线程的共享内存程序,其中多个线程同时运行这段代码:

 将global视为所有线程共享的变量,而每个线程都有自己版本的变量local。一次只有一个线程可以持有锁,通过lock()声明它,并通过unlock()释放它。当变量global初始化为0时,n个线程同时运行这段代码并全部终止后,我们期望global的值为n。当然,这个程序中可能引发错误,比如忘记上锁,可能会导致各种错误的答案,在1到n之间的任何值都可能。


为了证明上面的程序是正确的,让我们把它形式化为一个变迁系统。

首先,状态集定义如下:

这里各种状态对应于命令式代码中的程序计数器( program counters )。前四种状态分别表示程序计数器位于程序代码中匹配行的前面。最后一种状态类型表示程序计数器超过了函数的末尾。只有写状态携带额外的信息,在本例中是变量local的值。在其他的程序计数器上,我们可以证明变量local的值对进一步的转换没有影响,所以我们不需要存储它。我们将单独说明变量global的值,稍后将对其进行描述。

 

单个线程的变迁系统定义为:

 我们定义状态包括包括global的值、表示当前是否使用锁的布尔值以及线程的本地状态 P。

 

下面是指定的初始状态

 

下面四个推理规则解释了单个线程可以在程序计数器之间进行的四种变迁关系,根据需要读取和写入共享状态。(对应4行代码的执行过程,Lock->Read->Write->Unlock->Done)

 注意,这些规则将允许线程在不持有锁的情况下读写共享状态。规则还允许任何线程释放锁,而不考虑该线程是否必须是当前锁的持有者。我们必须使用一个基于不变量的证明来证明,事实上,没有隐藏的违反基于锁的并发规则的行为。

 因此,单线程的变迁系统定义为:

 

 


 

如果只有一个线程在运行,当然不会有任何的违规行为!然而,我们一直小心地以通用方式描述系统 L,其状态为一对共享组件和私有组件。我们可以定义一个多线程系统的通用概念,两个系统共享一些状态并维护自己的私有状态。

定义6.8:假设T1和T2是两个变迁系统

它们的状态集包含了一个共同的共享状态集S,并且共享状态的初始值都是S0,×是笛卡尔积。我们定义两个系统的并行组合(parallel composition) T1 | T2为:

 

用下面的推理规则定义新的转换关系->,包含两种情况:第一个线程先运行,改变的就是shared的部分和第一个线程的私有状态;第二个线程先运行,改变的就是shared的部分和第二个线程的私有状态。

 

因此,两个线程的变迁系统定义为:

 

注意,运算符|是经过仔细定义的,因此它的输出适合作为自身的另一个实例的输入。L | L是一个模拟两个线程运行上面代码的变迁系统,也可以有L | ( L | L ) 作为基于那段代码的三线程系统,( L | L ) | ( L | L ) 作为基于那段代码的四线程系统,等等。

还要注意|用我们在变迁关系中的第一个非确定性(non-determinism)例子构建了变迁系统。也就是说,给定一个特定的起始状态,在给定数量的执行步骤之后,它可能会到达多个不同的位置。一般来说,在线程交错并发的情况下,可能的最终状态集在步骤数量上呈指数增长,这一事实让并发的软件测试人员痛苦不已!我们将使用一个不变量来降低复杂性,而不是考虑程序的所有可能运行。


首先,我们应该清楚我们想要证明这个程序的意义。在本节的剩余部分,让我们将注意力限制在两个线程的情况上;n线程的情况留给读者作为练习!

定理6.9:对于两个线程的变迁系统L | L 其中任意的可达状态((g, l),(p1, p2)),如果p1=p2=Done,那么g=2。

 也就是说,当两个线程都终止时,global的值等于2。

作为实现一个不变量的第一步,将函数C的功能定义把私有状态转换为数字,捕获一个线程在某个状态下的贡献,也就是总结该线程已将全局变量的值加了多少:

 

 接下来,我们定义一个函数,给定线程的私有状态,该函数确定该线程是否持有锁:

 

下面是最主要的设计:函数S实现从两个私有状态中得出共享状态:

 

最后一个帮助我们编写不变量的要素是一个谓词O(p, p'),它捕捉在给定一个线程的状态p的情况下,另一个线程的状态p'何时与p的状态的所有含义兼容,主要是在锁的互斥方面:

 

最终,不变量的定义如下:

 

通常情况下,定义不变量是证明的困难部分,其余部分遵循我们用于阶乘程序的标准方法。

回顾一下这个方法,首先我们用定理6.5证明I确实是L | L的一个不变量。(证明过程非常长……)

 

作者也提供了以上证明的自动化证明版本,简短很多了。 

接下来,我们使用定理6.6来表明I蕴含了感兴趣的原始属性,即结束的程序状态的global的值为2。

 

大多数操作都在第一步中,我们必须通过所有不同步骤的繁琐细节,在每个情况下使用算术推理来推导一个矛盾(从这个起始状态不可能到该步骤)或表明一个特定的新状态也属于不变量。像往常一样,我们将这些实现细节留在对应的Coq代码里。

读者此时可能会担心提出不变量会相当乏味!在下一章中,我们会遇到一种在某些有限但重要的情况下自动寻找不变量的技术。

第六章到此结束。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值