tla-ts 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +34 -41
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +166 -30
  4. package/dist/index.d.ts +166 -30
  5. package/dist/index.js +1 -1
  6. package/dist/parallel-worker.cjs +2 -2
  7. package/dist/parallel-worker.js +2 -2
  8. package/dist/parallel.cjs +1 -1
  9. package/dist/parallel.js +2 -2
  10. package/dist/run-cli.cjs +2 -1
  11. package/dist/run-cli.js +2 -1
  12. package/dist/transfer.cjs +388 -39
  13. package/dist/transfer.js +388 -39
  14. package/package.json +4 -2
  15. package/src/ast.ts +82 -5
  16. package/src/bags.ts +161 -71
  17. package/src/builtins.ts +203 -23
  18. package/src/bundled-modules.ts +63 -32
  19. package/src/check.ts +904 -230
  20. package/src/codegen.ts +52 -18
  21. package/src/config.ts +39 -11
  22. package/src/environment.ts +485 -140
  23. package/src/errors.ts +22 -0
  24. package/src/evaluate.ts +486 -136
  25. package/src/exit-codes.ts +75 -0
  26. package/src/fingerprints.ts +2 -2
  27. package/src/index.ts +9 -4
  28. package/src/instances.ts +221 -66
  29. package/src/java-overrides.ts +241 -0
  30. package/src/json.ts +153 -72
  31. package/src/levels.ts +850 -0
  32. package/src/lexer.ts +97 -27
  33. package/src/liveness.ts +47 -91
  34. package/src/parallel-worker.ts +57 -28
  35. package/src/parallel.ts +116 -62
  36. package/src/parser.ts +703 -183
  37. package/src/pretty-print.ts +259 -0
  38. package/src/randomization.ts +24 -113
  39. package/src/resolve.ts +472 -41
  40. package/src/run-cli.ts +142 -44
  41. package/src/runtime.ts +63 -28
  42. package/src/spec-description.ts +509 -0
  43. package/src/standard-levels.ts +105 -0
  44. package/src/standard-modules.ts +223 -92
  45. package/src/subexpressions.ts +21 -35
  46. package/src/substitution-levels.ts +184 -0
  47. package/src/successors.ts +261 -46
  48. package/src/symmetry.ts +65 -21
  49. package/src/tableau.ts +43 -37
  50. package/src/tlc-ext.ts +120 -33
  51. package/src/tlc-module.ts +71 -47
  52. package/src/tlc-modules.ts +1022 -0
  53. package/src/tlc-order.ts +497 -0
  54. package/src/tlc-random.ts +411 -0
  55. package/src/tlc-summary.ts +109 -0
  56. package/src/transfer.ts +4 -4
  57. package/src/values.ts +1186 -164
package/src/check.ts CHANGED
@@ -1,41 +1,77 @@
1
- import type { Assumption, Expression, SourceLocation } from "./ast";
2
- import { parseConfig, type ModelConfig } from "./config";
1
+ import type { Assumption, Definition, Expression, SourceLocation } from "./ast";
2
+ import { parseConfig, type ConfigValue, type ModelConfig } from "./config";
3
3
  import { getReadVariables } from "./dependencies";
4
4
  import { createEnvironment } from "./environment";
5
- import { TlaConfigError, TlaEvaluationError, TlaUnsupportedError } from "./errors";
5
+ import { getErrorExitCode, TlaConfigError, TlaEvaluationError, TlaModuleError } from "./errors";
6
+ import { EXIT_STATUS, setExitCode, setNextStatePhase, withLivenessExitCode } from "./exit-codes";
6
7
  import {
8
+ describeRange,
7
9
  evaluate,
8
10
  expectBoolean,
11
+ formatRange,
12
+ getChildren,
9
13
  getDefinition,
14
+ isConstantLevel,
15
+ isImpure,
10
16
  type Context,
11
17
  type Environment,
12
18
  type Scope,
13
19
  } from "./evaluate";
14
20
  import {
15
- ACTION_LEVEL,
16
21
  createLivenessChecker,
17
22
  expandTemporal,
18
23
  getLevel,
19
- TEMPORAL_LEVEL,
20
- toUnscopedExpression,
24
+ type Lasso,
21
25
  type LiveSuccessor,
22
26
  type ScopedExpression,
23
27
  type TemporalProperty,
24
28
  } from "./liveness";
25
- import { parseModule } from "./parser";
26
- import { getInitialStates, getSuccessors } from "./successors";
29
+ import { ACTION_LEVEL, createLevelAnalyzer, TEMPORAL_LEVEL } from "./levels";
30
+ import { prettyPrint } from "./pretty-print";
31
+ import {
32
+ forEachInitialState,
33
+ forEachSuccessor,
34
+ getInitialStates,
35
+ getSuccessors,
36
+ type Successor,
37
+ } from "./successors";
38
+ import {
39
+ getActionLabel,
40
+ getNextActions,
41
+ getSpecificationValue,
42
+ type SpecAction,
43
+ } from "./spec-description";
27
44
  import { getRepresentative, getSymmetryGroup } from "./symmetry";
45
+ import { getTlcStringOrder, getTlcVariableOrder } from "./tlc-order";
46
+ import { createTlcFingerprinter, getTlcFingerprint } from "./tlc-random";
28
47
  import { createCanonicalizer } from "./canonical";
29
48
  import { getPossibleCounts, type CheckerRuntime } from "./runtime";
30
- import { combineHash, formatValue, getValueHash, isSameValue, type Value } from "./values";
49
+ import { createCounterExample } from "./tlc-ext";
50
+ import { getFingerprintDistance, OUT_DEGREE_BUCKETS } from "./tlc-summary";
51
+ import {
52
+ combineHash,
53
+ formatValue,
54
+ getValueHash,
55
+ isFunctionValue,
56
+ isSameValue,
57
+ setStringOrder,
58
+ toFiniteFunction,
59
+ type Value,
60
+ } from "./values";
31
61
 
32
62
  export interface CheckOptions {
33
63
  source: string;
34
64
  config: string | ModelConfig;
35
65
  loadModule?: (name: string) => string | undefined;
66
+ loadClass?: (name: string) => Uint8Array | undefined;
36
67
  onPrint?: (text: string) => void;
68
+ onPause?: () => void;
37
69
  readFile?: (path: string) => string | undefined;
38
70
  writeFile?: (path: string, content: string) => void;
71
+ seed?: bigint;
72
+ workers?: number;
73
+ // TLC's -lncheck: "final" checks liveness only on the complete state graph.
74
+ livenessCheck?: "default" | "final";
39
75
  }
40
76
 
41
77
  export type CheckStatus =
@@ -45,10 +81,13 @@ export type CheckStatus =
45
81
  | "property-violation"
46
82
  | "liveness-violation"
47
83
  | "postcondition-violation"
48
- | "deadlock";
84
+ | "deadlock"
85
+ | "error";
49
86
 
50
87
  export interface TraceStep {
51
88
  action: string | undefined;
89
+ // TLC's name, parameters and location for the action that took this step.
90
+ label: string | undefined;
52
91
  state: Record<string, Value>;
53
92
  }
54
93
 
@@ -56,41 +95,68 @@ export interface Violation {
56
95
  name: string;
57
96
  trace: TraceStep[];
58
97
  loopStart: number | undefined;
98
+ loopLabel?: string;
59
99
  message?: string;
100
+ counts?: string;
101
+ unwitnessed?: Postcondition;
102
+ }
103
+
104
+ interface Postcondition {
105
+ name: string;
106
+ message: string;
107
+ counts?: string;
108
+ }
109
+
110
+ // An evaluation that failed during the search, with the behavior up to the state TLC was working on.
111
+ export interface EvaluationFailure {
112
+ message: string;
113
+ trace: TraceStep[];
114
+ initialState: Record<string, Value> | undefined;
115
+ exitCode: number;
60
116
  }
61
117
 
62
118
  export interface CheckResult {
63
119
  status: CheckStatus;
64
120
  violation: Violation | undefined;
121
+ failure: EvaluationFailure | undefined;
65
122
  generatedStates: number;
66
123
  distinctStates: number;
124
+ queuedStates: number;
67
125
  depth: number;
126
+ // How many states that TLC counts as expanded found each number of new states, when TLC prints
127
+ // the outdegree of the state graph.
128
+ outDegrees: number[] | undefined;
129
+ // The smallest distance between the fingerprints of two distinct states, when TLC estimates a
130
+ // collision from the actual fingerprints.
131
+ fingerprintDistance: bigint | undefined;
68
132
  }
69
133
 
70
- export interface NamedExpression {
134
+ interface NamedExpression {
71
135
  name: string;
72
136
  expression: Expression;
73
137
  scope: Scope | undefined;
74
138
  }
75
139
 
76
- export interface StateCheck extends NamedExpression {
140
+ interface StateCheck extends NamedExpression {
77
141
  variables: number[] | undefined;
78
142
  }
79
143
 
80
- export interface ActionProperty {
144
+ interface ActionProperty {
81
145
  name: string;
146
+ box: Expression;
82
147
  action: Expression;
83
148
  subscript: Expression;
84
149
  scope: Scope | undefined;
85
150
  }
86
151
 
87
- export interface Specification {
88
- init: Expression;
89
- next: Expression;
152
+ interface Specification {
153
+ init: Expression | undefined;
154
+ next: Expression | undefined;
155
+ scope: Scope | undefined;
90
156
  temporals: ScopedExpression[];
91
157
  }
92
158
 
93
- export interface Properties {
159
+ interface Properties {
94
160
  initialPredicates: NamedExpression[];
95
161
  invariants: NamedExpression[];
96
162
  actionProperties: ActionProperty[];
@@ -99,18 +165,106 @@ export interface Properties {
99
165
 
100
166
  const START: SourceLocation = { line: 0, column: 0 };
101
167
 
168
+ const INITIAL_RECOVERY_FAILURE = "Failed to recover the initial state from its fingerprint.";
169
+ const NEXT_RECOVERY_FAILURE = "Failed to recover the next state from its fingerprint.";
170
+
102
171
  const createIdentifier = (name: string): Expression => ({
103
172
  kind: "Identifier",
104
173
  name,
105
174
  location: START,
106
175
  });
107
176
 
108
- const getDefinitionBody = (name: string, environment: Environment) => {
177
+ // TLC leaves the kind out when it rejects arguments to the specification or a property.
178
+ const UNNAMED_ARGUMENT_KINDS = new Set(["name", "property"]);
179
+
180
+ // TLC checks liveness on the graph so far 3 seconds into the search and then every minute, once
181
+ // the graph has grown by a tenth and liveness checking has taken at most a fifth of the time.
182
+ const PERIODIC_START = 3000;
183
+ const PROGRESS_INTERVAL = 60_000;
184
+ const LIVENESS_GROWTH = 0.1;
185
+ const LIVENESS_RATIO = 0.2;
186
+
187
+ const getConfigOperator = (kind: string, name: string, environment: Environment) => {
109
188
  const definition = getDefinition(name, environment);
110
- if (definition?.kind !== "Operator" || definition.params.length > 0) {
111
- throw new TlaConfigError(`${name} is not defined as an operator without arguments`);
189
+ if (!definition) {
190
+ throw new TlaConfigError(
191
+ `The ${kind} ${name} specified in the configuration file\nis not defined in the specification.`,
192
+ );
193
+ }
194
+ if (definition.kind === "Operator" && definition.params.length > 0) {
195
+ throw new TlaConfigError(
196
+ UNNAMED_ARGUMENT_KINDS.has(kind)
197
+ ? `TLC requires ${name} not to take any argument.`
198
+ : `TLC requires ${kind} not to take any argument, but one was given: ${name}`,
199
+ );
200
+ }
201
+ return definition;
202
+ };
203
+
204
+ const getConfigDefinition = (kind: string, name: string, environment: Environment) => {
205
+ const definition = getConfigOperator(kind, name, environment);
206
+ if (isConstantLevel(definition, environment)) {
207
+ throw new TlaConfigError(`The ${kind} ${name} cannot be a constant.`);
208
+ }
209
+ return definition;
210
+ };
211
+
212
+ // TLC evaluates every constant definition before it reads the configuration. It keeps a literal
213
+ // number or string, and a definition that fails to evaluate, as an operator.
214
+ const getConstantValue = (name: string, definition: Definition, environment: Environment) => {
215
+ if (!isConstantLevel(definition, environment)) return undefined;
216
+ if (definition.body.kind === "Number" || definition.body.kind === "String") return undefined;
217
+ try {
218
+ return evaluate(createIdentifier(name), getContext(environment, undefined, undefined));
219
+ } catch (error) {
220
+ if (error instanceof TlaEvaluationError) return undefined;
221
+ throw error;
112
222
  }
113
- return definition.body;
223
+ };
224
+
225
+ const getConfigPredicate = (kind: string, name: string, environment: Environment) => {
226
+ const definition = getConfigOperator(kind, name, environment);
227
+ const value = getConstantValue(name, definition, environment);
228
+ if (value !== undefined && value !== true) {
229
+ throw new TlaConfigError(`The ${kind} of ${name} is equal to ${formatValue(value)}`);
230
+ }
231
+ return value === undefined ? definition : undefined;
232
+ };
233
+
234
+ const getInvariants = (names: string[], environment: Environment) => {
235
+ const analyzer = createLevelAnalyzer({
236
+ constants: new Set(environment.constants.keys()),
237
+ variables: new Set(environment.variables),
238
+ getDefinition: (name) => getDefinition(name, environment),
239
+ });
240
+ return names.flatMap((name) => {
241
+ const definition = getConfigPredicate("invariant", name, environment);
242
+ if (!definition) return [];
243
+ const levels = analyzer.analyzeDefinition(definition, new Map());
244
+ if (levels.level >= ACTION_LEVEL) {
245
+ const note =
246
+ levels.params.size === 0
247
+ ? ""
248
+ : "\nNote that a bug can cause TLC to incorrectly report this error.\nIf you believe your TLA+ or PlusCal specification to be correct,\nplease check if this bug described in LevelNode.java starting at line 590ff affects you.";
249
+ throw new TlaConfigError(
250
+ `The invariant ${name} is not a state predicate (one with no primes or temporal operators).${note}`,
251
+ );
252
+ }
253
+ return [{ name, expression: createIdentifier(name), scope: undefined }];
254
+ });
255
+ };
256
+
257
+ const toAliasRecord = (value: Value, fieldOrder: string[]) => {
258
+ if (!isFunctionValue(value)) return undefined;
259
+ const record = toFiniteFunction(value);
260
+ if (!record.domain.every((key) => typeof key === "string")) return undefined;
261
+ const entries = record.domain.map((key, index): [string, Value] => [
262
+ String(key),
263
+ record.range[index],
264
+ ]);
265
+ return Object.fromEntries(
266
+ entries.sort((left, right) => fieldOrder.indexOf(left[0]) - fieldOrder.indexOf(right[0])),
267
+ );
114
268
  };
115
269
 
116
270
  const getConjuncts = (expression: Expression, environment: Environment): Expression[] => {
@@ -125,39 +279,66 @@ const getConjuncts = (expression: Expression, environment: Environment): Express
125
279
  return [expression];
126
280
  };
127
281
 
128
- export const getSpecification = (config: ModelConfig, environment: Environment): Specification => {
282
+ const isConstantTrue = (item: ScopedExpression, environment: Environment, usage: string) => {
283
+ const conjunct = item.expression;
284
+ if (conjunct.kind !== "Identifier" || getLevel(conjunct, item.scope, environment) > 0) {
285
+ return false;
286
+ }
287
+ const value = evaluate(conjunct, getContext(environment, undefined, undefined, item.scope));
288
+ if (value === false) {
289
+ throw new TlaConfigError(`The spec is trivially false because ${conjunct.name} is false.`);
290
+ }
291
+ if (value !== true) {
292
+ throw new TlaConfigError(
293
+ `The operator ${conjunct.name}, which equals ${formatValue(value)},\ncannot be used as a ${usage}`,
294
+ );
295
+ }
296
+ return true;
297
+ };
298
+
299
+ const getSpecification = (config: ModelConfig, environment: Environment): Specification => {
129
300
  if (config.specification === undefined) {
130
- if (config.init === undefined)
131
- throw new TlaConfigError(
132
- "The configuration file did not specify the initial state predicate",
133
- );
134
- if (config.next === undefined)
135
- throw new TlaConfigError("The configuration does not specify NEXT");
136
- getDefinitionBody(config.init, environment);
137
- getDefinitionBody(config.next, environment);
301
+ const getOperator = (name: string | undefined, description: string) => {
302
+ if (name === undefined) return undefined;
303
+ const body = getConfigOperator(description, name, environment).body;
304
+ const identifier = createIdentifier(name);
305
+ // TLC evaluates a literal body later and reports that it is not a boolean.
306
+ const isLiteral = body.kind === "Number" || body.kind === "String";
307
+ if (!isLiteral && getLevel(identifier, undefined, environment) === 0) {
308
+ throw new TlaConfigError(`The ${description} ${name} cannot be a constant.`);
309
+ }
310
+ return identifier;
311
+ };
138
312
  return {
139
- init: createIdentifier(config.init),
140
- next: createIdentifier(config.next),
313
+ init: getOperator(config.init, "initial predicate"),
314
+ next: getOperator(config.next, "next state action"),
315
+ scope: undefined,
141
316
  temporals: [],
142
317
  };
143
318
  }
144
- const initConjuncts: Expression[] = [];
145
- const nextActions: Expression[] = [];
319
+ const initConjuncts: ScopedExpression[] = [];
320
+ const nextActions: ScopedExpression[] = [];
146
321
  const temporals: ScopedExpression[] = [];
147
- const body = getDefinitionBody(config.specification, environment);
148
- const getUnscoped = (expression: Expression, scope: Scope | undefined) => {
149
- const unscoped = toUnscopedExpression(expression, scope);
150
- if (!unscoped) {
151
- throw new TlaUnsupportedError(
152
- `The specification ${config.specification} has an initial predicate or next-state relation inside a quantified formula`,
322
+ const definition = getConfigOperator("name", config.specification, environment);
323
+ const value = getConstantValue(config.specification, definition, environment);
324
+ if (value !== undefined) {
325
+ throw new TlaConfigError(
326
+ `The value of ${config.specification} is equal to ${formatValue(value)}`,
327
+ );
328
+ }
329
+ expandTemporal(definition.body, undefined, environment, false).forEach((item) => {
330
+ const conjunct = item.expression;
331
+ if (isConstantTrue(item, environment, "spec")) return;
332
+ if (conjunct.kind === "UnboundedQuantifier" && conjunct.isTemporal) {
333
+ throw setExitCode(
334
+ new TlaConfigError(
335
+ "TLC does not support temporal existential, nor universal, quantification over state variables.",
336
+ ),
337
+ EXIT_STATUS.error,
153
338
  );
154
339
  }
155
- return unscoped;
156
- };
157
- expandTemporal(body, undefined, environment).forEach((item) => {
158
- const conjunct = item.expression;
159
340
  if (conjunct.kind === "Always" && conjunct.expression.kind === "BoxAction") {
160
- nextActions.push(getUnscoped(conjunct.expression.action, item.scope));
341
+ nextActions.push({ expression: conjunct.expression.action, scope: item.scope });
161
342
  return;
162
343
  }
163
344
  if (
@@ -178,21 +359,54 @@ export const getSpecification = (config: ModelConfig, environment: Environment):
178
359
  `TLC cannot handle the action-level conjunct of the specification ${config.specification}`,
179
360
  );
180
361
  }
181
- initConjuncts.push(getUnscoped(conjunct, item.scope));
362
+ initConjuncts.push(item);
182
363
  });
183
- if (nextActions.length !== 1) {
364
+ if (nextActions.length > 1) {
184
365
  throw new TlaConfigError(
185
- `The specification ${config.specification} must contain exactly one [][Next]_vars`,
366
+ `The specification ${config.specification} must contain at most one [][Next]_vars`,
186
367
  );
187
368
  }
369
+ const items = [...initConjuncts, ...nextActions];
370
+ const getName = (index: number) => `$specification${index}`;
371
+ const scope = items.reduce<Scope | undefined>(
372
+ (parent, item, index) =>
373
+ item.scope === undefined
374
+ ? parent
375
+ : {
376
+ name: getName(index),
377
+ binding: {
378
+ kind: "Thunk",
379
+ expression: item.expression,
380
+ scope: item.scope,
381
+ state: undefined,
382
+ next: undefined,
383
+ cache: undefined,
384
+ },
385
+ parent,
386
+ },
387
+ undefined,
388
+ );
389
+ const getReference = (item: ScopedExpression, index: number): Expression =>
390
+ item.scope === undefined
391
+ ? item.expression
392
+ : {
393
+ kind: "Identifier",
394
+ name: getName(index),
395
+ location: item.expression.location,
396
+ resolution: { kind: "Local" },
397
+ };
188
398
  return {
189
- init: { kind: "And", operands: initConjuncts, location: START },
190
- next: nextActions[0],
399
+ init:
400
+ initConjuncts.length === 0
401
+ ? undefined
402
+ : { kind: "And", operands: initConjuncts.map(getReference), location: START },
403
+ next: nextActions.length === 0 ? undefined : getReference(nextActions[0], initConjuncts.length),
404
+ scope,
191
405
  temporals,
192
406
  };
193
407
  };
194
408
 
195
- export const getProperties = (config: ModelConfig, environment: Environment): Properties => {
409
+ const getProperties = (config: ModelConfig, environment: Environment): Properties => {
196
410
  const properties: Properties = {
197
411
  initialPredicates: [],
198
412
  invariants: [],
@@ -200,12 +414,15 @@ export const getProperties = (config: ModelConfig, environment: Environment): Pr
200
414
  liveness: [],
201
415
  };
202
416
  config.properties.forEach((name) => {
203
- const body = getDefinitionBody(name, environment);
204
- expandTemporal(body, undefined, environment).forEach((item) => {
417
+ const definition = getConfigPredicate("property", name, environment);
418
+ if (!definition) return;
419
+ expandTemporal(definition.body, undefined, environment, true).forEach((item) => {
205
420
  const conjunct = item.expression;
421
+ if (isConstantTrue(item, environment, "property")) return;
206
422
  if (conjunct.kind === "Always" && conjunct.expression.kind === "BoxAction") {
207
423
  properties.actionProperties.push({
208
424
  name,
425
+ box: conjunct.expression,
209
426
  action: conjunct.expression.action,
210
427
  subscript: conjunct.expression.subscript,
211
428
  scope: item.scope,
@@ -233,13 +450,38 @@ export const getProperties = (config: ModelConfig, environment: Environment): Pr
233
450
  return properties;
234
451
  };
235
452
 
236
- export const getNamedExpressions = (names: string[], environment: Environment): NamedExpression[] =>
237
- names.map((name) => {
238
- getDefinitionBody(name, environment);
239
- return { name, expression: createIdentifier(name), scope: undefined };
453
+ const getNamedExpressions = (
454
+ kind: string,
455
+ names: string[],
456
+ environment: Environment,
457
+ ): NamedExpression[] =>
458
+ names.flatMap((name) =>
459
+ getConfigPredicate(kind, name, environment)
460
+ ? [{ name, expression: createIdentifier(name), scope: undefined }]
461
+ : [],
462
+ );
463
+
464
+ const getBodyRange = (name: string, environment: Environment) => {
465
+ const definition = getDefinition(name, environment);
466
+ return definition?.kind === "Operator" ? definition.bodyRange : undefined;
467
+ };
468
+
469
+ const getPossiblePredicates = (names: string[], environment: Environment) =>
470
+ names.map((name, index) => {
471
+ if (names.indexOf(name) !== index) {
472
+ throw new TlaConfigError(`_POSSIBLE lists ${name} more than once`);
473
+ }
474
+ getConfigDefinition("possible", name, environment);
475
+ const expression = createIdentifier(name);
476
+ if (getLevel(expression, undefined, environment) === TEMPORAL_LEVEL) {
477
+ throw new TlaConfigError(
478
+ `The possible of ${name} is equal to a temporal formula; only state- and action-level predicates are supported`,
479
+ );
480
+ }
481
+ return { name, expression, scope: undefined };
240
482
  });
241
483
 
242
- export const getStateChecks = (named: NamedExpression[], environment: Environment): StateCheck[] =>
484
+ const getStateChecks = (named: NamedExpression[], environment: Environment): StateCheck[] =>
243
485
  named.flatMap((item) =>
244
486
  getConjuncts(item.expression, environment).map((expression) => ({
245
487
  name: item.name,
@@ -249,11 +491,7 @@ export const getStateChecks = (named: NamedExpression[], environment: Environmen
249
491
  })),
250
492
  );
251
493
 
252
- export const isUnchangedFor = (
253
- stateCheck: StateCheck,
254
- state: Value[],
255
- parent: Value[] | undefined,
256
- ) => {
494
+ const isUnchangedFor = (stateCheck: StateCheck, state: Value[], parent: Value[] | undefined) => {
257
495
  const variables = stateCheck.variables;
258
496
  if (!parent || !variables) return false;
259
497
  for (let index = 0; index < variables.length; index++) {
@@ -262,10 +500,40 @@ export const isUnchangedFor = (
262
500
  return true;
263
501
  };
264
502
 
265
- export interface Search {
503
+ const WIDE_CHARACTER = /[\u0100-\uffff]/;
504
+
505
+ const DESERIALIZERS = new Set(["JsonDeserialize", "ndJsonDeserialize"]);
506
+
507
+ const isWideConfigValue = (value: ConfigValue): boolean =>
508
+ value.kind === "String"
509
+ ? WIDE_CHARACTER.test(value.value)
510
+ : value.kind === "Set" && value.elements.some(isWideConfigValue);
511
+
512
+ const isWideExpression = (expression: Expression): boolean =>
513
+ (expression.kind === "String" && WIDE_CHARACTER.test(expression.value)) ||
514
+ (expression.kind === "Apply" && DESERIALIZERS.has(expression.operator)) ||
515
+ getChildren(expression).some(isWideExpression);
516
+
517
+ // TLC fingerprints a string by the low byte of each character, so it merges states whose
518
+ // strings differ only above U+00FF. Strings reach a state from literals, the config, or files.
519
+ const hasWideString = (config: ModelConfig, environment: Environment) =>
520
+ config.constants.some(
521
+ (constant) =>
522
+ isWideConfigValue(constant.value) || (constant.args ?? []).some(isWideConfigValue),
523
+ ) ||
524
+ [...environment.definitions.values()].some(
525
+ (definition) =>
526
+ isWideExpression(definition.body) ||
527
+ (definition.kind === "Function" &&
528
+ definition.bounds.some((bound) => isWideExpression(bound.set))),
529
+ );
530
+
531
+ interface Search {
266
532
  properties: Properties;
267
533
  spec: Specification;
268
534
  getFingerprintState: (state: Value[]) => Value[];
535
+ getStoredFingerprints: (states: Value[][], fingerprintStates: Value[][]) => BigInt64Array;
536
+ getTlcOrder: () => number[];
269
537
  isSymmetric: boolean;
270
538
  findViolatedInvariant: (state: Value[], parent?: Value[]) => StateCheck | undefined;
271
539
  satisfiesConstraints: (state: Value[], parent?: Value[]) => boolean;
@@ -274,9 +542,8 @@ export interface Search {
274
542
  satisfiesActionConstraints: (state: Value[], next: Value[]) => boolean;
275
543
  trackStatePossible: (state: Value[]) => void;
276
544
  trackActionPossible: (state: Value[], next: Value[]) => void;
277
- findPostconditionViolation: (
278
- print: (text: string) => void,
279
- ) => Omit<Violation, "loopStart"> | undefined;
545
+ findUnwitnessedPossible: () => Postcondition | undefined;
546
+ findPostconditionViolation: () => Postcondition | undefined;
280
547
  }
281
548
 
282
549
  const getContext = (
@@ -284,11 +551,39 @@ const getContext = (
284
551
  state: Value[] | undefined,
285
552
  next: Value[] | undefined,
286
553
  scope?: Scope,
287
- ): Context => ({ environment, scope, state, next });
554
+ ) => ({ environment, scope, state, next });
288
555
 
289
556
  const holds = (expression: Expression, context: Context, description: string) =>
290
557
  expectBoolean(evaluate(expression, context), description);
291
558
 
559
+ // TLC names a check that is not a boolean by the location of the predicate it evaluates.
560
+ const holdsLikeTlc = (
561
+ expression: Expression,
562
+ context: Context,
563
+ getMessage = getExpectedBoolean,
564
+ ) => {
565
+ const value = evaluate(expression, context);
566
+ if (typeof value !== "boolean") {
567
+ throw new TlaEvaluationError(getMessage(expression, context.environment));
568
+ }
569
+ return value;
570
+ };
571
+
572
+ // SANY prints a number by its digits and any other expression by its location.
573
+ const getExpectedBoolean = (expression: Expression, environment: Environment) => {
574
+ const definition =
575
+ expression.kind === "Identifier" ? getDefinition(expression.name, environment) : undefined;
576
+ const body = definition?.kind === "Operator" ? definition.body : expression;
577
+ const range = definition?.kind === "Operator" ? definition.bodyRange : undefined;
578
+ const location =
579
+ body.kind === "Number"
580
+ ? (body.text ?? String(body.value))
581
+ : range
582
+ ? formatRange(range)
583
+ : describeRange(body);
584
+ return `TLC expected a boolean value, but did not find one. ${location}`;
585
+ };
586
+
292
587
  const ignorePossible = () => {};
293
588
 
294
589
  const trackPossible = (items: NamedExpression[], runtime: CheckerRuntime, context: Context) =>
@@ -320,36 +615,56 @@ export const findAssumptionViolation = (
320
615
  for (const assumption of assumptions.filter((candidate) => !candidate.isAxiom)) {
321
616
  const failure = getConstantFailure(assumption.expression, environment, "an assumption");
322
617
  if (failure) {
323
- const name =
324
- assumption.name ?? `line ${assumption.location.line}, column ${assumption.location.column}`;
325
- return { name, trace: [], message: failure.message };
618
+ return { name: describeRange(assumption.expression), trace: [], message: failure.message };
326
619
  }
327
620
  }
328
621
  return undefined;
329
622
  };
330
623
 
331
- export const createSearch = (config: ModelConfig, environment: Environment): Search | undefined => {
624
+ export const createSearch = (
625
+ config: ModelConfig,
626
+ environment: Environment,
627
+ getVariableOrder: () => string[],
628
+ ): Search | undefined => {
629
+ const configInvariants = getInvariants(config.invariants, environment);
630
+ const spec = getSpecification(config, environment);
332
631
  const properties = getProperties(config, environment);
333
- const invariants = getStateChecks(
334
- [...getNamedExpressions(config.invariants, environment), ...properties.invariants],
632
+ const invariants = getStateChecks([...configInvariants, ...properties.invariants], environment);
633
+ const isCheckingBehaviors =
634
+ invariants.length > 0 ||
635
+ properties.actionProperties.length > 0 ||
636
+ properties.liveness.length > 0;
637
+ if (
638
+ spec.init === undefined &&
639
+ (isCheckingBehaviors ||
640
+ properties.initialPredicates.length > 0 ||
641
+ environment.variables.length > 0)
642
+ ) {
643
+ throw new TlaConfigError("The configuration file did not specify the initial state predicate.");
644
+ }
645
+ if (spec.next === undefined && isCheckingBehaviors) {
646
+ throw new TlaConfigError("The configuration file did not specify the next state predicate.");
647
+ }
648
+ const constraints = getStateChecks(
649
+ getNamedExpressions("constraint", config.constraints, environment),
335
650
  environment,
336
651
  );
337
- const isConstantModel =
338
- config.specification === undefined &&
339
- config.init === undefined &&
340
- environment.variables.length === 0;
341
- if (isConstantModel) return undefined;
342
- const spec = getSpecification(config, environment);
652
+ const actionConstraints = getNamedExpressions(
653
+ "action constraint",
654
+ config.actionConstraints,
655
+ environment,
656
+ );
657
+ if (config.rlReward !== undefined) getConfigOperator("rlreward", config.rlReward, environment);
658
+ if (config.periodic !== undefined) getConfigOperator("periodic", config.periodic, environment);
659
+ const possible = getPossiblePredicates(config.possible, environment);
343
660
  if (environment.variables.length === 0) return undefined;
344
661
  const symmetryGroup =
345
662
  config.symmetry === undefined ? [] : getSymmetryGroup(config.symmetry, environment);
663
+ if (config.view !== undefined) getConfigOperator("view function", config.view, environment);
346
664
  const view =
347
- config.view === undefined ? undefined : getNamedExpressions([config.view], environment)[0];
348
- const constraints = getStateChecks(
349
- getNamedExpressions(config.constraints, environment),
350
- environment,
351
- );
352
- const actionConstraints = getNamedExpressions(config.actionConstraints, environment);
665
+ config.view === undefined
666
+ ? undefined
667
+ : { name: config.view, expression: createIdentifier(config.view), scope: undefined };
353
668
  const actionProperties = properties.actionProperties.map((property) => {
354
669
  const unchanged: Expression = {
355
670
  kind: "Unchanged",
@@ -358,72 +673,130 @@ export const createSearch = (config: ModelConfig, environment: Environment): Sea
358
673
  };
359
674
  return { ...property, unchanged };
360
675
  });
361
- const possible = getNamedExpressions(config.possible, environment);
362
676
  const isActionPossible = (item: NamedExpression) =>
363
677
  getLevel(item.expression, undefined, environment) >= ACTION_LEVEL;
364
678
  const statePossible = possible.filter((item) => !isActionPossible(item));
365
679
  const actionPossible = possible.filter(isActionPossible);
366
680
  const runtime = environment.runtime;
681
+ const tlcOrder = { indices: undefined as number[] | undefined };
682
+ const getTlcOrder = () => {
683
+ if (tlcOrder.indices) return tlcOrder.indices;
684
+ const variableOrder = getVariableOrder();
685
+ const getTlcPosition = (index: number) => {
686
+ const position = variableOrder.indexOf(environment.variables[index]);
687
+ return position === -1 ? environment.variables.length : position;
688
+ };
689
+ tlcOrder.indices = environment.variables
690
+ .map((_, index) => index)
691
+ .sort((left, right) => getTlcPosition(left) - getTlcPosition(right));
692
+ return tlcOrder.indices;
693
+ };
694
+ // A representative in TLC's variable order is the one whose fingerprint TLC reports.
695
+ const order = symmetryGroup.length > 0 ? getTlcOrder() : undefined;
696
+ const getViewValue = (expression: Expression, state: Value[]) =>
697
+ evaluate(expression, getContext(environment, state, undefined));
698
+ const getTlcStateFingerprint = (state: Value[]) => {
699
+ const indices = getTlcOrder();
700
+ const representative =
701
+ symmetryGroup.length === 0 ? state : getRepresentative(state, symmetryGroup, indices);
702
+ return getTlcFingerprint(
703
+ view === undefined
704
+ ? indices.map((index) => representative[index])
705
+ : [getViewValue(view.expression, representative)],
706
+ );
707
+ };
708
+ const isFingerprintedLikeTlc = hasWideString(config, environment);
709
+ runtime.getFingerprint = getTlcStateFingerprint;
710
+ const findUnwitnessedPossible = () => {
711
+ const unwitnessed = possible.find((item) => !runtime.possibleCounts.get(item.name));
712
+ if (!unwitnessed) return undefined;
713
+ const range = getBodyRange(unwitnessed.name, environment);
714
+ const location = range ? ` at ${formatRange(range)}` : "";
715
+ return {
716
+ name: unwitnessed.name,
717
+ message: `The _POSSIBLE predicate ${unwitnessed.name}${location} was never witnessed.\nOthers may be unwitnessed too; only witnessed predicates appear with a\nnon-zero count in the record printed above.\nSee https://explain.tlapl.us/possible-conditions for additional details.`,
718
+ counts: prettyPrint(formatValue(getPossibleCounts(runtime), true)),
719
+ };
720
+ };
367
721
  return {
368
722
  properties,
369
723
  spec,
370
724
  isSymmetric: symmetryGroup.length > 0,
725
+ getTlcOrder,
726
+ getStoredFingerprints: (states, fingerprintStates) => {
727
+ if (view !== undefined || isFingerprintedLikeTlc) {
728
+ return BigInt64Array.from(states, getTlcStateFingerprint);
729
+ }
730
+ const indices = getTlcOrder();
731
+ const getFingerprint = createTlcFingerprinter();
732
+ return BigInt64Array.from(fingerprintStates, (state) =>
733
+ getFingerprint(indices.map((index) => state[index])),
734
+ );
735
+ },
371
736
  getFingerprintState: (state) => {
737
+ if (isFingerprintedLikeTlc) return [String(getTlcStateFingerprint(state))];
372
738
  const representative =
373
- symmetryGroup.length === 0 ? state : getRepresentative(state, symmetryGroup);
374
- return view === undefined
375
- ? representative
376
- : [evaluate(view.expression, getContext(environment, representative, undefined))];
739
+ symmetryGroup.length === 0 ? state : getRepresentative(state, symmetryGroup, order);
740
+ return view === undefined ? representative : [getViewValue(view.expression, representative)];
377
741
  },
378
742
  findViolatedInvariant: (state, parent) =>
379
- invariants.find(
380
- (invariant) =>
381
- !isUnchangedFor(invariant, state, parent) &&
382
- !holds(
743
+ invariants.find((invariant) => {
744
+ if (isUnchangedFor(invariant, state, parent)) return false;
745
+ try {
746
+ return !holdsLikeTlc(
383
747
  invariant.expression,
384
748
  getContext(environment, state, undefined, invariant.scope),
385
- `invariant ${invariant.name}`,
386
- ),
387
- ),
749
+ );
750
+ } catch (error) {
751
+ // TLC reports a failure only on a state after the initial states as an invariant failure.
752
+ if (!parent) throw error;
753
+ throw withHeadline(
754
+ setExitCode(error, EXIT_STATUS.safetyEvaluationFailure),
755
+ `Evaluating invariant ${invariant.name} failed.`,
756
+ );
757
+ }
758
+ }),
388
759
  satisfiesConstraints: (state, parent) =>
389
760
  constraints.every(
390
761
  (constraint) =>
391
762
  isUnchangedFor(constraint, state, parent) ||
392
- holds(
393
- constraint.expression,
394
- getContext(environment, state, undefined),
395
- `constraint ${constraint.name}`,
396
- ),
763
+ holdsLikeTlc(constraint.expression, getContext(environment, state, undefined)),
397
764
  ),
398
765
  findFailedInitialPredicate: (state) =>
399
766
  properties.initialPredicates.find(
400
767
  (predicate) =>
401
- !holds(
768
+ !holdsLikeTlc(
402
769
  predicate.expression,
403
770
  getContext(environment, state, undefined, predicate.scope),
404
- `property ${predicate.name}`,
405
771
  ),
406
772
  ),
407
773
  findViolatedActionProperty: (state, next) =>
408
- actionProperties.find(
409
- (property) =>
410
- !holds(
411
- property.action,
412
- getContext(environment, state, next, property.scope),
413
- `property ${property.name}`,
414
- ) &&
415
- !holds(
416
- property.unchanged,
417
- getContext(environment, state, next, property.scope),
418
- "a subscript",
419
- ),
420
- ),
774
+ actionProperties.find((property) => {
775
+ try {
776
+ return (
777
+ !holdsLikeTlc(
778
+ property.action,
779
+ getContext(environment, state, next, property.scope),
780
+ () =>
781
+ `Attempted to evaluate an expression of form [A]_e, but A was not a boolean.\n${describeRange(property.box)}`,
782
+ ) &&
783
+ !holds(
784
+ property.unchanged,
785
+ getContext(environment, state, next, property.scope),
786
+ "a subscript",
787
+ )
788
+ );
789
+ } catch (error) {
790
+ throw withHeadline(error, `Evaluating action property ${property.name} failed.`);
791
+ }
792
+ }),
421
793
  satisfiesActionConstraints: (state, next) =>
422
794
  actionConstraints.every((constraint) =>
423
- holds(
795
+ holdsLikeTlc(
424
796
  constraint.expression,
425
797
  getContext(environment, state, next),
426
- `action constraint ${constraint.name}`,
798
+ (expression) =>
799
+ `TLC threw an unexpected exception.\nThis was probably caused by an error in the spec or model.\nSee the User Output or TLC Console for clues to what happened.\nThe exception was a java.lang.RuntimeException\n: ${getExpectedBoolean(expression, environment)}`,
427
800
  ),
428
801
  ),
429
802
  trackStatePossible:
@@ -436,20 +809,14 @@ export const createSearch = (config: ModelConfig, environment: Environment): Sea
436
809
  ? ignorePossible
437
810
  : (state, next) =>
438
811
  trackPossible(actionPossible, runtime, getContext(environment, state, next)),
439
- findPostconditionViolation: (print) => {
440
- const unwitnessed = possible.find((item) => !runtime.possibleCounts.get(item.name));
441
- if (unwitnessed) {
442
- print(formatValue(getPossibleCounts(runtime), true));
443
- return {
444
- name: unwitnessed.name,
445
- trace: [],
446
- message: `The _POSSIBLE predicate ${unwitnessed.name} was never witnessed.`,
447
- };
448
- }
812
+ findUnwitnessedPossible,
813
+ findPostconditionViolation: () => {
814
+ const unwitnessed = findUnwitnessedPossible();
815
+ if (unwitnessed) return unwitnessed;
449
816
  for (const name of config.postconditions) {
450
817
  const failure = getConstantFailure(createIdentifier(name), environment, "a postcondition");
451
818
  if (failure) {
452
- return { name, trace: [], message: failure.message ?? `Postcondition ${name} is false.` };
819
+ return { name, message: failure.message ?? `Postcondition ${name} is false.` };
453
820
  }
454
821
  }
455
822
  return undefined;
@@ -457,37 +824,114 @@ export const createSearch = (config: ModelConfig, environment: Environment): Sea
457
824
  };
458
825
  };
459
826
 
460
- export const check = (options: CheckOptions): CheckResult => {
827
+ const withHeadline = (error: unknown, headline: string) =>
828
+ error instanceof TlaEvaluationError
829
+ ? setExitCode(
830
+ new TlaEvaluationError(`${headline}\n${error.message}`, error),
831
+ getErrorExitCode(error),
832
+ )
833
+ : error;
834
+
835
+ export const checkSequentially = (options: CheckOptions): CheckResult => {
836
+ setNextStatePhase(false);
461
837
  const config = typeof options.config === "string" ? parseConfig(options.config) : options.config;
462
- const print = options.onPrint ?? console.log;
463
- const specification = createEnvironment(parseModule(options.source), config, {
838
+ const onPrint = options.onPrint ?? console.log;
839
+ // TLC prints again while it rebuilds a trace, between the states it prints. Printing those lines
840
+ // before the trace would misplace them, so rebuilding a trace prints nothing.
841
+ const output = { isMuted: false };
842
+ const print = (text: string) => {
843
+ if (!output.isMuted) onPrint(text);
844
+ };
845
+ const withoutOutput = <Result>(rebuild: () => Result) => {
846
+ output.isMuted = true;
847
+ const result = rebuild();
848
+ output.isMuted = false;
849
+ return result;
850
+ };
851
+ setStringOrder(getTlcStringOrder(options.source, config, options.loadModule));
852
+ const specification = createEnvironment(config, {
853
+ source: options.source,
464
854
  loadModule: options.loadModule,
855
+ loadClass: options.loadClass,
465
856
  print,
466
857
  files: { readFile: options.readFile, writeFile: options.writeFile },
858
+ seed: options.seed,
467
859
  });
468
860
  const environment = specification.environment;
469
861
  const runtime = environment.runtime;
862
+ runtime.pause = options.onPause ?? (() => {});
863
+ runtime.getSpecification = () =>
864
+ getSpecificationValue(
865
+ config,
866
+ environment,
867
+ specification.variableLocations,
868
+ specification.instanceActions,
869
+ );
470
870
  const statistics = { depth: 0 };
471
871
  const createResult = (
472
872
  status: CheckStatus,
473
873
  violation?: Omit<Violation, "loopStart"> & { loopStart?: number },
874
+ failure?: EvaluationFailure,
474
875
  ): CheckResult => ({
475
876
  status,
476
877
  violation: violation && { ...violation, loopStart: violation.loopStart },
878
+ failure,
477
879
  generatedStates: runtime.generatedStates,
478
880
  distinctStates: runtime.distinctStates,
479
- depth: statistics.depth,
881
+ queuedStates: runtime.queueSize,
882
+ depth: runtime.generatedStates === 0 ? 0 : Math.max(statistics.depth, 1),
883
+ outDegrees: undefined,
884
+ fingerprintDistance: undefined,
885
+ });
886
+ const outDegrees = Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0);
887
+ const expansion = { newStates: 0 };
888
+ const getOutDegreeBucket = () => Math.min(expansion.newStates, OUT_DEGREE_BUCKETS - 1);
889
+ // TLC counts the state it was expanding when an evaluation failed.
890
+ const getPartialOutDegrees = () =>
891
+ outDegrees.map((count, degree) => (degree === getOutDegreeBucket() ? count + 1 : count));
892
+ const createSuccess = (): CheckResult => ({
893
+ ...createResult("success"),
894
+ outDegrees: [...outDegrees],
895
+ fingerprintDistance: getFingerprintDistance(
896
+ runtime.distinctStates,
897
+ runtime.generatedStates,
898
+ () =>
899
+ search
900
+ ? search.getStoredFingerprints(states, fingerprintStates)
901
+ : BigInt64Array.from(states, runtime.getFingerprint),
902
+ ),
480
903
  });
481
904
 
905
+ const search = createSearch(config, environment, () =>
906
+ getTlcVariableOrder(options.source, options.loadModule),
907
+ );
482
908
  const assumptionViolation = findAssumptionViolation(specification.assumptions, environment);
483
909
  if (assumptionViolation) return createResult("assumption-violation", assumptionViolation);
484
-
485
- const search = createSearch(config, environment);
486
- if (!search) return createResult("success");
487
- const properties = search.properties;
910
+ if (!search) return createSuccess();
911
+ const hasPostconditions = config.postconditions.length > 0 || config.possible.length > 0;
912
+ const reportViolation = (
913
+ status: CheckStatus,
914
+ violation: Omit<Violation, "loopStart"> & { loopStart?: number },
915
+ getLoopAction?: () => string | undefined,
916
+ ) => {
917
+ if (hasPostconditions) {
918
+ const start = violation.loopStart;
919
+ runtime.counterExample = createCounterExample(
920
+ violation.trace,
921
+ start === undefined || !getLoopAction ? undefined : { start, action: getLoopAction() },
922
+ );
923
+ }
924
+ const unwitnessed = search.findUnwitnessedPossible();
925
+ if (!unwitnessed) search.findPostconditionViolation();
926
+ return createResult(status, unwitnessed ? { ...violation, unwitnessed } : violation);
927
+ };
488
928
  const spec = search.spec;
489
929
  const getFingerprintState = search.getFingerprintState;
490
- const liveness = createLivenessChecker(properties.liveness, spec.temporals, environment);
930
+ const nextActions = { value: undefined as SpecAction[] | undefined };
931
+ const isReducing = config.view !== undefined || config.symmetry !== undefined;
932
+ const liveness = withLivenessExitCode(() =>
933
+ createLivenessChecker(search.properties.liveness, spec.temporals, environment),
934
+ );
491
935
  const isCanonicalizedFirst = search.isSymmetric || liveness !== undefined;
492
936
 
493
937
  const states: Value[][] = [];
@@ -498,30 +942,146 @@ export const check = (options: CheckOptions): CheckResult => {
498
942
  const buckets = new Map<number, number | number[]>();
499
943
  const canonicalizeState = createCanonicalizer();
500
944
 
501
- const toRecord = (state: Value[]) =>
502
- Object.fromEntries(environment.variables.map((name, index) => [name, state[index]]));
503
- const getTrace = (
504
- index: number,
505
- last?: { state: Value[]; action: string | undefined },
506
- ): TraceStep[] => {
507
- const steps: TraceStep[] = last ? [{ action: last.action, state: toRecord(last.state) }] : [];
508
- for (let current = index; current !== -1; current = parents[current]) {
509
- steps.push({ action: actions[current], state: toRecord(states[current]) });
945
+ const toRecord = (state: Value[]): Record<string, Value> =>
946
+ Object.fromEntries(
947
+ search.getTlcOrder().map((index) => [environment.variables[index], state[index]]),
948
+ );
949
+ const getAliasState = (
950
+ alias: string,
951
+ steps: Successor[],
952
+ position: number,
953
+ loopStart: number | undefined,
954
+ ) => {
955
+ const state = steps[position].state;
956
+ const next = (steps[position + 1] ?? steps[loopStart ?? position]).state;
957
+ // TLC evaluates TLCGet("level") and Trace in an alias at the state's position in the trace.
958
+ runtime.getLevel = () => position + 1;
959
+ runtime.getBehavior = () => steps.slice(0, position + 1).map((step) => step.state);
960
+ try {
961
+ const body = getConfigDefinition("alias", alias, environment).body;
962
+ const value = evaluate(createIdentifier(alias), getContext(environment, state, next));
963
+ const fieldOrder = body.kind === "Record" ? body.fields.map((field) => field.name) : [];
964
+ return toAliasRecord(value, fieldOrder) ?? toRecord(state);
965
+ } catch (error) {
966
+ if (!(error instanceof TlaConfigError || error instanceof TlaEvaluationError)) throw error;
967
+ return { ...toRecord(state), _ALIASEvalError: error.message };
510
968
  }
511
- return steps.reverse();
512
- };
513
- const getPathTrace = (path: number[]) =>
514
- path.map((stateIndex, position): TraceStep => {
515
- if (position === 0) return { action: undefined, state: toRecord(states[stateIndex]) };
516
- const previous = path[position - 1];
517
- const action =
518
- previous === stateIndex
519
- ? "Stuttering"
520
- : getSuccessors(spec.next, states[previous], environment).find((successor) =>
521
- isSameState(successor.state, states[stateIndex]),
522
- )?.action;
523
- return { action, state: toRecord(states[stateIndex]) };
969
+ };
970
+ // TLC labels a step with the first action that takes it, evaluated as when the search expanded
971
+ // the previous state.
972
+ const getLabel = (index: number, previous: Value[], state: Value[]) => {
973
+ nextActions.value ??= getNextActions(config, environment, specification.instanceActions);
974
+ const expanding = { ...current };
975
+ Object.assign(current, { index, isExpanding: true, candidate: undefined });
976
+ const fingerprintState = getFingerprintState(state);
977
+ const action = nextActions.value.find((item) =>
978
+ getSuccessors(item.expression, previous, environment, item.scope).some((successor) =>
979
+ isSameState(getFingerprintState(successor.state), fingerprintState),
980
+ ),
981
+ );
982
+ Object.assign(current, expanding);
983
+ return action && getActionLabel(action);
984
+ };
985
+ const toTrace = (steps: Successor[], path: number[], loopStart?: number): TraceStep[] => {
986
+ const alias = config.alias;
987
+ const labels = withoutOutput(() => {
988
+ // TLC rebuilds a trace from the initial predicate, which can reset what TLCSet stored.
989
+ if (steps.length > 1 && isImpure(environment)) getSpecInitialStates();
990
+ // TLC finds the action that takes each step before it evaluates the alias.
991
+ return steps.map((step, position) =>
992
+ position === 0 || step.action === "Stuttering"
993
+ ? undefined
994
+ : getLabel(path[position - 1], steps[position - 1].state, step.state),
995
+ );
524
996
  });
997
+ return steps.map((step, position) => ({
998
+ action: step.action,
999
+ label: labels[position],
1000
+ state:
1001
+ alias === undefined
1002
+ ? toRecord(step.state)
1003
+ : getAliasState(alias, steps, position, loopStart),
1004
+ }));
1005
+ };
1006
+ const getSpecInitialStates = () =>
1007
+ spec.init === undefined ? [] : getInitialStates(spec.init, environment, spec.scope);
1008
+ const getTraceSuccessors = (state: Value[]) =>
1009
+ spec.next === undefined ? [] : getSuccessors(spec.next, state, environment, spec.scope);
1010
+ const hasFingerprint = (state: Value[], index: number) =>
1011
+ isSameState(getFingerprintState(state), fingerprintStates[index]);
1012
+ const findSuccessorWithFingerprint = (state: Value[], index: number) =>
1013
+ getTraceSuccessors(state).find((successor) => hasFingerprint(successor.state, index));
1014
+ const getPathStates = (path: number[], nextFailure: string) => {
1015
+ const initial =
1016
+ isReducing || isImpure(environment)
1017
+ ? getSpecInitialStates().find((state) => hasFingerprint(state, path[0]))
1018
+ : states[path[0]];
1019
+ if (!initial) throw new TlaEvaluationError(INITIAL_RECOVERY_FAILURE);
1020
+ const steps: Successor[] = [{ state: initial, action: undefined }];
1021
+ for (let position = 1; position < path.length; position++) {
1022
+ const previous = steps[position - 1].state;
1023
+ if (path[position - 1] === path[position]) {
1024
+ steps.push({ state: previous, action: "Stuttering" });
1025
+ continue;
1026
+ }
1027
+ const successor = findSuccessorWithFingerprint(previous, path[position]);
1028
+ if (!successor) throw new TlaEvaluationError(nextFailure);
1029
+ steps.push(successor);
1030
+ }
1031
+ return steps;
1032
+ };
1033
+ const getTrace = (index: number, last?: Successor) => {
1034
+ const path: number[] = [];
1035
+ for (let position = index; position !== -1; position = parents[position]) {
1036
+ path.unshift(position);
1037
+ }
1038
+ const steps =
1039
+ isReducing && path.length > 1
1040
+ ? getPathStates(path, INITIAL_RECOVERY_FAILURE)
1041
+ : path.map((position) => ({ state: states[position], action: actions[position] }));
1042
+ if (!last) return toTrace(steps, path);
1043
+ const action = isReducing
1044
+ ? getTraceSuccessors(states[index]).find((successor) =>
1045
+ isSameState(successor.state, last.state),
1046
+ )?.action
1047
+ : last.action;
1048
+ return toTrace([...steps, { state: last.state, action }], path);
1049
+ };
1050
+ // TLC prints an initial state on its own without evaluating the alias.
1051
+ const getStateTrace = (index: number) =>
1052
+ parents[index] === -1
1053
+ ? [{ action: undefined, label: undefined, state: toRecord(states[index]) }]
1054
+ : getTrace(index);
1055
+ // TLC names the initial state it was checking unless a standard module's Java code failed.
1056
+ const toFailure = (
1057
+ error: unknown,
1058
+ trace: TraceStep[],
1059
+ initialState?: Value[],
1060
+ failureOutDegrees?: number[],
1061
+ ) => {
1062
+ if (!(error instanceof TlaEvaluationError)) throw error;
1063
+ return {
1064
+ ...createResult("error", undefined, {
1065
+ message: error.message,
1066
+ trace,
1067
+ initialState:
1068
+ initialState && !(error instanceof TlaModuleError) ? toRecord(initialState) : undefined,
1069
+ exitCode: getErrorExitCode(error),
1070
+ }),
1071
+ outDegrees: failureOutDegrees,
1072
+ };
1073
+ };
1074
+ const getPathTrace = (path: number[], loopStart?: number) => {
1075
+ const steps = getPathStates(path, NEXT_RECOVERY_FAILURE);
1076
+ const last = steps[steps.length - 1].state;
1077
+ const isClosed =
1078
+ !isReducing ||
1079
+ loopStart === undefined ||
1080
+ loopStart === path.length - 1 ||
1081
+ findSuccessorWithFingerprint(last, path[loopStart]) !== undefined;
1082
+ if (!isClosed) throw new TlaEvaluationError(NEXT_RECOVERY_FAILURE);
1083
+ return toTrace(steps, path, loopStart);
1084
+ };
525
1085
  const getStateHash = (state: Value[]) => {
526
1086
  let hash = 0x811c9dc5;
527
1087
  for (let index = 0; index < state.length; index++)
@@ -572,21 +1132,32 @@ export const check = (options: CheckOptions): CheckResult => {
572
1132
  isExpanding: false,
573
1133
  candidate: undefined as Value[] | undefined,
574
1134
  candidateLevel: 0,
1135
+ livenessSource: -1,
575
1136
  };
576
1137
  const getPath = (index: number): Value[][] =>
577
1138
  index === -1 ? [] : [...getPath(parents[index]), states[index]];
1139
+ runtime.isSeen = (state) => {
1140
+ const fingerprintState = getFingerprintState(state);
1141
+ return findStateIndex(getStateHash(fingerprintState), fingerprintState) !== -1;
1142
+ };
578
1143
  runtime.getBehavior = (state) => {
579
1144
  const index = state === current.candidate ? -1 : states.indexOf(state);
580
1145
  return index === -1 ? [...getPath(current.index), state] : getPath(index);
581
1146
  };
1147
+ runtime.getCurrentState = () => (current.index === -1 ? undefined : states[current.index]);
1148
+ runtime.setLivenessSource = (index) => {
1149
+ current.livenessSource = index;
1150
+ };
582
1151
  runtime.getLevel = (state) => {
1152
+ const source = current.livenessSource;
1153
+ if (source !== -1) return state === states[source] ? depths[source] : depths[source] + 1;
583
1154
  if (state === current.candidate) return current.candidateLevel;
584
1155
  if (current.index === -1) return 0;
585
1156
  if (state === states[current.index]) return depths[current.index];
586
1157
  return current.isExpanding ? depths[current.index] + 1 : 0;
587
1158
  };
588
1159
 
589
- for (const state of getInitialStates(spec.init, environment)) {
1160
+ const visitInitialState = (state: Value[]) => {
590
1161
  canonicalizeState(state);
591
1162
  runtime.generatedStates++;
592
1163
  runtime.initialStates++;
@@ -594,10 +1165,10 @@ export const check = (options: CheckOptions): CheckResult => {
594
1165
  current.candidateLevel = 1;
595
1166
  const fingerprintState = getFingerprintState(state);
596
1167
  const hash = getStateHash(fingerprintState);
597
- const isSeen = findStateIndex(hash, fingerprintState) !== -1;
598
- const isInModel = isSeen || search.satisfiesConstraints(state);
1168
+ const isInModel = search.satisfiesConstraints(state);
599
1169
  if (isInModel) search.trackStatePossible(state);
600
- if (isSeen) continue;
1170
+ const isSeen = isInModel && findStateIndex(hash, fingerprintState) !== -1;
1171
+ if (isSeen) return undefined;
601
1172
  if (isInModel) {
602
1173
  countDistinctState(1);
603
1174
  runtime.queueSize++;
@@ -605,100 +1176,203 @@ export const check = (options: CheckOptions): CheckResult => {
605
1176
  }
606
1177
  const violated = search.findViolatedInvariant(state);
607
1178
  if (violated) {
608
- return createResult("invariant-violation", {
1179
+ return reportViolation("invariant-violation", {
609
1180
  name: violated.name,
610
- trace: [{ action: undefined, state: toRecord(state) }],
1181
+ trace: toTrace([{ action: undefined, state }], []),
611
1182
  });
612
1183
  }
613
1184
  const failedPredicate = search.findFailedInitialPredicate(state);
614
1185
  if (failedPredicate) {
615
- return createResult("liveness-violation", {
1186
+ return reportViolation("liveness-violation", {
616
1187
  name: failedPredicate.name,
617
- trace: [{ action: undefined, state: toRecord(state) }],
1188
+ trace: toTrace([{ action: undefined, state }], []),
618
1189
  });
619
1190
  }
620
- if (!isInModel) continue;
1191
+ if (!isInModel) return undefined;
621
1192
  markSeen(hash, states.length);
622
1193
  addState(state, fingerprintState, -1, undefined);
623
- }
624
-
625
- for (let index = 0; index < states.length && !runtime.isStopped; index++) {
1194
+ return undefined;
1195
+ };
1196
+ const visitSuccessor = (index: number, successor: Successor, liveSuccessors: LiveSuccessor[]) => {
626
1197
  const state = states[index];
627
- current.index = index;
628
- runtime.queueSize = states.length - index - 1;
629
- current.isExpanding = true;
630
- const successors = getSuccessors(spec.next, state, environment);
631
- current.isExpanding = false;
632
- if (successors.length === 0 && config.checkDeadlock) {
633
- return createResult("deadlock", { name: "deadlock", trace: getTrace(index) });
1198
+ runtime.generatedStates++;
1199
+ current.candidate = successor.state;
1200
+ current.candidateLevel = depths[index] + 1;
1201
+ if (isCanonicalizedFirst) canonicalizeState(successor.state);
1202
+ const fingerprintState = getFingerprintState(successor.state);
1203
+ const hash = getStateHash(fingerprintState);
1204
+ const found = findStateIndex(hash, fingerprintState);
1205
+ const isStateInModel = search.satisfiesConstraints(successor.state, state);
1206
+ if (isStateInModel) search.trackStatePossible(successor.state);
1207
+ const isInModel = isStateInModel && search.satisfiesActionConstraints(state, successor.state);
1208
+ if (isInModel) search.trackActionPossible(state, successor.state);
1209
+ const existing = isInModel ? found : -1;
1210
+ if (existing !== -1 && liveness) {
1211
+ const stored = states[existing];
1212
+ const isStored = stored.every((value, position) => value === successor.state[position]);
1213
+ liveSuccessors.push({ index: existing, state: isStored ? stored : successor.state });
634
1214
  }
635
- const liveSuccessors: LiveSuccessor[] = [];
636
- for (const successor of successors) {
637
- runtime.generatedStates++;
638
- current.candidate = successor.state;
639
- current.candidateLevel = depths[index] + 1;
640
- if (isCanonicalizedFirst) canonicalizeState(successor.state);
641
- const fingerprintState = getFingerprintState(successor.state);
642
- const hash = getStateHash(fingerprintState);
643
- const found = findStateIndex(hash, fingerprintState);
644
- const isStateInModel = found !== -1 || search.satisfiesConstraints(successor.state, state);
645
- if (isStateInModel) search.trackStatePossible(successor.state);
646
- const isInModel = isStateInModel && search.satisfiesActionConstraints(state, successor.state);
647
- if (isInModel) search.trackActionPossible(state, successor.state);
648
- const existing = isInModel ? found : -1;
649
- if (existing !== -1 && liveness) {
650
- const stored = states[existing];
651
- const isStored = stored.every((value, position) => value === successor.state[position]);
652
- liveSuccessors.push({ index: existing, state: isStored ? stored : successor.state });
653
- }
654
- const isNew = isInModel && existing === -1;
655
- if (isNew) countDistinctState(depths[index] + 1);
656
- if (existing === -1) {
1215
+ const isNew = isInModel && existing === -1;
1216
+ if (isNew) {
1217
+ countDistinctState(depths[index] + 1);
1218
+ expansion.newStates++;
1219
+ }
1220
+ if (existing === -1) {
1221
+ try {
657
1222
  const violated = search.findViolatedInvariant(successor.state, state);
658
1223
  if (violated) {
659
- return createResult("invariant-violation", {
1224
+ return reportViolation("invariant-violation", {
660
1225
  name: violated.name,
661
1226
  trace: getTrace(index, successor),
662
1227
  });
663
1228
  }
1229
+ } catch (error) {
1230
+ // TLC prints the outdegree when it cannot reproduce the failure, as for an invariant here.
1231
+ return toFailure(error, getTrace(index, successor), undefined, getPartialOutDegrees());
664
1232
  }
665
- const violatedProperty = search.findViolatedActionProperty(state, successor.state);
666
- if (violatedProperty) {
667
- return createResult("liveness-violation", {
668
- name: violatedProperty.name,
669
- trace: getTrace(index, successor),
670
- });
671
- }
672
- if (!isNew) continue;
673
- if (!isCanonicalizedFirst) canonicalizeState(successor.state);
674
- markSeen(hash, states.length);
675
- if (liveness) liveSuccessors.push({ index: states.length, state: successor.state });
676
- addState(successor.state, fingerprintState, index, successor.action);
677
- runtime.queueSize++;
678
1233
  }
679
- const accepted = liveness?.addState(index, state, liveSuccessors);
680
- if (accepted) {
681
- return createResult("property-violation", {
1234
+ const violatedProperty = search.findViolatedActionProperty(state, successor.state);
1235
+ if (violatedProperty) {
1236
+ return reportViolation("liveness-violation", {
1237
+ name: violatedProperty.name,
1238
+ trace: getTrace(index, successor),
1239
+ });
1240
+ }
1241
+ if (!isNew) return undefined;
1242
+ if (!isCanonicalizedFirst) canonicalizeState(successor.state);
1243
+ markSeen(hash, states.length);
1244
+ if (liveness) liveSuccessors.push({ index: states.length, state: successor.state });
1245
+ addState(successor.state, fingerprintState, index, successor.action);
1246
+ runtime.queueSize++;
1247
+ return undefined;
1248
+ };
1249
+
1250
+ const outcome: { result: CheckResult | undefined } = { result: undefined };
1251
+ if (spec.init !== undefined) {
1252
+ try {
1253
+ forEachInitialState(
1254
+ spec.init,
1255
+ environment,
1256
+ (state) => {
1257
+ try {
1258
+ outcome.result = visitInitialState(state);
1259
+ } catch (error) {
1260
+ outcome.result = toFailure(error, [], state);
1261
+ }
1262
+ return outcome.result !== undefined;
1263
+ },
1264
+ spec.scope,
1265
+ );
1266
+ } catch (error) {
1267
+ return toFailure(error, []);
1268
+ }
1269
+ }
1270
+ if (outcome.result) return outcome.result;
1271
+
1272
+ const next = spec.next;
1273
+ if (next === undefined) {
1274
+ if (states.length > 0) {
1275
+ throw new TlaEvaluationError(
1276
+ "No next state actions defined to generate successor states from.",
1277
+ );
1278
+ }
1279
+ return createSuccess();
1280
+ }
1281
+ const expandState = (index: number) => {
1282
+ const state = states[index];
1283
+ const liveSuccessors: LiveSuccessor[] = [];
1284
+ const successorCount = forEachSuccessor(
1285
+ next,
1286
+ state,
1287
+ environment,
1288
+ (successor) => {
1289
+ try {
1290
+ outcome.result = visitSuccessor(index, successor, liveSuccessors);
1291
+ } catch (error) {
1292
+ outcome.result = toFailure(error, getTrace(index, successor));
1293
+ }
1294
+ return outcome.result !== undefined;
1295
+ },
1296
+ spec.scope,
1297
+ );
1298
+ current.isExpanding = false;
1299
+ if (outcome.result) return outcome.result;
1300
+ if (successorCount === 0 && config.checkDeadlock) {
1301
+ return reportViolation("deadlock", { name: "deadlock", trace: getStateTrace(index) });
1302
+ }
1303
+ try {
1304
+ const accepted = liveness?.addState(index, state, liveSuccessors);
1305
+ current.livenessSource = -1;
1306
+ if (!accepted) return undefined;
1307
+ return reportViolation("property-violation", {
682
1308
  name: accepted.name,
683
1309
  trace: getPathTrace(accepted.path),
684
1310
  });
1311
+ } catch (error) {
1312
+ return toFailure(error, getStateTrace(index), undefined, getPartialOutDegrees());
1313
+ }
1314
+ };
1315
+ const reportLasso = (lasso: Lasso) => {
1316
+ const path = [...lasso.prefix, ...lasso.cycle];
1317
+ const trace = getPathTrace(path, lasso.prefix.length);
1318
+ const back = path[lasso.prefix.length];
1319
+ const last = path[path.length - 1];
1320
+ const loopLabel =
1321
+ back === last ? undefined : withoutOutput(() => getLabel(last, states[last], states[back]));
1322
+ return reportViolation(
1323
+ "liveness-violation",
1324
+ { name: lasso.name, trace, loopStart: lasso.prefix.length, loopLabel },
1325
+ () =>
1326
+ back === last
1327
+ ? trace[trace.length - 1].action
1328
+ : getTraceSuccessors(states[last]).find((successor) =>
1329
+ isSameState(successor.state, states[back]),
1330
+ )?.action,
1331
+ );
1332
+ };
1333
+ const isPeriodic = liveness !== undefined && options.livenessCheck !== "final";
1334
+ const startedAt = performance.now();
1335
+ const periodic = { at: startedAt + PERIODIC_START, size: 1, time: 0 };
1336
+ const findPeriodicViolation = (expanded: number) => {
1337
+ const now = performance.now();
1338
+ periodic.at = now + PROGRESS_INTERVAL;
1339
+ const isGrown = (expanded - periodic.size) / periodic.size > LIVENESS_GROWTH;
1340
+ if (!isGrown || periodic.time > (now - startedAt) * LIVENESS_RATIO) return undefined;
1341
+ periodic.size = expanded;
1342
+ const lasso = liveness?.findViolation();
1343
+ current.livenessSource = -1;
1344
+ periodic.time += performance.now() - now;
1345
+ return lasso;
1346
+ };
1347
+ setNextStatePhase(true);
1348
+ for (let index = 0; index < states.length && !runtime.isStopped; index++) {
1349
+ current.index = index;
1350
+ current.candidate = undefined;
1351
+ runtime.queueSize = states.length - index - 1;
1352
+ current.isExpanding = true;
1353
+ try {
1354
+ outcome.result = expandState(index);
1355
+ } catch (error) {
1356
+ outcome.result = toFailure(error, getStateTrace(index));
1357
+ }
1358
+ if (outcome.result) return outcome.result;
1359
+ outDegrees[getOutDegreeBucket()]++;
1360
+ expansion.newStates = 0;
1361
+ if (isPeriodic && performance.now() >= periodic.at) {
1362
+ const lasso = findPeriodicViolation(index + 1);
1363
+ if (lasso) return reportLasso(lasso);
685
1364
  }
686
1365
  }
687
1366
 
688
1367
  current.index = -1;
689
1368
  current.candidate = undefined;
690
1369
  const lasso = liveness?.findViolation();
691
- if (lasso) {
692
- return createResult("liveness-violation", {
693
- name: lasso.name,
694
- trace: getPathTrace([...lasso.prefix, ...lasso.cycle]),
695
- loopStart: lasso.prefix.length,
696
- });
697
- }
1370
+ current.livenessSource = -1;
1371
+ if (lasso) return reportLasso(lasso);
698
1372
 
699
- const postconditionViolation = search.findPostconditionViolation(print);
1373
+ const postconditionViolation = search.findPostconditionViolation();
700
1374
  if (postconditionViolation) {
701
- return createResult("postcondition-violation", postconditionViolation);
1375
+ return createResult("postcondition-violation", { ...postconditionViolation, trace: [] });
702
1376
  }
703
- return createResult("success");
1377
+ return createSuccess();
704
1378
  };