Spec Sharp Overview [10-13] supersandpro翻译 [原稿]
1.2 类约定
指定一个库或者抽象的使用规则主要是通过方法约定来完成的,它清楚的说明了调用者希望得到什么以及调用者能够从实现中得到什么。要指定一个实现的设计,程 序员主要使用规范来约束实现数据的取值范围。这些规范被称为对象不变量,并且清楚的说明了每个对象的数据域在对象的稳定状态时应该保持什么。例如,类片断
class AttendanceRecord {
Student[ ]! students;
bool[ ]! absent ;
invariant students.Length == absent .Length;
声明了student和absent数组的长度应该是一样的。
正如我们可以从上面这个简单的例子看到的,让对象不变量总是保持是不可能的,因为不可能在语言中同时改变两个数组的长度。这就是我们说对象不变量在稳定状 态时保持的原因,它的本质是说这个对象不是当前操作的对象。使用我们针对对象不变量的方法论[3,45,6],相对于被暴露时,Spec#要明确当一个对 象处于它的稳定状态,这表明这个对象对修改是脆弱的。Spec#介绍了一个块声明expose,它明确指定何时一个对象的不变量可以暂时被破坏:下面的声 明
expose (o) {
S;
}
在子声明S的持续期间内暴露了对象o,它可能对o的成员变量进行操作。因为对面向对象的程序中的成员变量修改倾向于压缩在声明这些成员变量的类中,所以表 达式o就经常是this。对象不变量假设在expose声明的结尾再次保持,而Spec#会通过一个运行时检查来确保这项假设。对象不变量在构造函数结尾 时也要被检查一次(虽然有允许在其他地方对一个对象不变量的初始检查的灵活性;在这里我们忽略这些细节)。
通过默认值,无论何时一个类或者它的超类有一个已声明的不变量,这个类的每个公共方法都有一个隐式的
expose (this) { . . . }
在方法体的周围。我们的初步经验证实这个默认值消除了对明确expose声明的大多数需要。在需要重入的情况中,可以通过一个在方法上的定制特性来使默认值失效。
暴露一个对象不是幂等的。这就是说,当o已经被暴露时如果再到达expose(o)...就会是一个已检查的运行时错误。从这个方面来看,暴露机制和并发 编程中的thread-non-reentrant互斥体很相似了,其中的监视器不变量[34]就是和我们的对象不变量相似的概念。如果暴露是幂等的,那 么你就不能够在expose代码块中依赖对象不变量来立即保持,同样的thread-reentrant互斥体的幂等性表明你不能够依赖监视器不变量在互 斥体被请求时保持。
为了让Spec#的object-invariant方法论是可靠的,所有对一个成员变量o.f的修改必须在o暴露时才能发生。此外,这个方法论使用一个 所有权关系来把对象组织到一个树形层次中。这个关系是状态依赖的,它允许所有权转换。这样的修改和所有权是通过Boogie执行的,而不是在运行时执行 的。
对象不变量可以被任何类声明。为了支持不变量的模块检查,一个类可以不需要知道它的超类和将来的子类的不变量,根据每个不变量被声明的类的不同这些对象不变量被分割进入类框架[3,17]。expose机制来处理类框架。
为了减少程序员增加expose声明的初始开销并且以向后兼容性更好的方式处理非虚方法(见[3]),Spec#允许一个expose声明暴露多于一个类 框架。要解释这个特性,我们首先需要展示Spec#中expose声明的更加一般化的形式,这就是
expose (o upto T) { . . . }
其中T是表达式o的静态类型的一个超类。如果“upto T”被省略了,T就默认为表达式o的静态类型。比上面所描述的更精确的说法是,这个声明暴露从已经暴露的类框架之上的所有o的类框架(*)(同时 exposing类框架T它自己)。非幂等性要求至少一个类框架作为操作的一部分被暴露。在expose块的尾部,在进入时被暴露的类框架都要改为非暴露 的,并且那些类框架中每一个的对象不变量都要被检查。这是在运行时通过编译器发送的动态结束方法完成的。
暴露的一个未知数目的类框架,特别是在为那些声明位置可能是未知的类框架检查不变量时,指向了一个模块的问题,即静态验证。因此,我们在Boogie中为expose使用一个更严格的模块。特别地,
expose (o upto T) { . . . }
的预处理条件是o的T类框架是非暴露的——那就是,o的底层派生的非暴露类框架是T的一个子类——Boogie通过要求o的最底层派生非暴露类框架必须是 T来强调这个预处理条件。用这个方法,Boogie就有能力找到它需要在expose块的结尾要检查的所有的对象不变量。作为结果,运行时行为和 Boogie确保的东西的区别意味着程序员可以开始更容易的写出和运行Spec#程序,但是这样一来他们可能需要投入更多的努力以获得对程序正确性的更高 信心,程序的正确性是由Boogie确保的(正像要确保Boogie的修改和所有权规则都符合要求所要付出的额外努力一样)。
对象不变量只允许涉及常量,成员变量,数组元素,状态独立的方法,和受限的方法。如果一个方法不依赖于可变的状态,则称它是状态独立的。一个受限方法可能 依赖于拥有它的对象的状态,Spec#编译器引入一个保守影响分析来检查这些属性都被遵守了。
Spec#也支持类不变量,类不变量在静态成员变量的文档假设中是很有用的。类不变量的方法论及约束和对象不变量的方法论及约束是相似的,只是类不变量没有继承[44]。expose声明简单地使用一个类替代一个对象作为一个参数。
1.3 其它细节
约定中的异常 如果Spec#中的一个约定的评估过程中有异常抛出,则异常就打包到一个约定评估异常中并且传播出去。这是和JML中的约定运行时评估相对应的,其中这样的异常被捕获并且周围的公式像它按照一定的规则返回一个布尔值一样来处理,见[14]。
在规范上的定制特性 C#提供定制特性来作为一个把任意数据放在程序结构上的方法,例如类,方法,和成员变量。一个定制特性是编译到元数据中的,元数据的标准格式允许很多应用 程序来读取依附于特殊声明的定制特性。Spec#也允许每个规范子句使用定制特性来注释。
定制特性允许第三方工具的用户使用工具特定的方法来标记规范。例如,Spec#编译器使用Conditional定制特性来控制在当前的版本中哪个规范被发出作为运行时检查。例如,对于下面的方法
int BinarySearch(object[ ]! a, object o, int lo, int hi)
requires 0 <= lo && lo <= hi && hi <= a.Length;
[Conditional (“DEBUG”)] requires IsSorted(a, lo, hi);
{ . . . }
编译器为调试版本中的每个预处理条件发出运行时检查,但在非调试版本中只为第一个预处理条件发出一个检查。这支持了调试断言中的通常编程风格(见,例如[53])。
纯粹性 我们想有这样的属性,当一个程序打开约定检查时能正常运行,而当其中的一些约定检查关闭时它也能够正常运行。因此,我们要求所有约定中出现的表达式都是纯 粹的,也就是说它们没有副作用并且不会抛出任何已检查的异常。编译器通过使用一个适当的作用系统来确保这种情况。我们正在考虑对纯粹性的更自由的定义,比 如观测纯粹性[7]和由Salcianu和Rinard完成的大量的分析提供的资料[58]。
2 系统体系结构
体系结构上,Spec#编程系统由编译器,一个运行库,和Boogie验证器组成。编译器已经被完全嵌入到Microsoft Visual Studio环境当中,根据项目系统,构建过程,设计工具,语法强调,以及IntelliSense上下文敏感的编辑与文档辅助。
Spec#编译器和普通的编译器的差别在于它不仅仅根据一个Spec#语言写的程序产生可执行的代码,它也保持所有规范为一个语言独立的格式。使可用的规 范成为一个独立的编译通过的单元意味着程序分析和验证工具可以消费这些规范,同时既不需要修改Spec#编译器也不需要写一个全新的源语言编译器。
Spec#编译器可以将规范保存到和编译代码相同的二进制代码因为它指定的目标是Microsoft的.NET通用语言运行时(CLR)。CLR为使多种 类型的信息与类型系统(类型,方法,成员变量等等)的大多数元素相关联提供了丰富的元数据工具。Spec#编译器把一个规范附加到存在一个规范的每个程序 组建上。(技术上来讲,这些规范是作为定制特性中的字符串来保存的。所有的名字都完全解决了;当这个提交很繁琐的提交了格式,它使任何工具消费它都容易得 多。)
作为结果,我们作了这个设计决定就是让Boogie消费编译过的代码,而不是源代码。一个额外的好处是Boogie可以用来验证Spec#之外的语言写的 代码,只要对每一个附加到这样的代码上的约定有一个out-of-hand处理就可以。我们对附加到.NET框架基础类库(BCL)的规范使用这样的一个 处理,见2.2部分。
2.0 运行时检查
Spec#预处理条件和后处理条件都被转换为内联代码。我们这样做不只是因为性能的考虑,也是为了避免在编译代码中创建额外的方法和成员变量。所有这样的 内联代码都附加了标签,这样与Spec#约定相对应的代码可以与Spec#程序其他部分的代码区分开来。这样的区分是任何从元数据中消费Spec#约定的 分析工具所要求的。例如,Boogie必须能够判断方法中的非约定代码是否与后处理条件相符合,而不是非约定代码后面连接检查后处理条件的代码。内联代码 评估这些条件,如果违反,则抛出适当的约定异常。
为了检查对象不变量,编译器为每个声明了一个不变量的类添加一个新的方法。特殊的对象成员变量,例如不变量级别[3]和对象的所有者[45],都被添加到 每个类层次的子树中使用Spec#特性的顶层超类(super-most class)中。正如我们在第一部分中提到的,运行时不会执行整个方法论;例如运行时检查不会去检查一个对象在更新一个成员变量之前是否被暴露了。这意味 着一个能被Boogie捕获的错误可能在运行时不会被检测到。
2.1 静态验证
从中间语言(包括元数据),Spec#的静态程序验证器,Boogie,在它自己的中间语言BoogiePL中构建一个程序。BoogiePL是一个拥有 过程的简单语言,过程的实现大多数情况下是由四种类型的声明组成的基础代码块:指定,断言,假设,和过程调用(cf.[47])。
一个推理系统可以处理BoogiePL程序,使用过程间抽象解释[15,57]来获得如循环不变量这样的属性。任何继承属性都被作为断言声明或者假设声明 添加到程序当中。然后BoogiePL程序通过几个转换,作为提交给一个自动定理证明程序的条件验证结束。这些转换,诸如通过引入破坏(havoc)声明 剪掉所有循环来生成一个非循环控制流图,是通过一个保持分析正确性的方法来完成的。一个破坏声明为一个变量分配任意的一个值;为分配给一个循环的所有变量 引入破坏声明会使定理证明程序考虑一个任意的循环迭代器。所有从定理证明程序回来的反馈都在它被发布给用户[43]之前安排回到源程序。这样作的结果就是 程序员只能通过在程序源级别做一些修改来与Boogie的验证器交互,例如增加约定。
当前,Boogie使用Simplify定理证明程序[18],但我们打算转到一个由Microsoft Reserach开发的新的实验法则验证器上去。
指定一个库或者抽象的使用规则主要是通过方法约定来完成的,它清楚的说明了调用者希望得到什么以及调用者能够从实现中得到什么。要指定一个实现的设计,程 序员主要使用规范来约束实现数据的取值范围。这些规范被称为对象不变量,并且清楚的说明了每个对象的数据域在对象的稳定状态时应该保持什么。例如,类片断
class AttendanceRecord {
Student[ ]! students;
bool[ ]! absent ;
invariant students.Length == absent .Length;
声明了student和absent数组的长度应该是一样的。
正如我们可以从上面这个简单的例子看到的,让对象不变量总是保持是不可能的,因为不可能在语言中同时改变两个数组的长度。这就是我们说对象不变量在稳定状 态时保持的原因,它的本质是说这个对象不是当前操作的对象。使用我们针对对象不变量的方法论[3,45,6],相对于被暴露时,Spec#要明确当一个对 象处于它的稳定状态,这表明这个对象对修改是脆弱的。Spec#介绍了一个块声明expose,它明确指定何时一个对象的不变量可以暂时被破坏:下面的声 明
expose (o) {
S;
}
在子声明S的持续期间内暴露了对象o,它可能对o的成员变量进行操作。因为对面向对象的程序中的成员变量修改倾向于压缩在声明这些成员变量的类中,所以表 达式o就经常是this。对象不变量假设在expose声明的结尾再次保持,而Spec#会通过一个运行时检查来确保这项假设。对象不变量在构造函数结尾 时也要被检查一次(虽然有允许在其他地方对一个对象不变量的初始检查的灵活性;在这里我们忽略这些细节)。
通过默认值,无论何时一个类或者它的超类有一个已声明的不变量,这个类的每个公共方法都有一个隐式的
expose (this) { . . . }
在方法体的周围。我们的初步经验证实这个默认值消除了对明确expose声明的大多数需要。在需要重入的情况中,可以通过一个在方法上的定制特性来使默认值失效。
暴露一个对象不是幂等的。这就是说,当o已经被暴露时如果再到达expose(o)...就会是一个已检查的运行时错误。从这个方面来看,暴露机制和并发 编程中的thread-non-reentrant互斥体很相似了,其中的监视器不变量[34]就是和我们的对象不变量相似的概念。如果暴露是幂等的,那 么你就不能够在expose代码块中依赖对象不变量来立即保持,同样的thread-reentrant互斥体的幂等性表明你不能够依赖监视器不变量在互 斥体被请求时保持。
为了让Spec#的object-invariant方法论是可靠的,所有对一个成员变量o.f的修改必须在o暴露时才能发生。此外,这个方法论使用一个 所有权关系来把对象组织到一个树形层次中。这个关系是状态依赖的,它允许所有权转换。这样的修改和所有权是通过Boogie执行的,而不是在运行时执行 的。
对象不变量可以被任何类声明。为了支持不变量的模块检查,一个类可以不需要知道它的超类和将来的子类的不变量,根据每个不变量被声明的类的不同这些对象不变量被分割进入类框架[3,17]。expose机制来处理类框架。
为了减少程序员增加expose声明的初始开销并且以向后兼容性更好的方式处理非虚方法(见[3]),Spec#允许一个expose声明暴露多于一个类 框架。要解释这个特性,我们首先需要展示Spec#中expose声明的更加一般化的形式,这就是
expose (o upto T) { . . . }
其中T是表达式o的静态类型的一个超类。如果“upto T”被省略了,T就默认为表达式o的静态类型。比上面所描述的更精确的说法是,这个声明暴露从已经暴露的类框架之上的所有o的类框架(*)(同时 exposing类框架T它自己)。非幂等性要求至少一个类框架作为操作的一部分被暴露。在expose块的尾部,在进入时被暴露的类框架都要改为非暴露 的,并且那些类框架中每一个的对象不变量都要被检查。这是在运行时通过编译器发送的动态结束方法完成的。
暴露的一个未知数目的类框架,特别是在为那些声明位置可能是未知的类框架检查不变量时,指向了一个模块的问题,即静态验证。因此,我们在Boogie中为expose使用一个更严格的模块。特别地,
expose (o upto T) { . . . }
的预处理条件是o的T类框架是非暴露的——那就是,o的底层派生的非暴露类框架是T的一个子类——Boogie通过要求o的最底层派生非暴露类框架必须是 T来强调这个预处理条件。用这个方法,Boogie就有能力找到它需要在expose块的结尾要检查的所有的对象不变量。作为结果,运行时行为和 Boogie确保的东西的区别意味着程序员可以开始更容易的写出和运行Spec#程序,但是这样一来他们可能需要投入更多的努力以获得对程序正确性的更高 信心,程序的正确性是由Boogie确保的(正像要确保Boogie的修改和所有权规则都符合要求所要付出的额外努力一样)。
对象不变量只允许涉及常量,成员变量,数组元素,状态独立的方法,和受限的方法。如果一个方法不依赖于可变的状态,则称它是状态独立的。一个受限方法可能 依赖于拥有它的对象的状态,Spec#编译器引入一个保守影响分析来检查这些属性都被遵守了。
Spec#也支持类不变量,类不变量在静态成员变量的文档假设中是很有用的。类不变量的方法论及约束和对象不变量的方法论及约束是相似的,只是类不变量没有继承[44]。expose声明简单地使用一个类替代一个对象作为一个参数。
1.3 其它细节
约定中的异常 如果Spec#中的一个约定的评估过程中有异常抛出,则异常就打包到一个约定评估异常中并且传播出去。这是和JML中的约定运行时评估相对应的,其中这样的异常被捕获并且周围的公式像它按照一定的规则返回一个布尔值一样来处理,见[14]。
在规范上的定制特性 C#提供定制特性来作为一个把任意数据放在程序结构上的方法,例如类,方法,和成员变量。一个定制特性是编译到元数据中的,元数据的标准格式允许很多应用 程序来读取依附于特殊声明的定制特性。Spec#也允许每个规范子句使用定制特性来注释。
定制特性允许第三方工具的用户使用工具特定的方法来标记规范。例如,Spec#编译器使用Conditional定制特性来控制在当前的版本中哪个规范被发出作为运行时检查。例如,对于下面的方法
int BinarySearch(object[ ]! a, object o, int lo, int hi)
requires 0 <= lo && lo <= hi && hi <= a.Length;
[Conditional (“DEBUG”)] requires IsSorted(a, lo, hi);
{ . . . }
编译器为调试版本中的每个预处理条件发出运行时检查,但在非调试版本中只为第一个预处理条件发出一个检查。这支持了调试断言中的通常编程风格(见,例如[53])。
纯粹性 我们想有这样的属性,当一个程序打开约定检查时能正常运行,而当其中的一些约定检查关闭时它也能够正常运行。因此,我们要求所有约定中出现的表达式都是纯 粹的,也就是说它们没有副作用并且不会抛出任何已检查的异常。编译器通过使用一个适当的作用系统来确保这种情况。我们正在考虑对纯粹性的更自由的定义,比 如观测纯粹性[7]和由Salcianu和Rinard完成的大量的分析提供的资料[58]。
2 系统体系结构
体系结构上,Spec#编程系统由编译器,一个运行库,和Boogie验证器组成。编译器已经被完全嵌入到Microsoft Visual Studio环境当中,根据项目系统,构建过程,设计工具,语法强调,以及IntelliSense上下文敏感的编辑与文档辅助。
Spec#编译器和普通的编译器的差别在于它不仅仅根据一个Spec#语言写的程序产生可执行的代码,它也保持所有规范为一个语言独立的格式。使可用的规 范成为一个独立的编译通过的单元意味着程序分析和验证工具可以消费这些规范,同时既不需要修改Spec#编译器也不需要写一个全新的源语言编译器。
Spec#编译器可以将规范保存到和编译代码相同的二进制代码因为它指定的目标是Microsoft的.NET通用语言运行时(CLR)。CLR为使多种 类型的信息与类型系统(类型,方法,成员变量等等)的大多数元素相关联提供了丰富的元数据工具。Spec#编译器把一个规范附加到存在一个规范的每个程序 组建上。(技术上来讲,这些规范是作为定制特性中的字符串来保存的。所有的名字都完全解决了;当这个提交很繁琐的提交了格式,它使任何工具消费它都容易得 多。)
作为结果,我们作了这个设计决定就是让Boogie消费编译过的代码,而不是源代码。一个额外的好处是Boogie可以用来验证Spec#之外的语言写的 代码,只要对每一个附加到这样的代码上的约定有一个out-of-hand处理就可以。我们对附加到.NET框架基础类库(BCL)的规范使用这样的一个 处理,见2.2部分。
2.0 运行时检查
Spec#预处理条件和后处理条件都被转换为内联代码。我们这样做不只是因为性能的考虑,也是为了避免在编译代码中创建额外的方法和成员变量。所有这样的 内联代码都附加了标签,这样与Spec#约定相对应的代码可以与Spec#程序其他部分的代码区分开来。这样的区分是任何从元数据中消费Spec#约定的 分析工具所要求的。例如,Boogie必须能够判断方法中的非约定代码是否与后处理条件相符合,而不是非约定代码后面连接检查后处理条件的代码。内联代码 评估这些条件,如果违反,则抛出适当的约定异常。
为了检查对象不变量,编译器为每个声明了一个不变量的类添加一个新的方法。特殊的对象成员变量,例如不变量级别[3]和对象的所有者[45],都被添加到 每个类层次的子树中使用Spec#特性的顶层超类(super-most class)中。正如我们在第一部分中提到的,运行时不会执行整个方法论;例如运行时检查不会去检查一个对象在更新一个成员变量之前是否被暴露了。这意味 着一个能被Boogie捕获的错误可能在运行时不会被检测到。
2.1 静态验证
从中间语言(包括元数据),Spec#的静态程序验证器,Boogie,在它自己的中间语言BoogiePL中构建一个程序。BoogiePL是一个拥有 过程的简单语言,过程的实现大多数情况下是由四种类型的声明组成的基础代码块:指定,断言,假设,和过程调用(cf.[47])。
一个推理系统可以处理BoogiePL程序,使用过程间抽象解释[15,57]来获得如循环不变量这样的属性。任何继承属性都被作为断言声明或者假设声明 添加到程序当中。然后BoogiePL程序通过几个转换,作为提交给一个自动定理证明程序的条件验证结束。这些转换,诸如通过引入破坏(havoc)声明 剪掉所有循环来生成一个非循环控制流图,是通过一个保持分析正确性的方法来完成的。一个破坏声明为一个变量分配任意的一个值;为分配给一个循环的所有变量 引入破坏声明会使定理证明程序考虑一个任意的循环迭代器。所有从定理证明程序回来的反馈都在它被发布给用户[43]之前安排回到源程序。这样作的结果就是 程序员只能通过在程序源级别做一些修改来与Boogie的验证器交互,例如增加约定。
当前,Boogie使用Simplify定理证明程序[18],但我们打算转到一个由Microsoft Reserach开发的新的实验法则验证器上去。
版权声明:本文由作者Tony Qu原创, 未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则视为侵权。