tla-ts 0.0.1 → 0.0.3-dev.3cf2ad0

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 +85 -40
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +216 -30
  4. package/dist/index.d.ts +216 -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 +48 -13
  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 +183 -202
  49. package/src/parallel.ts +163 -84
  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 +70 -22
  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 +1234 -176
package/src/symmetry.ts CHANGED
@@ -5,9 +5,11 @@ import {
5
5
  createFunction,
6
6
  createSet,
7
7
  createSortedFunction,
8
+ createSortedSet,
8
9
  isEnumerable,
9
10
  isFunctionValue,
10
11
  isModelValue,
12
+ isSameValue,
11
13
  isSetValue,
12
14
  toFiniteFunction,
13
15
  toFiniteSet,
@@ -18,6 +20,9 @@ import {
18
20
 
19
21
  type Images = Map<string, ModelValue>;
20
22
 
23
+ const CACHE_LIMIT = 1 << 22;
24
+ const CACHED_SIZE_LIMIT = 64;
25
+
21
26
  interface Permutation {
22
27
  images: Images;
23
28
  cache: WeakMap<ObjectValue, Value>;
@@ -31,7 +36,7 @@ const getPermutationKey = (permutation: Images) =>
31
36
  .sort()
32
37
  .join(",");
33
38
 
34
- const composePermutations = (first: Images, second: Images): Images => {
39
+ const composePermutations = (first: Images, second: Images) => {
35
40
  const composed: Images = new Map();
36
41
  const names = new Set([...first.keys(), ...second.keys()]);
37
42
  names.forEach((name) => {
@@ -42,14 +47,14 @@ const composePermutations = (first: Images, second: Images): Images => {
42
47
  return composed;
43
48
  };
44
49
 
45
- const toImages = (value: Value, name: string): Images => {
50
+ const toImages = (value: Value, name: string) => {
46
51
  if (!isFunctionValue(value)) {
47
52
  throw new TlaConfigError(`The symmetry values of ${name} must be functions`);
48
53
  }
49
- const fn = toFiniteFunction(value);
54
+ const functionValue = toFiniteFunction(value);
50
55
  const permutation: Images = new Map();
51
- fn.domain.forEach((key, index) => {
52
- const image = fn.range[index];
56
+ functionValue.domain.forEach((key, index) => {
57
+ const image = functionValue.range[index];
53
58
  if (!isModelValue(key) || !isModelValue(image)) {
54
59
  throw new TlaConfigError("Symmetry function must have model values as domain and range");
55
60
  }
@@ -103,7 +108,7 @@ export const getSymmetryGroup = (name: string, environment: Environment): Permut
103
108
  }));
104
109
  };
105
110
 
106
- const permuteElements = (elements: Value[], permutation: Permutation): Value[] | undefined => {
111
+ const permuteElements = (elements: Value[], permutation: Permutation) => {
107
112
  for (let index = 0; index < elements.length; index++) {
108
113
  const element = elements[index];
109
114
  const permuted = permuteValue(element, permutation);
@@ -118,8 +123,32 @@ const permuteElements = (elements: Value[], permutation: Permutation): Value[] |
118
123
  return undefined;
119
124
  };
120
125
 
121
- const CACHE_LIMIT = 1 << 22;
122
- const CACHED_SIZE_LIMIT = 16;
126
+ // A permutation maps distinct elements to distinct elements and keeps the elements it doesn't move
127
+ // in order, so it sorts only the moved ones and merges them back.
128
+ const permuteSortedElements = (elements: Value[], permutation: Permutation) => {
129
+ const kept: Value[] = [];
130
+ const moved: Value[] = [];
131
+ for (const element of elements) {
132
+ const permuted = permuteValue(element, permutation);
133
+ if (permuted === element) kept.push(element);
134
+ else moved.push(permuted);
135
+ }
136
+ if (moved.length === 0) return undefined;
137
+ moved.sort(compareValues);
138
+ const merged: Value[] = [];
139
+ let keptIndex = 0;
140
+ let movedIndex = 0;
141
+ while (keptIndex < kept.length && movedIndex < moved.length) {
142
+ merged.push(
143
+ compareValues(kept[keptIndex], moved[movedIndex]) < 0
144
+ ? kept[keptIndex++]
145
+ : moved[movedIndex++],
146
+ );
147
+ }
148
+ while (keptIndex < kept.length) merged.push(kept[keptIndex++]);
149
+ while (movedIndex < moved.length) merged.push(moved[movedIndex++]);
150
+ return merged;
151
+ };
123
152
 
124
153
  const getSize = (value: ObjectValue) =>
125
154
  value.kind === "Set"
@@ -135,7 +164,10 @@ const permuteValue = (value: Value, permutation: Permutation): Value => {
135
164
  if (size === 0 || size > CACHED_SIZE_LIMIT) return computePermutedValue(value, permutation);
136
165
  const cached = permutation.cache.get(value);
137
166
  if (cached !== undefined) return cached;
138
- const permuted = computePermutedValue(value, permutation);
167
+ const computed = computePermutedValue(value, permutation);
168
+ // A permutation often maps a value to an equal one. Keeping the original lets the comparison
169
+ // with the smallest state so far stop at identity.
170
+ const permuted = computed !== value && isSameValue(computed, value) ? value : computed;
139
171
  if (permutation.cacheSize >= permutation.cacheLimit) {
140
172
  permutation.cache = new WeakMap();
141
173
  permutation.cacheSize = 0;
@@ -145,20 +177,22 @@ const permuteValue = (value: Value, permutation: Permutation): Value => {
145
177
  return permuted;
146
178
  };
147
179
 
148
- const computePermutedValue = (value: ObjectValue, permutation: Permutation): Value => {
180
+ const computePermutedValue = (value: ObjectValue, permutation: Permutation) => {
149
181
  const kind = value.kind;
150
182
  if (kind === "Set") {
151
- const elements = permuteElements(value.elements, permutation);
152
- return elements ? createSet(elements) : value;
183
+ const elements = permuteSortedElements(value.elements, permutation);
184
+ return elements ? createSortedSet(elements) : value;
153
185
  }
154
186
  if (kind === "Function" || (kind === "LazyFunction" && isEnumerable(value.domain))) {
155
- const fn = toFiniteFunction(value);
156
- const domain = permuteElements(fn.domain, permutation);
157
- const range = permuteElements(fn.range, permutation) ?? fn.range;
187
+ const functionValue = toFiniteFunction(value);
188
+ const domain = permuteElements(functionValue.domain, permutation);
189
+ const range = permuteElements(functionValue.range, permutation) ?? functionValue.range;
158
190
  if (domain) {
159
191
  return createFunction(domain.map((key, index): [Value, Value] => [key, range[index]]));
160
192
  }
161
- return range === fn.range ? fn : createSortedFunction(fn.domain, range);
193
+ return range === functionValue.range
194
+ ? functionValue
195
+ : createSortedFunction(functionValue.domain, range);
162
196
  }
163
197
  if (kind === "Interval" || !isSetValue(value) || !isEnumerable(value)) return value;
164
198
  const set = toFiniteSet(value);
@@ -173,20 +207,34 @@ const permuteRemaining = (state: Value[], permuted: Value[], permutation: Permut
173
207
  return permuted;
174
208
  };
175
209
 
176
- const getSmallerPermutation = (state: Value[], minimum: Value[], permutation: Permutation) => {
210
+ const getSmallerPermutation = (
211
+ state: Value[],
212
+ minimum: Value[],
213
+ permutation: Permutation,
214
+ order: number[] | undefined,
215
+ ) => {
177
216
  const permuted: Value[] = [];
178
- for (let index = 0; index < state.length; index++) {
217
+ for (let position = 0; position < state.length; position++) {
218
+ const index = order ? order[position] : position;
179
219
  const value = permuteValue(state[index], permutation);
180
220
  const comparison = compareValues(value, minimum[index]);
181
221
  if (comparison > 0) return undefined;
182
- permuted.push(value);
183
- if (comparison < 0) return permuteRemaining(state, permuted, permutation);
222
+ if (!order) permuted.push(value);
223
+ if (comparison < 0) {
224
+ return order
225
+ ? state.map((item) => permuteValue(item, permutation))
226
+ : permuteRemaining(state, permuted, permutation);
227
+ }
184
228
  }
185
229
  return undefined;
186
230
  };
187
231
 
188
- export const getRepresentative = (state: Value[], group: Permutation[]): Value[] =>
232
+ export const getRepresentative = (
233
+ state: Value[],
234
+ group: Permutation[],
235
+ order?: number[],
236
+ ): Value[] =>
189
237
  group.reduce(
190
- (minimum, permutation) => getSmallerPermutation(state, minimum, permutation) ?? minimum,
238
+ (minimum, permutation) => getSmallerPermutation(state, minimum, permutation, order) ?? minimum,
191
239
  state,
192
240
  );
package/src/tableau.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import type { Expression } from "./ast";
2
2
  import { TlaEvaluationError } from "./errors";
3
3
  import type { Scope } from "./evaluate";
4
+ import { ACTION_LEVEL, TEMPORAL_LEVEL } from "./levels";
4
5
 
5
- export interface LiveBoolean {
6
+ interface LiveBoolean {
6
7
  kind: "Boolean";
7
8
  value: boolean;
8
9
  }
9
10
 
10
- export interface LiveState {
11
+ interface LiveState {
11
12
  kind: "State";
12
13
  expression: Expression;
13
14
  scope: Scope | undefined;
@@ -19,7 +20,7 @@ export interface LiveEnabled {
19
20
  scope: Scope | undefined;
20
21
  }
21
22
 
22
- export interface LiveAction {
23
+ interface LiveAction {
23
24
  kind: "Action";
24
25
  expression: Expression;
25
26
  unchanged: Expression | undefined;
@@ -27,12 +28,12 @@ export interface LiveAction {
27
28
  enabled?: LiveEnabled;
28
29
  }
29
30
 
30
- export interface LiveUnary {
31
+ interface LiveUnary {
31
32
  kind: "Not" | "Always" | "Eventually" | "Next";
32
33
  body: LiveExpression;
33
34
  }
34
35
 
35
- export interface LiveJunction {
36
+ interface LiveJunction {
36
37
  kind: "And" | "Or";
37
38
  operands: LiveExpression[];
38
39
  }
@@ -45,7 +46,7 @@ export type LiveExpression =
45
46
  | LiveUnary
46
47
  | LiveJunction;
47
48
 
48
- export interface TableauNode {
49
+ interface TableauNode {
49
50
  particle: LiveExpression[];
50
51
  successors: number[];
51
52
  statePredicates: LiveExpression[];
@@ -56,7 +57,7 @@ export interface Tableau {
56
57
  initialCount: number;
57
58
  }
58
59
 
59
- export interface ErrorModel {
60
+ interface ErrorModel {
60
61
  eventuallyAlways: number[];
61
62
  infinitelyOftenStates: number[];
62
63
  infinitelyOftenActions: number[];
@@ -84,14 +85,12 @@ interface Triple {
84
85
  }
85
86
 
86
87
  const STATE_LEVEL = 1;
87
- const ACTION_LEVEL = 2;
88
- const TEMPORAL_LEVEL = 3;
89
88
  const MAXIMUM_DISJUNCTS = 2147483647;
90
89
 
91
- export const TRUE: LiveBoolean = { kind: "Boolean", value: true };
92
- export const FALSE: LiveBoolean = { kind: "Boolean", value: false };
90
+ const TRUE: LiveBoolean = { kind: "Boolean", value: true };
91
+ const FALSE: LiveBoolean = { kind: "Boolean", value: false };
93
92
 
94
- const createBoolean = (value: boolean) => (value ? TRUE : FALSE);
93
+ export const createBoolean = (value: boolean) => (value ? TRUE : FALSE);
95
94
 
96
95
  const isAtom = (expression: LiveExpression) =>
97
96
  expression.kind === "State" || expression.kind === "Enabled" || expression.kind === "Action";
@@ -265,6 +264,10 @@ const toDisjunctiveNormalForm = (expression: LiveExpression): LiveExpression =>
265
264
  const total = { value: 1 };
266
265
  for (const operand of expression.operands) {
267
266
  const normal = toDisjunctiveNormalForm(operand);
267
+ if (normal.kind === "And") {
268
+ items.push(...normal.operands);
269
+ continue;
270
+ }
268
271
  if (normal.kind === "Or") {
269
272
  total.value *= normal.operands.length;
270
273
  if (total.value > MAXIMUM_DISJUNCTS) {
@@ -272,12 +275,8 @@ const toDisjunctiveNormalForm = (expression: LiveExpression): LiveExpression =>
272
275
  "TLC cannot handle the temporal formula because it exceeds the maximum supported size in disjunctive normal form",
273
276
  );
274
277
  }
275
- items.push(normal);
276
- } else if (normal.kind === "And") {
277
- items.push(...normal.operands);
278
- } else {
279
- items.push(normal);
280
278
  }
279
+ items.push(normal);
281
280
  }
282
281
  if (total.value === 1) return { kind: "And", operands: items };
283
282
  const conjunctions = Array.from({ length: total.value }, (): LiveExpression[] => []);
@@ -427,9 +426,13 @@ const getExpansion = (
427
426
  term: LiveExpression,
428
427
  repeated: "Always" | "Eventually",
429
428
  junction: "And" | "Or",
430
- ): [LiveExpression, LiveExpression] | undefined => {
431
- if (term.kind === repeated && "body" in term) return [term.body, createUnary("Next", term)];
432
- if (term.kind === junction && "operands" in term) return [term.operands[0], term.operands[1]];
429
+ ) => {
430
+ if (term.kind === repeated && "body" in term) {
431
+ return { first: term.body, second: createUnary("Next", term) };
432
+ }
433
+ if (term.kind === junction && "operands" in term) {
434
+ return { first: term.operands[0], second: term.operands[1] };
435
+ }
433
436
  return undefined;
434
437
  };
435
438
 
@@ -437,10 +440,10 @@ const getTriples = (
437
440
  closure: LiveExpression[],
438
441
  repeated: "Always" | "Eventually",
439
442
  junction: "And" | "Or",
440
- ): Triple[] =>
443
+ ) =>
441
444
  closure.flatMap((term): Triple[] => {
442
445
  const parts = getExpansion(term, repeated, junction);
443
- return parts ? [{ whole: term, first: parts[0], second: parts[1] }] : [];
446
+ return parts ? [{ whole: term, first: parts.first, second: parts.second }] : [];
444
447
  });
445
448
 
446
449
  const isSameParticle = (left: LiveExpression[], right: LiveExpression[]) =>
@@ -451,7 +454,7 @@ const unionParticles = (left: LiveExpression[][], right: LiveExpression[][]) =>
451
454
  ...right,
452
455
  ];
453
456
 
454
- const getParticleClosure = (terms: LiveExpression[]): LiveExpression[][] => {
457
+ const getParticleClosure = (terms: LiveExpression[]) => {
455
458
  const closure = getPositiveClosure(terms);
456
459
  const alphas = getTriples(closure, "Always", "And");
457
460
  const betas = getTriples(closure, "Eventually", "Or");
@@ -460,9 +463,11 @@ const getParticleClosure = (terms: LiveExpression[]): LiveExpression[][] => {
460
463
  for (const term of particle) {
461
464
  const parts = getExpansion(term, "Eventually", "Or");
462
465
  if (!parts) continue;
463
- const [first, second] = parts;
464
- if (isMember(particle, first) || isMember(particle, second)) continue;
465
- return unionParticles(expand([...particle, first]), expand([...particle, second]));
466
+ if (isMember(particle, parts.first) || isMember(particle, parts.second)) continue;
467
+ return unionParticles(
468
+ expand([...particle, parts.first]),
469
+ expand([...particle, parts.second]),
470
+ );
466
471
  }
467
472
  const beta = betas.find(
468
473
  (triple) =>
@@ -478,9 +483,8 @@ const getParticleClosure = (terms: LiveExpression[]): LiveExpression[][] => {
478
483
  for (let index = 0; index < expanded.length; index++) {
479
484
  const parts = getExpansion(expanded[index], "Always", "And");
480
485
  if (!parts) continue;
481
- const [first, second] = parts;
482
- if (!isMember(expanded, first)) expanded.push(first);
483
- if (!isMember(expanded, second)) expanded.push(second);
486
+ if (!isMember(expanded, parts.first)) expanded.push(parts.first);
487
+ if (!isMember(expanded, parts.second)) expanded.push(parts.second);
484
488
  }
485
489
  const state = { isChanged: true };
486
490
  while (state.isChanged) {
@@ -509,7 +513,7 @@ const createTableauNode = (particle: LiveExpression[]): TableauNode => ({
509
513
  statePredicates: particle.filter((term) => getLiveLevel(term) <= STATE_LEVEL),
510
514
  });
511
515
 
512
- const createTableau = (formula: LiveExpression): Tableau => {
516
+ const createTableau = (formula: LiveExpression) => {
513
517
  const nodes = getParticleClosure([formula]).map(createTableauNode);
514
518
  const initialCount = nodes.length;
515
519
  for (let index = 0; index < nodes.length; index++) {
@@ -529,11 +533,13 @@ export const isFulfilling = (particle: LiveExpression[], promise: LiveUnary) =>
529
533
  !isMember(particle, promise) || isMember(particle, promise.body);
530
534
 
531
535
  const classify = (expression: LiveExpression, classification: Classification) => {
532
- if (expression.kind === "Eventually" && expression.body.kind === "Always") {
533
- if (getLiveLevel(expression.body.body) < TEMPORAL_LEVEL) {
534
- classification.eventuallyAlways.push(expression.body.body);
535
- return;
536
- }
536
+ if (
537
+ expression.kind === "Eventually" &&
538
+ expression.body.kind === "Always" &&
539
+ getLiveLevel(expression.body.body) < TEMPORAL_LEVEL
540
+ ) {
541
+ classification.eventuallyAlways.push(expression.body.body);
542
+ return;
537
543
  }
538
544
  const infinitelyOften = getInfinitelyOftenBody(expression);
539
545
  if (infinitelyOften) {
@@ -592,13 +598,13 @@ export const getSolutions = (formula: LiveExpression): Solution[] => {
592
598
  if (group) group.classifications.push(classification);
593
599
  else groups.push({ formula: temporal, classifications: [classification] });
594
600
  }
595
- return groups.map((group): Solution => {
601
+ return groups.map((group) => {
596
602
  const binary = group.formula && makeBinary(group.formula);
597
603
  const promises: LiveUnary[] = [];
598
604
  if (binary) extractPromises(binary, promises);
599
605
  const stateChecks: LiveExpression[] = [];
600
606
  const actionChecks: LiveExpression[] = [];
601
- const models = group.classifications.map((classification): ErrorModel => ({
607
+ const models = group.classifications.map((classification) => ({
602
608
  infinitelyOftenActions: classification.infinitelyOftenActions.map((check) =>
603
609
  addToBin(actionChecks, check),
604
610
  ),
package/src/tlc-ext.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import {
2
- defineConstant,
2
+ createJavaOverrideError,
3
3
  defineHigherOrderOperator,
4
4
  defineLazyOperator,
5
5
  defineOperator,
@@ -8,9 +8,11 @@ import {
8
8
  getThunk,
9
9
  getValue,
10
10
  } from "./builtins";
11
- import { TlaEvaluationError } from "./errors";
11
+ import { TlaEvaluationError, TlaModuleError, TlaUnsupportedError } from "./errors";
12
+ import { getTlcFingerprint } from "./tlc-random";
12
13
  import {
13
14
  evaluate,
15
+ expectBoolean,
14
16
  getChildren,
15
17
  getDefinition,
16
18
  isLocallyResolved,
@@ -18,6 +20,7 @@ import {
18
20
  type Environment,
19
21
  } from "./evaluate";
20
22
  import type { Definition, Expression } from "./ast";
23
+ import type { TraceStep } from "./check";
21
24
  import {
22
25
  getNamedRegisters,
23
26
  getPossibleCounts,
@@ -31,18 +34,74 @@ import {
31
34
  createSet,
32
35
  createTuple,
33
36
  formatValue,
37
+ getDomain,
34
38
  getSequenceElements,
35
- getValueHash,
36
39
  getValueKey,
40
+ isFunctionValue,
37
41
  toFiniteSet,
38
42
  valuesEqual,
39
43
  type Value,
40
44
  } from "./values";
41
45
 
42
- const EMPTY_COUNTER_EXAMPLE = createRecord([
43
- ["action", createSet([])],
44
- ["state", createSet([])],
45
- ]);
46
+ const OVERRIDE_PARAMETERS =
47
+ "tlc2.tool.impl.Tool,tla2sany.semantic.ExprOrOpArgNode[],tlc2.util.Context,tlc2.tool.TLCState,tlc2.tool.TLCState,int,tlc2.tool.coverage.CostModel";
48
+
49
+ const ASSERT_ERROR_METHOD = `public static synchronized tlc2.value.impl.Value tlc2.module.TLCExt.assertError(${OVERRIDE_PARAMETERS})`;
50
+
51
+ const TRACE_METHOD = `public static tlc2.value.impl.TupleValue tlc2.module.TLCExt.getTrace(${OVERRIDE_PARAMETERS}) throws java.io.IOException`;
52
+
53
+ const PICK_SUCCESSOR_METHOD = `public static synchronized tlc2.value.impl.Value tlc2.module.TLCExt.pickSuccessor(${OVERRIDE_PARAMETERS})`;
54
+
55
+ const createEmptyCounterExample = () =>
56
+ createRecord([
57
+ ["action", createSet([])],
58
+ ["state", createSet([])],
59
+ ]);
60
+
61
+ const createBuiltinLocation = () =>
62
+ createRecord([
63
+ ["beginLine", 0],
64
+ ["beginColumn", 0],
65
+ ["endLine", 0],
66
+ ["endColumn", 0],
67
+ ["module", "--TLA+ BUILTINS--"],
68
+ ]);
69
+
70
+ // TLC's record for an action it doesn't store, such as every action in breadth-first search.
71
+ export const createActionRecord = (action: string | undefined) =>
72
+ createRecord([
73
+ ["name", action ?? "UnnamedAction"],
74
+ ["location", createBuiltinLocation()],
75
+ ]);
76
+
77
+ // TLC's CounterExample is a graph of the violating behavior: the states numbered from 1, an edge
78
+ // for each step, and for a lasso an edge from the last state back to where the loop starts.
79
+ export const createCounterExample = (
80
+ trace: TraceStep[],
81
+ loop: { start: number; action: string | undefined } | undefined,
82
+ ) => {
83
+ const states = trace.map((step, index) =>
84
+ createTuple([index + 1, createRecord(Object.entries(step.state))]),
85
+ );
86
+ const steps = states
87
+ .slice(1)
88
+ .map((state, index) =>
89
+ createTuple([states[index], createActionRecord(trace[index + 1].action), state]),
90
+ );
91
+ const closing = loop
92
+ ? [
93
+ createTuple([
94
+ states[states.length - 1],
95
+ createActionRecord(loop.action),
96
+ states[loop.start],
97
+ ]),
98
+ ]
99
+ : [];
100
+ return createRecord([
101
+ ["action", createSet([...steps, ...closing])],
102
+ ["state", createSet(states)],
103
+ ]);
104
+ };
46
105
 
47
106
  const getRegister = (runtime: CheckerRuntime, key: Value) => {
48
107
  if (typeof key === "number") return runtime.registers.get(key);
@@ -61,7 +120,7 @@ const setRegister = (runtime: CheckerRuntime, key: Value, value: Value) => {
61
120
  runtime.namedRegisters.set(key, value);
62
121
  return;
63
122
  }
64
- throw new TlaEvaluationError(
123
+ throw new TlaModuleError(
65
124
  `The first argument of TLCSet should be a nonnegative integer, but instead it is:\n${formatValue(key)}`,
66
125
  );
67
126
  };
@@ -112,13 +171,20 @@ export const createTlcExtOperators = (
112
171
  const right = getValue(args, 1, "AssertEq");
113
172
  const isEqual = valuesEqual(left, right);
114
173
  if (!isEqual) {
115
- print("Assertion failed");
174
+ print(formatValue("Assertion failed", true));
116
175
  print(formatValue(left, true));
117
176
  print(formatValue(right, true));
118
177
  }
119
178
  return isEqual;
120
179
  }),
121
180
  defineLazyOperator("AssertError", 2, (args) => {
181
+ const message = getOperator(args, 0, "AssertError").expression;
182
+ if (message && message.kind !== "String") {
183
+ throw createJavaOverrideError(
184
+ ASSERT_ERROR_METHOD,
185
+ `In computing AssertError, a non-string expression (line ${message.location.line}, col ${message.location.column}) was used as the err of an AssertError(err, exp).`,
186
+ );
187
+ }
122
188
  const expected = getThunk(args, 0, "AssertError")();
123
189
  try {
124
190
  getThunk(args, 1, "AssertError")();
@@ -144,46 +210,68 @@ export const createTlcExtOperators = (
144
210
  },
145
211
  defineStateOperator("Trace", 0, (_, context) => {
146
212
  const state = context?.state;
147
- if (!state || !isCompleteState(state)) {
148
- throw new TlaEvaluationError(
149
- "In evaluating TLCExt!Trace, the state is not completely specified yet",
213
+ if (!context || !state || !isCompleteState(state)) {
214
+ const unassigned = (context?.environment.variables ?? []).filter(
215
+ (_, index) => state?.[index] === undefined,
216
+ );
217
+ throw createJavaOverrideError(
218
+ TRACE_METHOD,
219
+ `In evaluating TLCExt!Trace, the state is not completely specified yet (variable${unassigned.length > 1 ? "s" : ""} ${unassigned.join(", ")} undefined).`,
150
220
  );
151
221
  }
152
222
  return createTuple(
153
223
  runtime.getBehavior(state).map((step) => toStateRecord(step, context.environment)),
154
224
  );
155
225
  }),
156
- defineConstant("CounterExample", EMPTY_COUNTER_EXAMPLE),
226
+ defineStateOperator(
227
+ "CounterExample",
228
+ 0,
229
+ () => runtime.counterExample ?? createEmptyCounterExample(),
230
+ ),
157
231
  defineOperator("ToTrace", 1, (args) => {
158
232
  const counterExample = getValue(args, 0, "ToTrace");
233
+ if (
234
+ !isFunctionValue(counterExample) ||
235
+ !valuesEqual(getDomain(counterExample), createSet(["action", "state"]))
236
+ ) {
237
+ throw new TlaEvaluationError(
238
+ `The argument of ToTrace should be a CounterExample, but instead it is:\n${formatValue(counterExample)}`,
239
+ );
240
+ }
159
241
  const states = toFiniteSet(applyFunction(counterExample, "state")).elements.map(
160
242
  (pair) => getSequenceElements(pair) ?? [],
161
243
  );
162
244
  return createTuple(
163
- [...states]
164
- .sort((left, right) => Number(left[0]) - Number(right[0]))
165
- .map((pair) => pair[1]),
245
+ states.sort((left, right) => Number(left[0]) - Number(right[0])).map((pair) => pair[1]),
166
246
  );
167
247
  }),
168
248
  defineOperator("TLCModelValue", 1, (args) => {
169
249
  const name = getValue(args, 0, "TLCModelValue");
170
250
  if (typeof name !== "string") {
171
- throw new TlaEvaluationError(
251
+ throw new TlaModuleError(
172
252
  `The argument of ModelValue should be a string, but instead it is:\n${formatValue(name)}`,
173
253
  );
174
254
  }
175
255
  return createModelValue(name);
176
256
  }),
177
- defineLazyOperator("TLCDefer", 1, (args) => {
178
- getThunk(args, 0, "TLCDefer")();
179
- return true;
180
- }),
257
+ // TLC runs the deferred expression only in simulation, when it extends a behavior.
258
+ defineLazyOperator("TLCDefer", 1, () => true),
181
259
  defineOperator("TLCNoOp", 1, (args) => getValue(args, 0, "TLCNoOp")),
182
260
  defineLazyOperator("PickSuccessor", 1, (args, context) => {
183
- if (getThunk(args, 0, "PickSuccessor")() === true) return true;
184
261
  const next = context?.next;
185
- if (!next || !isCompleteState(next)) return true;
186
- throw new TlaEvaluationError("PickSuccessor needs interactive input to extend the behavior");
262
+ if (!next || !isCompleteState(next)) {
263
+ throw createJavaOverrideError(
264
+ PICK_SUCCESSOR_METHOD,
265
+ 'Cannot invoke "tlc2.value.IValue.fingerPrint(long)" because "minVals[i]" is null',
266
+ );
267
+ }
268
+ if (runtime.isSeen(next)) return true;
269
+ if (expectBoolean(getThunk(args, 0, "PickSuccessor")(), "the condition of PickSuccessor")) {
270
+ return true;
271
+ }
272
+ throw new TlaUnsupportedError(
273
+ "PickSuccessor needs interactive input to extend the behavior with a new state",
274
+ );
187
275
  }),
188
276
  defineLazyOperator("TLCCache", 2, (args, context) => {
189
277
  const expression = getOperator(args, 0, "TLCCache");
@@ -200,18 +288,20 @@ export const createTlcExtOperators = (
200
288
  cache.set(closure, value);
201
289
  return value;
202
290
  }),
203
- defineOperator("TLCFP", 1, (args) => getValueHash(getValue(args, 0, "TLCFP")) | 0),
291
+ defineOperator("TLCFP", 1, (args) =>
292
+ Number(BigInt.asIntN(32, getTlcFingerprint([getValue(args, 0, "TLCFP")]))),
293
+ ),
204
294
  defineStateOperator("TLCEvalDefinition", 1, (args, context) => {
205
295
  const name = getValue(args, 0, "TLCEvalDefinition");
206
296
  const definition =
207
297
  typeof name === "string" && context ? getDefinition(name, context.environment) : undefined;
208
298
  if (!definition || typeof name !== "string" || !context) {
209
- throw new TlaEvaluationError(
299
+ throw new TlaModuleError(
210
300
  `The argument of TLCEvalDefinition should be a name of a definition reachable from the root module, but instead it is:\n${formatValue(name)}`,
211
301
  );
212
302
  }
213
303
  if (definition.kind === "Operator" && definition.params.length > 0) {
214
- throw new TlaEvaluationError(
304
+ throw new TlaModuleError(
215
305
  `The argument of TLCEvalDefinition should be a zero-arity definition, but instead it is:\n${name}`,
216
306
  );
217
307
  }
@@ -225,12 +315,9 @@ export const createPossibleOperators = (
225
315
  runtime: CheckerRuntime,
226
316
  ): Builtin[] => [
227
317
  defineStateOperator("_Track", 2, (args) => {
228
- const name = getValue(args, 1, "_Track");
229
- const count = runtime.possibleCounts.get(String(name)) ?? 0;
230
- runtime.possibleCounts.set(
231
- String(name),
232
- getValue(args, 0, "_Track") === true ? count + 1 : count,
233
- );
318
+ const name = String(getValue(args, 1, "_Track"));
319
+ const count = runtime.possibleCounts.get(name) ?? 0;
320
+ runtime.possibleCounts.set(name, getValue(args, 0, "_Track") === true ? count + 1 : count);
234
321
  return true;
235
322
  }),
236
323
  defineStateOperator("_Counts", 0, () => getPossibleCounts(runtime)),