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
并将赋值语句 添加到配置文件的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只用来检查假设时,不需要从配置文件中读取信息,不过你仍然需要提供一个配置文件,哪怕是空的也行。