本节详细介绍了异步接口规约的写作过程,及作者对规约某些设定的现实考量。
本节要点有:
- 通过
EXTENDS Naturals这样的语句引入模块;
- 通过
CONSTANT Data这样的方式引入规约入参;
- 通过
VARIABLES val,rdy,ack这种方式声明状态变量;
- 类型不变量、状态函数、状态谓词的定义;
- 关于类型定义、初始谓词、动作等书写格式的约定:
∧,∨缩进对齐,如
Init≜∧val∈Data∧rdy∈{0,1}∧ack=rdy
- 动作的书写约定:首先是动作的“使能”条件,然后是对状态变量的新值进行赋值,最后所有不变的变量用
UNCHANGED v这种方式表示,如:
Send≜∧rdy=ack∧val′∈Data∧rdy′=1−rdy∧ UNCHANGED ack
- 最后,完整的规约如下
Spec≜Init∧□[Next]⟨val,rdy,ack⟩
现在让我们在模块
AsynchInterface中定义异步接口,因为规约中会用到自然数减法,因此通过
EXTENDS Naturals语句引入
Naturals模块,以包含减法运算符“
−”的定义。接下来,我们来确定
val的可能取值,即什么数值才允许被发送。我们当然也可以写没有任何取值限制的规约,如发送方先发送
37,接着发送
−15
,然后再发
Nat(完整的自然数集)。但是,任何真实的设备都只能发送一组受限制的值,我们可以选择一些特定的集合,例如
32位数字集,不过,无论用于发送
32位数字还是
128位数字,该协议都是相同的。因此,我们在允许发送任何内容和仅发送
32位数字集这两个极端之间进行折衷,设定只有数据集
Data中的数据才可以被发送,常量
Data是规约的一个参数。由下列语句声明:
CONSTANT Data我们的3个变量声明如下:
VARIABLES val,rdy,ack关键字
VARIABLE和
VARIABLES含义相同,区别只在定义的变量数,
CONSTANT和
CONSTANTS也是如此。
变量
rdy可以取任意的值,例如
−1/2,即存在将
−1/2赋值给
rdy的状态。在讨论规约时,我们通常会说
rdy只能取值0或1,我们真正的意思是,在满足规约的任何行为的每种状态下,
rdy的值都等于0或1。不必理解完整的规约也可以理解这一点。通过告诉读者变量在满足规约的行为中可以取哪些值,可以使规约更易于理解。我们可以用注释来做到这一点,但是我更喜欢使用这样的定义:
TypeInvariant≜(val∈Data)∧(rdy∈{0,1})∧(ack∈{0,1})
我称集合
{0,1}是
rdy的类型,称
TypeInvariant为类型不变量。让我们更精确地定义类型和其他一些术语:
- 状态函数是一个普通表达式(一个没有
′或
□的表达式),可以包含变量和常量。
- 状态谓词是布尔值状态函数。
- 规约
Spec的
Inv不变量是状态谓词,使得
Spec⟹Inv是一个定理。
- 变量
v在规约
Spec中具有类型
T当且仅当
v∈T是
Spec的不变量。
如下书写方式可以使
TypeInvariant的定义更易于阅读:
TypeInvariant≜∧val∈Data∧rdy∈{0,1}∧ack∈{0,1}上式中每个合取词都以一个
∧开头,且都必须完全处于
∧的右边。(该合取词可能占据多行。)对于析取词,我们使用相同的方式。在使用此“项目符号列表”(bulleted-list)表示法时,所有的
∧或
∨都必须精确对齐(即使在ASCII版本中),上式中这种缩进很重要,我们可以因此不用括号,这种书写方式在合取词和析取词嵌套的时候特别有用。
公式
TypeInvariant不会出现在规约中,因为规约会蕴含
TypeInvariant为不变量,因此不必显式定义它。实际上,它的不变性将被断言为一个定理。
初始谓词很简单直接,开始时,
val可以等于
Data的任一元素,
rdy和
ack也可以要么均为0,要么均为1:
Init≜∧val∈Data∧rdy∈{0,1}∧ack=rdy现在开始定义“下一个状态动作”
Next。协议的步骤要么是发送值,要么是接收值,我们定义
Send和
Rcv两个动作来分别表示它们。
Next步骤(满足
Next动作的步骤)要么是
Send步骤,要么是
Rcv步骤,因此它是
Send∧Rcv 步骤。这样,我们将
Next定义为
Send∧Rcv 。下面让我们分别定义
Send和
Rcv。
我们认为,在一个状态下,动作
Send被“使能”,则意味着此时可以执行一个“
Send”步骤了。上述示例行为也可以看出这一点:
Send被使能当且仅当
rdy等于
ack。通常,我们关于动作的第一个问题是它何时被使能?因此,动作的定义通常从其使能条件开始。因此,
Send定义中的第一个合取词是
rdy=ack,下一个合取词告诉我们变量
val,
rdy和
ack的新值是什么。
val的新值
val′ 可以是
Data的任何元素,即任意满足
val′∈Data的值。
rdy的值从0变为1或从1变为0,因此
rdy′等于
1−rdy(因为
1=1−0和
0=1−1),
ack的值保持不变。
TLA+定义
UNCHANGED v来表示表达式
v在新旧状态下都具有相同的值。更准确地说,
UNCHANGED v等价于
v′=v,其中
v′是通过给
v的所有变量加
′得到的表达式。因此,我们这样定义
Send:
Send≜∧rdy=ack∧val′∈Data∧rdy′=1−rdy∧ UNCHANGED ack(我可以用
ack′=ack代替
UNCHANGED ack,但我更喜欢后者。)
一个
Rcv步骤被使能当且仅当
rdy与
ack的值不相等,这个步骤会改变
ack的值,但
val和
rdy保持不变,也即(
val,
rdy)“对”保持不变。TLA+使用“
⟨”和“
⟩”包裹有序元组,所以
Rcv断言
⟨val,rdy⟩保持不变。(在ASCII版本中,这对括号被记作<<和>>。)因此
Rcv定义如下:
Rcv≜∧rdy=ack∧ack′=1−ack∧ UNCHANGED ⟨val,rdy⟩
就像在小时时钟示例中一样,完整的规约
Spec应该允许重叠步骤,在这种情况下,所有这三个变量保持不变,即
Spec允许
⟨val,rdy,ack⟩保持不变。
Spec的定义是
Spec≜Init∧□[Next]⟨val,rdy,ack⟩
模块
AsynchInterface还声明了
TypeInvariant不变量,完整的规约如下图3.1显示: