TLA+ 《Specifying Systems》翻译初稿——Chapter 14 The TLC Model Checker

Chapter 14 The TLC Model Checker


TLC是一个用于查找TLA+ specification中错误的程序。它由袁宇设计和开发,并得到了莱斯利·兰波特、马克·海登和马克·塔特尔的帮助。它可以通过TLA官网获得。本章介绍TLC Version2。在我编写本文档时,Version 2仍在开发中,目前只有Version 1可用。请查阅软件附带的文档,了解它是什么版本,以及它与这里描述的版本的区别。

14.1 Introduction to TLC

TLC可以处理遵循如下标准格式的公式:

(14.1)Init \wedge \square[Next]_{vars}\wedge Temporal

其中,Init是初始谓词,Next是Next-state action,vars是所有变量的元组,Temporal通常表示Liveness时态公式。Liveness公式在第八章中有描述。如果您的specification不包含时态公式,也就是它的形式为Init \wedge \square[Next]_{vars},那么您可以忽略对时态逻辑检查的讨论。TLC不处理隐藏运算符\boldsymbol{\color{Red} {\exists}}(时态存在量词),如果需要检查用\boldsymbol{\exists}隐藏变量的specification,可以检查它的子specification,在其中这些变量是可见的。

在specification中查找错误最有效的方法是尝试验证它是否满足其属性(properties)。TLC可以检查specification是否满足(蕴含)的这一大类TLA+公式,这类公式的主要限制是公式中不能包含\boldsymbol{\exists}。您还可以只运行TLC而不检查任何属性,在这种情况下,它将只查找下列两种类型的错误:

  • “Silly”错误。如第6.2节所解释的,"silly"表达式如3+\left \langle 1,2 \right \rangle,其形式不符合TLA+的语义。如果某个特定的behavior是否符合规定,取决于"silly"表达式的含义,那么这个specification是不正确的。
  • 死锁。无死锁经常是我们希望一个specification需要满足的一个特殊性质;它是用不变性来表示的:\square(Enabled \: Next)。此属性的一个反例是一个导致死锁的behavior序列, 即到达一个Next未使能的状态,因此不可能有进一步的重叠步骤。TLC通常默认检查死锁,但也可以禁用此检查,因为对于某些系统,死锁可能只是表示behavior成功终止。

我们用一个简单的例子来展示TLC的使用:下面是一个Alternating Bit Protocol Specification,该协议通常用于在有损的FIFO传输线上发送数据。算法设计人员可能会将协议描述为如下所示的系统:

当1bit位上的值sBit和sAck相等时,发送方可以发送一个值。它将变量"sent"设置为要发送的值,并设置补码sBit。该值最终被投递到接收方,被赋给变量rcvd,同时接收方设置补码rBit,并给发送方回响应sAck。发送方收到sAck后,允许发送下一个值。该协议使用两条有损FIFO传输线:发送者在msgQ上发送数据和控制信息,接收者在ackQ上发送确认。

完整的Alternating Bit Protocol Specification在图14.1上,可以在下文中找到。除了liveness条件外,其他表述都相当清晰。由于消息可能会从队列中经常性地丢失,所以需要对接收消息的action设置Strong Fairness属性,以确保重发的消息最终能够被收到。不过,不要担心specification的细节。现在,你所需要知道的,就是下面这些声明和变量:

CONSTANT Data
VARIABLES msgQ, ackQ, sBit, sAck, rBit, sent, rcvd

上图中,

  • msgQ是由\left \{ 0,1 \right \}\times Data集合中的元素组成的序列;
  • ackQ 是由\left \{ 0,1 \right \}集合中的元素组成的序列;
  • sBit, sAck 和 rBit是 \left \{ 0,1 \right \}集合中的元素;
  • sent 和rcvd 是 Data集合中的元素.

TLC的输入包括TLA+模块文件和配置文件。TLC假定specification具有公式(14.1)的形式。配置文件告诉TLC specification的名称和要检查的属性。例如,AlternatingBit模块的配置文件包含声明

\textrm{SPECIFICATION}\: ABSpec

这个语句是告诉TLC, 待检查的specification名称是ABSpec,如果Specification的格式为Init \wedge \square[Next]_{vars}(无Liveness条件),则无需使用SPECIFICATION 语句,可以通过在配置文件中添加以下两个语句来声明初始状态谓词和Next-State Action:

\\ \textrm{INIT} \: Init\\ \textrm{NEXT}\: Next

要检查的属性用PROPERTY语句指定。例如,为了检查ABTypeInv不变量,即Specification \Rightarrow \square ABTypeInv,可以在模块AlternatingBit的配置文件中添加如下定义:
InvProperty \triangleq \square ABTypeInv
并将语句 \textrm{PROPERTY }InvProperty 写入配置文件中。不变性检查非常常见,因此TLC允许您将以下语句放入配置文件中:
\textrm{INVARIANT} ABTypeInv
INVARIANT语句必须指定一个状态谓词。若要检查PROPERTY语句的不变性,指定的属性必须为\square P形式(因为\textrm{PROPERTY }P只是让TLC检查该specification是否蕴含P,也就是 P 在满足specification的每个behavior的初始状态中为 TRUE)。

TLC通过生成并校验一系列满足specification的behavior来工作。 为此,首先要给specification指定一个模型(model)。 要定义模型,我们必须为specification的常量参数赋值。 AlternatingBit协议Specification的唯一常量参数是Data。 通过在配置文件中放置以下声明,我们可以告诉TLC,Data为包含名为d1和d2两个任意元素的集合:

\textrm{CONSTANT}\:Data = \left \{d1, d2}{ \right \}

(我们可以使用包含至少一个字母的字母或数字串作为元素名称)

有两种使用TLC的方法。 默认方法是模型检查(model checking),这种方式将尝试查找所有可达的状态,即所有满足公式Init \wedge \square[Next]_{vars}的behavior中可能出现的状态。 我们还可以在仿真模式下运行TLC,在该模式下,它会随机生成behavior,而无需尝试检查所有可达的状态。 这里我们我们先考虑模型检查,模拟模式将在第243页的14.3.2节介绍。
对于AlternatingBit协议,不可能彻底检查所有可达状态,因为消息序列可以任意变长,因此存在无限多个可达状态。 我们必须进一步约束模型使其有限,也就是说,它仅允许有限数量的可能状态。 为此,我们定义了一个称为约束的状态谓词,该谓词声明了序列长度的界限。 例如,以下约束断言msgQ和ackQ的长度最多为2:

\\ \wedge Len(msgQ)\leqslant 2 \\ \wedge Len(ackQ)\leqslant 2

与其以这种方式指定序列长度的界限,不如让它们作为参数并在配置文件中赋值。 我们不想在Specification中加入仅为TLC方便考虑的声明和定义。 因此,我们编写了一个名为MCAlternatingBit的新模块,该模块扩展了AlternatingBit模块,可以用作TLC的输入。 该模块显示在下一页的图14.2中。 下一页的图14.3中显示了该模块的可能配置文件。 请注意,在这种情况下,配置文件必须为Specification的所有常量参数指定值,即AlternatingBit模块中的参数Data和模块MCAlternatingBit本身中声明的两个参数。 您可以使用第3.5节(第32页)中所述的TLA +注释语法在配置文件中添加注释。

当指定约束Constr时,TLC会检查满足Init \wedge \square [Next]_{vars} \wedge \square Constr规约的behavior的每个状态。 在本章的其余部分,这些状态将称为可达状态。

让TLC检查类型不变式会捕获许多简单的错误。 当我们纠正了所有可以找到的错误后,我们便希望寻找不太明显的错误。 一个常见的错误是某个操作在应启用时未启用,从而导致无法达到某些状态。 您可以通过第252页上介绍的coverage选项来发现某个操作是否从未启用。要发现某个操作有时是否被错误地禁用,可以尝试检查Liveness。 AlternatingBit协议中明显Liveness属性是,发送方发送的每个消息最终都将被传递给接收方。 当满足如下条件: sent = d \: and \:sBit \neq sAck时, 一个消息d被发送。 因此,描述该属性的一种简单方法是:

\\SentLeadsToRcvd \triangleq \\ \indent \indent \forall d \in Data : (sent = d ) \wedge (sBit \neq sAck ) \leadsto (rcvd = d )

公式SentLeadsToRcvd断言,对于任何数值d,如果在sBit不等于sAck时sent的值等于d,则rcvd最终必须等于d。 这并不是说所有发送的消息都会最终传递到位,例如,对特定值d发送两次但仅接收一次的behavior也满足公式。 但是,该公式足以满足我们的目的,因为该协议不依赖于实际发送的值。 如果可能出现相同的值发送两次但仅接收一次,则也有可能发送两个不同的值而仅接收到一个,后者违反了SentLeadsToRcvd。 因此,我们将SentLeadsToRcvd的定义添加到模块MCAlternatingBit中,并将以下语句添加到配置文件中:

检查liveness属性比其他类型的检查要慢得多,因此,只有在通过检查不变性发现尽可能多的错误之后,才执行此操作。检查类型正确性和属性SentLeadsToRcvd是开始查找错误的好方法。但最终,我们希望了解该协议是否符合其specification。但是,我们(可能)没有它的规范。实际上,在实践中通常需要我们检查系统设计的正确性,而无需对系统应该做什么做任何正式specification。在这种情况下,我们可以编写事后规范。下一页的图14.4中的ABCorrectness模块就是这种对alternating bit 协议的正确性的specification。它实际上是协议specification的简化版本,在该协议中,变量rcvd,rBit和sAck不是从消息中读取,而是直接从其他进程的变量中获取。我们要检查AlternatingBit模块的规范ABSpec是否蕴含ABCorrectness模块的公式ABCSpec。为此,我们通过添加以下语句来修改模块MCAlternatingBit \textrm{INSTANCE}\; ABCorrectness,然后将配置文件的PROPERTY语句修改为
\textrm{PROPERTIES} \:ABCSpec \:SentLeadsToRcvd
此示例是非典型的,因为正确性specification ABCSpec不涉及变量隐藏(时态存在量词)。 现在让我们假设模块ABCorrectness确实声明了另一个变量h,该变量出现在ABCSpec中,并且alternating bit协议的正确性条件是隐藏了h的ABCSpec。 然后,在TLA +中正式表示正确性条件,如下所示:

\\AB (h) \triangleq \textrm{INSTANCE} \:ABCorrectness \\ \textrm{THEOREM}\: ABSpec \Rightarrow \: \exists h :AB (h)!ABCSpec

TLC无法直接检查该定理,因为TLC目前无法处理时间存在量词。 我们将以与尝试证明该定理相同的方式通过TLC检验该定理,即通过使用细化映射。 如62页5.8节所述,我们将根据AlternatingBit模块的变量定义状态函数oh,然后证明(14.2) ABSpec \Rightarrow AB (oh)!ABCSpec

为了让TLC检查该定理,我们将添加定义ABCSpecBar \triangleq AB(oh)!ABCSpec, 并让TLC检查属性ABCSpecBar.

TLC检查属性时,实际上并不会验证specification是否蕴含了该属性。 相反,它检查(i)specification的safety部分隐含了property的safety部分,以及(ii)specification是否蕴含了属性的liveness部分。 例如,假设规格Spec和属性Prop为

Spec \triangleq Init \wedge \square[Next]_{vars}\wedge Temporal

Prop \triangleq ImpliedInit \wedge \square[ImpliedAction]_{pvars} \wedge ImpliedTemporal

这里 Temporal 和ImpliedTemporal 是liveness 属性. 在这里, TLC校验如下两个公式

Init \wedge \square[Next]_{vars} \Rightarrow ImpliedInit \wedge \square[ImpliedAction]_{pvars}

Spec \Rightarrow ImpliedTemporal

这意味着不能使用TLC来检查non-machine-closed specification 是否满足safety要求。 (Machine closure在8.9.2节中讨论(请参阅第111页。)下面的14.3节更准确地描述了TLC如何检查属性。

14.2 What TLC Can Cope With(TLC处理资源)

没有任何模型检查器可以处理所有的specification(这些specification是我们所能用的像TLA +这样具有表现力的语言编写的)。 不过,TLC似乎能够处理人们实际编写的大多数TLA + specification。 使用TLC处理specification可能需要一些技巧,但是通常可以在不对specification本身进行任何更改的情况下完成。
本节说明了TLC可以和不能解决的问题,并提供了一些使其解决的方法。 理解TLC局限性的最好方法是了解其工作方式。 因此,本节描述TLC如何“执行”specification。

14.2.1 TLC Values(TLC值)

一个state是给一组变量赋值的操作。 TLA +允许您描述大量不同类型的数值——例如,所有素数序列的集合。 TLC只能计算受限的一类数值,称为TLC数值。 这些数值是根据以下四种原始值构建的

  • Booleans:布尔值,其值为TRUE 或 FALSE ;
  • Integers Values: 整数值,如 3和-1;
  • Strings Values:字符串,如“abc”;
  • Model Values: 这些是在配置文件的CONSTANT语句中引入的值。 例如,第227页图14.3所示的配置文件引入了模型值d1和d2。 假定具有不同名称的Model Value是不同的。

TLC数值可以归纳定义为

  1. 原始值
  2. 有限的具有可比性的TLC数值的集合(在下面定义可比性)
  3. 对于f值域中的所有x,函数f的域为TLC值,f[x]也为TLC值。

举例来说,根据第1,2条规则,(14.3)\left \{ \left \{ "a","b" \right \} ,\left \{ "b","c" \right \} ,\left \{ "c","d" \right \} \right \}是一个TLC值,因为,根据规则1,“a”,"b","c","d"都是TLC值,根据规则2可以推导出14.3也是一个TLC值,既然元组和记录都是函数,由规则3可以推导出一条由TLC值组成记录或者元组也是一个TLC值.  例如   \left \langle 1,"a", 2,"b" \right \rangle也是一个 TLC 值.

为了完善TLC值的定义,我必须解释规则2中的可比性。基本思想是两个值应该是可比较的当且仅当TLA +语义确定是相等还是不相等。 例如,字符串和数字是不可比较的,因为TLA +的语义不能判定"abc"和42是否相等。 因此集合{“abc”,42}不是TLC值; 规则2不适用,因为“ abc”和42不具有可比性。 另一方面,{“ abc”}和{4,2}具有可比性,因为元素数量不同的集合必然不相等。 因此,两个元素{{“abc”},{4,2}}也是TLC值。 TLC认为模型值可与任何其他值进行比较,但不相等。 第14.7.2节中给出了可比性的更精确的规则定义。

14.2.2 How TLC Evaluates Expressions(TLC如何计算表达式)

检查规范需要计算表达式。 例如,TLC通过计算各个可到达状态下的不变性来进行不变性检查,即计算其TLC值是否为TRUE。 要了解TLC可以做什么和不能做什么,必须知道它如何计算表达式的,TLC以非常直接的方式计算表达式,通常采用“从左到右”的方式计算子表达式,特别的:

  • 计算p\wedge q时,先计算p的值,如果p值为TRUE,则继续计算q的值;
  • 计算p \vee q时,先计算p的值,如果p值为FALSE,则继续计算q的值,即以\lnot p\wedge q方式计算p \vee q
  • 计算IF p THEN e1 ELSE e2时,先计算p,再继续计算e1 或者 e2

为了理解这些规则的重要性,我们来看一个简单的例子。 如果x等于\left \langle \right \rangle,TLC无法评估表达式x[1],因为\left \langle \right \rangle\left [ 1 \right ]没有意义。 (空序列是一个函数,其值域是空集,因此不包含1。)第一条规则意味着,如果x等于\left \langle \right \rangle,则TLC可以计算表达式(x \neq \left \langle \right \rangle) \wedge (x[1] = 0), 但不能计算表达式(x[1] = 0) \wedge(x \neq \left \langle \right \rangle)(因为计算此表达式是,根据规则1,会先计算x[1] = 0,TLC会报错,因为不能计算)

幸运的是,我们会很自然地编写第一个公式而不是第二个公式,因为它更容易理解。 人们可以通过从左到右的“心理计算”来理解公式,这与TLC的做法很相似。

TLC计算\exists x \in S:p时,是将集合S中的元素s_{1} ,\cdots ,s_{n}(其中i = 1,\cdots,n), 经过一定的顺序,逐个代替变量x,代入公式p,计算p的值, TLC以非常简单的方式枚举集合S的元素,如果集合显然不是有限的,则会终止并声明错误。举例来说,集合\left \{ 0,1,2,3 \right \}0..3是非常明显可被遍历的有限集,在计算\exists x \in S:p时,会先对S中的元素进行遍历,所以\left \{ i\in0..5:i<4 \right \}可被计算而\left \{ i\in Nat:i<4 \right \}不能。

TLC计算\forall x \in S:p 和\textrm{CHOOSE} \;x \in S:p 时,都是和对 \exists x \in S:p一样,先遍历S中的所有元素,TLA+的语义指定对\textrm{CHOOSE} \;x \in S:p,如果S中没有一个元素满足p, 则返回一个任意值,不过,这种情况通常是由于出现了某种错误导致,所以TLC会把它当成错误处理。 注意到表达式 \textrm{IF} \;n>5\; \boldsymbol{\textrm{THEN} }\;\textrm{CHOOSE }i \in 1..n: i> 5 \;\textrm{ELSE} \;42不会报错,因为当n\leq5的时候不会进入CHOOSE子句,当n>5时TLC才会在计算CHOOSE子句时报错。

TLC无法计算“无界”量词或CHOOSE表达式——即具有以下形式之一的表达式:

\exists x:p\indent\forall x:p\indent \textrm{CHOOSE} \;x:p

TLC无法计算其值不是TLC值的任何表达式,如上文第14.2.1节中所定义的。 特别的,TLC仅可计算其值是一个有限集的集值表达式,并且仅当其值域是一个有限集时,才可以评估一个函数值表达式。 TLC仅在能遍历集合S时,才会计算以下形式的表达式:

\exists x \in S:p\indent\forall x \in S :p\indent \textrm{CHOOSE} \;x \in S:p

\left \{ x \in S:p{ \right \}\indent\left \{e: x \in S{ \right \}\indent \left \{ x \in S \mapsto e{ \right \}

\mathrm{SUBSET} \;S\indent \mathrm{UNION}\; S

TLC经常可以计算某些表达式,却它不能计算所有的子表达式,举个例子:TLC可以计算\left [ n \in Nat \mapsto n*(n+1) \right ][3]的值为12,但它不能计算\left [ n \in Nat \mapsto n*(n+1) \right ]的值,这个表达式的值是一个值域为Nat的函数表达式(一个函数为TLC值当且仅当其值域是一个有限集)

TLC通过简单的递归procedure来计算由递归定义的函数。 如果f由f[x \in S]\triangleq e定义,则TLC通过用c代替x 计算e来得出f[c]的值。 这意味着它无法处理某些合法的定义。 例如,参考第68页的以下定义:

\\mr[n \in S]\triangleq\\ \indent \left [ f \mapsto if \;n=0 \; then\; 17\; else \; mr[n-1].f*mr[n].g, \\ \indent g \mapsto if \;n=0 \; then\; 42\; else \; mr[n-1].f*mr[n-1].g \right ]

为了计算mr[3], 我们在表达式中用3代替n来计算\triangleq右边的值,不过因为mr[3]也出现在等式右边,所以TLC认为它是一个无限循环,从而报错,合法的递归定义导致如上死循环的毕竟是少数,可以换一种符合TLC的写法,回到我们之前的交互递归定义f[n]= if \;n=0 \; then\; 17\; else \; f[n-1]*g[n], \\ \indent g[n] = if \;n=0 \; then\; 42\; else \; f[n-1]*g[n-1]

子表达式mr[n]出现在定义mr[n]的表达式中,因为f[n]取决于g[n]。 为了消除它,我们必须重写相互递归,以便f[n]仅取决于f[n-1]和g[n-1],我们可以通过展开f[n]表达式中g[n]的定义来做到这一点, 由于else子句仅适用于n\neq 0的情况,因此我们可以将f[n]的表达式重写为

f[n]= if \;n=0 \; then\; 17\; else \; f[n-1]*(f[n-1]+g[n-1]), 这样原公式可以推导成如下形式:

\\mr[n \in S]\triangleq\\ \indent \left [ f \mapsto if \;n=0 \; then\; 17\; else \; mr[n-1].f*(mr[n-1].f+mr[n-1].g), \par\indent g \mapsto if \;n=0 \; then\; 42\; else \; mr[n-1].f*mr[n-1].g \right ]]

这样,TLC就可以计算mr[3]的值而不出问题了。

 第14.2.6节的第240页描述了如何计算ENABLED谓词和复合action操作符".",  第14.3节介绍了TLC如何计算用于时态检测的时态逻辑公式。
如果不确定TLC是否可以对表达式求值,请尝试看看。 但是不要在检查整个specification的过程中检查该表达式。 相反,做一个小例子,让TLC仅计算该表达式。 有关如何将TLC用作TLA +计算器,请参见第14.5.3页的说明。

14.2.3 Assignment and Replacement(赋值和替换)

正如我们在alternating bit示例中看到的那样,配置文件必须确定每个常量参数的值。 要将TLC值v分配给specification的常量参数c,我们在配置文件的CONSTANT语句中写入c = v。 值v可以是原始TLC值或以\left \{ v_{1}, \cdots, v_{n} \right \}形式编写的有限的原始TLC值集。 例如{1,-3,2}。 在v中,将a1或foo之类的任何非数字字符序列,带引号的字符串,或TRUE或FALSE都视作model value。

在赋值表达式c = v中,符号c不必是常数,也可以是已定义的符号,此赋值语句可以使TLC忽略c的实际定义,并以v为它的值。 当TLC无法根据其定义计算出c的值时,通常使用这种赋值语句。 特别是,像在下面例子中,TLC无法根据定义计算NotAnS的值

NotAnS\triangleq \mathrm{CHOOSE} \; n : n \notin S

因为TLC无法计算无边界的CHOOSE表达式。 您可以通过在配置文件的CONSTANT语句中为NotAnS分配一个值来覆盖此定义。 例如,赋值NotAnS = NS 可以使TLC为NotAnS分配model value NS。 TLC忽略了NotAnS的实际定义。 如果在specification中使用名称NotAnS,你可能希望TLC在报错的消息中使用名称NotAnS而不是NS。 因此,您可以会使用赋值语句NotAnS = NotAnS,它将model value NotAnS分配给符号NotAnS。 请记住,在赋值语句c = v中,必须在TLA +模块中定义或声明符号c,并且v必须是原始TLC值或此类值的有限集合。
配置文件的CONSTANT语句还可以包含形式为c\leftarrow d的替换,其中c和d是TLA +中定义的符号,这将使TLC在执行计算时将c替换为d。 替换的一种用途是为操作符参数赋值。 例如,假设我们要使用TLC检查第5.6节(第54页)的write-through cache specification,WriteThroughCache模块扩展了MemoryInterface模块,该模块包含声明

constants Send (_,_,_,_ ), Reply(_,_,_,_ ),...

我们必须告诉TLC如何计算操作符Send  和 Reply, 我们可以先写一个名为MCWriteThroughCache 的模块,该模块是模块WriteThroughCache的扩展,在其中定义两个操作符

\\MCSend (p, d , old , new) \triangleq \cdots \\ MCReply(p, d , old , new ) \triangleq \cdots

然后,我们将替换内容
\\Send \leftarrow MCSend,\\ Reply \leftarrow MCReply

添加到配置文件的CONSTANT语句中, 替换也可以是一个定义的符号替换另一个。 在specification中,我们通常会编写最简单可行的定义。 对于TLC而言,最简单的定义并不总是最容易用的定义。 例如,假设我们的specification需要一个Sort运算符,则如果S是一个有限的数字集合,则Sort\left ( S \right )是一个按升序排列的包含所有S元素的序列。 我们在SpecMod模块中的规范可以使用如下简单的定义:

Sort(S)\triangleq \mathrm{CHOOSE} \; s \in \left [ 1..Cardinality(S)\rightarrow S \right ]: \par \indent \indent \forall i,j \in \mathrm{DOMAIN}\;s: (i<j)\Rightarrow (s[i]<s[j])

为了计算包含n个元素的集合S的Sort(S ),TLC必须遍历函数集合[1..n \rightarrow S]中的n^{n}个元素, 这可能太慢了, 我们可以编写一个模块MCSpecMod来扩展SpecMod并在其中定义FastSort,以便在应用于有限的数字集时它等于Sort,但可以让TLC计算得更快。 这样我们就可以在包含了替换 Sort <-FastSort的配置文件基础上运行TLC,在14.4节,第250页给出了FastSort的一种可行定义。

 

14.2.4 Evaluating Temporal Formulas(计算时态公式)

14.2.2节(第231页)说明了TLC可以“计算”哪种常规表达式。 TLC可以“检查”的specification和属性是时态公式; 本节描述了它可以处理的时态公式的类别。TLC可以计算满足如下条件的TLA+时态公式

(i)该公式是nice的——参见下一段中定义的术语,且

(ii)TLC可以计算组成该公式的所有常规表达式;

例如,形式为P\leadsto Q是nice的,所以TLC可以计算它当且仅当可以计算P和Q。 (下面的14.3节解释了TLC计算时态公式的组成表达式时涉及哪些状态和状态对。)

时态公式是nice的当且仅当它是以下四类公式的并集:

  1. State Predicate:状态谓词
  2. Invariance Formula: 不变性公式,形如 \square P的公式 , 这里 P 是一个状态谓词.
  3. Box-Action  Formula:  形如 \square \left [A \right ]_{v}的公式 , 这里 A 是一个action, v 是一个 state 函数.
  4. Simple Temporal Formula:为了方便定义这种公式, 我们先引入如下定义:
  • 简单的布尔运算符:由命题逻辑的运算符以及对有限常量集的量化组成:\wedge \quad \vee \quad \neg \quad \Rightarrow \quad \equiv \quad \text { TRUE } \quad \text { FALSE }
  • 简单时间状态公式(A temporal state formula):是通过在状态谓词上应用简单布尔运算符和时态运算符(\square,\diamond,\leadsto而获得的。 例如,如果N为常数,则\forall i \in 1 .. N: \square((x=i) \Rightarrow \exists j \in 1.. i: \diamond(y=j))是时态公式。
  • action公式:\text{WF}_{v} (A) \quad \text{SF}_{v} (A) \quad \square \Diamond[A]_{v} \quad \Diamond \square[A]_{v},其中A是action,而v是状态函数:

\text{WF}_{v} (A) ,\text{SF}_{v} (A)的子表达式有\left [ A \right ]_{v}, \text{ENABLED} \left \langle A \right \rangle_{v}(在第240页描述了ENABLED公式的计算方式), 

这样,就可以将上述第4项Simple Temporal Formula定义为 通过应用简单的布尔运算符,组合了时态公式和简单的action公式而构成的公式。

为方便起见,我们从时态公式的类别中排除不变性公式,则这四类nice的时态公式是不相交的。

这样TLC就可以计算下面的时态公式了:

\forall i \in 1.. N: \diamond(y=i) \Rightarrow \mathrm{WF}_{y}\left(\left(y^{\prime}=y+1\right) \wedge(y \geq i)\right)

如果N是一个常数,因为这是一个简单的时态公式(因此是nice的),TLC可以评估其所有组成部分的表达式。 TLC无法评估\Diamond \left \langle x'=1 \right \rangle _{x},因为这不是一个nice的公式。 TLC也无法评估公式\text{WF} \left \langle x'[1]=0 \right \rangle _{x},因为在step s\rightarrow t,状态t中,如果x = \left \langle \right \rangle,则无法计算\Diamond \left \langle x'[1]=0 \right \rangle _{x}

PROPERTY语句可以设定TLC可以计算的任何公式。 SPECIFICATION语句的公式必须恰好包含一个作为Box-Action公式的合取词。 该合词指定了下一个状态action。

14.2.5 Overriding Modules(模块覆盖)

TLC无法根据标准Naturals模块中包含的“+“定义来计算2 + 2。 即使我们真的使用TLC定义的计算总和的“+”定义,也算的不快。 像+这样的算术运算符可以直接用编写TLC的语言Java来实现。 这是通过TLC的通用机制实现的,该机制允许模块被JAVA类覆盖,该JAVA类实现该模块中定义的运算符。 当TLC遇到extended Naturals语句时,它将加载覆盖Naturals模块的Java类,而不是读取模块本身。 有Java类可以覆盖以下标准模块:Naturals,Integers,Sequences,FiniteSets和Bags。 (下面的第14.4节中描述的TLC模块也被Java类覆盖。)有经验的Java程序员会发现编写Java类来覆盖模块并不难。

14.2.6 How TLC Computes States

TLC评估不变量时,它会计算不变量的值,该值可以为TRUE或FALSE。当TLC评估initial predicate或者 next-state aciton时,它会计算一组状态——对于initial predicate,会计算所有初始状态的集合,而对于next-state action,则是计算从给定的开始状态(unprimed)开始,可能的后继状态(primed状态)的集合。

我将描述TLC如何针对next-state action执行此操作。初始谓词的评估也是类似的。
回想一下,一个state是给变量赋值的操作。 TLC是这样计算给定状态s的后续状态的:先给s状态中所有unprimed(未加  ' ) 的变量赋值,接下来评估next- state action操作,来计算给定状态s的primed值。 TLC按第14.2.2节所述评估下一状态动作。
(第231页),但我会接下来描述两个区别。该描述假定TLC已经执行了配置文件的CONSTANT语句指定的所有赋值和替换,并且已展开了所有定义。因此,下一个状态操作是一个仅包含变量,primed变量,模型值以及内置TLA +运算符和常量的公式。

第一个区别是TLC在评估next-state action时,针对析取词(disjunction or)不是从左至右,相反的,在评估子公式A_{1} \vee \cdots \vee A_{n}时,它将计算分成n个独立的计算,每一个都是独立的子公式A_{i},类似的,在计算\exists x \in S:p时,TLC会对S的每一个元素分别计算。蕴含操作P \Rightarrow Q则是计算它对等的析取操作\lnot P \vee Q, 举个例子:TLC会将公式(A \Rightarrow B) \vee(C \wedge(\exists i \in S: D(i)) \wedge E)拆成3个独立的子公式\lnot A,BC \wedge(\exists i \in S: D(i)) \wedge E分别计算的。在最后的析取计算前,我们首先需要计算C,如果C为TRUE,再对每一个S中的元素,独立计算D(i) \wedge E,对每一个D(i) \wedge E,也是先计算D(i),如果为TRUE,再计算E.

第二个区别是TLC在评估next-state action时,对任意变量x,计算如x'=ex'尚未赋值 这种形式的表达式时,表达式赋值为TRUE,再计算表达式e的值赋给x'。TLC计算表达式x' \in S的对等表达式 \exists v \in S:x'=v. 计算表达式\text{UNCHANGED}\; x的对等表达式:x'=x, 对任意的变量x,评估\text{UNCHANGED} \;\left \left \langle e_{1},\cdots,e_{n} \right \rangle \right时,会对每一个e_{i}f分别计算\text{UNCHANGED} \; e_{1} \wedge \cdots \wedge \text{UNCHANGED} \; e_{n},这样,\text{UNCHANGED} \;\left \langle x, \left \langle y, z \right \rangle \right \rangle也是当做 x'=x \; \wedge y'=y \; \wedge z'=z计算。

除了在评估x'=e这种形式的表达式时,如果遇到尚未赋值的primed变量,TLC会报告错误。 如果合取词的值为假,则评估停止,返回"没有发现状态"。 完成并赋值为TRUE的评估将"找到状态",该状态由分配给primed变量的值确定。 在后一种情况下,如果尚未为某些primed变量分配值,TLC将报告错误。
为了说明这是如何工作的,让我们考虑TLC如何评估next-state action:

(14.4) \begin{aligned} &\vee \wedge \mathrm{x'} \in 1 .. Len(y) \\ &\;\;\;\wedge y'=\text {Append}(\text {Tail}(y), x') \\ &\vee \wedge x'=x+1 \\ &\;\;\;\wedge y'= \text {Append}(y, x') \end{aligned}

我们先考虑起始状态x=1, \quad y=\left \langle 2,3 \right \rangle, TLC先独立计算这2个析取词,首先计算\begin{array}{l}{ \mathrm{x}^{\prime} \in 1 .. \operatorname{Len}(y)} \end{array}, 如上面的说明,TLC计算它等价的\exists i \in 1..Len(y):x'=i, 既然Len(y)=2, 那么TLC将这个式子拆成两个独立的子公式:

(14.5)\\ \wedge x'=1 \\ \wedge y'=\text {Append}\left(\text {Tail}(y), x^{\prime}\right)}     \\ \wedge x'=2 \\ \wedge y'=\text {Append}\left(\text {Tail}(y), x^{\prime}\right)}

TLC计算14.5的第一个action如下:它计算第一个合取词,取值为TRUE,并将x值1赋给x'; 然后,它计算第二个合取词,取值为TRUE,并将值Append(Tail(\left \langle 2,3 \right \rangle),1)分配给y'。 因此,评估(14.5)的第一个action会发现其后续状态是x=1y=\left \langle 3,1 \right \rangle。 类似地,评估(14.5)的第二个动作会发现其后续状态为x=2y=\left \langle 3,2 \right \rangle。 TLC以类似的方式评估(14.4)的第二个析取关系,得到其后续状态是x=2y=\left \langle 2,3,2\right \rangle。 因此,对(14.4)的评估发现了三个后序状态。
接下来,考虑TLC如何在x=1且y等于空序列\left \langle \right \rangle的状态下评估(14.4)下一状态action。 由于Len(y)=01..0是空集\left \{ \right \},TLC将第一个析取项评估为

\begin{array}{l}{\wedge \exists i \in\{\}: x^{\prime}=i} \\ {\wedge y^{\prime}=Append \left(Tail(y), x^{\prime}\right)}\end{array}

评估第一个合取词会产生错误,因此会停止对(14.4)的第一个合取词进行评估,表明没有发现后继状态。 评估第二个析取关系会得其后续状态出x=2y=\left \langle 2 \right \rangle
由于TLC从左到右评估合取,因此它们的顺序会影响TLC是否可以评估下一状态动作。 例如,假设(14.4)的第一个析取语中的两个析取语颠倒了,像这样:

\\{\wedge y^{\prime}=Append \left(\text{Tail}(y), x^{\prime}\right)} \\ {\wedge x^{\prime} \in 1..Len(y)}

当TLC评估此action的第一个合取词时,它在将值赋给x'之前先遇到表达式\\{ y^{\prime}=Append \left(\text{Tail}(y), x^{\prime}\right)},因此它会报告错误。此外,即使我们将x'更改为x,TLC仍无法评估以y=\left \langle \right \rangle为起始状态的动作,因为在评估第一个合取词时,它将遇到Silly表达式Tail\left (\left \langle \right \rangle \right )
上面给出的关于TLC如何评估任意next-state action的描述足以解释它在几乎所有实际情况下如何工作的。但是,它并不完全准确。例如,按字面解释,这意味着TLC可以处理以下两个next-state actions, 它们在逻辑上均等价于(x' = \text{TRUE}) \wedge(y' = 1)

(14.6) \left(x^{\prime}=\left(y^{\prime}=1\right)\right) \wedge\left(x^{\prime}=\mathrm{TRUE}\right) \quad \text { IF } \quad x^{\prime}=\text { TRUE THEN } y^{\prime}=1 \text { ELSE FALSE }
实际上,TLC在处理这些异常的next-state actions时都将产生错误消息。
请记住,TLC通过使用类似评估初始谓词的方式来计算初始状态,与其从有初始值的unprimed变量开始,再将其赋值给primed变量,不如直接赋值给unprimed变量。

TLC评估ENABLED公式的方式基本上与评估next-state action的方式相同。更准确地说,要评估\text{ENABLED}\;A的公式,TLC会计算其后继状态,就好像A是next-state action一样。如果存在后继状态当且仅当公式的计算结果为TRUE。为了检查步骤s\rightarrow t是否满足action A和B的合成action A \cdot B,TLC首先计算所有状态u,以使s\rightarrow u是A step,然后再检查u\rightarrow t是否是针对某些此类u的B step。
TLC在检查属性时可能也需要评估action,在这种情况下,它会像评估其他表达式一样评估action,并且即使评估类似(14.6)的奇怪action也毫不费力。

14.3 How TLC Checks Properties

上面的14.2节说明了TLC如何计算表达式以及计算初始状态和后继状态。本节描述TLC如何使用评估检查属性——首先用于模型检查模式(默认),然后用于仿真模式。
首先,让我们定义一些从配置文件中获得的公式,在这些定义中,specification conjunct是SPECIFICATION语句(如果有)命名的公式的合取词,
property conjunct是以PROPERTY语句命名的公式的合取词,而空公式集的合取值是定义为TRUE。这些定义使用了上面第235页的14.2.4节中定义的四类nice的时态公式。

  • INIT specification的初始状态谓词。它由INIT或SPECIFICATION语句指定。在后一种情况下,是由所有都是状态谓词的specification conjunct的联合(合取)。
  • Next specification的next-state action。它由NEXT语句或SPECIFICATION语句指定。在后一种情况下,specification中需要存在形式为\square[A]_{v}的specification conjunct, 其中A为action,这样的合取词不得超过一个。
  • Temporal specification的合取操作,既不是状态谓词,也不是盒式公式。通常是specification的Liveness条件。
  • Invariant  每个状态谓词 I的合取,这些状态谓词I由INVARIANT语句命名,或者是某些等于\square I的属性合词
  • ImpliedInit 是每个为状态谓词的property conjunct的合取。
  • ImpliedAction 每个action[[A]_{v}的合取,存在property conjunct等于\square[A]_{v}
  • ImpliedTemporal 每个property conjunct的合取,是简单的时态公式,但不具有\square I的形式,其中I是状态谓词。
  • Constraint 由CONSTRAINT语句命名的所有状态谓词的合取。
  • ActionConstraint 由ACTION-CONSTRAINT语句命名的所有action的合取。action约束与普通约束类似,不同之处在于它消除了可能的转换而不是类似state(状态有可能不是最终TLC计算的形式,需要经常预处理)。普通约束P等效于action约束P'

 14.3.1 Model-Checking Mode

TLC有两种数据结构,一个以state为节点组成的图\mathcal{G},一个由states组成的序列\mathcal{U}\mathcal{G}的一个state说的是\mathcal{G}的一个节点。图\mathcal{G}是TLC迄今为止所有可达状态图的一部分, \mathcal{U}包含\mathcal{G}中states, 这些states还有后续状态未被TLC计算。TLC在计算过程中一直满足如下不变量:

  • \mathcal{G}的state满足Constraint谓词;
  • \mathcal{G}中的每一个状态s, 边s\rightarrow s也在\mathcal{G}中;
  • 如果\mathcal{G}中有一个从状态s到不同状态t的边,那么t是s的后继状态,它满足ActionConstraint。 换句话说,步骤s\rightarrow t满足Next \wedge ActionConstraint;
  • \mathcal{G}中的每一个状态 ,\mathcal{G}内都存在一条从初始状态(满足Init谓词)到这个状态的路径;
  • \mathcal{U}是由\mathcal{G}中不同状态的节点组成的序列;
  • 对于每一个在\mathcal{G}中而不在\mathcal{U}中的状态s,对每一个满足Constraint,从而使s\rightarrow t满足Next \wedge ActionConstraint的状态t,状态t和s\rightarrow t的边也在\mathcal{G}中;

TLC执行如下算法,起始条件是\mathcal{G}\mathcal{U}都为空:

  1. 检查赋给常量参数的值是否满足specification中的所有ASSUME假设;
  2. 如上文第14.2.6节所述,通过评估初始谓词Init来计算初始状态集。 对于找到的每个初始状态s:

            (a)对状态s,计算InvariantImpliedInit谓词,如果任一值为FALSE,则报错并停止;

            (b)对状态s,如果Constraint谓词为TRUE,则将s加入序列\mathcal{U},并在\mathcal{G}中加入节点s和边s\rightarrow s

  3. \mathcal{U}为非空序列,按如下操作:

        (a)从\mathcal{U}中移除第一个state s

        (b)如上14.2.6所述,计算以s为起始状态的next-state action所有可能的后续state,组成集合T

        (c)如果T为空,且deadlock选项未被选中,则报告一个deadlock死锁,并停止;

        (d)对T中的每个状态t,执行如下操作:

            (i)如果Invariant对状态t为FALSEImpliedInit对step s\rightarrow t为FALSE,则报错并停止;

            (ii)如果谓词Constraint对状态t为TRUE,并且step s\rightarrow t满足约束ActionConstraint,则

                A.如果t不在\mathcal{G}中,则将其加入到\mathcal{U}的尾部,并将node t 和边t\rightarrow t加入\mathcal{G}

                B.将边s\rightarrow t加入\mathcal{G}.

TLC可以在多线程下运行,step 3(b)-(d),对不同的状态s,在不同的线程上并发运行。参见253页有关worker选项的描述。

如果公式ImpliedTemporal不等于TRUE,则只要在上述过程中加入边s\rightarrow t,TLC都会为step s\rightarrow t计算出出现公式Temporal和ImpliedTemporal中的所有谓词和action(在加入任何边的时候,包括在2(b)和3(d)ii.中所示的自循环s\rightarrow st\rightarrow t,都执行上述操作);

在周期性计算和结束计算\mathcal{G}时,TLC按如下方式检查ImpliedTemporal属性:

设定\mathcal{T}是由每一个满足如下条件的behavior τ组成的集合,τ是一个状态序列, 该序列从初始状态开始,是\mathcal{G}中一条无限的state路径,举例来说,对\mathcal{G}中每一个初始状态s,\mathcal{T}包含路径s\rightarrow s\rightarrow s \rightarrow \cdots). 注意\mathcal{T}中的每个behavior都满足公式Init\wedge \square[Next]_{vars}. TLC也检查是否\mathcal{T}中的每个behavior都满足Temporal\Rightarrow ImpliedTemporal.(这只是概念上发生,实际上TLC不会分开检查每个behavior)。参见后续Section14.3.5,第247页的讨论:为什么TLC不会如你所期望地检查ImpliedTemporal属性。

只有在所有可达states集合是有限时,对\mathcal{G}的计算才会终止,否则,TLC会永远运行下去,直到资源耗尽或者手动停止。

TLC并不总是执行如上所述的3个步骤。只有在检查一个没有常量的模型的时候才会执行步骤2,此种情况配置文件必须指定一个Init公式。只有当配置文件中指定Next公式时,TLC才会执行步骤3,如果它指定了Invariant,ImpliedAction或ImpliedTemporal公式,则必须执行步骤3。

14.3.2 Simulation Mode

在仿真模式下,TLC重复构造并检查有最大长度限制的各个behavior。可以使用depth选项指定最大长度,如下面第251页所述。 (其默认值是100个状态。)在模拟模式下,TLC一直运行直到停止。
为了创建和检查一个behavior,TLC使用上面描述的过程来构造图
\mathcal{G}——但有以下区别:在计算了初始状态的集合,并且在计算了状态s的后继集合
\mathcal{T}之后,TLC会随机选择该集合的元素。如果元素不满足约束条件,则停止计算。否则,TLC仅将该状态放入\mathcal{G}\mathcal{U},并检查Invariant和ImpliedInit或ImpliedAction公式。 (实际上不维护队列\mathcal{U},因为它永远不会包含多个元素。)当生成的状态数达到指定的最大状态数时,\mathcal{G}的构造停止,并检查Temporal隐含ImpliedTemporal公式。然后,从\mathcal{G}\mathcal{U}为空开始,TLC重复该过程。
TLC的选择不是严格随机的,而是使用伪随机数生成器从随机选择的种子生成的。如果TLC发现错误,则会打印出种子和另一个称为aril的值。如下文第14.5.1节所述,使用key和aril选项,您可以让TLC按你指定的方式显示错误消息。

14.3.3 Views and Fingerprints

在上面关于TLC如何检查属性的描述中,我写道图\mathcal{G}的节点是状态。那不是很正确。 \mathcal{G}的节点是称为view的状态函数的值。 TLC的默认视图是所有已声明变量的元组,state由其值确定。但是,您可以通过在配置文件中添加以下语句,将视图指定为其他状态函数myview:

VIEW myview

其中myview是已定义或声明为变量的标识符。
当TLC计算初始状态时,它将其view而不是状态本身放在\mathcal{G}中。(状态s的视图是状态s中VIEW状态函数的值。)如果存在多个具有相同view的初始状态,则仅将其中一个放入队列\mathcal{U}中。 TLC不是将边s\rightarrow t,而是将 从s的view到t的view的边插入图\mathcal{G}。在上述算法的步骤3(d)ii.A中,TLC检查t的视图是否在\mathcal{G}中。
使用默认视图以外的view时,TLC可能会在找到所有可达状态之前停止。对于其执行的状态,它会正确执行safety检查,即Invariant,ImpliedInit和ImpliedAction检查。此外,如果在这些属性之一中发现错误,则会打印出正确的反例(状态的有限序列)。但是,它可能会错误地检查ImpliedTemporal属性。因为TLC正在构造的图\mathcal{G}不是实际的可达性图,所以当(??)不存在时,它可能会报告ImpliedTemporal属性中的错误,从而打印出虚假的反例。
指定非标准view可能导致TLC不检查许多状态。当不需要检查具有相同视图的不同状态时,应执行此操作。最有可能的替代view是一个由一些但不是全部声明的变量组成的元组。例如,您可能添加了一个或多个变量来帮助调试规范。使用原始变量的元组作为视图,可以在不增加TLC必须探索的状态数量的情况下添加调试变量。如果检查的属性未提及调试变量,则TLC将查找原始规范的所有可到达状态,并将正确检查所有属性。
在实际的实现中,图的节点不是状态的view,而是这些view的fingerprint。 TLC指纹是由“哈希”功能生成的64位数字。理想情况下,两个不同视图具有相同指纹的概率为2^{-64},这是一个非常小的数字。但是,有可能发生冲突,这意味着TLC错误地认为两个不同的view是相同的,因为它们具有相同的指纹。如果发生这种情况,TLC将不会探索其应查看的所有状态。特别是,使用默认view时,TLC将报告它已经检查了所有可到达的状态,实际上可能不是。
终止时,TLC打印出两个fingerprint发生冲突的估算概率。第一是基于这样的假设:两个不同view具有相同fingerprint的的概率为2^{-64}。(在此假设下,如果TLC生成了n个具有m个不同fingerprint的view,则发生碰撞的概率约为m*(n-m)*2^{-64}.)但是,生成状态的过程是高度非随机的,没有已知的fingerprint方案可以保证TLC中两个不同状态生成相同fingerprint的概率实际上为2^{-64}。因此,TLC还打印了一个发生碰撞的可能性的实证估计。根据观察,如果发生碰撞,则很可能还会有“near miss”。估计对TLC生成的不同fingerprint\left \langle f_{1},f_{2} \right \rangle对,发生碰撞概率的最大值是1/\left | f_{1}-f_{2} \right |。实际上,除非TLC产生数十亿个不同的状态,否则碰撞的可能性非常小。
view和fingerprint仅适用于模型检查模式。在模拟模式下,TLC忽略任何VIEW语句。

14.3.4 Taking Advantage of Symmetry

第5章的内存specification在处理器 Proc集合中是对称的。 直观上,这意味着对处理器进行排列并不会更改behavior是否满足specification。 为了更精确地定义对称性,我们首先需要一些定义。
有限集S的排列是一个函数,其域和范围都等于S。 换句话说,π是S 的一个排列当且仅当

(S=\mathrm{DOMAIN}\; \pi) \wedge(\forall w \in S: \exists v \in S: \pi[v]=w)

一个permutation是一个函数,这个函数是它的值域(有限)的一个排列。如果π是集合S元素的一个排列而s是一个状态,π函数将s中属于集合S的值v用π[v]代替形成新的state,记做s^{\pi}。为了更形象得了解s^{\pi}的含义,可以看这个例子:对集合\left \{ "a","b","c" \right \},π排列如下:\pi["a"]="b",\pi["b"]="c",\pi["c"]="a", 在状态s,变量x和y的值如下:

\\x= \left \langle "b","c","d" \right \rangle\\ y=[i \in \left \{ "a","b" \right \}x \mapsto \text{IF}\; i="a" \;\text{THEN}\;7 \;\text{ELSE}\;42]],则在状态s^{\pi},变量x和y的值如下:

\\x= \left \langle "c","a","d" \right \rangle\\ y=[i \in \left \{ "b","c" \right \}x \mapsto \text{IF}\; i="b" \;\text{THEN}\;7 \;\text{ELSE}\;42]]

上面例子可以给你一个s^{\pi}直观的印象,我不打算给出一个严格的定义:如果\sigma是一个behavior s_{1},s_{2},\cdots ,\sigma^{\pi}记做s_{1}^{\pi},s_{2}^{\pi},\cdots

现在我们可以在这个基础上定义对称性了,规约 Spec对于某个排列π具有对称性当且仅当下述满足下列条件:对任意behavior \sigma\sigma满足公式Spec当且仅当 \sigma^{\pi}也满足Spec。

第5章的内存specification对于Proc的排列是对称的,这也意味着如果TLC对Proc的一个排列π检查过behavior \sigma^{\pi},那就没有必要再检查behavior \sigma了(因为在\sigma上发现错误也会出现在\sigma^{\pi}中)。我们可以将下面语句加入配置文件,以让TLC利用这个对称特性:

SYMMETRY Perms

这里Perms是在模块中定义的Proc的所有排列的集合Permutations(Proc),(Permutations操作符在TLC模块中定义,参见下面Section14.4)。SYMMETRY语句让TLC修改P241-242的算法如下:如果对Proc的一个排列π,如果s^{\pi}已经在队列\mathcal{U}和图\mathcal{G}中,则不需要再将s放入其中。如果有n个process进程,则我们可以减少待检查的状态数到n!

第5章的内存specification对于内存地址Adr的排列也是对称的。我们可以像之前process排列一样利用这个对称性,定义对称集合(由SYMMETRY语句指定):

Permutations(Proc) \cup Permutations(Adr) 

一般来说,SYMMETRY语句可以指定任意对称集合Π,Π内任意元素都是model value集合的一个排列。更精确的是,Π的每个元素π,都是由配置文件CONSTANT语句指定的model value组成的集合的一个排列(如果配置文件中未含SYMMETRY语句,则对称集Π为空集

为了解释对给定一个任意的对称集Π,TLC会如何操作,我先引入一些定义:如果τ是一个由Π中排列组成的序列 \left \langle \pi1,\cdots,\pi n \right \rangle,令

s^{\tau}=\left ( \cdots \left (\left ( s^{\pi 1} \right ) ^{\pi 2} \right ) \cdots \right )^{\pi n}(如果τ是空集,则s^{\tau}=s)定义\widehat{s}是状态s的等价类,是由Π中所有排列组成的所有序列s^{\tau}的集合,对任意状态s,TLC只在队列\mathcal{U}和图\mathcal{G}中,保留一个\widehat{s}中的元素。下面是对241-242页,step2(b)算法的修改:只在队列\mathcal{U}和图\mathcal{G}中没有包含任意\widehat{s}中的元素时,TLC才将状态s加入队列\mathcal{U}和图\mathcal{G}中。step3(d)ii也被修改为如下条件:

A.如果\widehat{t}中没有元素在\mathcal{G}内,则将t加入到\mathcal{U}的队尾,将节点t和边t\rightarrow t加入\mathcal{G}

B.将边s\rightarrow tt加入\mathcal{G},这里tt是\widehat{t}中现在唯一在\mathcal{G}中的元素;

当VIEW语句出现在配置文件中时,将按照上面的14.3.3节中的描述修改这些变更,以便将view而不是state放入\mathcal{G}
如果被检查的specification和属性 确实相对于对称集中的所有排列对称,则TLC的Invariant,ImpliedInit和ImpliedAction检查
将发现并正确报告如果省略SYMMETRY语句将会发现的任何错误。但是,TLC可能会错误地执行ImpiredTemporal检查、遗漏错误、报告不存在的错误或通过不正确的反例报告实际错误。因此,仅当您完全了解TLC在做什么时,才可以执行ImpliedTemporal检查时使用SYMMETRY语句。
如果specification和属性相对于对称集中的所有排列不是对称的,则TLC如果确实发现错误,则可能无法打印错误跟踪。在这种情况下,它将打印错误消息:

Failed to recover the state from
its fingerprint.

对称集仅在模型检查模式下使用。 模拟模式下TLC会将其忽略。

14.3.5 Limitations of Liveness Checking

如果某specification违反了safety属性,会有一个有限模型生成的behavior来反应该情况。因此,原则上可以通过TLC发现违反情况。用任何有限模型都不可能发现对liveness属性的违反。要了解为什么,请考虑以下简单specification EvenSpec,该specification中x初始化为0,并每次递增2:
\text { EvenSpec } \triangleq(x=0) \wedge \square\left[x^{\prime}=x+2\right]_{x} \wedge \mathrm{WF}_{x}\left(x^{\prime}=x+2\right)

显然,在满足EvenSpec的任何行为中x都不等于1。因此,EvenSpec不满足活度属性\Diamond(x=1)。假设我们要求TLC检查EvenSpec是否蕴含\Diamond(x=1)。为了使TLC终止,我们必须提供一个约束,将其限制为仅生成有限数量的可达状态。然后,TLC生成的所有满足(x=0) \wedge [x' = x + 2]_{x}的无限行为都将以无数个重复步骤结束。在任何此类行为中,Action x' = x + 2 总是处于使能状态,但仅发生有限数量的x' = x + 2个步骤,因此\textrm{WF}_{x}(x' = x + 2)为假(参见WeakFairness的定义,必须有无限数量的x' = x + 2才能为TRUE)。因此,TLC将不会报告错误,因为公式\textrm{WF}_{x}(x' = x + 2)\Rightarrow \Diamond(x=1)被它产生的所有无限行为所满足。
在进行时态检查时,请确保您的模型将允许生成满足specification liveness条件的无限behavior。例如,考虑由第227页的图14.3的配置文件定义的alternating bit protocol specification的有限模型。您应该确信,它允许满足公式ABFairness的无限行为。
验证TLC是否正在执行您“期望”的liveness检查是个好主意,应确保在检查时,不满足liveness属性的specification都会报告错误。

14.4 The TLC Module

此页面上的图14.5中的标准TLC模块定义了使用TLC时方便使用的运算符。TLC运行的模块通常会EXTENDS TLC模块,该模块会被其Java实现所覆盖。
模块TLC以如下语句开始:

LOCAL INSTANCE Naturals
如第171页中所述,这类似于EXTENDS 语句,不同的是,任何其他EXTENDS或INSTANCE模块TLC的模块都无法获取Naturals模块中包含的定义。同样的,下一条语句在本地实例化Sequences模块。
接下来,模块TLC定义了三个运算符Print,Assert和JavaTime。它们仅在运行TLC调试模块时有用,可以帮助您查找跟踪问题。
定义运算符Print,使Print(out,val)等于val。但是,当TLC评估此表达式时,它将打印out和val的值。您可以将打印表达式添加到specification中以帮助定位错误。例如,如果您的specification包含
\\ \wedge Print("a",TRUE) \\ \wedge P\\ \wedge Print("b",TRUE)\\
并且TLC在报告错误之前打印“a”而不打印“b”,然后在TLC评估P时发生错误。如果您知道错误在哪里但不知道为什么会发生,则可以添加Print表达式,以向您提供有关TLC计算值的更多信息。
要了解什么时候打印什么,您必须知道TLC如何计算表达式,这在14.2和14.3节中有解释。 TLC通常会对表达式进行多次评估,因此在specification中插入Print表达式会产生大量输出。限制输出量的一种方法是将Print表达式放在IF/THEN表达式内,因此仅在感兴趣的情况下执行。
接下来,TLC模块定义运算符Assert,如果val等于TRUE,则Assert(val, out)等于TRUE。如果val不等于TRUE,则评估Assert(val, out)会使TLC打印out的值并停止。 (在这种情况下,Assert(val, out)的值无关紧要)。
接下来,将运算符JavaTime定义为等于任意自然数。但是,TLC在评估时不遵循JavaTime的定义。取而代之的是,对JavaTime进行评估时才会得出进行评估的时间,以自1970年1月1日世界标准时间00:00以来经过的毫秒数为单位,再模2^{31}。如果TLC生成状态的速度很慢,则将JavaTime运算符与Print表达式结合使用可以帮助你明白为什么会这么慢。如果TLC花费太多时间评估一个运算符,则可以换一个等价的更有效率的的运算符。 (请参阅第234页的14.2.3节。)
接下来,TLC模块定义运算符:>和@@,表达式d_{1}:> e_{1} \;@@ \cdots @@ \;d_{n}:> e_{n}是一个值域为\left \{ d_{1},\cdots,d_{n} \right \}, 且对 i=1,\cdots,nf\left [ d_{i} \right ]=e_{i}的函数。例如,序列\left \langle "ab","cd" \right \rangle,是一个值域为\left \{ 1,2 \right \}的函数,可以写成1:>"ab"\; @@ \; 2:"cd"
TLC使用这些运算符来呈现 待打印的函数值或者报告一个错误,不过,习惯上一般以在specification中出现的方式打印值,因此通常将序列打印为序列,而不是使用:>和@@运算符。
接下来,如果S是有限集,则将Permutations(S)定义为S的所有排列的集合。可以使用Permutations运算符为上面第14.3.4节中描述的SYMMETRY语句指定一组排列。可以通过定义一个集合\left \{ \pi_{1},\cdots,\pi_{n} \right \}来使用更复杂的对称性,集合中每一个\pi_{i}都是一个显式函数,可以用:>,和@@运算符编写。例如,考虑一个存储系统的specification,其中每个地址都以某种方式与处理器相关联。该specification在两种排列下是对称的:一种排列是与同一处理器关联的地址,另一种排列是与一组地址有关联的处理器。假设我们告诉TLC使用两个处理器和四个地址,其中地址a11和12与处理器p1相关联,并且地址a21和a22与处理器p2相关联。通过为TLC提供以下以下排列组合作为对称集,可以使TLC充分利用对称性:
\\Permutations(\left \{a11,a1 \right \})\; \cup\; {p1:> p2 @@ p2:> p1 \\ @@ a11 :> a21 @@ a21 :> a11 \\ @@ a12 :> a22 @@ a22 :> a12}
排列p1:> p2 @@ \cdots @@ a22:> a12交换处理器及其关联的地址。只是互换a21和a22的排列不需要明确指定,因为它是通过交换处理器,交换a11和a12并再次交换处理器获得的。
TLC模块通过定义运算符SortSeq结束,它可以用于将运算符替换为更有效率的TLC运算符。如果s是有限序列,\prec是其上的完整排序关系(排序算子)SortSeq(s,\prec)是s经过\prec排序得到的新序列。举例来说,SortSeq(<3,1,3,8>,>)等于<8,3,3,1>. SortSeqde的 JAVA实现让TLC更有效率地实现排序算法。举例如下:下面是我们用SortSeq定义一个FastSort操作符去替换235页定义的Sort操作符:

\\ \text { FastSort(S) } \triangleq} \\ \indent {\text {LET } \operatorname{MakeSeq}[S S \in \text { SUBSET } S] \triangleq} \\ \indent \indent \text {IF } SS=\{\} \\ \indent \indent \text {THEN }\langle\rangle \\ \indent \indent \text {ELSE LET } s s & \triangleq \text { CHOOSE } s s \in S S: \text { TRUE } \\ \indent \indent \indent \indent \text {IN } \text { Append }(MakeSeq[SS \backslash\{s s\}], \text { ss }) \\ \indent \text {IN } \text { SortSeq(MakeSeq }[S],<)

14.5 How to Use TLC

14.5.1 Running TLC

究竟如何运行TLC取决于您所使用的操作系统以及如何配置。您可能会在命令行模式键入以下格式的命令
program_name options spec_file
这里program_name 取决于操作系统,可能是java tlatk.TLC。
spec_file是包含TLA + specification文件的名称。每个TLA +模块对应一个单独的文件,例如模块M对应的文件名为M.tla。也可以省略扩展名.tla。

options是由零个或多个以下选项组成的序列:

-deadlock
告诉TLC不要检查死锁。 除非指定此选项,否则TLC将在发现死锁(即无后续的可到达状态)时停止。

-simulate
告诉TLC以仿真模式运行,其中CHOOSE语句生成随机的behavior,而不是生成所有可达状态。 (请参阅以上第14.3.2节)

-depth num
此选项使TLC在模拟模式下生成最大长度为num的behavior。 如果没有此选项,TLC将生成最多100个行程。只有当使用Simulation选项时,此选项才有意义。

-seed num
在仿真模式下,TLC生成的behavior序列是由提供给伪随机数生成器的初始种子确定。 通常,种子是随机生成的。 此选项使TLC将种子设为num,该种子必须为-2^{63} \rightarrow 2^{63}-1之间的整数。
以相同的seed和aril(在下面的aril选项)为初始值在模拟模式下运行TLC两次将产生相同的结果。 仅当使用Simulation选项时,此选项才有意义。

-aril num

此选项使TLC在模拟模式下将num用作aril。 aril是最初种子的变种。 当TLC在模拟模式下发现错误时,它会同时打印出初始种子和一个编号。 使用此初始种子和aril将导致生成的第一个跟踪是该错误跟踪。 添加Print表达式通常不会改变TLC生成跟踪的顺序。 因此,如果跟踪没有告诉您出了什么问题,可以尝试仅对该跟踪再次运行TLC以打印出其他信息。

-coverage num

此选项使TLC每num分钟并在执行结束时打印“coverage ”信息。 对于每个为变量赋值的action 联合(∧)TLC会打印在构造新状态时实际使用该联合的次数。 打印的值可能不准确,但是其大小可以提供有用的信息。 特别是,值0表示从未“执行”next-state action一部分。 这可能表明specification中存在错误,或者可能意味着TLC正在检查的模型太小而无法执行那部分操作。

-recover run_id

该选项使TLC不是从头开始执行specification,而是从最后一个检查点的位置开始执行。 TLC执行到该检查点时,将打印运行标识符。 (在执行TLC时该标识符是相同的。)run_id的值应为该运行标识符.

-cleanup

TLC在运行时会创建许多文件。 完成后,它们将被全部删除。 如果TLC发生错误,或者在错误结束之前将其停止,则TLC可能会留下一些大文件。 清理选项使TLC删除先前运行创建的所有文件。 如果当前正在同一目录中运行另一个TLC副本,则不要使用此选项。 如果这样做,可能会导致其他副本失败.

-difftrace num

当TLC发现错误时,它将打印错误跟踪。 通常,该trace被打印为一系列完整的状态,对每一个状态,都列出了所有已声明变量的值。 diff跟踪选项使TLC打印每个状态的简化版本,仅列出其值与先前状态不同的变量。 这样可以更轻松地查看每个步骤中发生的情况,只是会比较难于找到完整状态.

-terse

通常,TLC会完全展开出现在错误消息中或Print表达式输出中的值。 terse的选项使TLC改为打印这些值的部分或较短版本.

-workers num

可以在多处理器计算机上使用多个线程加快第241-242页上描述的TLC执行算法的步骤3(b)-(d)。 该选项使TLC在查找可达状态时使用num个线程。 使用比计算机上实际处理器更多的线程没有意义。 如果省略该选项,则TLC使用单个线程。

-config config_file

指定配置文件名为config_file,必须是扩展名为.cfg的文件。 扩展名 .cfg可以从配置文件中省略。 如果省略此选项,则假定配置文件具有与指定文件相同的名称,只是扩展名不同.

-nowarning

有TLA +表达式在语法上合法的,但实际运行中可能出现错误。 例如,如果v不是f的值域的元素,则表达式 [f except ![v ] = e] 可能不正确。 (在这种情况下,该表达式仅等于f)TLC在遇到这种不太可能出错的表达式时通常会发出警告。 此选项禁止显示这些警告. 

 

14.5.2 Debugging a specification

编写specification时,它通常包含错误。 运行TLC的目的是找到尽可能多的错误。 我们希望specification中的错误将导致TLC报告错误。 调试的挑战是在specification中查找导致TLC报告错误的错误。 在解决这个挑战之前,让我们先检查一下在没有错误的时候,TLC的正常输出:

TLC's Normal Output

运行TLC时,第一行打印是版本号和创建日期::

TLC Version 2.12 of 26 May 2003

在你需要上报TLC的任何问题的时候,首先包含这条信息。接下来,TLC描述它的运行模式:模型检查 或者 模拟模式,模型检查模式输出

Model-checking

这种模式会穷尽所有可达状态,模拟模式可能输出:

Running  Random  Simulation  with  seed  1901803014088851111,

1901803014088851111是初始种子,在251-252页有说明。

假设我们现在的运行模式是模型检查,如果我们让TLC做liveness检查,会有如下输出:

Implied-temporal  checking--relative complexity = 8.

TLC用于liveness检查的时间大约与相对复杂度成正比。 即使相对复杂度为1,检查活动性也要比检查安全性花费更长的时间。 因此,如果相对复杂度不小,除非模型非常小,否则TLC可能需要很长时间才能完成。 在仿真模式下,很大的复杂度意味着TLC将无法仿真很多行为。 相对复杂度取决于子项的数量和时态公式中要量化的集合的大小。
TLC接下来打印一条消息,例如

Finished computing initial states:

4 states generated, with 2 of them distinct.

这表明,在评估初始状态时,TLC生成了4个状态,其中有2个不同的状态。 然后,TLC打印一个或多个消息,例如

Progress(9): 2846 states generated, 984 distinct states found. 856 states left on queue.

此消息表明,TLC到目前为止已构建了一个状态图G,其直径为9,它已生成并检查了2846个状态,发现984个不同的状态,并且未探索状态的队列包含856个状态。 运行一段时间后,TLC大约每五分钟生成一次这些进度报告。 对于大多数specification,队列的状态数在执行开始时单调增加,在结束时单调减少。 因此,进度报告为执行可能需要多长时间提供了有用的指导。

注:(G的直径是满足如下条件的最小的d,即从一个初始状态出发,走遍G图上所有可达状态的最少步数,这也是TLC在对状态集进行广度优先探索时所达到的深度。 当使用多线程(由worker选项指定)时,直径TLC报告可能不太正确。)

当TLC成功结束,会有打印

Model checking completed. No error has been found. 

接下来可能会有如下打印:

Estimates of the probability that TLC did not check all reachable states because two distinct states had the same fingerprint:

calculated (optimistic): .000003

based on the actual fingerprints: .00007

如第244页所述,这是TLC对fingerprint概率的两个估计。 最后的打印如下:

2846 states generated, 984 distinct states found,

0 states left on queue.

The  state graph has diameter 15

上面打印输出总的状态数和状态图的直径。

TLC在运行时还可能输出:

-- Checkpointing run states/99-05-20-15-47-55 completed

这表明它已经设置了一个检查点,如果计算机发生故障,您可以使用该检查点来重新启动TLC。 (如第260页的14.5.3节所述,检查点还具有其他用途。)运行标识符states/99-05-20-15-47-55与restore选项一起使用,可以从检查点所在的位置重新启动TLC。 如果仅看到此打印消息的一部分,则是由于TLC接管检查点时您的计算机崩溃了—所有检查点都被破坏的可能性很小,如果这样您必须从头开始重新启动TLC。

Error Reports

一般在specification中发现的第一个问题可能是语法错误。 TLC提示:

ParseException in parseSpec:

接下来是语法分析器生成的错误消息。 第十二章描述了如何解析分析器给出的错误消息。 边写specification边解析会迅速捕获许多简单的错误。
如上文第14.3.1节所述,TLC执行三个基本阶段。 在第一阶段,它检查假设。 在第二步中,它计算初始状态; 第三,在未探索状态队列中生成状态的后继状态。 您可以通过以下方式判断它是否已进入第三阶段是否已打印“已计算初始状态”消息。

当发现正在检查的属性之一不成立时,就会触发TLC直接推送错误报告。 假设我们将不变式ABTypeInv的第一个联合替换为\wedge msgQ \in Seq(Data)引入错误, 参见在alternating bit specification(第223和224页的图14.1), TLC会快速找到这个错误并打印

Invariant ABTypeInv is violated

接下来会打印一个最小长度的behavior,其 state 不满足不变式ABTypeInv,behavior打印如下:

STATE 1: <Initial predicate>

/\ rBit = 0

/\ sBit = 0

/\ ackQ  =  << >>

/\ rcvd  = d1

/\ sent  = d1

/\ sAck = 0

/\ msgQ = << >>

 

STATE 2: <Action at line 66 in AlternatingBit>

/\ rBit = 0

/\ sBit = 1

/\ ackQ  =  << >>

/\ rcvd  = d1

/\ sent  = d1

/\ sAck = 0

/\ msgQ = << << 1, d1 >> >>

 

TLC将每个状态打印为确定该状态的TLA +谓词。 打印状态时,TLC使用TLC模块中定义的运算符:>和@@描述功能。 (请参阅第248页的14.4节。)

定位最困难的错误通常是在TLC被迫评估它无法处理的表达式时遇到的,或者是“silly”的错误,因为TLA +的语义未指定其值。 例如,让我们通过将Lose定义中的第二个合取词替换为alternating bit protocol ,将典型的“ off-by-one”错误引入alternating bit protocol 中:

\\ \exists i \in 1.. Len(q) :\\ \indent q' = [j \in 1 .. (Len(q) -1) \mapsto \textrm{IF} \: j < i \; \boldsymbol{\textrm{THEN }}q[j-1] \\ \indent \indent \indent \indent \indent \indent \indent \indent \indent \boldsymbol{\textrm{ELSE }}\;q[j]]

如果q的长度大于1,将Lose(q)[1]定义为等于q[0],如果q是序列,则这是一个无意义的值。 (序列q的值域是集合1 .. Len(q),其中不包含0),运行TLC会生成错误消息:

Error: Applying tuple

<< << 1, d1 >>, << 1, d1 >> >>

to integer 0 which is out of domain.

然后打印出导致错误的behavior。 TLC在评估下一状态动作以计算某些状态s的后继状态时会发现错误,并且s是该行为中的最后一个状态。如果在评估不变式或蕴含action时发生了错误,则TLC会在behavior的最后状态或步骤对其进行评估。
最后,TLC打印错误的位置:

The error occurred when TLC was evaluating the nested expressions at the following positions:

  1. Line 57, column 7 to line 59, column 60 in AlternatingBit
  2. Line 58, column 55 to line 58, column 60 in AlternatingBit

第一个位置标识“Lose”定义的第二个合取词;第二个标识表达式q [j-1]。这告诉您在TLC评估q [j-1]时发生了错误,这是对Lose定义的第二个合取项进行评估的一部分。您必须从打印的跟踪中推断出它是在评估action LoseMsg包含的Lose的定义时发生的错误。通常,TLC打印一棵嵌套表达式的树,最高层在最上面。TLC很少会像您期待的那样精确地定位错误,通常,它只是将其范围缩小到一个公式的合取或析取部分。您可能需要插入打印表达式以查找问题。有关定位错误的更多建议,请参见第259页的讨论。

14.5.3 Hints on Using TLC

Start Small

约束和对常量参数的赋值定义了specification模型。 TLC检查specification需要多长时间取决于specification和模型的大小。 TLC在600MHz工作站上运行,每秒可为alternating bit protocol specification找到大约700个不同的可达状态。对于某些specification,TLC生成状态所花费的时间随着模型的大小而增加。随着生成状态变得更加复杂,它也会增加。对于某些更大型一点的specification,TLC每秒发现的可达状态可能少于一个。
您应该始终从一个很小的模型开始测试
specification,TLC可以快速检查该模型。让一组process和data只有一个元素。让队列的长度为1。未经测试的specification可能会有很多错误。小型模型将迅速捕获大多数简单错误。当非常小的模型没有发现更多错误时,您可以对更大的模型运行TLC,以尝试捕获更多的细微错误。
弄清TLC可以处理多大模型的一种方法是根据参数估算可达状态的大约数量。但是,这可能很难。如果您做不到,请逐渐增加模型规模。可达状态的数量通常是模型参数的指数函数。随着b的增加,a^{b}的值增长非常快。
许多系统都有错误,可能这些错误只会在大的对于TLC无法彻底检查的模型上显示。在让TLC检查你的耐心可以容忍的最大模型specification后,您可以在仿真模式下运行它。随机模拟不是捕捉细微错误的有效方法,但是值得尝试, 没准就幸运地捕获错误了呢。

Be Suspicious of Success

第247页的14.3.5节说明了为什么在TLC未发现违反liveness属性的情况下您应该保持怀疑:有限模型可能掩盖错误。即使TLC在检查safety属性时没有发现错误,也应该多加怀疑。因为不采取任何措施也可轻松满足safety要求。例如,假设我们忘记了将SndNewValue操作包含进alternating bit protocol specification's的next-state操作中,这样,发送方将永远不会尝试发送任何值,但是这样生成的规范仍将满足协议的正确性条件,即模块ABCorrectness的公式ABCSpec。 (specification不要求必须发送值。)
第252页上描述的coverage选项提供了一种解决此类问题的方法,另一种方法是确保TLC在应被违反的属性中发现错误。例如,如果
alternating bit protocol正在发送消息,则send的值应被更改。您可以通过检查TLC是否报告该属性被违反来验证它是否确实发生了变化
\forall d \in \text { Data }:(\operatorname{sent}=d) \Rightarrow \square(\operatorname{sent} =d)
一个很好的健壮性检查是验证TLC只有通过执行许多操作才能到达的状态。例如,第5.6节的caching memory specification应具有可达的状态,在该状态下,特定处理器在memQ队列中同时具有读取和两个写入操作。达到这种状态需要处理器执行两次写入操作,之后读取未缓存的地址。我们可以通过设置不变量来让TLC检查这种状态是否可达,该不变量声明memQ中的同一处理器没有两写一读的操作。 (当然,这需要一个memQ足够大的模型)。检查是否达到某些状态的另一种方法是,在不变量的IF/THEN表达式中添加Print运算符,以在达到合适的状态时打印消息。

Let TLC Help You Figure Out What Went Wrong

当TLC报告一个不变量被违反时,该不变量的哪个部分为假可能并不明显。如果为变量的合取词分别命名,并在配置文件的INVARIANT语句中单独列出它们,则TLC会告诉您哪个连接词为假。但是,可能很难理解为什么即使单个合取词也是错误的,与其花费大量时间尝试自己解决问题,不如添加Print表达式并让TLC告诉您出了什么问题,这更容易一些。
如果从头开始使用许多Print表达式重新运行TLC,它将为所检查的每个状态打印输出。相反,您应该从不变量为false的状态开始TLC。定义描述该状态的谓词(例如ErrorState),并修改配置文件以将ErrorState用作初始谓词。编写ErrorState的定义很容易,只需将最后一个状态复制到TLC的错误跟踪中即可。
如果违反了任何safety属性,或者在评估next-state操作时TLC报告错误,也可以使用相同的技巧。对于形式为\square [A]_{v}的属性中的错误,请使用错误跟踪中的倒数第二个状态作为初始谓词,并使用跟踪中的最后一个状态(变量名称以撇号开头),重新运行TLC next-state的action。若要查找在评估下一个状态操作时发生的错误,请使用错误跟踪中的最后一个状态作为初始谓词。 (在这种情况下,TLC可能会在报告错误之前找到多个后继状态。)如果您在配置文件中引入了模型值,那么毫无疑问,它们会出现在TLC打印的状态中。因此,如果要将这些状态复制到模块中,则必须将模型值声明为常量参数,然后将相同名称的模型值分配给每个参数。例如,我们用于
alternating bit protocol的配置文件引入了模型值d1和d2。因此,我们将其添加到模块MCAlternatingBit声明中:

CONSTANTS d1, d2
并将赋值语句d1 = d1 d2 = d2添加到配置文件的CONSTANT语句中
分别将模型值d1和d2分配给常数参数d1和d2。

Don't Start Over After Every Error

消除了容易发现的错误之后,TLC可能必须运行很长时间才能发现错误。通常,要正确地纠正一个错误,需要进行多次尝试。如果您在更正错误后从头开始启动TLC,它可能会运行很长时间,仅报告您在更正中犯了一个愚蠢的错误。如果从正确的状态迈出一步时发现了错误,那么最好从该状态启动TLC来检查您的更正是否正确。如上所述,您可以通过定义一个新的初始谓词来做到这一点,该谓词等于TLC打印的状态。
避免出现错误后从头开始的另一种方法是使用检查点。检查点保存当前状态图
\mathcal{G}和未探索状态的队列\mathcal{U}它不会保存有关specification的任何其他信息。即使更改了specification,也可以从检查点重新启动TLC,只要specification的变量和它们可以假定的值没有改变即可。更准确地说,您可以从检查点重新启动,即在检查点未更改且对称集相同之前计算的任何状态的视图。当您纠正了TLC长时间运行后发现的错误时,您可能想使用restore选项(第252页)从最后一个检查点继续TLC,而不是让它重新检查它已经检查的所有状态。

Check Everything You Can

检查您的specification满所有足您认为应该检查的属性。 例如,您不应该只检查alternating bit protocol specification是否满足模块ABCorrectness的高级规范ABCSpec。 您还应该检查您希望它满足的较低级别的属性。 通过研究算法发现的一个这样的属性是,msgQ队列中不应有超过两个不同的消息。 因此,我们可以检查以下谓词是否不变量:
\text { Cardinality }(\{\operatorname{msg} Q[i]: i \in 1 .. \operatorname{Len}(\operatorname{msg} Q)\}) \leq 2
(我们必须通过在其EXTENDS语句中添加FiniteSets来将Cardinality定义添加到模块MCAlternatingBit中。)
最好检查尽可能多的不变性属性。 如果您认为某个状态谓词应该是不变的,请让TLC测试是否为不变。 发现谓词不是不变的可能不会显示错误,但是可能会告诉您一些有关规范的信息。

Be Creative

即使一个specification似乎超出了它可以处理的范围,TLC仍可以帮助对其进行检查。 例如,假设规范的next-state action的形式为\exists n \in Nat:A(n), TLC无法在无限集上的量化评估,因此显然无法处理此规范。 但是,我们可以使TLC通过使用配置文件的CONSTANT语句将n替换为有限集合0..n(对于某些n)来评估量化公式。 此替换将彻底改变规范的含义。 但是,它可能仍然可以让TLC发现specification中的错误。 永远不要忘记,使用TLC的目的不是验证规范是否正确,而是发现错误。

Use TLC as a TLA+ Calculator

对TLA +某些方面的误解可能会导致specification出错, 可以通过在小示例上运行TLC来检查您对TLA +的理解。 TLC可以检查假设,因此您可以通过检查没有specification的模块(仅ASSUME语句)将其变成TLA +计算器。 例如,如果g等于

\left[f \text { EXCEPT } ![d]=e_{1}, ![d]=e_{2}\right],那么g[d]的值是什么?你可以让TLC检查一个模型,其包含如下语句

\begin{aligned} \text{ ASSUME} & \text{ LET } f \triangleq[i \in 1 .. 10 \mapsto 1] \\ & \quad \quad \;\;\, g \triangleq[f \text { EXCEPT } ![2]=3, ![2]=4] \\ & \text { IN} \text { Print } (g[2], \text { TRUE }) \end{aligned}

也可以通过检查如下语句,检查(F \Rightarrow G) \equiv(\neg F \vee G)是否是一个重言式:

\text { ASSUME }\; \forall F, G \in \text { BOOLEAN }:(F \Rightarrow G) \equiv(\neg F \vee G)

TLC甚至会为你寻找一个推测的反例。是否每个集合都可以写成两个不同集合的析取形式?TLC会检查1..4的所有子集:

\begin{aligned} \text {ASSUME } & \forall S \in \text{SUBSET}(1 .. 4): \\ & \text {IF } \exists T, U \in \text{SUBSET}(1 .. 4):(T \neq U) \wedge(S=T \cup U) \\ & \text{THEN TRUE } \\ & \text {ELSE } \text {Print}(S, \text {TRUE}) \end{aligned}

当TLC只用来检查假设时,不需要从配置文件中读取信息,不过你仍然需要提供一个配置文件,哪怕是空的也行。

14.6 What TLC Doesn't Do(TLC不能做什么)

我们希望TLC生成满足specification的所有behaviors 。但是没有程序可以针对任意specification执行此操作。我已经提到了TLC的一些局限性,您可能还会遇到其他限制。

其中一点是覆盖Naturals和Integers模块的Java类仅处理范围为-2^{31}..(2^{31}-1)中的数字。如果任何计算生成的值超出此范围,则TLC会报错。TLC不能生成满足任意specification的所有behaviors ,但可以实现更轻松的目标,即确保它确实生成的每个behavior 都满足该specification。但出于效率考虑,TLC并不总是能够达到这一目标。它以两种方式背离TLA +的语义:

  • TLC没有保留CHOOSE的精确语义。如第16.1节所述,如果S等于T,则\textrm{CHOOSE}\; x \in S:P应该等于\textrm{CHOOSE}\: x \in T:P,但是,仅当S和T在语法上相同时,TLC才能保证这一点。例如,TLC可能会为两个表达式计算不同的值:

\textrm{CHOOSE} \; x \in \left \{ 1,2,3 \right \}:x<3            \textrm{CHOOSE} \; x \in \left \{ 3,2,1 \right \}:x<3

CASE表达式存在类似的TLA +语义冲突,其语义在第16.1.4节中CHOOSE之后定义。

  • TLC 不保留TLA +语义中字符串的表示。 在TLA +中,字符串“abc”是三元素序列|,即具有定义域\left \{ 1,2,3 \right \}的函数。TLC将字符串视为原始值,而不是函数。因此,合法的TLA +表达式“abc” [2]会被视为错误。

14.7 The Fine Print(附录)

本节会详细描述在上文中概述的TLC的两个方面:配置文件的语法和TLC中数值的精确定义。

14.7.1 The Grammar of the Configuration File(配置文件的语法)

在下一页图14.6中,TLA + ConfigFileGrammar模块描述了TLC配置文件的语法。 更准确地说,是定义了ConfigFileGrammar模块的语句集合 ConfigGrammar.File 描述了配置文件(不带注释)的正确语法。 ConfigFileGrammar模块扩展了BNFGrammars模块(见11.1.4节,第179页)。下面是配置文件的其他一些限制,这些限制未在ConfigFileGrammar模块中提及:

  • 最多只可以有一个INIT和一个NEXT语句;
  • 最多只能有一个SPECIFICATION语句,但前提是没有INIT或NEXT语句。 (有关何时必须出现这些语句的条件,请参阅第14.3.1节的第243页);
  • 最多可以有一个VIEW语句;
  • 最多一个SYMMETRY语句;
  • 允许其他语句的多个实例。 例如,如下这两个语句

\\\textrm{INVARIANT} \;Inv1\\ \textrm{INVARIANT} \;Inv2, Inv3

指定TLC将检查三个不变式Inv1,Inv2和Inv3,等价于下面语句

\textrm{INVARIANT} \;Inv1,Inv2, Inv3

14.7.2 Comparable TLC Values(数值比较)

第14.2.1节(第230页)介绍了TLC值。该描述不完整,因为它没有确切定义何时值可比较。准确的定义是,当且仅当以下规则暗示它们是两个时,两个TLC值才是可比较的:

  1. 两个原始值是可以比较的 当且仅当 它们具有相同的值类型,此规则意味着“abc”和“123”是可比较的,但“abc”和123不是可比较的;
  2. 模型值可与任何值比较(它仅等于其自身);
  3. 两个集合可以比较,当且仅当:两组元素数量不同,或者元素数量相同,并且一组中的所有元素与另一组中的所有元素具有可比性。该规则意味着{1}和{“ a”,“ b”}是可比较的,而{1,2}和{2,3}是可比较的。但是,{1,2}和{“ a”,“ b”}不可比较。
  4. 两个函数f和g可以比较,当且仅当(i)它们的定义域是可比;(ii)如果它们的定义域是相等的,则f[x]和g[x]对于它们域中的每个元素x都是可比较的。该规则意味着<1,2>和<“ a”,“ b”,“ c”>是可比较的,并且<1,“ a”>和<2,“ bc”>是可比较的。但是,<1、2>和<“ a”,“ b”>是不可比的。
发布了4 篇原创文章 · 获赞 1 · 访问量 5542

猜你喜欢

转载自blog.csdn.net/robinhzp/article/details/103251846