dafny guide

介绍

Dafny 中的以下注释片段表示数组的每个元素都是严格正的:

forall k: int :: 0 <= k < a.Length ==> 0 < a[k] 

这表示对于k作为数组索引的所有整数,该索引处的值大于零。

Methods

Dafny 在许多方面类似于典型的命令式编程语言。有方法、变量、类型、循环、if 语句、数组、整数等等。任何 Dafny 程序的基本单元之一是方法。方法是一段命令式的、可执行的代码。在其他语言中,它们可能被称为过程或函数,但在 Dafny 中,术语“函数”是为一个不同的概念保留的,我们将在后面介绍。方法的声明方式如下:

method Abs(x: int) returns (y: int)
{
	...
}

这声明了一个名为“Abs”的方法,它接受一个名为“x ”的整数参数,并返回一个名为“ y”的整数。请注意,每个参数和返回值都需要类型,并在冒号 ( :)后跟在每个名称之后。此外,返回值是命名的,并且可以有多个返回值,如下所示:


method MultipleReturns(x: int,y int) returns(more: int,less: int)
{
	...
}

方法体是包含在大括号内的代码,直到现在它被巧妙地表示为“ … ”(这不是Dafny 语法)。主体由一系列语句组成,例如熟悉的命令式赋值、if语句、循环、其他方法调用、返回语句等,例如方法可以实现为:

method MultipleReturns(x: int,y: int) returns (more: int,less: int)
{
	more := x + y;
	less := x - y;
}

赋值使用“:=”而不是“=”,简单的语句后边跟着分号,空格和注释可以被忽略。

返回值:

method Abs(x: int) returns (y: int)
{
	if x < 0
		{ return -x; }
	else
		{ return x; }
}

一个警告是他们总是需要在分支周围使用大括号,即使分支只包含一个语句(复合或其他)。

前置条件与后置条件

Dafny 的真正力量来自于对这些方法进行注释以指定其行为的能力。例如,我们观察到的一个属性Abs方法是无论输入如何,结果始终大于或等于零。有几种方法可以给出注解,但一些最常见和最基本的是方法前置 和后置条件。
方法的这个属性Abs,即结果总是非负的,是后置条件的一个例子:它在方法返回后为真。使用ensures 关键字声明的后置条件作为方法声明的一部分在返回值(如果存在)之后和方法主体之前给出。

method Abs(x: int) return (y: int)
	ensures 0 <= y
{
	...
}

你可以在这里看到为什么返回值被赋予名称。这使得它们很容易在方法的后置条件中引用。当表达式为真时,我们说后置条件成立。后置条件必须适用于函数的每次调用和每个可能的返回点(包括函数体末尾的隐式返回点)。在这种情况下,我们表达的唯一属性是返回值始终至少为零。

有时,我们希望为我们的代码建立多个属性。在这种情况下,我们有两个选择。我们可以使用布尔值和运算符 ( )将这两个条件连接在一起,也可以编写多个规范。后者与前者基本相同,但性质不同。

method MultipleReturns(x: int, y: int) returns (more: int, less: int)
   ensures less < x
   ensures x < more
{
   more := x + y;
   less := x - y;
}

后置条件也可以写成:

ensures less < x && x < more

甚至

ensures less < x < more

Dafny 实际上拒绝了这个程序,声称第一个后置条件不成立(即不成立)。这意味着 Dafny 无法证明每次方法返回时此注释都成立。一般情况下,导致 Dafny 验证错误的主要原因有两个:与代码不一致的规范,以及它不够“聪明”以证明所需属性的情况。区分这两种可能性可能是一项艰巨的任务,但幸运的是,Dafny 和它所基于的 Boogie/Z3 系统非常聪明,并且将证明代码和规范相匹配而不会大惊小怪。

在这种情况下,Dafny 说代码有错误是正确的。问题的关键是它y 是一个整数,所以它可以是负数。如果y为负(或零),则more 实际上可以小于或等于x。除非y严格大于零,否则我们的方法不会按预期工作。这恰恰是一个前提的想法。前置条件与后置条件类似,不同之处在于它之前必须为真 一个方法被调用。当你调用一个方法时,你的工作是建立(使)先决条件,Dafny 将使用证明来强制执行。同样,当您编写方法时,您可以假设前提条件,但必须建立后置条件。方法的调用者然后假设后置条件在方法返回后成立。

先决条件有自己的关键字,requires。我们可以给出必要的前提条件MultipleReturns 如下:

method MultipleReturns(x: int, y: int) returns (more: int, less: int)
   requires 0 < y
   ensures less < x < more
{
   more := x + y;
   less := x - y;
}

断言

与前置条件和后置条件不同,断言放置在方法中间的某个位置。与前两个注释一样,断言有一个关键字assert,后跟布尔表达式和终止简单语句的分号。断言说当控制到达代码的那部分时,特定的表达式总是成立。例如,以下是在虚拟方法中简单使用断言:

method Testing()
{
   assert 2 < 3;
}

Dafny 证明了这种方法是正确的,因为 2 总是小于 3。断言有多种用途,但其中最主要的是检查您对不同点的真实期望是否真的是真实的。您可以使用它来检查基本的算术事实,如上所述,但它们也可以用于更复杂的情况。断言是调试注释的强大工具,通过检查 Dafny 能够证明您的代码的内容。例如,我们可以使用它来调查 Dafny 对Abs函数的了解。

声明变量:

var x: int := 5;

在这种情况下可以删除类型注释:

var x := 5;

可以一次声明多个变量:

var x, y, z: bool := 1, 2, true;

显式类型声明仅适用于紧接在前面的变量,因此这里的bool声明仅适用于z,而不适用于x或y,它们都被推断为ints。我们需要变量是因为我们想讨论Abs 方法的返回值。我们不能Abs直接放入规范,因为该方法可能会改变内存状态等问题。所以我们捕获调用的返回值Abs如下:

method Testing()
{
   var v := Abs(3);
   assert 0 <= v;
}

Functions

function abs(x: int): int
{
   ...
}

这声明了一个被调用的函数abs,它接收一个整数,并返回一个整数(第二个int),与在其主体中可以包含各种语句的方法不同,函数主体必须仅包含一个具有正确类型的表达式。这里我们的 body 必须是一个整数表达式。为了实现绝对值函数,我们需要使用if 表达式。if 表达式类似于其他语言中的三元运算符。

function abs(x: int): int
{
   if x < 0 then -x else x
}

那么为什么要用函数而不用方法呢,关键在于函数可以在规范中直接使用,所以我们可以我们可以这么写:

assert abs(3) == 3;

事实上,我们不仅可以直接编写这个语句而不捕获到局部变量,我们甚至不需要编写我们使用该方法所做的所有后置条件(尽管通常函数可以并且确实具有前置和后置条件)。功能的限制正是让 Dafny 做到这一点的原因。与方法不同,Dafny 在考虑其他函数时不会忘记函数体。所以它可以扩展上面断言中abs的定义,确定结果实际上是3。

循环不变量

虽然循环给 Dafny 带来了问题。Dafny 无法提前知道代码将在循环中运行多少次。但是 Dafny 需要考虑通过程序的所有路径,其中可能包括多次循环。为了让 Dafny 能够使用循环,您需要提供循环不变量,另一种注释。

循环不变量是在进入循环时以及在每次执行循环体之后保持不变的表达式。与前置条件和后置条件一样,不变量是为循环的每次执行保留的属性,使用我们见过的相同布尔表达式表示。例如,我们在上面的循环中看到,如果i 开始为正值,则它保持正值。所以我们可以使用它自己的关键字将不变量添加到循环中:

   var i := 0;
   while i < n
      invariant 0 <= i
   {
      i := i + 1;
   }

当您指定一个不变量时,Dafny 证明了两件事:该不变量在进入循环时保持不变,并且由循环保留。保留,我们的意思是假设不变量在循环开始时成立,我们必须证明执行一次循环体会使不变量再次成立。

终止

Dafny 通过使用decrease这个注释证明代码终止,即不会永远循环。对于很多事情,Dafny 能够猜出正确的注释,但有时需要明确说明。事实上,对于我们目前看到的所有代码,Dafny 已经能够自己做这个证明,这就是为什么我们还没有明确看到decrease注释的原因。Dafny 证明终止有两个地方:循环和递归。这两种情况都需要明确的注释或 Dafny 的正确猜测。

顾名思义,递减注释给出了随着每次循环迭代或递归调用而递减的 Dafny 和表达式。Dafny 在使用递减表达式时需要验证两个条件:表达式实际上变小了,并且它是有界的。很多时候,整数值(自然或普通整数)是减少的数量,但也可以使用其他东西。(有关详细信息,请参阅参考资料。)对于整数,假定界限为零。例如,以下是循环中减少的正确使用(当然有自己的关键字):

	while 0 < i
      invariant 0 <= i
      decreases i
   {
      i := i - 1;
   }

在这里,Dafny 拥有证明终止所需的所有要素。i每次循环迭代该变量都会变小,并且下限为零。这很好,除了循环是从大多数循环向后计数的,这些循环倾向于向上计数而不是向下计数。在这种情况下,减少的不是计数器本身,而是计数器与上限之间的距离。下面给出了处理这种情况的简单技巧:

while i < n
  invariant 0 <= i <= n
  decreases n - i
 {
      i := i + 1;
 }

这实际上是 Dafny 对这种情况的猜测,因为它看到并假设n-i是减少的数量。

数组

数组是语言的内置部分,有自己的类型array,其中T是另一种类型。现在我们只考虑整数数组,数组可以是空,并且有一个内置的长度字段a.Length。必须证明所有数组访问都在边界内,这是 Dafny 无运行时错误安全保证的一部分。要创建一个新数组,必须用new关键字分配它,但现在我们只使用将先前分配的数组作为参数的方法。
我们可能想要对数组做的最基本的事情之一是搜索它以查找特定的键,并返回我们可以找到键的位置的索引(如果它存在)。我们有两个搜索结果,每个结果都有不同的正确性条件。如果算法返回一个索引(即非负整数),那么该键应该出现在该索引处。这可以表示如下:

method Find(a: array<int>, key: int) returns (index: int)
   ensures 0 <= index ==> index < a.Length && a[index] == key
{
   // Open in editor for a challenge...
}

这里的数组索引是安全的,因为蕴涵运算符是短路的。短路是指如果左边的部分为假,那么不管第二部分的真值如何,蕴涵都已经为真,因此不需要对其进行评估。

量词

Dafny 中的量词通常采用forall表达式的形式,也称为全称量词。顾名思义,如果某个属性对某个集合的所有元素都成立,则该表达式为真。现在,我们将考虑整数集。下面给出了一个包含在断言中的全称量词示例:

assert forall k :: k < k + 1;

量词为其考虑的集合中的每个元素引入一个临时名称。在这种情况下,这称为绑定变量k。绑定变量有一个类型,它几乎总是被推断而不是显式给出,并且通常是int无论如何。一对冒号(::)将绑定变量及其可选类型与量化属性(必须是bool类型)分隔开来。通常,量化无限集(例如所有整数)并不是很有用。相反,量词通常用于量化数组或数据结构中的所有元素。我们通过使用蕴涵运算符对数组执行此操作,使量化属性对于不是索引的值非常正确。

assert forall k :: 0 <= k < a.Length ==> ...a[k]...;

这表示数组的每个元素都有一些属性。这意味着k在评估表达式的第二部分之前确保它实际上是数组中的有效索引。Dafny 不仅可以使用这个事实来证明数组是安全访问的,而且还可以将它必须考虑的整数集减少到仅作为数组索引的整数集。

使用量词,说键不在数组中很简单:

forall k :: 0 <= k < a.Length ==> a[k] != key

因此,我们的方法后置条件变为:

method Find(a: array<int>, key: int) returns (index: int)
   ensures 0 <= index ==> index < a.Length && a[index] == key
   ensures index < 0 ==> forall k :: 0 <= k < a.Length ==> a[k] != key
{
   ...
}

我们可以通过多种方式填充此方法的主体,但也许最简单的是线性搜索,实现如下:

index := 0;
   while index < a.Length
   {
      if a[index] == key { return; }
      index := index + 1;
   }
   index := -1;

如您所见,我们在 while 循环中省略了循环不变量,因此 Dafny 在其中一个后置条件上给了我们一个验证错误。我们得到错误的原因是 Dafny 不知道循环实际上涵盖了所有元素。为了让 Dafny 相信这一点,我们必须编写一个不变量,说明当前索引之前的所有内容都已被查看(并且不是键)。就像后置条件一样,我们可以使用量词来表达这个属性:

invariant forall k :: 0 <= k < index ==> a[k] != key

最终代码:

method Find(a: array<int>, key: int) returns (index: int)
   ensures 0 <= index ==> index < a.Length && a[index] == key
   ensures index < 0 ==> forall k :: 0 <= k < a.Length ==> a[k] != key
{
   index := 0;
   while index < a.Length
      invariant 0 <= index <= a.Length
      invariant forall k :: 0 <= k < index ==> a[k] != key
   {
      if a[index] == key { return; }
      index := index + 1;
   }
   index := -1;
}
  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值