没有任何模型检查器可以处理所有的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)
{{"a","b"},{"b","c"},{"c","d"}}
是一个TLC值,因为,根据规则1,“a”,“b”,“c”,“d"都是TLC值,根据规则2可以推导出14.3也是一个TLC值,既然元组和记录都是函数,由规则3可以推导出一条由TLC值组成记录或者元组也是一个TLC值.
例如
⟨1,"a",2,"b"⟩也是一个 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∧q时,先计算p的值,如果p值为TRUE,则继续计算q的值;
- 计算
p∨q时,先计算p的值,如果p值为FALSE,则继续计算q的值,即以
¬p∧q方式计算
p∨q;
- 计算IF p THEN e1 ELSE e2时,先计算p,再继续计算e1 或者 e2;
为了理解这些规则的重要性,我们来看一个简单的例子。 如果x等于
⟨⟩,TLC无法评估表达式x[1],因为
⟨⟩[1]没有意义。(空序列是一个函数,其值域是空集,因此不包含1。)第一条规则意味着,如果x等于
⟨⟩,则TLC可以计算表达式
(x=⟨⟩)∧(x[1]=0),但不能计算表达式
(x[1]=0)∧(x=⟨⟩)(因为计算此表达式是,根据规则1,会先计算
x[1]=0,TLC会报错,因为不能计算)幸运的是,我们会很自然地编写第一个公式而不是第二个公式,因为它更容易理解。
人们可以通过从左到右的"心理计算"来理解公式,这与TLC的做法很相似。TLC计算
∃x∈S:p时,是将集合S中的元素
s1,⋯,sn(其中
i=1,⋯,n),经过一定的顺序,逐个代替变量x,代入公式p,计算p的值,TLC以非常简单的方式枚举集合S的元素,如果集合显然不是有限的,则会终止并声明错误。举例来说,集合
{0,1,2,3}和
0..3是非常明显可被遍历的有限集,在计算
∃x∈S:p时,会先对S中的元素进行遍历,所以
{i∈0..5:i<4}可被计算而
{i∈Nat:i<4}不能。
TLC计算
∀x∈S:p 和
CHOOSEx∈S:p时,都是和对
∃x∈S:p一样,先遍历S中的所有元素,TLA+的语义指定对
CHOOSEx∈S:p,如果S中没有一个元素满足p,则返回一个任意值,不过,这种情况通常是由于出现了某种错误导致,所以TLC会把它当成错误处理。
注意到表达式
IFn5THENCHOOSE i∈1..n:i5ELSE42不会报错,因为当
n≤5的时候不会进入CHOOSE子句,当
n5时TLC才会在计算CHOOSE子句时报错。
TLC无法计算"无界"量词或CHOOSE表达式------即具有以下形式之一的表达式:
∃x:p∀x:pCHOOSEx:p
TLC无法计算其值不是TLC值的任何表达式,如上文第14.2.1节中所定义的。
特别的,TLC仅可计算其值是一个有限集的集值表达式,并且仅当其值域是一个有限集时,才可以评估一个函数值表达式。TLC仅在能遍历集合S时,才会计算以下形式的表达式:
∃x∈S:p∀x∈S:pCHOOSEx∈S:p
{x∈S:p}{e:x∈S}{x∈S↦e}
SUBSETSUNIONS
TLC经常可以计算某些表达式,却它不能计算所有的子表达式,举个例子:TLC可以计算
[n∈Nat↦n∗(n+1)][3]的值为12,但它不能计算
[n∈Nat↦n∗(n+1)]的值,这个表达式的值是一个值域为Nat的函数表达式(一个函数为TLC值当且仅当其值域是一个有限集)TLC通过简单的递归procedure来计算由递归定义的函数。 如果f由
f[x∈S]≜e定义,则TLC通过用c代替x 计算e来得出f[c]的值。
这意味着它无法处理某些合法的定义。 例如,参考第68页的以下定义:
mr[n∈S]≜[f↦ifn=0then17elsemr[n−1].f∗mr[n].g,g↦ifn=0then42elsemr[n−1].f∗mr[n−1].g]
为了计算mr[3],我们在表达式中用3代替n来计算
≜右边的值,不过因为mr[3]也出现在等式右边,所以TLC认为它是一个无限循环,从而报错,合法的递归定义导致如上死循环的毕竟是少数,可以换一种符合TLC的写法,回到我们之前的交互递归定义
f[n]=ifn=0then17elsef[n−1]∗g[n],g[n]=ifn=0then42elsef[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=0的情况,因此我们可以将f[n]的表达式重写为
f[n]=ifn=0then17elsef[n−1]∗(f[n−1]+g[n−1]),
这样原公式可以推导成如下形式:
mr[n∈S]≜[f↦ifn=0then17elsemr[n−1].f∗(mr[n−1].f+mr[n−1].g),g↦ifn=0then42elsemr[n−1].f∗mr[n−1].g]]
这样,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值或以
{v1,⋯,vn}形式编写的有限的原始TLC值集。 例如{1,-3,2}。
在v中,将a1或foo之类的任何非数字字符序列,带引号的字符串,或TRUE或FALSE都视作model value。
在赋值表达式
c=v中,符号c不必是常数,也可以是已定义的符号,此赋值语句可以使TLC忽略c的实际定义,并以v为它的值。
当TLC无法根据其定义计算出c的值时,通常使用这种赋值语句。
特别是,像在下面例子中,TLC无法根据定义计算NotAnS的值
NotAnS≜CHOOSEn:n∈/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←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)≜⋯MCReply(p,d,old,new)≜⋯
然后,我们将替换内容
Send←MCSend,Reply←MCReply
添加到配置文件的CONSTANT语句中, 替换也可以是一个定义的符号替换另一个。在specification中,我们通常会编写最简单可行的定义。对于TLC而言,最简单的定义并不总是最容易用的定义。例如,假设我们的specification需要一个Sort运算符,则如果S是一个有限的数字集合,则
Sort(S)是一个按升序排列的包含所有S元素的序列。我们在SpecMod模块中的规范可以使用如下简单的定义:
Sort(S)≜CHOOSEs∈[1..Cardinality(S)→S]:∀i,j∈DOMAINs:(i<j)⇒(s[i]<s[j])
为了计算包含n个元素的集合S的Sort(S ),TLC必须遍历函数集合
[1..n→S]中的
nn个元素, 这可能太慢了,我们可以编写一个模块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⇝Q是nice的,所以TLC可以计算它当且仅当可以计算P和Q。(下面的14.3节解释了TLC计算时态公式的组成表达式时涉及哪些状态和状态对。)
时态公式是nice的当且仅当它是以下四类公式的并集:
- State Predicate:状态谓词
- Invariance Formula: 不变性公式,形如
□P的公式 , 这里 P 是一个状态谓词.
- Box-Action Formula: 形如
□[A]v的公式 , 这里 A 是一个action, v 是一个 state 函数.
- Simple Temporal Formula:为了方便定义这种公式,
我们先引入如下定义:
- 简单的布尔运算符:由命题逻辑的运算符以及对有限常量集的量化组成:
∧∨¬⇒≡ TRUE FALSE
- 简单时间状态公式(A temporal state formula):是通过在状态谓词上应用简单布尔运算符和时态运算符(
□,⋄,⇝)而获得的。
例如,如果N为常数,则
∀i∈1..N:□((x=i)⇒∃j∈1..i:⋄(y=j))是时态公式。
- action公式:
WFv(A)SFv(A)□⋄[A]v⋄□[A]v,其中A是action,而v是状态函数:
WFv(A),SFv(A)的子表达式有
[A]v,ENABLED⟨A⟩v(在第240页描述了ENABLED公式的计算方式),这样,就可以将上述第4项Simple Temporal Formula定义为通过应用简单的布尔运算符,组合了时态公式和简单的action公式而构成的公式。
为方便起见,我们从时态公式的类别中排除不变性公式,则这四类nice的时态公式是不相交的。这样TLC就可以计算下面的时态公式了:
∀i∈1..N:⋄(y=i)⇒WFy((y′=y+1)∧(y≥i))
如果N是一个常数,因为这是一个简单的时态公式(因此是nice的),TLC可以评估其所有组成部分的表达式。TLC无法评估
◊⟨x′=1⟩x,因为这不是一个nice的公式。 TLC也无法评估公式
WF⟨x′[1]=0⟩x,因为在step
s→t,状态t中,如果
x=⟨⟩,则无法计算
◊⟨x′[1]=0⟩x。
PROPERTY语句可以设定TLC可以计算的任何公式。SPECIFICATION语句的公式必须恰好包含一个作为Box-Action公式的合取词。该合词指定了下一个状态action。
14.2.5 Overriding Modules(模块覆盖)
TLC无法根据标准Naturals模块中包含的"+“定义来计算2 + 2。即使我们真的使用TLC定义的计算总和的”+"定义,也算的不快。像+这样的算术运算符可以直接用编写TLC的语言Java来实现。
这是通过TLC的通用机制实现的,该机制允许模块被JAVA类覆盖,该JAVA类实现该模块中定义的运算符。当TLC遇到EXTENDS 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)不是从左至右,相反的,在评估子公式
A1∨⋯∨An时,它将计算分成n个独立的计算,每一个都是独立的子公式
Ai,类似的,在计算
∃x∈S:p时,TLC会对S的每一个元素分别计算。蕴含操作
P⇒Q则是计算它对等的析取操作
¬P∨Q,
举个例子:TLC会将公式
(A⇒B)∨(C∧(∃i∈S:D(i))∧E)拆成3个独立的子公式
¬A,B和
C∧(∃i∈S:D(i))∧E分别计算的。在最后的析取计算前,我们首先需要计算C,如果C为TRUE,再对每一个S中的元素,独立计算
D(i)∧E,对每一个
D(i)∧E,也是先计算
D(i),如果为TRUE,再计算E.
第二个区别是TLC在评估next-state action时,对任意变量x,计算如
x′=e,
x′尚未赋值这种形式的表达式时,表达式赋值为TRUE,再计算表达式e的值赋给
x′。TLC计算表达式
x′∈S的对等表达式
∃v∈S:x′=v.
计算表达式
UNCHANGEDx的对等表达式:
x′=x,
对任意的变量x,评估
UNCHANGED ⟨e1⋯en⟩时,会对每一个
eif分别计算
UNCHANGEDe1∧⋯∧UNCHANGEDen,这样,
UNCHANGED ⟨x,⟨y,z⟩⟩也是当做
x′=x∧y′=y∧z′=z计算。
除了在评估
x′=e这种形式的表达式时,如果遇到尚未赋值的primed变量,TLC会报告错误。如果合取词的值为假,则评估停止,返回"没有发现状态"。
完成并赋值为TRUE的评估将"找到状态",该状态由分配给primed变量的值确定。
在后一种情况下,如果尚未为某些primed变量分配值,TLC将报告错误。
为了说明这是如何工作的,让我们考虑TLC如何评估next-state action:
(14.4)
∨∧x′∈1..Len(y)∧y′=Append(Tail(y),x′)∨∧x′=x+1∧y′=Append(y,x′)
我们先考虑起始状态
x=1,y=⟨2,3⟩, TLC先独立计算这2个析取词,首先计算
x′∈1..Len(y), 如上面的说明,TLC计算它等价的
∃i∈1..Len(y):x′=i, 既然
Len(y)=2,那么TLC将这个式子拆成两个独立的子公式:
(14.5)
∧x′=1∧y′=Append(Tail(y),x′)
∧x′=2∧y′=Append(Tail(y),x′)
TLC计算14.5的第一个action如下:它计算第一个合取词,取值为TRUE,并将x值1赋给
x′;然后,它计算第二个合取词,取值为TRUE,并将值
Append(Tail(⟨2,3⟩),1)分配给
y′。因此,评估(14.5)的第一个action会发现其后续状态是
x=1和
y=⟨3,1⟩。类似地,评估(14.5)的第二个动作会发现其后续状态为
x=2和
y=⟨3,2⟩。TLC以类似的方式评估(14.4)的第二个析取关系,得到其后续状态是
x=2和
y=⟨2,3,2⟩。
因此,对(14.4)的评估发现了三个后序状态。接下来,考虑TLC如何在
x=1且y等于空序列
⟨⟩的状态下评估(14.4)下一状态action。
由于
Len(y)=0和
1..0是空集
{},TLC将第一个析取项评估为
∧∃i∈{}:x′=i∧y′=Append(Tail(y),x′)
评估第一个合取词会产生错误,因此会停止对(14.4)的第一个合取词进行评估,表明没有发现后继状态。评估第二个析取关系会得其后续状态出
x=2和
y=⟨2⟩。
由于TLC从左到右评估合取,因此它们的顺序会影响TLC是否可以评估下一状态动作。例如,假设(14.4)的第一个析取语中的两个析取语颠倒了,像这样:
∧y′=Append(Tail(y),x′)∧x′∈1..Len(y)
当TLC评估此action的第一个合取词时,它在将值赋给
x′之前先遇到表达式
y′=Append(Tail(y),x′),因此它会报告错误。此外,即使我们将
x′更改为
x,TLC仍无法评估以
y=⟨⟩为起始状态的动作,因为在评估第一个合取词时,它将遇到Silly表达式
Tail(⟨⟩)。
上面给出的关于TLC如何评估任意next-state action的描述足以解释它在几乎所有实际情况下如何工作的。但是,它并不完全准确。例如,按字面解释,这意味着TLC可以处理以下两个next-state actions, 它们在逻辑上均等价于
(x′=TRUE)∧(y′=1):
(14.6)
(x′=(y′=1))∧(x′=TRUE) IF x′= TRUE THEN y′=1 ELSE FALSE
实际上,TLC在处理这些异常的next-state actions时都将产生错误消息。请记住,TLC通过使用类似评估初始谓词的方式来计算初始状态,与其从有初始值的unprimed变量开始,再将其赋值给primed变量,不如直接赋值给unprimed变量。
TLC评估ENABLED公式的方式基本上与评估next-state action的方式相同。更准确地说,要评估
ENABLEDA的公式,TLC会计算其后继状态,就好像A是next-state action一样。如果存在后继状态当且仅当公式的计算结果为TRUE。为了检查步骤
s→t是否满足action A和B的合成action
A⋅B,TLC首先计算所有状态
u,以使
s→u是A step,然后再检查
u→t是否是针对某些此类
u的B step。
TLC在检查属性时可能也需要评估action,在这种情况下,它会像评估其他表达式一样评估action,并且即使评估类似(14.6)的奇怪action也毫不费力。