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是初始谓词,Next是Next-state action,vars是所有变量的元组,Temporal通常表示Liveness时态公式。Liveness公式在第八章中有描述。如果您的specification不包含时态公式,也就是它的形式为,那么您可以忽略对时态逻辑检查的讨论。TLC不处理隐藏运算符(时态存在量词),如果需要检查用隐藏变量的specification,可以检查它的子specification,在其中这些变量是可见的。
在specification中查找错误最有效的方法是尝试验证它是否满足其属性(properties)。TLC可以检查specification是否满足(蕴含)的这一大类TLA+公式,这类公式的主要限制是公式中不能包含。您还可以只运行TLC而不检查任何属性,在这种情况下,它将只查找下列两种类型的错误:
- “Silly”错误。如第6.2节所解释的,"silly"表达式如,其形式不符合TLA+的语义。如果某个特定的behavior是否符合规定,取决于"silly"表达式的含义,那么这个specification是不正确的。
- 死锁。无死锁经常是我们希望一个specification需要满足的一个特殊性质;它是用不变性来表示的:。此属性的一个反例是一个导致死锁的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是由集合中的元素组成的序列;
- ackQ 是由集合中的元素组成的序列;
- sBit, sAck 和 rBit是 集合中的元素;
- sent 和rcvd 是 集合中的元素.
TLC的输入包括TLA+模块文件和配置文件。TLC假定specification具有公式(14.1)的形式。配置文件告诉TLC specification的名称和要检查的属性。例如,AlternatingBit模块的配置文件包含声明
这个语句是告诉TLC, 待检查的specification名称是ABSpec,如果Specification的格式为(无Liveness条件),则无需使用SPECIFICATION 语句,可以通过在配置文件中添加以下两个语句来声明初始状态谓词和Next-State Action:
要检查的属性用PROPERTY语句指定。例如,为了检查ABTypeInv不变量,即,可以在模块AlternatingBit的配置文件中添加如下定义:
并将语句 写入配置文件中。不变性检查非常常见,因此TLC允许您将以下语句放入配置文件中:
INVARIANT语句必须指定一个状态谓词。若要检查PROPERTY语句的不变性,指定的属性必须为形式(因为只是让TLC检查该specification是否蕴含P,也就是 P 在满足specification的每个behavior的初始状态中为 TRUE)。
TLC通过生成并校验一系列满足specification的behavior来工作。 为此,首先要给specification指定一个模型(model)。 要定义模型,我们必须为specification的常量参数赋值。 AlternatingBit协议Specification的唯一常量参数是Data。 通过在配置文件中放置以下声明,我们可以告诉TLC,Data为包含名为d1和d2两个任意元素的集合:
(我们可以使用包含至少一个字母的字母或数字串作为元素名称)
有两种使用TLC的方法。 默认方法是模型检查(model checking),这种方式将尝试查找所有可达的状态,即所有满足公式的behavior中可能出现的状态。 我们还可以在仿真模式下运行TLC,在该模式下,它会随机生成behavior,而无需尝试检查所有可达的状态。 这里我们我们先考虑模型检查,模拟模式将在第243页的14.3.2节介绍。
对于AlternatingBit协议,不可能彻底检查所有可达状态,因为消息序列可以任意变长,因此存在无限多个可达状态。 我们必须进一步约束模型使其有限,也就是说,它仅允许有限数量的可能状态。 为此,我们定义了一个称为约束的状态谓词,该谓词声明了序列长度的界限。 例如,以下约束断言msgQ和ackQ的长度最多为2:
与其以这种方式指定序列长度的界限,不如让它们作为参数并在配置文件中赋值。 我们不想在Specification中加入仅为TLC方便考虑的声明和定义。 因此,我们编写了一个名为MCAlternatingBit的新模块,该模块扩展了AlternatingBit模块,可以用作TLC的输入。 该模块显示在下一页的图14.2中。 下一页的图14.3中显示了该模块的可能配置文件。 请注意,在这种情况下,配置文件必须为Specification的所有常量参数指定值,即AlternatingBit模块中的参数Data和模块MCAlternatingBit本身中声明的两个参数。 您可以使用第3.5节(第32页)中所述的TLA +注释语法在配置文件中添加注释。
当指定约束Constr时,TLC会检查满足规约的behavior的每个状态。 在本章的其余部分,这些状态将称为可达状态。
让TLC检查类型不变式会捕获许多简单的错误。 当我们纠正了所有可以找到的错误后,我们便希望寻找不太明显的错误。 一个常见的错误是某个操作在应启用时未启用,从而导致无法达到某些状态。 您可以通过第252页上介绍的coverage选项来发现某个操作是否从未启用。要发现某个操作有时是否被错误地禁用,可以尝试检查Liveness。 AlternatingBit协议中明显Liveness属性是,发送方发送的每个消息最终都将被传递给接收方。 当满足如下条件: 时, 一个消息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 ,然后将配置文件的PROPERTY语句修改为
此示例是非典型的,因为正确性specification ABCSpec不涉及变量隐藏(时态存在量词)。 现在让我们假设模块ABCorrectness确实声明了另一个变量h,该变量出现在ABCSpec中,并且alternating bit协议的正确性条件是隐藏了h的ABCSpec。 然后,在TLA +中正式表示正确性条件,如下所示:
TLC无法直接检查该定理,因为TLC目前无法处理时间存在量词。 我们将以与尝试证明该定理相同的方式通过TLC检验该定理,即通过使用细化映射。 如62页5.8节所述,我们将根据AlternatingBit模块的变量定义状态函数oh,然后证明
为了让TLC检查该定理,我们将添加定义, 并让TLC检查属性ABCSpecBar.
TLC检查属性时,实际上并不会验证specification是否蕴含了该属性。 相反,它检查(i)specification的safety部分隐含了property的safety部分,以及(ii)specification是否蕴含了属性的liveness部分。 例如,假设规格Spec和属性Prop为
这里 Temporal 和ImpliedTemporal 是liveness 属性. 在这里, TLC校验如下两个公式
这意味着不能使用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数值可以归纳定义为
- 原始值
- 有限的具有可比性的TLC数值的集合(在下面定义可比性)
- 对于f值域中的所有x,函数f的域为TLC值,f[x]也为TLC值。
举例来说,根据第1,2条规则,(14.3)是一个TLC值,因为,根据规则1,“a”,"b","c","d"都是TLC值,根据规则2可以推导出14.3也是一个TLC值,既然元组和记录都是函数,由规则3可以推导出一条由TLC值组成记录或者元组也是一个TLC值. 例如 也是一个 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的值,如果p值为TRUE,则继续计算q的值;
- 计算时,先计算p的值,如果p值为FALSE,则继续计算q的值,即以方式计算
- 计算IF p THEN e1 ELSE e2时,先计算p,再继续计算e1 或者 e2
为了理解这些规则的重要性,我们来看一个简单的例子。 如果x等于,TLC无法评估表达式x[1],因为没有意义。 (空序列是一个函数,其值域是空集,因此不包含1。)第一条规则意味着,如果x等于,则TLC可以计算表达式, 但不能计算表达式(因为计算此表达式是,根据规则1,会先计算,TLC会报错,因为不能计算)
幸运的是,我们会很自然地编写第一个公式而不是第二个公式,因为它更容易理解。 人们可以通过从左到右的“心理计算”来理解公式,这与TLC的做法很相似。
TLC计算时,是将集合S中的元素(其中), 经过一定的顺序,逐个代替变量x,代入公式p,计算p的值, TLC以非常简单的方式枚举集合S的元素,如果集合显然不是有限的,则会终止并声明错误。举例来说,集合和是非常明显可被遍历的有限集,在计算时,会先对S中的元素进行遍历,所以可被计算而不能。
TLC计算 和 时,都是和对 一样,先遍历S中的所有元素,TLA+的语义指定对,如果S中没有一个元素满足p, 则返回一个任意值,不过,这种情况通常是由于出现了某种错误导致,所以TLC会把它当成错误处理。 注意到表达式 不会报错,因为当的时候不会进入CHOOSE子句,当时TLC才会在计算CHOOSE子句时报错。
TLC无法计算“无界”量词或CHOOSE表达式——即具有以下形式之一的表达式:
TLC无法计算其值不是TLC值的任何表达式,如上文第14.2.1节中所定义的。 特别的,TLC仅可计算其值是一个有限集的集值表达式,并且仅当其值域是一个有限集时,才可以评估一个函数值表达式。 TLC仅在能遍历集合S时,才会计算以下形式的表达式:
TLC经常可以计算某些表达式,却它不能计算所有的子表达式,举个例子:TLC可以计算的值为12,但它不能计算的值,这个表达式的值是一个值域为Nat的函数表达式(一个函数为TLC值当且仅当其值域是一个有限集)
TLC通过简单的递归procedure来计算由递归定义的函数。 如果f由定义,则TLC通过用c代替x 计算e来得出f[c]的值。 这意味着它无法处理某些合法的定义。 例如,参考第68页的以下定义:
为了计算mr[3], 我们在表达式中用3代替n来计算右边的值,不过因为mr[3]也出现在等式右边,所以TLC认为它是一个无限循环,从而报错,合法的递归定义导致如上死循环的毕竟是少数,可以换一种符合TLC的写法,回到我们之前的交互递归定义
子表达式mr[n]出现在定义mr[n]的表达式中,因为f[n]取决于g[n]。 为了消除它,我们必须重写相互递归,以便f[n]仅取决于f[n-1]和g[n-1],我们可以通过展开f[n]表达式中g[n]的定义来做到这一点, 由于else子句仅适用于的情况,因此我们可以将f[n]的表达式重写为
, 这样原公式可以推导成如下形式:
这样,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值或以形式编写的有限的原始TLC值集。 例如{1,-3,2}。 在v中,将a1或foo之类的任何非数字字符序列,带引号的字符串,或TRUE或FALSE都视作model value。
在赋值表达式c = v中,符号c不必是常数,也可以是已定义的符号,此赋值语句可以使TLC忽略c的实际定义,并以v为它的值。 当TLC无法根据其定义计算出c的值时,通常使用这种赋值语句。 特别是,像在下面例子中,TLC无法根据定义计算NotAnS的值
因为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和d是TLA +中定义的符号,这将使TLC在执行计算时将c替换为d。 替换的一种用途是为操作符参数赋值。 例如,假设我们要使用TLC检查第5.6节(第54页)的write-through cache specification,WriteThroughCache模块扩展了MemoryInterface模块,该模块包含声明
constants Send (_,_,_,_ ), Reply(_,_,_,_ ),...
我们必须告诉TLC如何计算操作符Send 和 Reply, 我们可以先写一个名为MCWriteThroughCache 的模块,该模块是模块WriteThroughCache的扩展,在其中定义两个操作符
然后,我们将替换内容
添加到配置文件的CONSTANT语句中, 替换也可以是一个定义的符号替换另一个。 在specification中,我们通常会编写最简单可行的定义。 对于TLC而言,最简单的定义并不总是最容易用的定义。 例如,假设我们的specification需要一个Sort运算符,则如果S是一个有限的数字集合,则是一个按升序排列的包含所有S元素的序列。 我们在SpecMod模块中的规范可以使用如下简单的定义:
为了计算包含n个元素的集合S的Sort(S ),TLC必须遍历函数集合中的个元素, 这可能太慢了, 我们可以编写一个模块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可以计算组成该公式的所有常规表达式;
例如,形式为是nice的,所以TLC可以计算它当且仅当可以计算P和Q。 (下面的14.3节解释了TLC计算时态公式的组成表达式时涉及哪些状态和状态对。)
时态公式是nice的当且仅当它是以下四类公式的并集:
- State Predicate:状态谓词
- Invariance Formula: 不变性公式,形如 的公式 , 这里 P 是一个状态谓词.
- Box-Action Formula: 形如 的公式 , 这里 A 是一个action, v 是一个 state 函数.
- Simple Temporal Formula:为了方便定义这种公式, 我们先引入如下定义:
- 简单的布尔运算符:由命题逻辑的运算符以及对有限常量集的量化组成:
- 简单时间状态公式(A temporal state formula):是通过在状态谓词上应用简单布尔运算符和时态运算符()而获得的。 例如,如果N为常数,则是时态公式。
- action公式:,其中A是action,而v是状态函数:
的子表达式有(在第240页描述了ENABLED公式的计算方式),
这样,就可以将上述第4项Simple Temporal Formula定义为 通过应用简单的布尔运算符,组合了时态公式和简单的action公式而构成的公式。
为方便起见,我们从时态公式的类别中排除不变性公式,则这四类nice的时态公式是不相交的。
这样TLC就可以计算下面的时态公式了:
如果N是一个常数,因为这是一个简单的时态公式(因此是nice的),TLC可以评估其所有组成部分的表达式。 TLC无法评估,因为这不是一个nice的公式。 TLC也无法评估公式,因为在step ,状态t中,如果,则无法计算。
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)不是从左至右,相反的,在评估子公式时,它将计算分成n个独立的计算,每一个都是独立的子公式,类似的,在计算时,TLC会对S的每一个元素分别计算。蕴含操作则是计算它对等的析取操作, 举个例子:TLC会将公式拆成3个独立的子公式和分别计算的。在最后的析取计算前,我们首先需要计算C,如果C为TRUE,再对每一个S中的元素,独立计算,对每一个,也是先计算,如果为TRUE,再计算E.
第二个区别是TLC在评估next-state action时,对任意变量x,计算如,尚未赋值 这种形式的表达式时,表达式赋值为TRUE,再计算表达式e的值赋给。TLC计算表达式的对等表达式 . 计算表达式的对等表达式:, 对任意的变量x,评估时,会对每一个f分别计算,这样,也是当做 计算。
除了在评估这种形式的表达式时,如果遇到尚未赋值的primed变量,TLC会报告错误。 如果合取词的值为假,则评估停止,返回"没有发现状态"。 完成并赋值为TRUE的评估将"找到状态",该状态由分配给primed变量的值确定。 在后一种情况下,如果尚未为某些primed变量分配值,TLC将报告错误。
为了说明这是如何工作的,让我们考虑TLC如何评估next-state action:
(14.4)
我们先考虑起始状态, TLC先独立计算这2个析取词,首先计算, 如上面的说明,TLC计算它等价的, 既然, 那么TLC将这个式子拆成两个独立的子公式:
(14.5)
TLC计算14.5的第一个action如下:它计算第一个合取词,取值为TRUE,并将x值1赋给; 然后,它计算第二个合取词,取值为TRUE,并将值分配给。 因此,评估(14.5)的第一个action会发现其后续状态是和。 类似地,评估(14.5)的第二个动作会发现其后续状态为和。 TLC以类似的方式评估(14.4)的第二个析取关系,得到其后续状态是和。 因此,对(14.4)的评估发现了三个后序状态。
接下来,考虑TLC如何在且y等于空序列的状态下评估(14.4)下一状态action。 由于和是空集,TLC将第一个析取项评估为
评估第一个合取词会产生错误,因此会停止对(14.4)的第一个合取词进行评估,表明没有发现后继状态。 评估第二个析取关系会得其后续状态出和。
由于TLC从左到右评估合取,因此它们的顺序会影响TLC是否可以评估下一状态动作。 例如,假设(14.4)的第一个析取语中的两个析取语颠倒了,像这样:
当TLC评估此action的第一个合取词时,它在将值赋给之前先遇到表达式,因此它会报告错误。此外,即使我们将更改为,TLC仍无法评估以为起始状态的动作,因为在评估第一个合取词时,它将遇到Silly表达式。
上面给出的关于TLC如何评估任意next-state action的描述足以解释它在几乎所有实际情况下如何工作的。但是,它并不完全准确。例如,按字面解释,这意味着TLC可以处理以下两个next-state actions, 它们在逻辑上均等价于:
(14.6)
实际上,TLC在处理这些异常的next-state actions时都将产生错误消息。
请记住,TLC通过使用类似评估初始谓词的方式来计算初始状态,与其从有初始值的unprimed变量开始,再将其赋值给primed变量,不如直接赋值给unprimed变量。
TLC评估ENABLED公式的方式基本上与评估next-state action的方式相同。更准确地说,要评估的公式,TLC会计算其后继状态,就好像A是next-state action一样。如果存在后继状态当且仅当公式的计算结果为TRUE。为了检查步骤是否满足action A和B的合成action ,TLC首先计算所有状态,以使是A step,然后再检查是否是针对某些此类的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中需要存在形式为的specification conjunct, 其中A为action,这样的合取词不得超过一个。
- Temporal specification的合取操作,既不是状态谓词,也不是盒式公式。通常是specification的Liveness条件。
- Invariant 每个状态谓词 I的合取,这些状态谓词I由INVARIANT语句命名,或者是某些等于的属性合词
- ImpliedInit 是每个为状态谓词的property conjunct的合取。
- ImpliedAction 每个action[的合取,存在property conjunct等于。
- ImpliedTemporal 每个property conjunct的合取,是简单的时态公式,但不具有的形式,其中I是状态谓词。
- Constraint 由CONSTRAINT语句命名的所有状态谓词的合取。
- ActionConstraint 由ACTION-CONSTRAINT语句命名的所有action的合取。action约束与普通约束类似,不同之处在于它消除了可能的转换而不是类似state(状态有可能不是最终TLC计算的形式,需要经常预处理)。普通约束P等效于action约束。
14.3.1 Model-Checking Mode
TLC有两种数据结构,一个以state为节点组成的图,一个由states组成的序列。的一个state说的是的一个节点。图是TLC迄今为止所有可达状态图的一部分, 包含中states, 这些states还有后续状态未被TLC计算。TLC在计算过程中一直满足如下不变量:
- 图的state满足Constraint谓词;
- 对中的每一个状态s, 边也在中;
- 如果中有一个从状态s到不同状态t的边,那么t是s的后继状态,它满足ActionConstraint。 换句话说,步骤满足;
- 对中的每一个状态 ,内都存在一条从初始状态(满足Init谓词)到这个状态的路径;
- 是由中不同状态的节点组成的序列;
- 对于每一个在中而不在中的状态s,对每一个满足Constraint,从而使满足的状态t,状态t和的边也在中;
TLC执行如下算法,起始条件是和都为空:
- 检查赋给常量参数的值是否满足specification中的所有ASSUME假设;
- 如上文第14.2.6节所述,通过评估初始谓词Init来计算初始状态集。 对于找到的每个初始状态s:
(a)对状态s,计算Invariant和ImpliedInit谓词,如果任一值为FALSE,则报错并停止;
(b)对状态s,如果Constraint谓词为TRUE,则将s加入序列,并在中加入节点s和边;
- 当为非空序列,按如下操作:
(a)从中移除第一个state s;
(b)如上14.2.6所述,计算以s为起始状态的next-state action所有可能的后续state,组成集合T;
(c)如果T为空,且deadlock选项未被选中,则报告一个deadlock死锁,并停止;
(d)对T中的每个状态t,执行如下操作:
(i)如果Invariant对状态t为FALSE或ImpliedInit对step 为FALSE,则报错并停止;
(ii)如果谓词Constraint对状态t为TRUE,并且step 满足约束ActionConstraint,则
A.如果t不在中,则将其加入到的尾部,并将node t 和边加入;
B.将边加入.
TLC可以在多线程下运行,step 3(b)-(d),对不同的状态s,在不同的线程上并发运行。参见253页有关worker选项的描述。
如果公式ImpliedTemporal不等于TRUE,则只要在上述过程中加入边,TLC都会为step 计算出出现公式Temporal和ImpliedTemporal中的所有谓词和action(在加入任何边的时候,包括在2(b)和3(d)ii.中所示的自循环和,都执行上述操作);
在周期性计算和结束计算时,TLC按如下方式检查ImpliedTemporal属性:
设定是由每一个满足如下条件的behavior τ组成的集合,τ是一个状态序列, 该序列从初始状态开始,是中一条无限的state路径,举例来说,对中每一个初始状态s,包含路径). 注意中的每个behavior都满足公式. TLC也检查是否中的每个behavior都满足.(这只是概念上发生,实际上TLC不会分开检查每个behavior)。参见后续Section14.3.5,第247页的讨论:为什么TLC不会如你所期望地检查ImpliedTemporal属性。
只有在所有可达states集合是有限时,对的计算才会终止,否则,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使用上面描述的过程来构造图——但有以下区别:在计算了初始状态的集合,并且在计算了状态s的后继集合之后,TLC会随机选择该集合的元素。如果元素不满足约束条件,则停止计算。否则,TLC仅将该状态放入和,并检查Invariant和ImpliedInit或ImpliedAction公式。 (实际上不维护队列,因为它永远不会包含多个元素。)当生成的状态数达到指定的最大状态数时,的构造停止,并检查Temporal隐含ImpliedTemporal公式。然后,从和为空开始,TLC重复该过程。
TLC的选择不是严格随机的,而是使用伪随机数生成器从随机选择的种子生成的。如果TLC发现错误,则会打印出种子和另一个称为aril的值。如下文第14.5.1节所述,使用key和aril选项,您可以让TLC按你指定的方式显示错误消息。
14.3.3 Views and Fingerprints
在上面关于TLC如何检查属性的描述中,我写道图的节点是状态。那不是很正确。 的节点是称为view的状态函数的值。 TLC的默认视图是所有已声明变量的元组,state由其值确定。但是,您可以通过在配置文件中添加以下语句,将视图指定为其他状态函数myview:
VIEW myview
其中myview是已定义或声明为变量的标识符。
当TLC计算初始状态时,它将其view而不是状态本身放在中。(状态s的视图是状态s中VIEW状态函数的值。)如果存在多个具有相同view的初始状态,则仅将其中一个放入队列中。 TLC不是将边,而是将 从s的view到t的view的边插入图。在上述算法的步骤3(d)ii.A中,TLC检查t的视图是否在中。
使用默认视图以外的view时,TLC可能会在找到所有可达状态之前停止。对于其执行的状态,它会正确执行safety检查,即Invariant,ImpliedInit和ImpliedAction检查。此外,如果在这些属性之一中发现错误,则会打印出正确的反例(状态的有限序列)。但是,它可能会错误地检查ImpliedTemporal属性。因为TLC正在构造的图不是实际的可达性图,所以当(??)不存在时,它可能会报告ImpliedTemporal属性中的错误,从而打印出虚假的反例。
指定非标准view可能导致TLC不检查许多状态。当不需要检查具有相同视图的不同状态时,应执行此操作。最有可能的替代view是一个由一些但不是全部声明的变量组成的元组。例如,您可能添加了一个或多个变量来帮助调试规范。使用原始变量的元组作为视图,可以在不增加TLC必须探索的状态数量的情况下添加调试变量。如果检查的属性未提及调试变量,则TLC将查找原始规范的所有可到达状态,并将正确检查所有属性。
在实际的实现中,图的节点不是状态的view,而是这些view的fingerprint。 TLC指纹是由“哈希”功能生成的64位数字。理想情况下,两个不同视图具有相同指纹的概率为,这是一个非常小的数字。但是,有可能发生冲突,这意味着TLC错误地认为两个不同的view是相同的,因为它们具有相同的指纹。如果发生这种情况,TLC将不会探索其应查看的所有状态。特别是,使用默认view时,TLC将报告它已经检查了所有可到达的状态,实际上可能不是。
终止时,TLC打印出两个fingerprint发生冲突的估算概率。第一是基于这样的假设:两个不同view具有相同fingerprint的的概率为。(在此假设下,如果TLC生成了n个具有m个不同fingerprint的view,则发生碰撞的概率约为.)但是,生成状态的过程是高度非随机的,没有已知的fingerprint方案可以保证TLC中两个不同状态生成相同fingerprint的概率实际上为。因此,TLC还打印了一个发生碰撞的可能性的实证估计。根据观察,如果发生碰撞,则很可能还会有“near miss”。估计对TLC生成的不同fingerprint对,发生碰撞概率的最大值是。实际上,除非TLC产生数十亿个不同的状态,否则碰撞的可能性非常小。
view和fingerprint仅适用于模型检查模式。在模拟模式下,TLC忽略任何VIEW语句。
14.3.4 Taking Advantage of Symmetry
第5章的内存specification在处理器 Proc集合中是对称的。 直观上,这意味着对处理器进行排列并不会更改behavior是否满足specification。 为了更精确地定义对称性,我们首先需要一些定义。
有限集S的排列是一个函数,其域和范围都等于S。 换句话说,π是S 的一个排列当且仅当
一个permutation是一个函数,这个函数是它的值域(有限)的一个排列。如果π是集合S元素的一个排列而s是一个状态,π函数将s中属于集合S的值v用π[v]代替形成新的state,记做。为了更形象得了解的含义,可以看这个例子:对集合,π排列如下:, 在状态s,变量x和y的值如下:
,则在状态,变量x和y的值如下:
上面例子可以给你一个的直观的印象,我不打算给出一个严格的定义:如果是一个behavior ,记做
现在我们可以在这个基础上定义对称性了,规约 Spec对于某个排列π具有对称性当且仅当下述满足下列条件:对任意behavior ,满足公式Spec当且仅当 也满足Spec。
第5章的内存specification对于Proc的排列是对称的,这也意味着如果TLC对Proc的一个排列π检查过behavior ,那就没有必要再检查behavior 了(因为在上发现错误也会出现在中)。我们可以将下面语句加入配置文件,以让TLC利用这个对称特性:
SYMMETRY Perms
这里Perms是在模块中定义的Proc的所有排列的集合Permutations(Proc),(Permutations操作符在TLC模块中定义,参见下面Section14.4)。SYMMETRY语句让TLC修改P241-242的算法如下:如果对Proc的一个排列π,如果已经在队列和图中,则不需要再将s放入其中。如果有n个process进程,则我们可以减少待检查的状态数到
第5章的内存specification对于内存地址Adr的排列也是对称的。我们可以像之前process排列一样利用这个对称性,定义对称集合(由SYMMETRY语句指定):
一般来说,SYMMETRY语句可以指定任意对称集合Π,Π内任意元素都是model value集合的一个排列。更精确的是,Π的每个元素π,都是由配置文件CONSTANT语句指定的model value组成的集合的一个排列(如果配置文件中未含SYMMETRY语句,则对称集Π为空集)
为了解释对给定一个任意的对称集Π,TLC会如何操作,我先引入一些定义:如果τ是一个由Π中排列组成的序列 ,令
(如果τ是空集,则)定义是状态s的等价类,是由Π中所有排列组成的所有序列的集合,对任意状态s,TLC只在队列和图中,保留一个中的元素。下面是对241-242页,step2(b)算法的修改:只在队列和图中没有包含任意中的元素时,TLC才将状态s加入队列和图中。step3(d)ii也被修改为如下条件:
A.如果中没有元素在内,则将t加入到的队尾,将节点t和边加入;
B.将边加入,这里tt是中现在唯一在中的元素;
当VIEW语句出现在配置文件中时,将按照上面的14.3.3节中的描述修改这些变更,以便将view而不是state放入。
如果被检查的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:
显然,在满足EvenSpec的任何行为中x都不等于1。因此,EvenSpec不满足活度属性。假设我们要求TLC检查EvenSpec是否蕴含。为了使TLC终止,我们必须提供一个约束,将其限制为仅生成有限数量的可达状态。然后,TLC生成的所有满足的无限行为都将以无数个重复步骤结束。在任何此类行为中,Action 总是处于使能状态,但仅发生有限数量的个步骤,因此为假(参见WeakFairness的定义,必须有无限数量的才能为TRUE)。因此,TLC将不会报告错误,因为公式被它产生的所有无限行为所满足。
在进行时态检查时,请确保您的模型将允许生成满足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包含
并且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以来经过的毫秒数为单位,再模。如果TLC生成状态的速度很慢,则将JavaTime运算符与Print表达式结合使用可以帮助你明白为什么会这么慢。如果TLC花费太多时间评估一个运算符,则可以换一个等价的更有效率的的运算符。 (请参阅第234页的14.2.3节。)
接下来,TLC模块定义运算符:>和@@,表达式是一个值域为, 且对 , 的函数。例如,序列,是一个值域为的函数,可以写成
TLC使用这些运算符来呈现 待打印的函数值或者报告一个错误,不过,习惯上一般以在specification中出现的方式打印值,因此通常将序列打印为序列,而不是使用:>和@@运算符。
接下来,如果S是有限集,则将Permutations(S)定义为S的所有排列的集合。可以使用Permutations运算符为上面第14.3.4节中描述的SYMMETRY语句指定一组排列。可以通过定义一个集合来使用更复杂的对称性,集合中每一个都是一个显式函数,可以用:>,和@@运算符编写。例如,考虑一个存储系统的specification,其中每个地址都以某种方式与处理器相关联。该specification在两种排列下是对称的:一种排列是与同一处理器关联的地址,另一种排列是与一组地址有关联的处理器。假设我们告诉TLC使用两个处理器和四个地址,其中地址a11和12与处理器p1相关联,并且地址a21和a22与处理器p2相关联。通过为TLC提供以下以下排列组合作为对称集,可以使TLC充分利用对称性:
排列交换处理器及其关联的地址。只是互换a21和a22的排列不需要明确指定,因为它是通过交换处理器,交换a11和a12并再次交换处理器获得的。
TLC模块通过定义运算符SortSeq结束,它可以用于将运算符替换为更有效率的TLC运算符。如果s是有限序列,是其上的完整排序关系(排序算子)SortSeq(s,)是s经过排序得到的新序列。举例来说,SortSeq(<3,1,3,8>,>)等于<8,3,3,1>. SortSeqde的 JAVA实现让TLC更有效率地实现排序算法。举例如下:下面是我们用SortSeq定义一个FastSort操作符去替换235页定义的Sort操作符:
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,该种子必须为之间的整数。
以相同的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的第一个联合替换为引入错误, 参见在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 中:
如果q的长度大于1,将Lose(q)[1]定义为等于q[0],如果q是序列,则这是一个无意义的值。 (序列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:
- Line 57, column 7 to line 59, column 60 in AlternatingBit
- Line 58, column 55 to line 58, column 60 in AlternatingBit
第一个位置标识“Lose”定义的第二个合取词;第二个标识表达式。这告诉您在TLC评估时发生了错误,这是对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的增加,的值增长非常快。
许多系统都有错误,可能这些错误只会在大的对于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是否报告该属性被违反来验证它是否确实发生了变化
一个很好的健壮性检查是验证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报告错误,也可以使用相同的技巧。对于形式为的属性中的错误,请使用错误跟踪中的倒数第二个状态作为初始谓词,并使用跟踪中的最后一个状态(变量名称以撇号开头),重新运行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打印的状态。
避免出现错误后从头开始的另一种方法是使用检查点。检查点保存当前状态图和未探索状态的队列。它不会保存有关specification的任何其他信息。即使更改了specification,也可以从检查点重新启动TLC,只要specification的变量和它们可以假定的值没有改变即可。更准确地说,您可以从检查点重新启动,即在检查点未更改且对称集相同之前计算的任何状态的视图。当您纠正了TLC长时间运行后发现的错误时,您可能想使用restore选项(第252页)从最后一个检查点继续TLC,而不是让它重新检查它已经检查的所有状态。
Check Everything You Can
检查您的specification满所有足您认为应该检查的属性。 例如,您不应该只检查alternating bit protocol specification是否满足模块ABCorrectness的高级规范ABCSpec。 您还应该检查您希望它满足的较低级别的属性。 通过研究算法发现的一个这样的属性是,msgQ队列中不应有超过两个不同的消息。 因此,我们可以检查以下谓词是否不变量:
(我们必须通过在其EXTENDS语句中添加FiniteSets来将Cardinality定义添加到模块MCAlternatingBit中。)
最好检查尽可能多的不变性属性。 如果您认为某个状态谓词应该是不变的,请让TLC测试是否为不变。 发现谓词不是不变的可能不会显示错误,但是可能会告诉您一些有关规范的信息。
Be Creative
即使一个specification似乎超出了它可以处理的范围,TLC仍可以帮助对其进行检查。 例如,假设规范的next-state action的形式为, TLC无法在无限集上的量化评估,因此显然无法处理此规范。 但是,我们可以使TLC通过使用配置文件的CONSTANT语句将n替换为有限集合(对于某些n)来评估量化公式。 此替换将彻底改变规范的含义。 但是,它可能仍然可以让TLC发现specification中的错误。 永远不要忘记,使用TLC的目的不是验证规范是否正确,而是发现错误。
Use TLC as a TLA+ Calculator
对TLA +某些方面的误解可能会导致specification出错, 可以通过在小示例上运行TLC来检查您对TLA +的理解。 TLC可以检查假设,因此您可以通过检查没有specification的模块(仅ASSUME语句)将其变成TLA +计算器。 例如,如果g等于
,那么g[d]的值是什么?你可以让TLC检查一个模型,其包含如下语句
也可以通过检查如下语句,检查是否是一个重言式:
TLC甚至会为你寻找一个推测的反例。是否每个集合都可以写成两个不同集合的析取形式?TLC会检查1..4的所有子集:
当TLC只用来检查假设时,不需要从配置文件中读取信息,不过你仍然需要提供一个配置文件,哪怕是空的也行。
14.6 What TLC Doesn't Do(TLC不能做什么)
我们希望TLC生成满足specification的所有behaviors 。但是没有程序可以针对任意specification执行此操作。我已经提到了TLC的一些局限性,您可能还会遇到其他限制。
其中一点是覆盖Naturals和Integers模块的Java类仅处理范围为中的数字。如果任何计算生成的值超出此范围,则TLC会报错。TLC不能生成满足任意specification的所有behaviors ,但可以实现更轻松的目标,即确保它确实生成的每个behavior 都满足该specification。但出于效率考虑,TLC并不总是能够达到这一目标。它以两种方式背离TLA +的语义:
- TLC没有保留CHOOSE的精确语义。如第16.1节所述,如果S等于T,则应该等于,但是,仅当S和T在语法上相同时,TLC才能保证这一点。例如,TLC可能会为两个表达式计算不同的值:
CASE表达式存在类似的TLA +语义冲突,其语义在第16.1.4节中CHOOSE之后定义。
- TLC 不保留TLA +语义中字符串的表示。 在TLA +中,字符串“abc”是三元素序列|,即具有定义域的函数。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语句;
- 允许其他语句的多个实例。 例如,如下这两个语句
指定TLC将检查三个不变式Inv1,Inv2和Inv3,等价于下面语句
14.7.2 Comparable TLC Values(数值比较)
第14.2.1节(第230页)介绍了TLC值。该描述不完整,因为它没有确切定义何时值可比较。准确的定义是,当且仅当以下规则暗示它们是两个时,两个TLC值才是可比较的:
- 两个原始值是可以比较的 当且仅当 它们具有相同的值类型,此规则意味着“abc”和“123”是可比较的,但“abc”和123不是可比较的;
- 模型值可与任何值比较(它仅等于其自身);
- 两个集合可以比较,当且仅当:两组元素数量不同,或者元素数量相同,并且一组中的所有元素与另一组中的所有元素具有可比性。该规则意味着{1}和{“ a”,“ b”}是可比较的,而{1,2}和{2,3}是可比较的。但是,{1,2}和{“ a”,“ b”}不可比较。
- 两个函数f和g可以比较,当且仅当(i)它们的定义域是可比;(ii)如果它们的定义域是相等的,则f[x]和g[x]对于它们域中的每个元素x都是可比较的。该规则意味着<1,2>和<“ a”,“ b”,“ c”>是可比较的,并且<1,“ a”>和<2,“ bc”>是可比较的。但是,<1、2>和<“ a”,“ b”>是不可比的。