TLA+ 《Specifying Systems》翻译初稿——Section 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 2 63 1 -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执行三个基本阶段:

  1. 检查假设。
  2. 计算初始状态;
  3. 在未探索状态队列中生成状态的后继状态。

您可以通过以下方式判断它是否已进入第三阶段是否已打印"已计算初始状态"消息。当发现正在检查的属性之一不成立时,就会触发TLC直接推送错误报告。

假设我们将不变式ABTypeInv的第一个联合替换为 m s g Q S e q ( D a t a ) \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 中:
i 1.. L e n ( q ) : q = [ j 1.. ( L e n ( q ) 1 ) IF j < i    THEN  q [ j 1 ] ELSE     q [ j ] ] \\ \exists i \in 1.. Len(q) :\\ \quad q' = [j \in 1 ..(Len(q) -1) \mapsto \textrm{IF} \: j < i \;\boldsymbol{\textrm{THEN }}q[j-1] \\ \qquad \qquad \qquad \qquad \qquad \qquad \quad \boldsymbol{\textrm{ELSE }}\;q[j]]

如果q的长度大于1,将Lose(q)[1]定义为等于q[0],如果q是序列,则这是一个无意义的值。
(序列q的值域是集合 1.. L e n ( 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 ] q[j-1] 。这告诉您在TLC评估 q [ j 1 ] 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 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是否报告该属性被违反来验证它是否确实发生了变化

d  Data  : ( sent = d ) ( sent = d ) \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报告错误,也可以使用相同的技巧。对于形式为 [ A ] v \square [A]_{v} 的属性中的错误,请使用错误跟踪中的倒数第二个状态作为初始谓词,并使用跟踪中的最后一个状态(变量名称以撇号开头),重新运行TLC next-state的action。若要查找在评估下一个状态操作时发生的错误,请使用错误跟踪中的最后一个状态作为初始谓词。

(在这种情况下,TLC可能会在报告错误之前找到多个后继状态。)如果您在配置文件中引入了模型值,那么毫无疑问,它们会出现在TLC打印的状态中。因此,如果要将这些状态复制到模块中,则必须将模型值声明为常量参数,然后将相同名称的模型值分配给每个参数。例如,我们用于alternating bit protocol的配置文件引入了模型值d1和d2。因此,我们将其添加到模块MCAlternatingBit声明中:

CONSTANTS d1, d2

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

Don’t Start Over After Every Error

消除了容易发现的错误之后,TLC可能必须运行很长时间才能发现错误。通常,要正确地纠正一个错误,需要进行多次尝试。如果您在更正错误后从头开始启动TLC,它可能会运行很长时间,仅报告您在更正中犯了一个愚蠢的错误。如果从正确的状态迈出一步时发现了错误,那么最好从该状态启动TLC来检查您的更正是否正确。如上所述,您可以通过定义一个新的初始谓词来做到这一点,该谓词等于TLC打印的状态。

避免出现错误后从头开始的另一种方法是使用检查点。检查点保存当前状态图 G \mathcal{G} 和未探索状态的队列 U \mathcal{U} 。它不会保存有关specification的任何其他信息。即使更改了specification,也可以从检查点重新启动TLC,只要specification的变量和它们可以假定的值没有改变即可。更准确地说,您可以从检查点重新启动,即在检查点未更改且对称集相同之前计算的任何状态的视图。当您纠正了TLC长时间运行后发现的错误时,您可能想使用restore选项(第252页)从最后一个检查点继续TLC,而不是让它重新检查它已经检查的所有状态。

Check Everything You Can

检查您的specification满所有足您认为应该检查的属性。例如,您不应该只检查alternating bit protocol specification是否满足模块ABCorrectness的高级规范ABCSpec。您还应该检查您希望它满足的较低级别的属性。通过研究算法发现的一个这样的属性是,msgQ队列中不应有超过两个不同的消息。因此,我们可以检查以下谓词是否不变量:

 Cardinality  ( { msg Q [ i ] : i 1.. Len ( msg Q ) } ) 2 \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的形式为 n N a t : A ( n ) \exists n \in Nat:A(n) , TLC无法在无限集上的量化评估,因此显然无法处理此规范。但是,我们可以使TLC通过使用配置文件的CONSTANT语句将n替换为有限集合 0.. n 0..n (对于某些n)来评估量化公式。此替换将彻底改变规范的含义。但是,它可能仍然可以让TLC发现specification中的错误。永远不要忘记,使用TLC的目的不是验证规范是否正确,而是发现错误。

Use TLC as a TLA+ Calculator

对TLA +某些方面的误解可能会导致specification出错,可以通过在小示例上运行TLC来检查您对TLA +的理解。

TLC可以检查假设,因此您可以通过检查没有specification的模块(仅ASSUME语句)将其变成TLA+计算器。 例如,如果g等于

[ f  EXCEPT  ! [ d ] = e 1 , ! [ d ] = e 2 ] \left[f \text { EXCEPT } ![d]=e_{1}, ![d]=e_{2}\right]

那么g[d]的值是什么?你可以让TLC检查一个模型,其包含如下语句

 ASSUME  LET  f [ i 1..10 1 ]       g [ f  EXCEPT  ! [ 2 ] = 3 , ! [ 2 ] = 4 ]  IN Print  ( g [ 2 ] ,  TRUE  ) \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 G ) ( ¬ F G ) (F \Rightarrow G) \equiv(\neg F \vee G) 是否是一个重言式:

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

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

ASSUME  S SUBSET ( 1..4 ) : IF  T , U SUBSET ( 1..4 ) : ( T U ) ( S = T U ) THEN TRUE  ELSE Print ( S , TRUE ) \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只用来检查假设时,不需要从配置文件中读取信息,不过你仍然需要提供一个配置文件,哪怕是空的也行。

发布了4 篇原创文章 · 获赞 1 · 访问量 5535

猜你喜欢

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