目录
1. 概述
在第一章中,我们将解释如何使用 Alloy 来探索一个非常简单的软件组件的设计,即大多数操作系统中存在的众所周知的垃圾箱 或回收站。目的是对如何使用 Alloy 指定和分析软件设计进行简要的(有时是肤浅的)概述。Alloy 规范语言和分析技术的全部细节将在以下章节中给出。
Trash 的目标是:
- 1. 使已删除的文件仍然可以恢复,直到执行清空 Trash 的(可撤消的)操作
- a. 当文件被删除时,它存储在 Trash 中;
- b. 仍在 Trash 中时,可以恢复文件;
- c. 清空后,Trash 中包含的所有文件将被永久删除。
- b. 仍在 Trash 中时,可以恢复文件;
- a. 当文件被删除时,它存储在 Trash 中;
更具体地说,在本章中,我们将展示如何在以下任务中使用 Alloy:
-
在非常抽象的层次上正式指定垃圾桶组件的结构和行为的设计。
-
通过仿真验证此设计,即使用 Alloy Analyzer 检查它是否允许 Trash 的某些预期行为。
-
引出并验证 Trash 组件的一些预期属性。
2. 指定软件设计
转换系统(Transition systems)是推理系统行为最标准的形式主义之一,是描述和推理软件系统设计的一种非常方便和抽象的形式主义。转换系统包含系统在随时间演化时可能处于的不同状态(states),识别其中哪些是可能的初始状态(initial states),并通过识别将每个状态连接到每个可能的后续状态的转换(transitions)来清楚地描述系统如何演化。
指定转换系统的第一步是描述其状态的结构。在 Alloy 中:
系统的结构是根据集合(sets)和关系(relations)来描述的。
集合(sets)称为签名(signatures),并使用关键字
sig
声明,后跟强制签名名称和括在大括号之间的可选字段(field)声明列表。
字段(fields)是将封闭签名的元素连接到其他元素的关系(relations)。
我们不会在本章中使用字段,因此我们在本例中的签名声明在名称后总是有一对空括号。
在 Alloy 中,签名由所谓的原子(atoms)组成,没有任何内部结构或任何特定语义的元素,其名称将由 Analyzer 自动生成。默认情况下,在 Alloy 中,所有签名和关系的值都是不可变的。要声明可变(或变量)签名,只需 var
在签名声明之前添加关键字。
在非常抽象的层面上,我们可以使用两个集合来描述 Trash 组件的状态:文件系统中随时存在的文件集合,以及当前在垃圾桶中的文件的子集。因此,在我们的 Alloy 规范中,我们将分别声明两个变量签名:File
和 Trash
。为了表明后者是前者的子集,在声明中, Trash
我们将使用关键字 in
后跟包含签名的名称。因此,Trash state
的 Alloy 规格如下。
var sig File {}
var sig Trash in File {}
在这个例子中,签名 File
是一个顶级签名(top-level signature),不包含在任何其他签名中,而是 Trash
一个子集签名(subset signatur)。
在 Alloy 中,所有顶级签名都是不相交的。可变顶级签名也是如此:
它们总是不相交的,即使在不同的时间点,这意味着原子(atoms)不能在状态转换中从一个顶级签名移动到另一个顶级签名。
在描述了我们系统的状态之后,我们现在可以继续指定它的预期行为,即 Trash 转换系统的初始状态和转换是什么。与许多正式规范语言不同,Alloy 没有特殊的语法来声明此转换系统。相反,它遵循 Leslie Lamport 在动作的时间逻辑(Temporal Logic of Actions)中引入的想法,即通过一个时态逻辑公式来隐式地指定转换系统,该公式识别哪些是其有效的执行轨迹。
轨迹(trace)是无限的状态序列,充分描述了系统的可能行为。
指定转换系统的公式通常由两部分组成:
- a. 一个公式指定什么是有效的初始状态;
- b. 另一个指定每个状态下可能的转换。
在我们的示例中,在初始状态下我们有一个空垃圾桶。要测试一组是否为空,我们可以使用关键字 no
,
后面跟要检查的集合表达式(为了检查它是否为非空,我们可以使用关键字 some
,并检查它是否至多包含一个元素,关键字 lone
)。要施加约束以限制系统的有效痕迹,我们可以使用关键字 fact
后跟一个可选的名称和一个用大括号括起来的公式。如果该公式不包含时间运算符,即如果它是一个普通的一阶公式,它约束状态中的集合和关系的值,那么它只需要保持在每条轨迹的初始状态。在我们的示例中,有效的初始状态因此可以由以下事实指定。
fact init {
no Trash
}
要指定有效的转换,首先考虑系统中的事件(events)(或操作)会更容易。每个事件都会引发许多转换。对于垃圾箱组件,我们有三个事件:
- a. 删除文件
- b. 恢复文件
- c. 清空垃圾箱
要指定一个事件,我们将编写一个公式,该公式适用于轨迹的特定状态,前提是该事件可以在该状态下发生,并且轨迹的下一个状态是该事件的有效结果。这两个条件通常分别称为守卫(guard)和事件的影响(effect)。为了指定影响,我们需要以某种方式评估下一个状态下公式的值,或者参考下一个状态下集合和关系的值。为了评估下一个状态下的公式,我们在它前面加上时间运算符 after
。为了评估下一个状态下的表达式,我们将其附加操作符'
。出于可读性原因,在单独的谓词(predicates)中指定每个事件很方便。
谓词是一个命名的公式,只有在调用时才会成立(例如在事实中)。
要声明谓词,应使用关键字 pred
,后跟谓词名称、可选的谓词参数列表和括在花括号中的公式。例如,空垃圾事件可以通过以下三个公式的结合来指定。
pred empty {
some Trash and // guard
after no Trash and // effect on Trash
File' = File - Trash // effect on File