蒙提霍尔C语言程序_科普 - 程序验证(2)- 不变式

本文深入探讨程序验证的基础知识,重点介绍了循环不变式和可满足性模理论(SMT)。通过C语言子集示例,解释了程序语法、谓词逻辑与SMT的基本概念,并详细阐述了循环不变式的可达性、归纳性和可证明性三个关键性质。此外,文章讨论了SMT在程序验证中的应用及其挑战,并鼓励读者思考如何为示例程序找到合适的循环不变式。
摘要由CSDN通过智能技术生成

前言

在上一篇文章中,我们对程序验证有了比较直观浅显的了解。 本篇文章将介绍更多的基础性知识,进一步加深了解。 本文的核心在于对循环不变式(Loop Invariant)的介绍,以及对可满足性模理论(Satisfiability Module Theory,SMT)的一些直观解释。

基础知识

程序语法

为了方便讨论,我们取C语言的子集(Subset),作为验证的对象语言。 为此,我们对C语言做以下限制:

  • 仅允许int类型的变量,以及固定长度的数组;
  • 允许形如v = expra[i] = expr的赋值语句,一般if分支,以及while循环;
  • 允许函数调用;

如下的线性搜索函数,满足以上要求,故可以作为我们的验证对象。 在往后的文章中,我们将尝试对该程序的功能正确性进行验证。

int LinearSearch(int[] a, int length, int key} {
  int i = 0; 
  int res = -1;
  while (res == -1 && i < length) 
  {
    if (a[i] == key) { res = i; }
    else { i = i + 1; }
  } 
  return res;
}

谓词逻辑与可满足性模理论

倘若本科期间学过《离散数学》课程,对于谓词逻辑(Predicate Logic,又称一阶逻辑,First Order Logic,FOL)肯定不会陌生。 谓词逻辑公式包含量词(Quantifier,如

),
逻辑运算符(Logical Operator,或称 逻辑连接词,Logical Connective,如
),
变量(Variable,如
),以及
谓词(Predicate,如
)和
函数(Function,如
)。

对一般的谓词逻辑而言,给定的谓词

没有解释的(Uninterpreted)。 而 判定谓词逻辑的 可满足性(Satisfiability)时,我们需要构造出谓词的 解释(Interpretation),以及在该解释下,使得谓词公式的值为 (True,即 可满足)的一组 变量赋值。 例如,谓词逻辑公式
,我们可以判定它是
可满足的(Satisfiable),其中一个可满足的 解释是: 谓词
表示
等值,而变量
可以分为整数

事实上,在判定谓词逻辑公式的可满足性时,构造谓词的解释是一件非常麻烦的事情。 所以,在大多数情况下,我们会在给出公式的同时,将其中的谓词固定解释为某种含义。 例如,我们可以将谓词

预先解释为
的值大于
的值, 同时,也可以给这种常用的
特殊谓词一个特殊的记号,例如将
记为
。 通常,我们可以称解释固定的谓词为
谓词常项(Constant Predicate)。

关于谓词逻辑的更多内容,在此我们并不展开,仅用几个例子来演示与验证相关的谓词逻辑。 有需要的话,读者可以寻找其他网络材料或教材自行学习。

  1. 长度为length的数组a为升序排列:
  2. 长度为length的数组a中存在值为key的元素:
  3. 加法交换律:
  4. 一般算术:
    永假式,不可满足)
  5. 基本算数性质:

可满足性模理论(Satisfiability Module Theory,SMT)是指在某些理论片段(Theory Fragment)下(即限定使用某些谓词常项以及函数),对给定一阶逻辑公式可满足性的判断。 这里“模”的意义和数论中的“模运算”类似,即在限制的理论下讨论公式的可满足性。 例如,以上1,2两个公式,属于数组理论(Array Theory),定义了基本算术运算(如

)以及数组的读写(如
)。 而3,4,5这三个公式,则属于线性算术理论(Linear Arithmetic Theory),定义了比较以及加减法等运算(如
)。 在讨论SMT公式的可满足性时,
一般不需要对谓词和函数进行解释。 因为谓词和函数的解释一般都在理论中确定了,比如线性算术理论中,加法函数(
)被确定为返回两个参数之和; 再比如在数组理论中,数组读取和写入函数(
)也都已有对应的解释。 当然,也有特例,在
非解释性函数理论(Uninterpreted Function Theory)中,允许定义未被解释的函数,而 可满足解将返回函数解释的构造。

通常,我们说在某些理论下,某个公式是有效的(Valid),当且仅当在任意的赋值下,该公式都为真。 而我们说某个公式是可满足的(Satisfiable),当且仅当该公式存在至少一组成真赋值。

同时,有一点非常重要,即可满足的对偶性(Duality Property)。 令

为一个一阶逻辑公式。
是有效的,当且仅当
是不可满足的。 例如,想要证明公式5的有效性,我们可以利用对偶性,证明
是不可满足的。 也就是证明
是不可满足的。 那么,我们对公式进行代入消去,可得
。 这个公式直接等值于
,故是不可满足的。 此外,相对而言,求解带
全称量词
)的SMT公式,比求解不含全称量词的公式要难得多。 所以,使用对偶性做适当的转换,也能显著提升公式的求解效率。

一般来说,在使用SMT时,我们会假定存在一个SMT求解器。其输入为一个在某些特定理论片段下的一阶逻辑公式(SMT公式),并输出该公式的可满足性。 倘若公式可满足,还将输出一组令公式可满足的解释。 比如例子中第1个公式是可满足的,可满足的解释可以为length = 3a = [1, 2, 3]; 第2个公式也是可满足的,可满足解释为key = 3a = [1, 3]; 第3个公式是有效的,即永真的,我们可以用对偶性来证明;第4个公式则是不可满足的,即不存在可满足的解。

循环不变式

在上一篇文章中,我们对循环不变式(Loop Invariant)已经有所接触。 我们花了不少的篇幅来解释循环不变式的含义,以及为什么可以用循环不变式来处理循环。 在这一小结,我们将用比较形式化(Formal)的方式,来定义和解释循环不变式。 并对程序验证中的上近似(Over-approximation,也称过近似)思想进行简单阐述。

首先,循环不变式有两条必须满足的性质,即可达性以及归纳性。 其次,在程序验证中,我们一般还要求循环不变式可证明我们需要验证的属性。 我们将使用以下的例子,结合形式化的定义,来介绍这些性质。

int i = 0;
while(i < 10) { // Inv(i) : 0 <= i <= 10
  i = i + 1;
}
assert(i == 10);

为了形式化地定义循环不变式,我们先做以下定义:

  • 来表示
    程序变量的值,
    可以被理解成一个由程序中所有变量组成的
    元组(Tuple),或者向量(Vector);
  • 表示
    循环前的代码对程序变量值的约束;
  • 表示
    循环不变式对程序变量值的约束;
  • (Guard)表表示
    循环条件对程序变量值的约束;
  • 表示
    循环体代码对程序变量值的修改,其中
    表示被循环体
    修改后的程序变量的值;
  • 表示
    循环后的代码对程序变量值的约束,一般为所需要验证的属性对程序变量值的要求。

在一行的例子中,这些定义分别为:

  • ,程序变量仅有
    i默认为整型变量
  • ,循环前的代码将变量
    i的值约束为0
  • ,循环不变式要求
    i010之间;
  • ,循环条件要求
    i的值小于10
  • ,表示循环体对变量的值
    i执行完循环体代码后的值i'之间的约束关系;
  • ,断言属性对变量
    i的值的要求。

为了便于理解以下形式化定义中的公式,我们给读者提供一条建议:

使用集合的维恩图(Venn Diagram)来理解蕴含关系。 我们可以把每个谓词都看作一个集合

即表示令
为真的所有
所形成的集合。 那么
可以被理解为
,即
的集合表示
被包含
的集合表示中。

有了以上的诸多比较形式化的定义,我们开始定义循环不变式所要满足的性质

1) 可达性(Reachable)

可达性这个名字的描述有些不准确。 这条性质实质是要求,循环不变式表示的状态集合

应该包含,所有经过循环头前的代码所能到达的状态,所形成的集合,即状态集合

在这个例子中,循环头前的代码形成的约束为

。 这就意味着,不变式应该包含所有满足
的状态。 实事上,
包含
,即
,所以可达性成立。

2) 归纳性(Inductive)

归纳性是循环不变式最重要的性质,它要求任何被包含在循环不变式中的状态,经过循环后,得到的新状态,仍然落在循环不变式中。 归纳性保证了循环不变式的不变性成立,即在循环的任意多次执行后,循环不变式始终成立。

在这个例子中,循环不变式

对应的状态集合为
。 我们取其中的一个元素
,即
,它满足循环条件
,经过循环后,得到的新状态为
,仍然被包含在循环不变式中。 同时,我们要注意,循环不变式中的状态
不满足循环条件,所以整个蕴含式的前提为假,蕴含式仍然成立。

我们再用逻辑公式来说明这个例子中的归纳性,式子如下:

经过代入和化简后可得以下的式子,显然是有效的,所以归纳性也满足。

注意,若

满足了可达性和归纳性,那么它就已经是一个循环不变式了,如本例中
是一个不变式。 但是将循环不变式
用于程序验证时,我们还要求它能够用于 证明属性成立
3) 可证明性(Provable)

可证明性质要求,在循环不变式中的状态,满足循环退出的条件后,需要满足我们要验证的性质。

事实上,我们可以把验证属性成立也看成是证明集合之间的包含关系。我 们可以用谓词

来表示程序运行到
属性断言前的状态集合,用
来表示
属性断言所约束的状态集合。 那么,我们验证属性成立,实际上是要证明
。 由于
难以准确描述,所以我们试图寻找一个循环不变式作为 中介,使得
。 如此,我们便
间接地验证了属性成立。 这就是我们在这一小节开始所说的 上近似思想。 我们寻找一个 便于表示的更大的集合(不变式)来 近似原本的集合(程序状态),在更大的集合上 证明属性成立,从而 间接证明在原本的集合上属性也成立。 这一思想,对于程序验证而言, 至关重要

以下便是不变式可证明性质的逻辑公式表述:

在这个例子中,可证明性质用以下公式表达:

这个式子显然也是有效的,所以我们寻找的不变式

能够用于证明我们所要求的属性。

小结

本篇原本还打算介绍程序语义中的一种公理语义,即霍尔逻辑(Floyd-Hoare Logic),但是感觉篇幅太长不太好,所以把这部分留到下一篇。 在本篇中,我们直观地对可满足性理论(SMT)有了更深入的了解,同时对于循环不变式也有了比较形式化的认识。 在此,多谈几点,供大家参考:

  1. 基于SMT求解的程序验证,实际上将逻辑推理验证算法进行的解耦合,研究验证算法的人只需要专注验证算法的改进,而不太需要关注后端的逻辑推理过程。这样的思路是可取的,但却导致了目前大多数的验证算法过度依赖SMT求解,以至于SMT求解成为验证算法的性能瓶颈。或许,我们可以有更好的思路。
  2. 验证算法对于循环不变式也非常依赖,循环不变式一般由人工提供。但目前也有许多自动生成循环不变式(Invariant Synthesis/Generation)的工作,有兴趣的读者可以自行了解。
  3. SMT实际上包含了诸多的理论,我们粗浅地介绍了数组理论,线性整数运算理论,也有提及非解释函数理论。不同的理论其求解的方法并不相同,有兴趣的读者可以取SMT-LIB的网站上去了解相关内容。

P.S. 读者可以提前思考一下,我们在本篇开头给出的例子程序,具有什么样的循环不变式。 比如说,对下标变量i而言,其值的不变式是什么? 可以用来验证什么属性?(简单) 再比如说,对于数组a来说,在循环执行的过程中,又具有什么样的不变式?(较难

int LinearSearch(int[] a, int length, int key} {
  int i = 0; 
  int res = -1;
  while (res == -1 && i < length) 
  {
    if (a[i] == key) { res = i; }
    else { i = i + 1; }
  } 
  return res;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值