TLA+入门1--基本概念和语法

648 阅读14分钟

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+。
比如我们现在要设计一个简单的转账系统,功能非常的简单,大概需求如下:

  1. 把From账户把里面一部分钱amount转到另一个To账户,前提是From账户里面的钱>=amount;如果From账户里面的钱<amount,应该转账失败;
  2. 用户可以一次启动多个转账;
  3. 转账步骤是一个非原子操作,既当一个转账在进行时,另一个新的转账操作可以发起;
    如果简单点粗略设计下,大概我们的伪代码应该如下:
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属性来验证在运行过程中,这个属性是否被违背:

image.png

运行模型:

image.png 从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 s2s1和s2两个集合取并集
s1 \intersect s2s1和s2两个集合取交集
s1 \ s2s1和s2两个集合取差集,取出在s1中,在s2中不存在的元素
s1 \X s2s1和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有几点需要注意:

  1. 所有的代码statement都应该归属于某个Label;
  2. 一般来说在一个Label内的都是一个原子操作;
  3. 如果在一个Label内调用while循环,那么这个label就不保证原子性了;
  4. 在一个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,会得到一个结果图,下面针对结果图做详细的解释:

image.png

  • Diameter:Diameter最长的行为的长度。如果TLC发现了一千条长度为2的行为和一条长度为20的行为,那么Diameter将被报告为20
  • States Found:发现的状态是指TLC探索了多少个系统状态。这包括检查器在不同路径上发现的重复状态。
  • Distinct States:所发现的特有的(unique)状态的数量。
  • 如果在一个label中,没有1个状态,那说明spec规约文件可能存在bug;

在上面的例子中,Init初始状态和Done完成状态,加上4个迭代状态,一共6个状态。

image.png

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中需要增加这个不变量:

image.png

接着就可以运行这个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)