1. TLA+简介
1.1 TLA+是什么
大多数软件的缺陷来自两个地方。
- 代码bug, 代码错误是指代码与我们的设计不一致--例如使用空指针,越界访问,多线程读写变量等,这些我们有很多寻找代码缺陷的技术。
- 还有一类,我们一般较少去思考,就是设计缺陷,比如设计上没有考虑一些异常场景,并发场景下如何控制,在一些异常场景下流程考虑等。如何在设计阶段就能很好的建模,并分析出这些问题,是TLA+考虑的问题。
TLA+是一种 "形式化规范语言",是一种设计系统的手段,可以让你直接测试这些设计。TLA+由图灵奖获得者Leslie Lamport开发,已经被AWS、微软和Crowdstrike等公司认可。可以认为TLA+是一种设计建模工具,有了它,可以很快的进行原型分析,找到设计上的漏洞,特别适合分析分布式协议安全,验证一些分布式共识算法的正确性等。
1.2 谁发明了TLA+
Lamport是一个非常厉害的神奇人物,在分布式领域绝对是超级大牛,他是2013年图灵奖获得者。
具体的成就大家自己去看看他在微软的网站介绍:
www.microsoft.com/en-us/resea…
1.3 TLA+的一个小例子
1.3.1 转账的小例子
下面先通过一个例子来简单介绍下TLA+。
比如我们现在要设计一个简单的转账系统,功能非常的简单,大概需求如下:
- 把From账户把里面一部分钱amount转到另一个To账户,前提是From账户里面的钱>=amount;如果From账户里面的钱<amount,应该转账失败;
- 用户可以一次启动多个转账;
- 转账步骤是一个非原子操作,既当一个转账在进行时,另一个新的转账操作可以发起;
如果简单点粗略设计下,大概我们的伪代码应该如下:
def transfer(from, to, amount)
if (from.balance >= amount) # guard
from.balance -= amount; # withdraw
to.balance += amount; # deposit
上面的设计是满足需求1的,既可以保证from账户里面的钱一定要大于等于amount。 但是这个是明显不满足第2条和第3条需求的。
比如假设小美现在手头上有10块钱,同时发起转账转账给大壮8块,转账给小帅7块,在guard流程判断阶段,因为10 > 8, 10 > 7 ,两个转账流程都是满足的, 都发起了转账,结果转完后,小美的账户变成了-5。
这个例子非常的简单,但是实际在大型软件设计的时候,像这样并发的例子一旦发生,必须通过测试去复现,而且需要去碰运气,有时候一个数据异常,需要长时间复现,很久才会出现,缺少有效的办法找到问题。 即使找到了问题,我们又如何修复这个问题呢,比如上面的例子,我们是不是可以通过一个增加锁的操作修复这个bug,还是仅仅是让这个问题变得更加的隐蔽,变得更加的难以复现了呢?
所以这就是TLA+试图解决的问题,用数学建模的方式尝试去证明设计的正确性!
下面我们通过TLA+来对这个转账的例子进行建模。
1.3.2 转账例子的TLA+建模
首先我们建模前,需要把需求再做一次梳理:
- 我们有一组账户。每个账户我们有一组账户。每个账户都有一个数字,代表余额。
- 任何账户都可以尝试向任何其他账户转移任何金额的资金。
- 转账时首先检查是否有足够的资金。如果有,则从第一个账户中减去该金额,并添加到第二个账户中。
- 转账是非原子性的。多个转账可以同时发生。
建模就是把上面的需求描述实现,同时放到一个叫做规约文件(specification)里面去。如果规约文件正确,无论模型在何种状态,上面的属性都应该被满足。在转账这个例子中,哪怕在一个极端异常的场景下,有一次账户余额出现负数,也可以认为是违反了规约的属性。
完成了规约文件和属性编写后,我们就要把这些内容放到一个模型检查器(Model Checker)中,模型检查器机会检查规约文件中的每一种可能的行为,判断是否都满足我们属性的要求,如果不满足,Model Checker会返回一个error trace信息,便于我们查看问题出在哪里。 先把问题简化为规约文件:
-------------------------------- MODULE Wire --------------------------------
EXTENDS Integers, TLC
People == {"alice", "bob"} \*定义一个集合
Money == 1..10 \*定义一个集合
NumTransfers == 2 \*
(*--algorithm wire
{
variables acct \in [People -> Money]; \* [People -> Money] 是一个Set,代表所有可能的账户情况
define
{
NoOverdrafts == \A p \in People: acct[p] >= 0 \*不变量约束,任意时刻,账户里面的钱都应该大于等于0
}
process(wire \in 1..NumTransfers) \*模拟1~NumTransfers 次转账
variables
amnt \in 1..5;
from \in People;
to \in People;
{
Check:
{
if(acct[from] >= amnt) \*这一行的check其实有可能是幻读,两次转账都认为条件成立,转账时From被扣款两次
{
Withdraw:
{
acct[from] := acct[from] - amnt;
};
Desposit:
{
acct[to] := acct[to] + amnt;
}
}
}
};
}
algorithm*)
\*以下代码是TLA+自动生成的,暂时不讨论
\* BEGIN TRANSLATION (chksum(pcal) = "48b19a81" /\ chksum(tla) = "58d7d06a")
VARIABLES acct, pc
(* define statement *)
NoOverdrafts == \A p \in People: acct[p] >= 0
VARIABLES amnt, from, to
vars == << acct, pc, amnt, from, to >>
ProcSet == (1..NumTransfers)
Init == (* Global variables *)
/\ acct \in [People -> Money]
(* Process wire *)
/\ amnt \in [1..NumTransfers -> 1..5]
/\ from \in [1..NumTransfers -> People]
/\ to \in [1..NumTransfers -> People]
/\ pc = [self \in ProcSet |-> "Check"]
Check(self) == /\ pc[self] = "Check"
/\ IF acct[from[self]] >= amnt[self]
THEN /\ pc' = [pc EXCEPT ![self] = "Withdraw"]
ELSE /\ pc' = [pc EXCEPT ![self] = "Done"]
/\ UNCHANGED << acct, amnt, from, to >>
Withdraw(self) == /\ pc[self] = "Withdraw"
/\ acct' = [acct EXCEPT ![from[self]] = acct[from[self]] - amnt[self]]
/\ pc' = [pc EXCEPT ![self] = "Desposit"]
/\ UNCHANGED << amnt, from, to >>
Desposit(self) == /\ pc[self] = "Desposit"
/\ acct' = [acct EXCEPT ![to[self]] = acct[to[self]] + amnt[self]]
/\ pc' = [pc EXCEPT ![self] = "Done"]
/\ UNCHANGED << amnt, from, to >>
wire(self) == Check(self) \/ Withdraw(self) \/ Desposit(self)
(* Allow infinite stuttering to prevent deadlock on termination. *)
Terminating == /\ \A self \in ProcSet: pc[self] = "Done"
/\ UNCHANGED vars
Next == (\E self \in 1..NumTransfers: wire(self))
\/ Terminating
Spec == Init /\ [][Next]_vars
Termination == <>(\A self \in ProcSet: pc[self] = "Done")
\* END TRANSLATION
=============================================================================
\* Modification History
那么这样的一个规约文件写好后,就可以使用NoOverdrafts属性来验证在运行过程中,这个属性是否被违背:
运行模型:
从TLC的结果可以很快的分析出,当两笔转账同时发起时,同时发起:if(acct[from] >= amnt)
,
两次判断都通过,导致from
调用两次Withdraw
,进而导致from账户的钱小于0.
其实这就是TLA+帮我们发现了转账设计的缺陷。
缺陷其实也非常的明显,因为我们把Check/Withdraw/Desposit分成了3个Label,相当于放到了3个事务中,如果把他们合并到一个Label中,即可保证事务的原子性:
-------------------------------- MODULE Wire --------------------------------
EXTENDS Integers, TLC
People == {"alice", "bob"} \*定义一个集合
Money == 1..10 \*定义一个集合
NumTransfers == 2 \*
(*--algorithm wire
{
variables acct \in [People -> Money]; \* [People -> Money] 是一个Set,代表所有可能的账户情况
define
{
NoOverdrafts == \A p \in People: acct[p] >= 0 \*不变量约束,任意时刻,账户里面的钱都应该大于等于0
}
process(wire \in 1..NumTransfers) \*模拟1~NumTransfers 次转账
variables
amnt \in 1..5;
from \in People;
to \in People;
{
Check:
{
if(acct[from] >= amnt) \*这一行的check其实有可能是幻读,两次转账都认为条件成立,转账时From被扣款两次
{
acct[from] := acct[from] - amnt ||
acct[to] := acct[to] + amnt;
}
}
};
}
algorithm*)
\* BEGIN TRANSLATION (chksum(pcal) = "f5b568c8" /\ chksum(tla) = "f118e70a")
VARIABLES acct, pc
(* define statement *)
NoOverdrafts == \A p \in People: acct[p] >= 0
VARIABLES amnt, from, to
vars == << acct, pc, amnt, from, to >>
ProcSet == (1..NumTransfers)
Init == (* Global variables *)
/\ acct \in [People -> Money]
(* Process wire *)
/\ amnt \in [1..NumTransfers -> 1..5]
/\ from \in [1..NumTransfers -> People]
/\ to \in [1..NumTransfers -> People]
/\ pc = [self \in ProcSet |-> "Check"]
Check(self) == /\ pc[self] = "Check"
/\ IF acct[from[self]] >= amnt[self]
THEN /\ acct' = [acct EXCEPT ![from[self]] = acct[from[self]] - amnt[self],
![to[self]] = acct[to[self]] + amnt[self]]
ELSE /\ TRUE
/\ acct' = acct
/\ pc' = [pc EXCEPT ![self] = "Done"]
/\ UNCHANGED << amnt, from, to >>
wire(self) == Check(self)
(* Allow infinite stuttering to prevent deadlock on termination. *)
Terminating == /\ \A self \in ProcSet: pc[self] = "Done"
/\ UNCHANGED vars
Next == (\E self \in 1..NumTransfers: wire(self))
\/ Terminating
Spec == Init /\ [][Next]_vars
Termination == <>(\A self \in ProcSet: pc[self] = "Done")
\* END TRANSLATION
=============================================================================
\* Modification History
对代码稍作调整,再使用TLC进行检查,就发现检查通过了。这样我们验证了把Check/Withdraw/Desposit三个操作放到一个事务中的设计方案,是无论何时都可以满足NoOverdrafts的要求的。
2. TLA+的基本操作
2.1 Operators和 Values
Operators类似于编程语言里面的函数概念。
Extends Intergers
MinutesToSeconds(m) == m * 60
但是Operators和编程语言中的函数有如下几点显著的区别:
- Operators 可以接受任意多个参数作为入参,不需要预先的声明;
- 没有默认参数/可选参数类似的概念;
- 如果一个Operator有两个参数,那么调用这个Operators时候必须要使用两个值;
- 如果一个Operator有没有参数,那么调用这个Operators时候可以不写括号;这种场景可以认为Operators退化为了一个Constant。
SecondsPerMinute == 60
2.2 IF-THEN-ELSE
可以在一个Operator中写IF-ELSE操作:
Abs(x) == IF x < 0 THEN -x ELSE x
这个很有用,在下面的例子中会做说明。
2.3 Values
2.3.1 TLA+中的类型说明
TLA+是一个没有类型的语言,只有4种原始类型和4种复杂类型。
原始类型:
- strings(必须使用双引号,不能使用单引号)
- booleans(只有TRUE/FALSE两种取值,两个布尔类型可以做一些逻辑操作或(
\/
), 且(/\
)等) - integers(在TLA+中仅有整型,没有Float浮点型)
- model values
复杂类型:
- sets(集合)
- sequences(数组)
- structures(结构)
- functions(函数)
在TLA+中每一种值类型都有自己的操作,不能混用。TLA+中有两种基本的操作 “等于”判断和“不等于”判断。 不同的类型之间是不允许使用等于/不等于操作判断的。
2.3.2 Sequences 数组
TLA+中的线性数组,基本格式写法:
<<a,b,c>>
有如下几个点需要注意:
- 要想使用Sequences,必须引用Sequences包,在规约文件开头写:
EXTENDS Sequences
- TLA+中的Sequences里面的元素可以是不同类型的;
- 使用下标可以访问Sequences中的元素:
seq[i]
; - Sequences是从1开始索引的;
Sequences支持的几种调用:
eg:s == <<"a">>
操作 | 作用 |
---|---|
Append(s, "b") | 向s中插入一个元素<<"a","b">> |
s \o <<"b","c">> | 两个Seq合并 s 变成<<"a","b","c">> |
Head(s) | 获取seq的第一个元素 返回"a" |
Tail(<<"a","b","c">>) | 返回截取第一个元素以后的Seq, <<"b","c">> |
Len(s) | 取长度,返回1 |
SubSeq(<<1,3,5>>,1,2) | 切片操作,返回第1个到第二个元素组成的Seq <<1,3>> |
2.3.3 Sets 集合
TLA+中的无序集合,元素具有无序性和唯一性。基本写法:
{1,2,3}
有如下几个点需要注意:
- 要想使用Sets,不需要引用包,但是如果使用函数计算Set的元素个数
Cardinality(set)
,在规约文件开头写:EXTENDS FiniteSets
- TLA+中的Sequences里面的元素不可以是不同类型的,这是Set和Sequences的一个区别;
Sets支持的几种操作:
操作 | 作用 |
---|---|
x \in set | 判断元素x是否在集合中存在 |
x \notin set | 判断元素x是否不在集合中存在 |
s1 \subseteq set | 判断s1是不是set的子集 |
s1 \union s2 | s1和s2两个集合取并集 |
s1 \intersect s2 | s1和s2两个集合取交集 |
s1 \ s2 | s1和s2两个集合取差集,取出在s1中,在s2中不存在的元素 |
s1 \X s2 | s1和s2两个集合取乘积 ,新集合里面的元素都是sequence |
a..b | 定义一个set,a,b都是整形数字,里面的元素{a,a+1,...b-1,b} (注意这个操作必须EXTENDS Integers) |
除了这些基本的操作,Set还有2个非常高级的用法:
-
Filter
:{x \in set : 条件}
Evens == {x \in 1..4 : x %2 =0} -
Map
:{x的表达式: x \in set}
Squares == {x*x : x \in 1..4}
这里举一个例子,比如想定义一个对时间的操作:
EXTENDS Integers, Sequences
ToSeconds(time) == time[1]*3600 + time[2]*60 + time[3] \*可以理解为一个函数,入参是一个seq,将其转换为一个数字
Earlier(t1,t2) == ToSeconds(t1) < ToSeconds(t2)\*这个函数是判断两个时间谁早
AddTimes(t1, t2) == <<t1[1] + t2[1], t1[2] + t2[2], t1[3] + t2[3]>> \*这个是做一个Time类型的增加运算
但是上面的AddTimes
函数是有缺陷的,如果如下调用:
AddTimes(<<2, 0, 1>>, <<1, 2, 80>>) = <<3, 2, 81>>
明显秒数变成了81是不合理的。其实在上面的例子中,我们隐含了要求传入的seq是应该满足取值范围是从<<0,0,0>>
到<23,59,59>>
的。
可以利用刚才介绍的set的特性,可以定义一个新的类型ClockType
:
ClockType = (0..23) \X (0..59) \X (0..59)
这里相当于定义了一个集合叫做ClockType,里面应该有24*60*60=86400
个元素。
2.3.3 LET操作
在上面的例子中,虽然定义了一个ClockType类型,但是并没有秒数到ClockType的转换操作。
按照正常的程序员思路写,入参是1个数字,根据公式转换为1个Seq(这个是ClockType集合中的一个元素),然后返回。
这里可以利用LET
关键字,先定义一些局部变量
,再在IN
关键字里面做操作。
ToClock2(seconds) ==
LET
h == seconds \div 3600
h_left == seconds % 3600
m == h_left \div 60
m_left == h_left % 60
s == m_left \div 60
IN
<<h, m, s>>
2.3.4 CHOOSE操作
上面介绍的是秒数转ClockType的操作,是一种程序员的思维.
换一个角度思考,入参是1个数字,根据公式转换为1个Seq(这个是ClockType集合中的一个元素)ClockType是一个集合。
其实按照数学公式的写法应该是在ClockType中选择一个元素,满足ToSecond(x) == seconds,入参是1个数字,根据公式转换为1个Seq(这个是ClockType集合中的一个元素)。
ToClock(seconds) == Choose x \in ClockType: ToSeconds(x) == seconds
这个实现有点类似于数学里面的逆函数。
3. TLA+的规约文件
3.1 规约文件简介
TLA+中进行形式化验证的代码,都统一取了一个名字叫Specifications(简单翻译过来就是规约文件)。 编写规约文件的代码就是TLA+,但这个TLA+语言偏向于数学,学习起来难度较大,所以TLA+的作者蓝姆波特就又开发了一个DSL语言叫做---PlusCal。 PlusCalc是一个DSL语言,本身语言风格有两种:
- pascal风格
- C风格
因为C风格比较符合一般码农的理解,后续都采用C风格。
3.2 简单的PlusCal例子
PlusCal代码必须写在(*-- *)中,这是一种多行注释。
开头写algorithm+名字 告诉了TLA+,这个其实是一个PlusCal,不能当作简单的多行注释。
使用Ctrl+T
就可以生成后续的TLA+代码。
-------------------------------- MODULE Test --------------------------------
EXTENDS Integers, TLC, Sequences
(*--algorithm Test {
variables
x = 2;
y = TRUE;
{
A:
{
x := x + 1;
};
B:
{
x := x + 1;
y := FALSE;
};
}
}
*)
\* BEGIN TRANSLATION (chksum(pcal) = "25df7154" /\ chksum(tla) = "1d801c1a")
VARIABLES x, y, pc
vars == << x, y, pc >>
Init == (* Global variables *)
/\ x = 2
/\ y = TRUE
/\ pc = "A"
A == /\ pc = "A"
/\ x' = x + 1
/\ pc' = "B"
/\ y' = y
B == /\ pc = "B"
/\ x' = x + 1
/\ y' = FALSE
/\ pc' = "Done"
(* Allow infinite stuttering to prevent deadlock on termination. *)
Terminating == pc = "Done" /\ UNCHANGED vars
Next == A \/ B
\/ Terminating
Spec == Init /\ [][Next]_vars
Termination == <>(pc = "Done")
\* END TRANSLATION
=============================================================================
3.2 Label标签
在一个复杂系统中,有多种并发,在一个标签内的操作认为是一个原子操作;上例中,A和B是两个原子操作。 关于Label有几点需要注意:
- 所有的代码statement都应该归属于某个Label;
- 一般来说在一个Label内的都是一个原子操作;
- 如果在一个Label内调用
while
循环,那么这个label就不保证原子性了; - 在一个Label内,只能对variable变量更新一次。
例如:
Sum:
while i <= Len(seq) do
x := x + seq[i];
i := i + 1;
end while;
这个Sum Label是无法保证原子性的,如果要保证原子性可以这样写:
macro Sum(seq)
{
while(i<= Len(seq))
{
x := x + seq[i];
i := i + 1;
};
}
Label1:
x := Sum(seq);
例如这样写是错误的,因为在Label中对seq这个variable进行了两次更新;
Label:
seq[1] := seq[1] + 1;
seq[2] := seq[2] - 1;
改成如下即可:
Label:
seq[1] := seq[1] + 1 ||
seq[2] := seq[2] - 1;
3.3 PlusCall的表达式
3.3.1 if
C风格的if
if(条件1)
{
xxx;
}
elsif(条件2)
{
yyy;
}
else
{
zzz;
};
- 注意结尾一定有一个分号;
- 可以在一个if条件内增加Label;
- 如果任何一个分支有一个标签,你必须在整个区块后面加上一个标签;
可以看下下面这个例子:
A:
if bool then
B:
skip;
else
skip;
end if;
x := 1;
如果 bool = TRUE
, 那么X:=1
归属于B Label
,如果bool=FALSE
,那么X:=1
归属于A Label
。
所以上面的这个例子是语法错误的。
3.3.2 macro
宏就是简单的重写规则,便于复用。例如刚才看到的
macro Sum(seq)
{
while(i<= Len(seq))
{
x := x + seq[i];
i := i + 1;
};
}
- macro 不归属于任何一个Label
3.3.3 with
with语句让你在标签中间创建临时性的分配语句。
with tmp_x = x, tmp_y = y do
y := tmp_x;
x := tmp_y;
end with;
3.3.4 while
while(i<= Len(seq))
{
x := x + seq[i];
i := i + 1;
};
- while语句是非原子性的;
3.4 一个简单的例子(数组重复元素检测)
PlusCal的代码:
-------------------------------- MODULE Test --------------------------------
EXTENDS Integers, TLC, Sequences
(*--algorithm Test {
variables
seq = <<1,2,3,2>>;
index = 1;
seen = {};
is_unique = TRUE;
{
Iterate:
{
while(index <= Len(seq))
{
if(seq[index] \notin seen)
{
seen := seen \union {seq[index]};
}
else
{
is_unique := FALSE;
};
index := index + 1;
};
}
}
}
*)
\*下面都是TLA+自动生成的代码
\* BEGIN TRANSLATION (chksum(pcal) = "de66605d" /\ chksum(tla) = "6d42786f")
VARIABLES seq, index, seen, is_unique, pc
vars == << seq, index, seen, is_unique, pc >>
Init == (* Global variables *)
/\ seq = <<1,2,3,2>>
/\ index = 1
/\ seen = {}
/\ is_unique = TRUE
/\ pc = "Iterate"
Iterate == /\ pc = "Iterate"
/\ IF index <= Len(seq)
THEN /\ IF seq[index] \notin seen
THEN /\ seen' = (seen \union {seq[index]})
/\ UNCHANGED is_unique
ELSE /\ is_unique' = FALSE
/\ seen' = seen
/\ index' = index + 1
/\ pc' = "Iterate"
ELSE /\ pc' = "Done"
/\ UNCHANGED << index, seen, is_unique >>
/\ seq' = seq
(* Allow infinite stuttering to prevent deadlock on termination. *)
Terminating == pc = "Done" /\ UNCHANGED vars
Next == Iterate
\/ Terminating
Spec == Init /\ [][Next]_vars
Termination == <>(pc = "Done")
\* END TRANSLATION
=============================================================================
\* Modification History
运行TLA+的Checker,会得到一个结果图,下面针对结果图做详细的解释:
- Diameter:Diameter最长的行为的长度。如果TLC发现了一千条长度为2的行为和一条长度为20的行为,那么Diameter将被报告为20
- States Found:发现的状态是指TLC探索了多少个系统状态。这包括检查器在不同路径上发现的重复状态。
- Distinct States:所发现的特有的(unique)状态的数量。
- 如果在一个label中,没有1个状态,那说明spec规约文件可能存在bug;
在上面的例子中,Init初始状态和Done完成状态,加上4个迭代状态,一共6个状态。
3.5 例子的扩展
上面的例子中,输入是写死的:
seq = <<1,2,3,2>>;
如果想有多个输入,可以写成如下样子:
-------------------------------- MODULE Test --------------------------------
EXTENDS Integers, TLC, Sequences
(*--algorithm Test {
variables
seq \in {<<1,2,3,2>>, <<1,2,3,4>>};
index = 1;
seen = {};
is_unique = TRUE;
{
Iterate:
{
while(index <= Len(seq))
{
if(seq[index] \notin seen)
{
seen := seen \union {seq[index]};
}
else
{
is_unique := FALSE;
};
index := index + 1;
};
}
}
}
*)
\*下面都是TLA+自动生成的代码
\* BEGIN TRANSLATION (chksum(pcal) = "119789f7" /\ chksum(tla) = "1d903bb9")
VARIABLES seq, index, seen, is_unique, pc
vars == << seq, index, seen, is_unique, pc >>
Init == (* Global variables *)
/\ seq \in {<<1,2,3,2>>, <<1,2,3,4>>}
/\ index = 1
/\ seen = {}
/\ is_unique = TRUE
/\ pc = "Iterate"
Iterate == /\ pc = "Iterate"
/\ IF index <= Len(seq)
THEN /\ IF seq[index] \notin seen
THEN /\ seen' = (seen \union {seq[index]})
/\ UNCHANGED is_unique
ELSE /\ is_unique' = FALSE
/\ seen' = seen
/\ index' = index + 1
/\ pc' = "Iterate"
ELSE /\ pc' = "Done"
/\ UNCHANGED << index, seen, is_unique >>
/\ seq' = seq
(* Allow infinite stuttering to prevent deadlock on termination. *)
Terminating == pc = "Done" /\ UNCHANGED vars
Next == Iterate
\/ Terminating
Spec == Init /\ [][Next]_vars
Termination == <>(pc = "Done")
\* END TRANSLATION
=============================================================================
\* Modification History
现在seq是从set中选择,set中每个元素都是4,所以diameter还是6。 States Found/Distinct States变成了12/14.
如果再修改下:
seq \in {<<1,2,3,2>>, <<1,2,3,4,5,6,7,8>>};
此时Diameter就会变成10.
上面的例子也说明:如果是变量是一个集合的元素,那么TLC将在每个可能的起始状态上测试模型。
4. Invariants(不变量)
在TLA+中,我们的基本测试是Invariants(不变量)。Invariants是指在程序的每一步都必须为真,无论初始值如何,无论我们在何种状态。 在编程语言中,最常见的Invariants就是静态类型。类比第三章提的 数组重复元素检测例子中,可以做如下约定:
- is_unique这个变量从头到尾都必须是一个BOOLEAN类型;
- seen里面元素都在S集合中;
- index的范围在1~Len(seq)+1中;
可以定义一个Operator:
TypeInvariant ==
/\ is_unique \in BOOLEAN
/\ seen \subseteq S
/\ index \in 1..Len(seq)+1
那么这个TypeInvariant的约束会一直存在,如果违背了这个约束,那么TLC就会报错。
正常的operator都是写在algorithm外面,但是这种invariant(不变量)这个Operator一般都放到define定义块中。
-------------------------------- MODULE Test --------------------------------
EXTENDS Integers, TLC, Sequences
S == 1 .. 10
(*--algorithm Test {
variables
seq \in S \X S \X S \X S;
index = 1;
seen = {};
is_unique = TRUE;
define
{
TypeInvariant == /\ is_unique \in BOOLEAN
/\ seen \subseteq S
/\ index \in 1..Len(seq)+1
}
{
Iterate:
{
while(index <= Len(seq))
{
if(seq[index] \notin seen)
{
seen := seen \union {seq[index]};
}
else
{
is_unique := FALSE;
};
index := index + 1;
};
}
}
}
*)
\*下面都是TLA+自动生成的代码
\* BEGIN TRANSLATION (chksum(pcal) = "f2e890ea" /\ chksum(tla) = "4207575d")
VARIABLES seq, index, seen, is_unique, pc
(* define statement *)
TypeInvariant == /\ is_unique \in BOOLEAN
/\ seen \subseteq S
/\ index \in 1..Len(seq)+1
vars == << seq, index, seen, is_unique, pc >>
Init == (* Global variables *)
/\ seq \in S \X S \X S \X S
/\ index = 1
/\ seen = {}
/\ is_unique = TRUE
/\ pc = "Iterate"
Iterate == /\ pc = "Iterate"
/\ IF index <= Len(seq)
THEN /\ IF seq[index] \notin seen
THEN /\ seen' = (seen \union {seq[index]})
/\ UNCHANGED is_unique
ELSE /\ is_unique' = FALSE
/\ seen' = seen
/\ index' = index + 1
/\ pc' = "Iterate"
ELSE /\ pc' = "Done"
/\ UNCHANGED << index, seen, is_unique >>
/\ seq' = seq
(* Allow infinite stuttering to prevent deadlock on termination. *)
Terminating == pc = "Done" /\ UNCHANGED vars
Next == Iterate
\/ Terminating
Spec == Init /\ [][Next]_vars
Termination == <>(pc = "Done")
\* END TRANSLATION
=============================================================================
\* Modification History
在TLC中需要增加这个不变量:
接着就可以运行这个Checker,看是否有地方违反了这个Invariant。
上面的例子中,其实我们写的Invariant是非常明确,且不会被违反的,运行这个Model Checker 也不会触发任何问题。
在实际建模的过程中,我们肯定会遇到这样一种场景:运行过程我并不关注,我们仅关注当这个场景完成后,最终结果是否正确。
还是以刚才”数组重复元素检测“这个举例,实际在每个状态运行完成,is_unique
这个变量的值可能会是TRUE或者FALSE,如果是TRUE,那么一定满足:
Cardinality(seen) = Len(s)
否则的话,如果is_unique
是FALSE:
Cardinality(seen) != Len(s)
那么可以定义一个Operator:
IsUnique(s) == Cardinality(seen) = Len(s)
IsCorrect == is_unique = IsUnique(seq)
但是这个IsCorrect能否作为一个Invariants呢?其实是不能的。测试下就发现TLC会报错:
Invariant IsCorrect is violated by the initial state:
/\ index = 1
/\ seen = {}
/\ seq = <<1, 1, 1, 1>>
/\ pc = "Iterate"
/\ is_unique = TRUE
刚才Invariant的定义中明确:Invariants是指在程序的每一步都必须为真,无论初始值如何,无论我们在何种状态。* 显然在这个上面的例子中,初始状态的时候,is_unique等于TRUE,但是IsUnique(seq)是判断Cardinality(seen) 为0,而seq长度为4,IsUnique返回值为False。所以TLC检查就会报错。 为了解决这个问题,就要引入一个新的概念----PC。
5. PC的概念
类似于写代码汇编中的PC寄存器,标识当前进程运行到哪条指令了,其中有两个特殊的状态:
- "Init":初始态
- "Done":结束态 有了这个PC来表示状态,那么再回到上面的例子,其实我们只是在结束的时候需要保证IsCorrect这个不定式成立的,所以可以重新修订下不定式:
IsCorrect == IF pc = "Done" THEN is_unique = IsUnique(seq) ELSE TRUE
也就是说在流程结束的时候做检查。
其实这是一个
IF A THEN B ELSE TRUE
句式,也就是说只有在A成立的情况下,才会去判断B是否为真,可以把这样的句式写成这样:A => B
。
上面的句式写成:pc = "Done" => is\_unique=IsUnique(seq)