tla-ts 0.0.1 → 0.0.3-dev.a50fc89

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