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/values.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { TlaEvaluationError } from "./errors";
1
+ import { TlaEvaluationError, TlaModuleError } from "./errors";
2
+ import { prettyPrint } from "./pretty-print";
2
3
 
3
4
  export interface ModelValue {
4
5
  kind: "ModelValue";
5
6
  name: string;
7
+ type: string | undefined;
6
8
  }
7
9
 
8
10
  export interface SetValue {
@@ -12,12 +14,20 @@ export interface SetValue {
12
14
  fingerprint: number;
13
15
  }
14
16
 
17
+ // A set enumeration whose elements TLC can't sort, such as {1, "a"}. TLC sorts a set enumeration
18
+ // only when it has to, so it still checks membership in one element by element.
19
+ export interface MixedSetValue {
20
+ kind: "MixedSet";
21
+ elements: Value[];
22
+ }
23
+
15
24
  export interface FunctionValue {
16
25
  kind: "Function";
17
26
  domain: Value[];
18
27
  range: Value[];
19
28
  hash: number;
20
29
  fingerprint: number;
30
+ writtenKeys: Value[] | undefined;
21
31
  }
22
32
 
23
33
  export interface IntervalValue {
@@ -71,10 +81,27 @@ export interface FilteredSetValue {
71
81
  test: (element: Value) => boolean;
72
82
  }
73
83
 
84
+ // UNION S when some element of S isn't a set enumeration. TLC enumerates it only when it has to.
85
+ export interface UnionValue {
86
+ kind: "Union";
87
+ base: Value;
88
+ }
89
+
90
+ interface LazyFunctionParameter {
91
+ name: string;
92
+ domain: Value;
93
+ }
94
+
95
+ export interface LazyFunctionSource {
96
+ parameters: LazyFunctionParameter[];
97
+ body: string;
98
+ }
99
+
74
100
  export interface LazyFunctionValue {
75
101
  kind: "LazyFunction";
76
102
  domain: Value;
77
103
  apply: (argument: Value) => Value;
104
+ source?: LazyFunctionSource;
78
105
  }
79
106
 
80
107
  export type Value =
@@ -83,6 +110,7 @@ export type Value =
83
110
  | string
84
111
  | ModelValue
85
112
  | SetValue
113
+ | MixedSetValue
86
114
  | FunctionValue
87
115
  | IntervalValue
88
116
  | InfiniteSetValue
@@ -93,12 +121,13 @@ export type Value =
93
121
  | SequenceSetValue
94
122
  | SetOperationValue
95
123
  | FilteredSetValue
124
+ | UnionValue
96
125
  | LazyFunctionValue;
97
126
 
98
127
  export type ObjectValue = Exclude<Value, boolean | number | string>;
99
128
 
100
- const MAXIMUM_INTEGER = 2147483647;
101
- const MINIMUM_INTEGER = -2147483648;
129
+ export const MAXIMUM_INTEGER = 2147483647;
130
+ export const MINIMUM_INTEGER = -2147483648;
102
131
  const MAXIMUM_ENUMERATED_SUBSET_BASE = 30;
103
132
 
104
133
  export const isSetValue = (value: Value) =>
@@ -113,7 +142,7 @@ export const isFunctionValue = (value: Value): value is FunctionValue | LazyFunc
113
142
  export const isModelValue = (value: Value): value is ModelValue =>
114
143
  typeof value === "object" && value.kind === "ModelValue";
115
144
 
116
- const describeKind = (value: Value) => {
145
+ export const describeKind = (value: Value) => {
117
146
  if (typeof value === "boolean") return "boolean";
118
147
  if (typeof value === "number") return "integer";
119
148
  if (typeof value === "string") return "string";
@@ -123,15 +152,59 @@ const describeKind = (value: Value) => {
123
152
 
124
153
  export const checkInteger = (value: number) => {
125
154
  if (!Number.isInteger(value) || value > MAXIMUM_INTEGER || value < MINIMUM_INTEGER) {
126
- throw new TlaEvaluationError(`Integer overflow: ${value} is outside TLC's 32-bit range`);
155
+ throw new TlaModuleError(`Integer overflow: ${value} is outside TLC's 32-bit range`);
127
156
  }
128
157
  return value;
129
158
  };
130
159
 
131
- const compareFunctions = (left: FunctionValue, right: FunctionValue): number => {
160
+ // TLC keeps a record, or a function with string keys that @@ builds, in the order the spec writes
161
+ // it until it sorts the value to compare or fingerprint it. ToString, Assert, and error messages
162
+ // show that order.
163
+ const writtenOrderUse = { isUsed: false };
164
+
165
+ const setWrittenOrder = (functionValue: FunctionValue, keys: Value[]) => {
166
+ writtenOrderUse.isUsed = true;
167
+ functionValue.writtenKeys = keys;
168
+ return functionValue;
169
+ };
170
+
171
+ export const getWrittenPairs = (functionValue: FunctionValue): [Value, Value][] => {
172
+ const keys = functionValue.writtenKeys ?? functionValue.domain;
173
+ return keys.map((key) => [key, functionValue.range[findIndex(functionValue.domain, key)]]);
174
+ };
175
+
176
+ export const createWrittenRecord = (names: Value[], range: Value[], writtenNames: Value[]) =>
177
+ setWrittenOrder(createSortedFunction(names, range), writtenNames);
178
+
179
+ // TLC sorts a value it compares even with itself, and every value inside it, so a value it has
180
+ // hashed or fingerprinted is already sorted.
181
+ const forgetWrittenOrder = (value: Value): void => {
182
+ if (typeof value !== "object" || (value.kind !== "Function" && value.kind !== "Set")) return;
183
+ if (value.hash !== 0 || value.fingerprint !== 0) return;
184
+ if (value.kind === "Set") return value.elements.forEach(forgetWrittenOrder);
185
+ value.writtenKeys = undefined;
186
+ value.range.forEach(forgetWrittenOrder);
187
+ };
188
+
189
+ const isRecordDomain = (domain: Value[]) => domain.every((key) => typeof key === "string");
190
+
191
+ // TLC compares a record only with another record, so comparing one with a function of another size
192
+ // can fail before the sizes decide.
193
+ const isRecordMismatch = (left: Value[], right: Value[]) =>
194
+ left.length > 0 && right.length > 0 && isRecordDomain(left) !== isRecordDomain(right);
195
+
196
+ const compareFunctions = (left: FunctionValue, right: FunctionValue) => {
197
+ if (writtenOrderUse.isUsed) {
198
+ left.writtenKeys = undefined;
199
+ right.writtenKeys = undefined;
200
+ }
132
201
  const leftDomain = left.domain;
133
202
  const rightDomain = right.domain;
134
- if (leftDomain.length !== rightDomain.length) return leftDomain.length - rightDomain.length;
203
+ if (leftDomain.length !== rightDomain.length) {
204
+ return isRecordMismatch(leftDomain, rightDomain)
205
+ ? compareLikeTlc(left, right)
206
+ : leftDomain.length - rightDomain.length;
207
+ }
135
208
  if (leftDomain !== rightDomain) {
136
209
  for (let index = 0; index < leftDomain.length; index++) {
137
210
  const keyComparison = compareValues(leftDomain[index], rightDomain[index]);
@@ -148,7 +221,7 @@ const compareFunctions = (left: FunctionValue, right: FunctionValue): number =>
148
221
  return 0;
149
222
  };
150
223
 
151
- const compareSets = (left: SetValue, right: SetValue): number => {
224
+ const compareSets = (left: SetValue, right: SetValue) => {
152
225
  const leftElements = left.elements;
153
226
  const rightElements = right.elements;
154
227
  if (leftElements.length !== rightElements.length)
@@ -161,8 +234,7 @@ const compareSets = (left: SetValue, right: SetValue): number => {
161
234
  return 0;
162
235
  };
163
236
 
164
- const getModelValueType = (value: Value) =>
165
- isModelValue(value) && value.name.length > 2 && value.name[1] === "_" ? value.name[0] : undefined;
237
+ const getModelValueType = (value: Value) => (isModelValue(value) ? value.type : undefined);
166
238
 
167
239
  const checkModelValueTypes = (left: Value, right: Value, isEquality: boolean) => {
168
240
  const leftType = getModelValueType(left);
@@ -185,41 +257,123 @@ const checkModelValueTypes = (left: Value, right: Value, isEquality: boolean) =>
185
257
  );
186
258
  };
187
259
 
188
- export const compareValues = (left: Value, right: Value): number => {
260
+ // TLC orders strings and model value names by when it first interned the text, so every
261
+ // check sets the order of its spec's strings before it builds any value.
262
+ const stringOrder = { ranks: Object.create(null) as Record<string, number>, size: 0 };
263
+
264
+ export const setStringOrder = (texts: Iterable<string>) => {
265
+ stringOrder.ranks = Object.create(null);
266
+ stringOrder.size = 0;
267
+ for (const text of texts) rankString(text);
268
+ };
269
+
270
+ export const rankString = (text: string) => {
271
+ if (stringOrder.ranks[text] === undefined) stringOrder.ranks[text] = stringOrder.size++;
272
+ return text;
273
+ };
274
+
275
+ export const getRankedStringCount = () => stringOrder.size;
276
+
277
+ export const getStringRank = (text: string) => stringOrder.ranks[rankString(text)];
278
+
279
+ const compareStrings = (left: string, right: string) => {
189
280
  if (left === right) return 0;
281
+ const ranks = stringOrder.ranks;
282
+ const leftRank = ranks[left];
283
+ const rightRank = ranks[right];
284
+ if (leftRank !== undefined && rightRank !== undefined) return leftRank - rightRank;
285
+ if (leftRank !== undefined) return -1;
286
+ if (rightRank !== undefined) return 1;
287
+ return left < right ? -1 : 1;
288
+ };
289
+
290
+ export const isOverriddenSet = (value: Value): value is InfiniteSetValue | SequenceSetValue =>
291
+ typeof value === "object" && (value.kind === "InfiniteSet" || value.kind === "SequenceSet");
292
+
293
+ const isOverriddenComparison = (left: Value, right: Value) =>
294
+ (isOverriddenSet(left) || isOverriddenSet(right)) &&
295
+ !(
296
+ typeof left === "object" &&
297
+ left.kind === "SequenceSet" &&
298
+ typeof right === "object" &&
299
+ right.kind === "SequenceSet"
300
+ );
301
+
302
+ // TLC implements Nat, Int, STRING, Seq(S), and Any in Java classes that compare only with each
303
+ // other and with model values. Comparing one with any other set is an error, worded by the class of
304
+ // the left operand.
305
+ const compareOverriddenSets = (
306
+ left: Value,
307
+ right: Value,
308
+ verb: "compare" | "check equality of",
309
+ ) => {
310
+ if (!isOverriddenSet(left)) {
311
+ throw new TlaEvaluationError(
312
+ `Attempted to ${verb} the set ${formatValue(isEnumerable(left) ? toFiniteSet(left) : left, true)} with the value:\n${formatValue(right, true)}`,
313
+ );
314
+ }
315
+ if (!isOverriddenSet(right)) {
316
+ throw new TlaEvaluationError(
317
+ `Attempted to compare overridden value ${formatValue(left, true)} with non-overridden value:\n${formatValue(right, true)}`,
318
+ );
319
+ }
320
+ const leftName = left.kind === "InfiniteSet" ? left.name : undefined;
321
+ const rightName = right.kind === "InfiniteSet" ? right.name : undefined;
322
+ if (leftName !== undefined && leftName === rightName) return 0;
323
+ if ((leftName === "Nat" && rightName === "Int") || (leftName === "Int" && rightName === "Nat")) {
324
+ return leftName === "Nat" ? -1 : 1;
325
+ }
326
+ throw new TlaEvaluationError(
327
+ `Attempted to compare ${formatValue(left, true)} with the value\n${formatValue(right, true)}`,
328
+ );
329
+ };
330
+
331
+ export const ANY_SET: InfiniteSetValue = { kind: "InfiniteSet", name: "Any" };
332
+
333
+ const compareAny = (other: Value): never => {
334
+ throw new TlaEvaluationError(`Attempted to compare ANY with the value\n${formatValue(other)}`);
335
+ };
336
+
337
+ export const compareValues = (left: Value, right: Value): number => {
338
+ if (left === right) {
339
+ if (writtenOrderUse.isUsed) forgetWrittenOrder(left);
340
+ return left === ANY_SET ? compareAny(left) : 0;
341
+ }
190
342
  if (typeof left === "object" && typeof right === "object") {
191
343
  const leftKind = left.kind;
192
344
  const rightKind = right.kind;
193
345
  if (leftKind === "Function" && rightKind === "Function") return compareFunctions(left, right);
194
346
  if (leftKind === "Set" && rightKind === "Set") return compareSets(left, right);
347
+ if (leftKind === "Interval" && rightKind === "Interval") {
348
+ const size = Math.max(0, left.high - left.low + 1);
349
+ const otherSize = Math.max(0, right.high - right.low + 1);
350
+ return size !== otherSize || size === 0 ? size - otherSize : left.low - right.low;
351
+ }
352
+ if (leftKind === "ModelValue" && rightKind === "ModelValue") {
353
+ if (left.type !== undefined && right.type !== undefined) {
354
+ checkModelValueTypes(left, right, false);
355
+ }
356
+ return compareStrings(left.name, right.name);
357
+ }
195
358
  if (leftKind === "ModelValue" || rightKind === "ModelValue") {
196
359
  checkModelValueTypes(left, right, false);
197
- if (leftKind === "ModelValue" && rightKind === "ModelValue") {
198
- return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
199
- }
200
360
  return leftKind === "ModelValue" ? -1 : 1;
201
361
  }
202
- const isAny = (value: ObjectValue) => value.kind === "InfiniteSet" && value.name === "Any";
203
- if (isAny(left) || isAny(right)) {
204
- throw new TlaEvaluationError(
205
- `Attempted to compare ANY with ${formatValue(isAny(left) ? right : left)}`,
206
- );
207
- }
208
- if (leftKind === "InfiniteSet" && rightKind === "InfiniteSet") {
209
- if (left.name === right.name) return 0;
210
- if (left.name !== "STRING" && right.name !== "STRING") return left.name === "Nat" ? -1 : 1;
362
+ if (left === ANY_SET || right === ANY_SET) return compareAny(left === ANY_SET ? right : left);
363
+ if (isSetValue(left) && isSetValue(right) && isOverriddenComparison(left, right)) {
364
+ return compareOverriddenSets(left, right, "compare");
211
365
  }
212
- const leftIsSet = isSetValue(left);
213
- if (leftIsSet !== isSetValue(right)) {
366
+ const isLeftSet = isSetValue(left);
367
+ if (isLeftSet !== isSetValue(right)) {
214
368
  throw new TlaEvaluationError(
215
369
  `Attempted to compare ${describeKind(left)} ${formatValue(left)} with ${describeKind(right)} ${formatValue(right)}`,
216
370
  );
217
371
  }
218
- if (leftIsSet) return compareSets(toFiniteSet(left), toFiniteSet(right));
372
+ if (isLeftSet) return compareSets(toFiniteSet(left), toFiniteSet(right));
219
373
  return compareFunctions(toFiniteFunction(left), toFiniteFunction(right));
220
374
  }
221
375
  if (typeof left === "number" && typeof right === "number") return left - right;
222
- if (typeof left === "string" && typeof right === "string") return left < right ? -1 : 1;
376
+ if (typeof left === "string" && typeof right === "string") return compareStrings(left, right);
223
377
  if (typeof left === "boolean" && typeof right === "boolean") return left < right ? -1 : 1;
224
378
  checkModelValueTypes(left, right, false);
225
379
  if (isModelValue(left)) return -1;
@@ -232,9 +386,14 @@ export const compareValues = (left: Value, right: Value): number => {
232
386
  const modelValues = new Map<string, ModelValue>();
233
387
 
234
388
  export const createModelValue = (name: string): ModelValue => {
389
+ rankString(name);
235
390
  const known = modelValues.get(name);
236
391
  if (known) return known;
237
- const value: ModelValue = { kind: "ModelValue", name };
392
+ const value: ModelValue = {
393
+ kind: "ModelValue",
394
+ name,
395
+ type: name.length > 2 && name[1] === "_" ? name[0] : undefined,
396
+ };
238
397
  modelValues.set(name, value);
239
398
  return value;
240
399
  };
@@ -252,10 +411,14 @@ export const createSortedFunction = (domain: Value[], range: Value[]): FunctionV
252
411
  range,
253
412
  hash: 0,
254
413
  fingerprint: 0,
414
+ writtenKeys: undefined,
255
415
  });
256
416
 
257
417
  export const valuesEqual = (left: Value, right: Value): boolean => {
258
- if (left === right) return true;
418
+ if (left === right) {
419
+ if (writtenOrderUse.isUsed) forgetWrittenOrder(left);
420
+ return left !== ANY_SET || compareAny(left);
421
+ }
259
422
  if (typeof left !== "object" || typeof right !== "object") {
260
423
  if (typeof left === typeof right) return false;
261
424
  if (typeof left === "object" || typeof right === "object") {
@@ -276,9 +439,52 @@ export const valuesEqual = (left: Value, right: Value): boolean => {
276
439
  checkModelValueTypes(left, right, true);
277
440
  return isModelValue(left) && isModelValue(right) && left.name === right.name;
278
441
  }
442
+ if (left.kind === "Subset" && right.kind === "Subset") return valuesEqual(left.base, right.base);
443
+ if (left.kind === "FunctionSet" && right.kind === "FunctionSet") {
444
+ const isUnit = isEmptySet(left.domain);
445
+ const isOtherUnit = isEmptySet(right.domain);
446
+ if (isUnit || isOtherUnit) return isUnit === isOtherUnit;
447
+ const isEmpty = isEmptySet(left.range);
448
+ const isOtherEmpty = isEmptySet(right.range);
449
+ if (isEmpty || isOtherEmpty) return isEmpty === isOtherEmpty;
450
+ return valuesEqual(left.domain, right.domain) && valuesEqual(left.range, right.range);
451
+ }
452
+ if (left.kind === "RecordSet" && right.kind === "RecordSet") {
453
+ if (isEmptySet(left)) return isEmptySet(right);
454
+ if (isEmptySet(right)) return false;
455
+ const getFields = (set: RecordSetValue) =>
456
+ set.names
457
+ .map((name, index): [string, Value] => [name, set.sets[index]])
458
+ .sort((first, second) => (first[0] < second[0] ? -1 : 1));
459
+ const otherFields = getFields(right);
460
+ return (
461
+ left.names.length === right.names.length &&
462
+ getFields(left).every(
463
+ (field, index) =>
464
+ field[0] === otherFields[index][0] && valuesEqual(field[1], otherFields[index][1]),
465
+ )
466
+ );
467
+ }
468
+ if (left.kind === "ProductSet" && right.kind === "ProductSet") {
469
+ if (isEmptySet(left)) return isEmptySet(right);
470
+ if (isEmptySet(right)) return false;
471
+ return (
472
+ left.factors.length === right.factors.length &&
473
+ left.factors.every((factor, index) => valuesEqual(factor, right.factors[index]))
474
+ );
475
+ }
279
476
  const isInfiniteComparison =
280
477
  isSetValue(left) && isSetValue(right) && !(isEnumerable(left) && isEnumerable(right));
281
- if (isInfiniteComparison) return getValueKey(left) === getValueKey(right);
478
+ if (isInfiniteComparison && isOverriddenComparison(left, right)) {
479
+ return compareOverriddenSets(left, right, "check equality of") === 0;
480
+ }
481
+ if (isInfiniteComparison) {
482
+ if (!isOverriddenSet(left)) {
483
+ const elements = toFiniteSet(left);
484
+ if (!isOverriddenSet(right)) return compareSets(elements, toFiniteSet(right)) === 0;
485
+ }
486
+ return getValueKey(left) === getValueKey(right);
487
+ }
282
488
  return compareValues(left, right) === 0;
283
489
  };
284
490
 
@@ -304,6 +510,357 @@ export const isSameValue = (left: Value, right: Value): boolean => {
304
510
  return getValueKey(left) === getValueKey(right);
305
511
  };
306
512
 
513
+ // The comparisons above fail with their own messages. These follow the compareTo and equals
514
+ // methods of TLC's Java value classes, which word a failure by the class of the left operand, so a
515
+ // failed comparison runs again through them to fail as TLC does.
516
+ interface TlcFunction {
517
+ domain: Value[];
518
+ range: Value[];
519
+ low: number | undefined;
520
+ }
521
+
522
+ const getIntervalLow = (domain: Value[]) => {
523
+ const first = domain.length === 0 ? 1 : domain[0];
524
+ return typeof first === "number" && domain.every((key, index) => key === first + index)
525
+ ? first
526
+ : undefined;
527
+ };
528
+
529
+ const toTlcFunction = (value: Value) => {
530
+ if (!isFunctionValue(value)) return undefined;
531
+ const functionValue = toFiniteFunction(value);
532
+ return {
533
+ domain: functionValue.domain,
534
+ range: functionValue.range,
535
+ low: getIntervalLow(functionValue.domain),
536
+ };
537
+ };
538
+
539
+ const toTlcRecord = (value: Value) => {
540
+ if (!isFunctionValue(value)) return undefined;
541
+ const functionValue = toFiniteFunction(value);
542
+ return functionValue.domain.every((key) => typeof key === "string") ? functionValue : undefined;
543
+ };
544
+
545
+ const compareModelValueWith = (modelValue: ModelValue, other: Value) => {
546
+ if (getModelValueType(modelValue) !== undefined) {
547
+ throw new TlaEvaluationError(
548
+ `Attempted to compare the typed model value ${formatValue(modelValue)} and the non-model value\n${formatValue(other)}`,
549
+ );
550
+ }
551
+ return 1;
552
+ };
553
+
554
+ const equalsModelValueWith = (modelValue: ModelValue, other: Value) => {
555
+ if (getModelValueType(modelValue) !== undefined) {
556
+ throw new TlaEvaluationError(
557
+ `Attempted to check equality of the typed model value ${formatValue(modelValue)} and the non-model value\n${formatValue(other)}`,
558
+ );
559
+ }
560
+ return false;
561
+ };
562
+
563
+ const compareModelValue = (left: ModelValue, right: Value) => {
564
+ const type = getModelValueType(left);
565
+ if (type === undefined) return isModelValue(right) ? compareStrings(left.name, right.name) : -1;
566
+ if (!isModelValue(right)) {
567
+ throw new TlaEvaluationError(
568
+ `Attempted to compare the typed model value ${formatValue(left)} and non-model value\n${formatValue(right)}`,
569
+ );
570
+ }
571
+ const otherType = getModelValueType(right);
572
+ if (otherType !== undefined && otherType !== type) {
573
+ throw new TlaEvaluationError(
574
+ `Attempted to compare the differently-typed model values ${formatValue(left)} and ${formatValue(right)}`,
575
+ );
576
+ }
577
+ return compareStrings(left.name, right.name);
578
+ };
579
+
580
+ const equalsModelValue = (left: ModelValue, right: Value) => {
581
+ const type = getModelValueType(left);
582
+ if (type === undefined) return isModelValue(right) && left.name === right.name;
583
+ if (!isModelValue(right)) {
584
+ throw new TlaEvaluationError(
585
+ `Attempted to check equality of typed model value ${formatValue(left)} and non-model value\n${formatValue(right)}`,
586
+ );
587
+ }
588
+ const otherType = getModelValueType(right);
589
+ if (otherType !== undefined && otherType !== type) {
590
+ throw new TlaEvaluationError(
591
+ `Attempted to check equality of the differently-typed model values ${formatValue(left)} and ${formatValue(right)}`,
592
+ );
593
+ }
594
+ return left.name === right.name;
595
+ };
596
+
597
+ const SCALAR_KINDS = {
598
+ number: { name: "integer", other: "non-integer", equality: "check equality of" },
599
+ string: { name: "string", other: "non-string", equality: "check equality of" },
600
+ boolean: { name: "boolean", other: "non-boolean", equality: "compare equality of" },
601
+ };
602
+
603
+ const describeScalarMismatch = (
604
+ left: number | string | boolean,
605
+ right: Value,
606
+ isEquality: boolean,
607
+ ) => {
608
+ const kind =
609
+ SCALAR_KINDS[
610
+ typeof left === "number" ? "number" : typeof left === "string" ? "string" : "boolean"
611
+ ];
612
+ return `Attempted to ${isEquality ? kind.equality : "compare"} ${kind.name} ${formatValue(left)} with ${kind.other}:\n${formatValue(right)}`;
613
+ };
614
+
615
+ const compareTlcFunctions = (functionValue: TlcFunction, left: Value, right: Value) => {
616
+ const other = toTlcFunction(right);
617
+ if (!other) {
618
+ if (isModelValue(right)) return compareModelValueWith(right, left);
619
+ throw new TlaEvaluationError(
620
+ `Attempted to compare the function ${formatValue(left)} with the value:\n${formatValue(right)}`,
621
+ );
622
+ }
623
+ if (functionValue.range.length !== other.range.length)
624
+ return functionValue.range.length - other.range.length;
625
+ const compareRanges = () => compareElementsLikeTlc(functionValue.range, other.range);
626
+ if (functionValue.low !== undefined && functionValue.range.length === 0) return 0;
627
+ if (functionValue.low !== undefined && other.low !== undefined) {
628
+ return functionValue.low === other.low ? compareRanges() : functionValue.low - other.low;
629
+ }
630
+ const intervalLow = functionValue.low ?? other.low;
631
+ if (intervalLow === undefined) {
632
+ return compareElementsLikeTlc(functionValue.domain, other.domain) || compareRanges();
633
+ }
634
+ const keys = functionValue.low === undefined ? functionValue.domain : other.domain;
635
+ for (let index = 0; index < keys.length; index++) {
636
+ const key = keys[index];
637
+ if (typeof key !== "number") {
638
+ throw new TlaEvaluationError(
639
+ `Attempted to compare integer with non-integer${functionValue.low === undefined ? "" : ":"}\n${formatValue(key)}.`,
640
+ );
641
+ }
642
+ const difference =
643
+ functionValue.low === undefined ? key - (intervalLow + index) : intervalLow + index - key;
644
+ if (difference !== 0) return difference;
645
+ }
646
+ return compareRanges();
647
+ };
648
+
649
+ const compareElementsLikeTlc = (left: Value[], right: Value[]) => {
650
+ for (let index = 0; index < left.length; index++) {
651
+ const comparison = compareLikeTlc(left[index], right[index]);
652
+ if (comparison !== 0) return comparison;
653
+ }
654
+ return 0;
655
+ };
656
+
657
+ const compareFunctionLikeTlc = (left: FunctionValue | LazyFunctionValue, right: Value) => {
658
+ const functionValue = toFiniteFunction(left);
659
+ const isTuple = left.kind === "Function" && getSequenceElements(functionValue) !== undefined;
660
+ if (isTuple) {
661
+ const tuple = isFunctionValue(right) ? getSequenceElements(right) : undefined;
662
+ if (!tuple) return compareTlcFunctions({ ...functionValue, low: 1 }, left, right);
663
+ return (
664
+ functionValue.range.length - tuple.length ||
665
+ compareElementsLikeTlc(functionValue.range, tuple)
666
+ );
667
+ }
668
+ if (left.kind === "Function" && functionValue.domain.every((key) => typeof key === "string")) {
669
+ const record = toTlcRecord(right);
670
+ if (!record) {
671
+ if (isModelValue(right)) return compareModelValueWith(right, left);
672
+ throw new TlaEvaluationError(
673
+ `Attempted to compare record:\n${formatValue(left)}\nwith non-record\n${formatValue(right)}`,
674
+ );
675
+ }
676
+ return (
677
+ functionValue.domain.length - record.domain.length ||
678
+ compareElementsLikeTlc(functionValue.domain, record.domain) ||
679
+ compareElementsLikeTlc(functionValue.range, record.range)
680
+ );
681
+ }
682
+ return compareTlcFunctions(
683
+ { ...functionValue, low: getIntervalLow(functionValue.domain) },
684
+ left,
685
+ right,
686
+ );
687
+ };
688
+
689
+ export const isEnumeratedSet = (value: Value): value is SetValue | MixedSetValue =>
690
+ typeof value === "object" && (value.kind === "Set" || value.kind === "MixedSet");
691
+
692
+ // TLC compares a lazy set, or a set it overrides in Java, through its own class before it compares
693
+ // elements. The comparisons above already fail as those classes do.
694
+ const isComparedByClass = (left: ObjectValue, right: Value) =>
695
+ isOverriddenSet(left) ||
696
+ isOverriddenSet(right) ||
697
+ (typeof right === "object" &&
698
+ left.kind === right.kind &&
699
+ (left.kind === "Interval" ||
700
+ left.kind === "Subset" ||
701
+ left.kind === "FunctionSet" ||
702
+ left.kind === "RecordSet" ||
703
+ left.kind === "ProductSet"));
704
+
705
+ const getSetElementsLikeTlc = (left: SetValue | MixedSetValue, right: Value) => {
706
+ const other = isEnumeratedSet(right) ? right : toFiniteSet(right);
707
+ return { elements: toFiniteSet(left).elements, otherElements: toFiniteSet(other).elements };
708
+ };
709
+
710
+ const compareSetLikeTlc = (left: ObjectValue, right: Value): number => {
711
+ if (isComparedByClass(left, right)) return compareValues(left, right);
712
+ if (!isEnumeratedSet(left)) return compareSetLikeTlc(toFiniteSet(left), right);
713
+ if (!isSetValue(right)) {
714
+ if (isModelValue(right)) return compareModelValueWith(right, left);
715
+ throw new TlaEvaluationError(
716
+ `Attempted to compare the set ${formatValue(left)} with the value:\n${formatValue(right)}`,
717
+ );
718
+ }
719
+ const sets = getSetElementsLikeTlc(left, right);
720
+ return (
721
+ sets.elements.length - sets.otherElements.length ||
722
+ compareElementsLikeTlc(sets.elements, sets.otherElements)
723
+ );
724
+ };
725
+
726
+ const compareLikeTlc = (left: Value, right: Value): number => {
727
+ if (typeof left !== "object") {
728
+ if (typeof right === typeof left) return compareValues(left, right);
729
+ if (isModelValue(right)) return compareModelValueWith(right, left);
730
+ throw new TlaEvaluationError(describeScalarMismatch(left, right, false));
731
+ }
732
+ if (left.kind === "ModelValue") return compareModelValue(left, right);
733
+ if (isFunctionValue(left)) return compareFunctionLikeTlc(left, right);
734
+ return compareSetLikeTlc(left, right);
735
+ };
736
+
737
+ const equalsElementsLikeTlc = (left: Value[], right: Value[]) =>
738
+ left.every((element, index) => equalsLikeTlc(element, right[index]));
739
+
740
+ const equalsTlcFunctions = (functionValue: TlcFunction, left: Value, right: Value) => {
741
+ const other = toTlcFunction(right);
742
+ if (!other) {
743
+ if (isModelValue(right)) return equalsModelValueWith(right, left);
744
+ throw new TlaEvaluationError(
745
+ `Attempted to check equality of the function ${formatValue(left)} with the value:\n${formatValue(right)}`,
746
+ );
747
+ }
748
+ if (functionValue.range.length !== other.range.length) return false;
749
+ if (functionValue.low !== undefined && other.low !== undefined) {
750
+ return (
751
+ (functionValue.range.length === 0 || functionValue.low === other.low) &&
752
+ equalsElementsLikeTlc(functionValue.range, other.range)
753
+ );
754
+ }
755
+ const intervalLow = functionValue.low ?? other.low;
756
+ if (intervalLow === undefined) {
757
+ return (
758
+ equalsElementsLikeTlc(functionValue.domain, other.domain) &&
759
+ equalsElementsLikeTlc(functionValue.range, other.range)
760
+ );
761
+ }
762
+ const keys = functionValue.low === undefined ? functionValue.domain : other.domain;
763
+ return (
764
+ keys.every((key, index) => {
765
+ if (typeof key !== "number") {
766
+ throw new TlaEvaluationError(
767
+ `Attempted to compare an integer with non-integer:\n${formatValue(key)}.`,
768
+ );
769
+ }
770
+ return key === intervalLow + index;
771
+ }) && equalsElementsLikeTlc(functionValue.range, other.range)
772
+ );
773
+ };
774
+
775
+ const equalsFunctionLikeTlc = (left: FunctionValue | LazyFunctionValue, right: Value) => {
776
+ const functionValue = toFiniteFunction(left);
777
+ const isTuple = left.kind === "Function" && getSequenceElements(functionValue) !== undefined;
778
+ if (isTuple) {
779
+ const tuple = isFunctionValue(right) ? getSequenceElements(right) : undefined;
780
+ if (!tuple) return equalsTlcFunctions({ ...functionValue, low: 1 }, left, right);
781
+ return (
782
+ functionValue.range.length === tuple.length &&
783
+ equalsElementsLikeTlc(functionValue.range, tuple)
784
+ );
785
+ }
786
+ if (left.kind === "Function" && functionValue.domain.every((key) => typeof key === "string")) {
787
+ const record = toTlcRecord(right);
788
+ if (!record) {
789
+ if (isModelValue(right)) return equalsModelValueWith(right, left);
790
+ throw new TlaEvaluationError(
791
+ `Attempted to check equality of record:\n${formatValue(left)}\nwith non-record\n${formatValue(right)}`,
792
+ );
793
+ }
794
+ return (
795
+ functionValue.domain.length === record.domain.length &&
796
+ equalsElementsLikeTlc(functionValue.domain, record.domain) &&
797
+ equalsElementsLikeTlc(functionValue.range, record.range)
798
+ );
799
+ }
800
+ return equalsTlcFunctions(
801
+ { ...functionValue, low: getIntervalLow(functionValue.domain) },
802
+ left,
803
+ right,
804
+ );
805
+ };
806
+
807
+ const equalsSetLikeTlc = (left: ObjectValue, right: Value): boolean => {
808
+ if (isComparedByClass(left, right)) return valuesEqual(left, right);
809
+ if (!isEnumeratedSet(left)) return equalsSetLikeTlc(toFiniteSet(left), right);
810
+ if (!isSetValue(right)) {
811
+ if (isModelValue(right)) return equalsModelValueWith(right, left);
812
+ throw new TlaEvaluationError(
813
+ `Attempted to check equality of the set ${formatValue(left)} with the value:\n${formatValue(right)}`,
814
+ );
815
+ }
816
+ const sets = getSetElementsLikeTlc(left, right);
817
+ return (
818
+ sets.elements.length === sets.otherElements.length &&
819
+ equalsElementsLikeTlc(sets.elements, sets.otherElements)
820
+ );
821
+ };
822
+
823
+ const equalsLikeTlc = (left: Value, right: Value): boolean => {
824
+ if (typeof left !== "object") {
825
+ if (typeof right === typeof left) return left === right;
826
+ if (isModelValue(right)) return equalsModelValueWith(right, left);
827
+ throw new TlaEvaluationError(describeScalarMismatch(left, right, true));
828
+ }
829
+ if (left.kind === "ModelValue") return equalsModelValue(left, right);
830
+ if (isFunctionValue(left)) return equalsFunctionLikeTlc(left, right);
831
+ return equalsSetLikeTlc(left, right);
832
+ };
833
+
834
+ // TLC's ValueVec.sort, which inserts each element into the sorted prefix by binary search.
835
+ const sortLikeTlc = (elements: Value[]) => {
836
+ const sorted: Value[] = [];
837
+ for (const element of elements) {
838
+ const bounds = { low: 0, high: sorted.length, comparison: 1 };
839
+ while (bounds.low < bounds.high) {
840
+ const middle = bounds.low + ((bounds.high - bounds.low) >> 1);
841
+ bounds.comparison = compareLikeTlc(element, sorted[middle]);
842
+ if (bounds.comparison === 0) break;
843
+ if (bounds.comparison < 0) bounds.high = middle;
844
+ else bounds.low = middle + 1;
845
+ }
846
+ if (bounds.comparison !== 0) sorted.splice(bounds.low, 0, element);
847
+ }
848
+ return sorted;
849
+ };
850
+
851
+ // TLC checks membership in a set enumeration it hasn't sorted element by element.
852
+ const isElementOfLikeTlc = (element: Value, elements: Value[]) =>
853
+ elements.some((member) => equalsLikeTlc(member, element));
854
+
855
+ export const isEqualLikeTlc = (left: Value, right: Value) => {
856
+ try {
857
+ return valuesEqual(left, right);
858
+ } catch (error) {
859
+ if (!(error instanceof TlaEvaluationError)) throw error;
860
+ return equalsLikeTlc(left, right);
861
+ }
862
+ };
863
+
307
864
  const isStrictlySorted = (elements: Value[]) => {
308
865
  for (let index = 1; index < elements.length; index++) {
309
866
  if (compareValues(elements[index - 1], elements[index]) >= 0) return false;
@@ -321,6 +878,17 @@ export const createSet = (elements: Value[]): SetValue => {
321
878
  return createSortedSet(unique);
322
879
  };
323
880
 
881
+ // TLC builds a set enumeration without sorting it, so one whose elements it can't compare still
882
+ // evaluates.
883
+ export const createEnumeratedSet = (elements: Value[]): SetValue | MixedSetValue => {
884
+ try {
885
+ return createSet(elements);
886
+ } catch (error) {
887
+ if (!(error instanceof TlaEvaluationError)) throw error;
888
+ return { kind: "MixedSet", elements };
889
+ }
890
+ };
891
+
324
892
  export const EMPTY_SET = createSortedSet([]);
325
893
 
326
894
  export const differenceSorted = (left: SetValue, right: SetValue): SetValue => {
@@ -428,18 +996,99 @@ export const createFunction = (pairs: [Value, Value][]): FunctionValue => {
428
996
  return createSortedFunction(domain, range);
429
997
  };
430
998
 
999
+ export const createWrittenFunction = (pairs: [Value, Value][]) => {
1000
+ const functionValue = createFunction(pairs);
1001
+ const isWrittenSorted =
1002
+ functionValue.domain.length !== pairs.length ||
1003
+ pairs.some((pair) => typeof pair[0] !== "string") ||
1004
+ pairs.every((pair, index) => pair[0] === functionValue.domain[index]);
1005
+ return isWrittenSorted
1006
+ ? functionValue
1007
+ : setWrittenOrder(
1008
+ functionValue,
1009
+ pairs.map((pair) => pair[0]),
1010
+ );
1011
+ };
1012
+
1013
+ const intervalDomains: Value[][] = [];
1014
+ const intervalValues: IntervalValue[] = [];
1015
+ const intervalArrays = new WeakMap<Value[], IntervalValue>();
1016
+
1017
+ // TLC keeps the domain of a tuple, or of a function built over an interval, as an interval. Every
1018
+ // function over 1..n shares one domain array per length, and the elements of any other interval
1019
+ // map to it, so the array's identity records it. Callers must not mutate the returned array.
1020
+ const getIntervalDomain = (length: number): Value[] => {
1021
+ const cached = intervalDomains[length];
1022
+ if (cached) return cached;
1023
+ const domain = Array.from({ length }, (_, index) => index + 1);
1024
+ intervalDomains[length] = domain;
1025
+ return domain;
1026
+ };
1027
+
1028
+ // The elements of an interval for a caller that only iterates them, so they need not record
1029
+ // the interval for DOMAIN.
1030
+ export const getIntervalElements = (value: IntervalValue): Value[] => {
1031
+ getCardinality(value);
1032
+ if (value.low === 1 && value.high >= 1) return getIntervalDomain(value.high);
1033
+ const elements: number[] = [];
1034
+ for (let element = value.low; element <= value.high; element++) elements.push(element);
1035
+ return elements;
1036
+ };
1037
+
1038
+ const isIntervalDomain = (domain: Value[]) =>
1039
+ domain.length > 0 && (domain === intervalDomains[domain.length] || intervalArrays.has(domain));
1040
+
1041
+ // TLC keeps an interval domain only for a function built over an interval itself, so a set
1042
+ // such as (1..2) \cap S, whose elements are the shared array, gets a copy.
1043
+ export const getFunctionDomain = (set: Value): Value[] => {
1044
+ const elements = toFiniteSet(set).elements;
1045
+ return isIntervalDomain(elements) && !(typeof set === "object" && set.kind === "Interval")
1046
+ ? [...elements]
1047
+ : elements;
1048
+ };
1049
+
1050
+ const getIntervalValue = (high: number): IntervalValue => {
1051
+ const cached = intervalValues[high];
1052
+ if (cached) return cached;
1053
+ const interval: IntervalValue = { kind: "Interval", low: 1, high };
1054
+ intervalValues[high] = interval;
1055
+ return interval;
1056
+ };
1057
+
431
1058
  export const createTuple = (elements: Value[]): FunctionValue =>
432
- createSortedFunction(
433
- elements.map((_, index) => index + 1),
434
- elements,
435
- );
1059
+ createSortedFunction(getIntervalDomain(elements.length), elements);
436
1060
 
437
1061
  export const createRecord = (fields: [string, Value][]): FunctionValue => createFunction(fields);
438
1062
 
1063
+ // TLC's Enumerable classes: every set except the ones it implements in Java.
1064
+ const isTlcEnumerable = (value: Value) => isSetValue(value) && !isOverriddenSet(value);
1065
+
1066
+ const formatUnion = (value: UnionValue, isStrict = false) =>
1067
+ `UNION(${formatValue(value.base, isStrict)})`;
1068
+
1069
+ const getUnionSets = (value: UnionValue) => {
1070
+ if (!isTlcEnumerable(value.base)) {
1071
+ throw new TlaEvaluationError(
1072
+ `Attempted to enumerate the nonenumerable set:\n${formatUnion(value)}`,
1073
+ );
1074
+ }
1075
+ return toFiniteSet(value.base).elements;
1076
+ };
1077
+
1078
+ const getUnionSet = (set: Value, index: number, union: UnionValue) => {
1079
+ if (isTlcEnumerable(set)) return set;
1080
+ throw new TlaEvaluationError(
1081
+ index === 0
1082
+ ? "Attempted to enumerate UNION(s), but some element of s is nonenumerable."
1083
+ : `Attempted to enumerate the nonenumerable set:\n${formatValue(set)}\nwhen enumerating:\n${formatUnion(union)}`,
1084
+ );
1085
+ };
1086
+
439
1087
  export const isEnumerable = (value: Value): boolean => {
440
1088
  if (typeof value !== "object") return false;
441
1089
  switch (value.kind) {
442
1090
  case "Set":
1091
+ case "MixedSet":
443
1092
  case "Interval":
444
1093
  return true;
445
1094
  case "InfiniteSet":
@@ -449,11 +1098,17 @@ export const isEnumerable = (value: Value): boolean => {
449
1098
  case "FilteredSet":
450
1099
  return isEnumerable(value.base);
451
1100
  case "FunctionSet":
452
- return isEnumerable(value.domain) && isEnumerable(value.range);
1101
+ return (
1102
+ (isEnumerable(value.domain) && isEnumerable(value.range)) ||
1103
+ getEmptiness(value.domain) === true ||
1104
+ getEmptiness(value) === true
1105
+ );
453
1106
  case "RecordSet":
454
- return value.sets.every(isEnumerable);
1107
+ return value.sets.every(isEnumerable) || getEmptiness(value) === true;
455
1108
  case "ProductSet":
456
- return value.factors.every(isEnumerable);
1109
+ return value.factors.every(isEnumerable) || getEmptiness(value) === true;
1110
+ case "Union":
1111
+ return isEnumerable(value.base) && toFiniteSet(value.base).elements.every(isEnumerable);
457
1112
  case "SetOperation":
458
1113
  if (value.operator === "\\cup") return isEnumerable(value.left) && isEnumerable(value.right);
459
1114
  if (value.operator === "\\cap") return isEnumerable(value.left) || isEnumerable(value.right);
@@ -463,7 +1118,42 @@ export const isEnumerable = (value: Value): boolean => {
463
1118
  }
464
1119
  };
465
1120
 
466
- const getCartesianProduct = (lists: Value[][]): Value[][] => {
1121
+ const getEmptiness = (value: Value): boolean | undefined => {
1122
+ if (typeof value !== "object") return undefined;
1123
+ switch (value.kind) {
1124
+ case "Set":
1125
+ case "MixedSet":
1126
+ return value.elements.length === 0;
1127
+ case "Interval":
1128
+ return value.low > value.high;
1129
+ case "InfiniteSet":
1130
+ return value.name === "Any" ? undefined : false;
1131
+ case "Subset":
1132
+ case "SequenceSet":
1133
+ return false;
1134
+ case "FunctionSet": {
1135
+ const isUnit = getEmptiness(value.domain);
1136
+ if (isUnit === undefined) return undefined;
1137
+ return isUnit ? false : getEmptiness(value.range);
1138
+ }
1139
+ case "RecordSet":
1140
+ case "ProductSet": {
1141
+ for (const part of value.kind === "RecordSet" ? value.sets : value.factors) {
1142
+ const isEmpty = getEmptiness(part);
1143
+ if (isEmpty !== false) return isEmpty;
1144
+ }
1145
+ return false;
1146
+ }
1147
+ case "SetOperation":
1148
+ case "FilteredSet":
1149
+ case "Union":
1150
+ return isEnumerable(value) ? toFiniteSet(value).elements.length === 0 : undefined;
1151
+ default:
1152
+ return undefined;
1153
+ }
1154
+ };
1155
+
1156
+ const getCartesianProduct = (lists: Value[][]) => {
467
1157
  const total = lists.reduce((product, list) => product * list.length, 1);
468
1158
  const combinations: Value[][] = [];
469
1159
  const positions = lists.map(() => 0);
@@ -491,7 +1181,7 @@ const advanceCombination = (indices: number[], length: number) => {
491
1181
  return false;
492
1182
  };
493
1183
 
494
- const getSortedSubsets = (base: Value[]): Value[] => {
1184
+ const getSortedSubsets = (base: Value[]) => {
495
1185
  const subsets: Value[] = [];
496
1186
  for (let size = 0; size <= base.length; size++) {
497
1187
  const indices = Array.from({ length: size }, (_, index) => index);
@@ -508,12 +1198,15 @@ const enumerateSet = (value: Value): Value[] => {
508
1198
  switch (value.kind) {
509
1199
  case "Set":
510
1200
  return value.elements;
511
- case "Interval":
512
- return Array.from(
513
- { length: Math.max(0, value.high - value.low + 1) },
514
- (_, index) => value.low + index,
515
- );
1201
+ case "MixedSet":
1202
+ return sortLikeTlc(value.elements);
1203
+ case "Interval": {
1204
+ const elements = getIntervalElements(value);
1205
+ if (elements !== intervalDomains[elements.length]) intervalArrays.set(elements, value);
1206
+ return elements;
1207
+ }
516
1208
  case "Subset": {
1209
+ if (isOverriddenSet(value.base)) getCardinality(value.base);
517
1210
  const base = toFiniteSet(value.base).elements;
518
1211
  if (base.length > MAXIMUM_ENUMERATED_SUBSET_BASE) {
519
1212
  throw new TlaEvaluationError(
@@ -523,29 +1216,83 @@ const enumerateSet = (value: Value): Value[] => {
523
1216
  return getSortedSubsets(base);
524
1217
  }
525
1218
  case "FunctionSet": {
526
- const domain = toFiniteSet(value.domain).elements;
527
- const range = toFiniteSet(value.range).elements;
1219
+ if (isEmptySet(value)) return [];
1220
+ if (isOverriddenSet(value.domain)) {
1221
+ throw new TlaEvaluationError(
1222
+ `Attempted to enumerate a set of the form [D -> R],but the domain D:\n${formatValue(value.domain)}\ncannot be enumerated.`,
1223
+ );
1224
+ }
1225
+ const domain = getFunctionDomain(value.domain);
1226
+ if (domain.length > 0 && isOverriddenSet(value.range)) {
1227
+ throw new TlaEvaluationError(
1228
+ `Attempted to enumerate a set of the form [D -> R],but the range R:\n${formatValue(value.range)}\ncannot be enumerated.`,
1229
+ );
1230
+ }
1231
+ const range = domain.length === 0 ? [] : toFiniteSet(value.range).elements;
528
1232
  return getCartesianProduct(domain.map(() => range)).map((combination): Value =>
529
1233
  createSortedFunction(domain, combination),
530
1234
  );
531
1235
  }
532
- case "RecordSet":
533
- return getCartesianProduct(value.sets.map((set) => toFiniteSet(set).elements)).map(
534
- (combination) =>
535
- createRecord(
536
- value.names.map((name, index): [string, Value] => [name, combination[index]]),
537
- ),
538
- );
539
- case "ProductSet":
540
- return getCartesianProduct(value.factors.map((factor) => toFiniteSet(factor).elements)).map(
541
- createTuple,
1236
+ case "RecordSet": {
1237
+ if (isEmptySet(value)) return [];
1238
+ const fields = value.sets.map((set, index) => {
1239
+ if (isOverriddenSet(set)) {
1240
+ throw new TlaEvaluationError(
1241
+ `Attempted to enumerate a set of the form [l1 : v1, ..., ln : vn],\nbut can't enumerate the value of the \`${value.names[index]}' field:\n${formatValue(set)}`,
1242
+ );
1243
+ }
1244
+ return toFiniteSet(set).elements;
1245
+ });
1246
+ return getCartesianProduct(fields).map((combination) =>
1247
+ createRecord(value.names.map((name, index): [string, Value] => [name, combination[index]])),
542
1248
  );
1249
+ }
1250
+ case "ProductSet": {
1251
+ if (isEmptySet(value)) return [];
1252
+ const factors = value.factors.map((factor, index) => {
1253
+ if (isOverriddenSet(factor)) {
1254
+ throw new TlaEvaluationError(
1255
+ `Attempted to enumerate a set of the form s1 \\X s2 ... \\X sn,\nbut can't enumerate s${index}:\n${formatValue(factor)}`,
1256
+ );
1257
+ }
1258
+ return toFiniteSet(factor).elements;
1259
+ });
1260
+ return getCartesianProduct(factors).map(createTuple);
1261
+ }
543
1262
  case "FilteredSet":
1263
+ if (isOverriddenSet(value.base)) {
1264
+ throw new TlaEvaluationError(
1265
+ `Attempted to enumerate { x \\in S : p(x) } when S:\n${formatValue(value.base)}\nis not enumerable`,
1266
+ );
1267
+ }
544
1268
  return toFiniteSet(value.base).elements.filter(value.test);
1269
+ case "Union": {
1270
+ const elements = getUnionSets(value).flatMap(
1271
+ (set, index) => toFiniteSet(getUnionSet(set, index, value)).elements,
1272
+ );
1273
+ return toFiniteSet(createEnumeratedSet(elements)).elements;
1274
+ }
545
1275
  case "SetOperation": {
546
- if (!isEnumerable(value)) {
1276
+ if (
1277
+ value.operator === "\\cup" &&
1278
+ (isOverriddenSet(value.left) || isOverriddenSet(value.right))
1279
+ ) {
1280
+ throw new TlaEvaluationError(
1281
+ `Attempted to enumerate S \\cup T when S:\n${formatValue(value.left)}\nand T:\n${formatValue(value.right)}\nare not both enumerable`,
1282
+ );
1283
+ }
1284
+ if (
1285
+ value.operator === "\\cap" &&
1286
+ isOverriddenSet(value.left) &&
1287
+ isOverriddenSet(value.right)
1288
+ ) {
1289
+ throw new TlaEvaluationError(
1290
+ `Attempted to enumerate S \\cap T when neither S:\n${formatValue(value.left)}\nnor T:\n${formatValue(value.right)}\nis enumerable`,
1291
+ );
1292
+ }
1293
+ if (value.operator === "\\" && isOverriddenSet(value.left)) {
547
1294
  throw new TlaEvaluationError(
548
- `Attempted to enumerate the non-enumerable set ${formatValue(value)}`,
1295
+ `Attempted to enumerate S \\ T when S:\n${formatValue(value.left)}\nis not enumerable.`,
549
1296
  );
550
1297
  }
551
1298
  if (value.operator === "\\cup")
@@ -571,12 +1318,16 @@ const finiteSets = new WeakMap<ObjectValue, SetValue>();
571
1318
 
572
1319
  export const toFiniteSet = (value: Value): SetValue => {
573
1320
  if (typeof value === "object" && value.kind === "Set") return value;
1321
+ if (typeof value === "object" && value.kind === "Interval") {
1322
+ return createSortedSet(enumerateSet(value));
1323
+ }
574
1324
  const cached = typeof value === "object" ? finiteSets.get(value) : undefined;
575
1325
  if (cached) return cached;
576
1326
  const elements = enumerateSet(value);
577
1327
  const isSorted =
578
1328
  typeof value === "object" &&
579
- (value.kind === "Interval" ||
1329
+ (value.kind === "MixedSet" ||
1330
+ value.kind === "Union" ||
580
1331
  value.kind === "SetOperation" ||
581
1332
  value.kind === "FilteredSet" ||
582
1333
  value.kind === "ProductSet" ||
@@ -587,23 +1338,48 @@ export const toFiniteSet = (value: Value): SetValue => {
587
1338
  return set;
588
1339
  };
589
1340
 
590
- const checkCardinality = (cardinality: number, set: Value) => {
591
- if (cardinality > MAXIMUM_INTEGER) {
592
- throw new TlaEvaluationError(
593
- `Overflow when computing the number of elements in ${formatValue(set)}`,
1341
+ // TLC's Enumerator for a lazy set streams the sets it combines until the set caches its elements,
1342
+ // so an element that two of them share comes up once for each.
1343
+ export const getStreamedElements = (value: Value): Value[] => {
1344
+ if (typeof value !== "object" || finiteSets.has(value)) return toFiniteSet(value).elements;
1345
+ if (value.kind === "Union") {
1346
+ return getUnionSets(value).flatMap((set, index) =>
1347
+ getStreamedElements(getUnionSet(set, index, value)),
594
1348
  );
595
1349
  }
596
- return cardinality;
1350
+ if (value.kind === "FilteredSet" && !isOverriddenSet(value.base)) {
1351
+ return getStreamedElements(value.base).filter(value.test);
1352
+ }
1353
+ if (value.kind !== "SetOperation") return toFiniteSet(value).elements;
1354
+ if (value.operator === "\\cup") {
1355
+ return isOverriddenSet(value.left) || isOverriddenSet(value.right)
1356
+ ? toFiniteSet(value).elements
1357
+ : [...getStreamedElements(value.left), ...getStreamedElements(value.right)];
1358
+ }
1359
+ const isLeftEnumerated = value.operator === "\\" || isEnumerable(value.left);
1360
+ const enumerated = isLeftEnumerated ? value.left : value.right;
1361
+ const other = isLeftEnumerated ? value.right : value.left;
1362
+ if (isOverriddenSet(enumerated)) return toFiniteSet(value).elements;
1363
+ return getStreamedElements(enumerated).filter(
1364
+ (element) => isElementOf(element, other) === (value.operator === "\\cap"),
1365
+ );
597
1366
  };
598
1367
 
599
1368
  export const isEmptySet = (value: Value): boolean => {
600
- if (typeof value !== "object") return toFiniteSet(value).elements.length === 0;
1369
+ if (typeof value !== "object" || !isSetValue(value)) {
1370
+ throw new TlaEvaluationError(`Shouldn't call isEmpty() on value ${formatValue(value)}`);
1371
+ }
601
1372
  switch (value.kind) {
602
1373
  case "Set":
1374
+ case "MixedSet":
603
1375
  return value.elements.length === 0;
604
1376
  case "Interval":
605
1377
  return value.low > value.high;
606
1378
  case "InfiniteSet":
1379
+ if (value.name === "Any") {
1380
+ throw new TlaEvaluationError("Shouldn't call isEmpty() on value ANY");
1381
+ }
1382
+ return false;
607
1383
  case "Subset":
608
1384
  case "SequenceSet":
609
1385
  return false;
@@ -612,41 +1388,143 @@ export const isEmptySet = (value: Value): boolean => {
612
1388
  case "RecordSet":
613
1389
  return value.sets.some(isEmptySet);
614
1390
  case "FunctionSet":
615
- return isEmptySet(value.range) && !isEmptySet(value.domain);
1391
+ return !isEmptySet(value.domain) && isEmptySet(value.range);
1392
+ case "Union":
1393
+ return getUnionSets(value).every((set, index) => isEmptySet(getUnionSet(set, index, value)));
616
1394
  default:
617
1395
  return toFiniteSet(value).elements.length === 0;
618
1396
  }
619
1397
  };
620
1398
 
621
- const getProductCardinality = (sets: Value[], set: Value) => {
622
- if (sets.some(isEmptySet)) return 0;
623
- return sets
624
- .map(getCardinality)
625
- .reduce((product, size) => checkCardinality(product * size, set), 1);
626
- };
1399
+ const getProductCardinality = (sets: Value[], describe: () => string) =>
1400
+ sets.reduce<number>((product, set) => {
1401
+ const size = product * getCardinality(set);
1402
+ if (size > MAXIMUM_INTEGER) throw new TlaEvaluationError(describe());
1403
+ return size;
1404
+ }, 1);
1405
+
1406
+ const describeOverflow = (value: Value) =>
1407
+ `Overflow when computing the number of elements in:\n${formatValue(value)}`;
627
1408
 
628
1409
  export const getCardinality = (value: Value): number => {
629
1410
  if (typeof value !== "object") return toFiniteSet(value).elements.length;
630
1411
  switch (value.kind) {
631
- case "Interval":
632
- return Math.max(0, value.high - value.low + 1);
633
- case "Subset":
634
- return checkCardinality(2 ** getCardinality(value.base), value);
635
- case "FunctionSet":
636
- if (isEmptySet(value)) return 0;
637
- return getProductCardinality(
638
- Array.from({ length: getCardinality(value.domain) }, () => value.range),
639
- value,
1412
+ case "Interval": {
1413
+ const size = Math.max(0, value.high - value.low + 1);
1414
+ if (size > MAXIMUM_INTEGER) {
1415
+ throw new TlaEvaluationError(
1416
+ `Size of interval value exceeds the maximum representable size (32bits): ${value.low}..${value.high}.`,
1417
+ );
1418
+ }
1419
+ return size;
1420
+ }
1421
+ case "InfiniteSet":
1422
+ case "SequenceSet":
1423
+ throw new TlaEvaluationError(
1424
+ `Attempted to compute the number of elements in the overridden value ${formatValue(value)}.`,
640
1425
  );
1426
+ case "Subset": {
1427
+ const size = getCardinality(value.base);
1428
+ if (size >= 31) throw new TlaEvaluationError(describeOverflow(value));
1429
+ return 2 ** size;
1430
+ }
1431
+ case "FunctionSet": {
1432
+ if (isEmptySet(value.domain)) return 1;
1433
+ if (isEmptySet(value.range)) return 0;
1434
+ const rangeSize = getCardinality(value.range);
1435
+ if (rangeSize === 1) return 1;
1436
+ const domainSize = getCardinality(value.domain);
1437
+ const size = { value: 1 };
1438
+ for (let index = 0; index < domainSize; index++) {
1439
+ size.value *= rangeSize;
1440
+ if (size.value > MAXIMUM_INTEGER) throw new TlaEvaluationError(describeOverflow(value));
1441
+ }
1442
+ return size.value;
1443
+ }
641
1444
  case "RecordSet":
642
- return getProductCardinality(value.sets, value);
1445
+ if (isEmptySet(value)) return 0;
1446
+ return getProductCardinality(value.sets, () => describeOverflow(value));
643
1447
  case "ProductSet":
644
- return getProductCardinality(value.factors, value);
1448
+ if (isEmptySet(value)) return 0;
1449
+ return getProductCardinality(
1450
+ value.factors,
1451
+ () => `Overflow when computing the number of elements in ${formatValue(value)}`,
1452
+ );
645
1453
  default:
646
1454
  return toFiniteSet(value).elements.length;
647
1455
  }
648
1456
  };
649
1457
 
1458
+ const describeInfiniteCheck = (value: Value) =>
1459
+ `Attempted to check if the set ${formatValue(value)}is finite.`;
1460
+
1461
+ export const isFiniteSet = (value: Value): boolean => {
1462
+ if (typeof value !== "object" || value.kind === "ModelValue") {
1463
+ throw new TlaEvaluationError(
1464
+ `Attempted to check if the ${describeKind(value)} ${formatValue(value)} is a finite set.`,
1465
+ );
1466
+ }
1467
+ switch (value.kind) {
1468
+ case "Set":
1469
+ case "MixedSet":
1470
+ case "Interval":
1471
+ case "Function":
1472
+ return true;
1473
+ case "LazyFunction":
1474
+ throw new TlaEvaluationError(
1475
+ `Attempted to check if the function:\n${formatValue(value)}\nis a finite set.`,
1476
+ );
1477
+ case "InfiniteSet":
1478
+ return false;
1479
+ case "SequenceSet":
1480
+ return isFiniteSet(value.base) && isEmptySet(value.base);
1481
+ case "Subset":
1482
+ return isFiniteSet(value.base);
1483
+ case "FunctionSet": {
1484
+ const isFiniteDomain = isFiniteSet(value.domain);
1485
+ if (isFiniteDomain && isEmptySet(value.domain)) return true;
1486
+ const isFiniteRange = isFiniteSet(value.range);
1487
+ if (isFiniteRange && isEmptySet(value.range)) return true;
1488
+ return isFiniteRange && (isFiniteDomain || getCardinality(value.range) === 1);
1489
+ }
1490
+ case "RecordSet":
1491
+ case "ProductSet": {
1492
+ const parts = value.kind === "RecordSet" ? value.sets : value.factors;
1493
+ const finiteness = { isAllFinite: true };
1494
+ for (const part of parts) {
1495
+ if (!isFiniteSet(part)) finiteness.isAllFinite = false;
1496
+ else if (isEmptySet(part)) return true;
1497
+ }
1498
+ return finiteness.isAllFinite;
1499
+ }
1500
+ case "Union":
1501
+ if (!isTlcEnumerable(value.base)) {
1502
+ throw new TlaEvaluationError(
1503
+ `Attempted to check if the nonenumerable set:\n${formatUnion(value)}\nis a finite set.`,
1504
+ );
1505
+ }
1506
+ return toFiniteSet(value.base).elements.every(isFiniteSet);
1507
+ case "SetOperation":
1508
+ if (value.operator === "\\cup") return isFiniteSet(value.left) && isFiniteSet(value.right);
1509
+ if (value.operator === "\\cap") {
1510
+ if (!isFiniteSet(value.left) && !isFiniteSet(value.right)) {
1511
+ throw new TlaEvaluationError(describeInfiniteCheck(value));
1512
+ }
1513
+ return true;
1514
+ }
1515
+ if (isFiniteSet(value.left)) return true;
1516
+ if (!isFiniteSet(value.right)) throw new TlaEvaluationError(describeInfiniteCheck(value));
1517
+ return false;
1518
+ case "FilteredSet":
1519
+ if (!isFiniteSet(value.base)) {
1520
+ throw new TlaEvaluationError(
1521
+ `Attempted to check if expression of form {x \\in S : p(x)} is a finite set, but cannot check if S:\n${formatValue(value.base)}\nis finite.`,
1522
+ );
1523
+ }
1524
+ return true;
1525
+ }
1526
+ };
1527
+
650
1528
  const findIndex = (sortedValues: Value[], target: Value) => {
651
1529
  const bounds = { low: 0, high: sortedValues.length - 1 };
652
1530
  while (bounds.low <= bounds.high) {
@@ -661,30 +1539,59 @@ const findIndex = (sortedValues: Value[], target: Value) => {
661
1539
 
662
1540
  export const getSequenceElements = (value: Value): Value[] | undefined => {
663
1541
  if (typeof value !== "object" || !isFunctionValue(value)) return undefined;
664
- const fn = toFiniteFunction(value);
665
- return fn.domain.every((key, index) => key === index + 1) ? fn.range : undefined;
1542
+ const functionValue = toFiniteFunction(value);
1543
+ return functionValue.domain.every((key, index) => key === index + 1)
1544
+ ? functionValue.range
1545
+ : undefined;
666
1546
  };
667
1547
 
668
1548
  const describeMembership = (element: Value, set: Value) =>
669
1549
  `Attempted to check if the value ${formatValue(element)} is an element of ${formatValue(set)}`;
670
1550
 
1551
+ const describeNonSetMembership = (element: Value, value: Value) => {
1552
+ const prefix = `Attempted to check if the value:\n${formatValue(element)}\nis an element of the`;
1553
+ if (!isFunctionValue(value)) return `${prefix} ${describeKind(value)} ${formatValue(value)}`;
1554
+ const domain =
1555
+ value.kind === "Function" || isEnumerable(value.domain) ? toFiniteFunction(value).domain : [];
1556
+ if (domain.length > 0 && domain.every((key) => typeof key === "string")) {
1557
+ return `Attempted to check if element:\n${formatValue(element)}\nis in the record:\n${formatValue(value)}`;
1558
+ }
1559
+ if (domain.every((key, index) => key === index + 1)) {
1560
+ return "Attempted to check set membership in a tuple value.";
1561
+ }
1562
+ return `${prefix} function ${formatValue(value)}`;
1563
+ };
1564
+
671
1565
  export const isElementOf = (element: Value, set: Value): boolean => {
672
- if (typeof set !== "object")
673
- throw new TlaEvaluationError(`Expected a set but found ${formatValue(set)}`);
1566
+ if (!isSetValue(set)) throw new TlaEvaluationError(describeNonSetMembership(element, set));
674
1567
  switch (set.kind) {
675
1568
  case "Set":
676
- return findIndex(set.elements, element) !== -1;
1569
+ try {
1570
+ return findIndex(set.elements, element) !== -1;
1571
+ } catch (error) {
1572
+ if (!(error instanceof TlaEvaluationError)) throw error;
1573
+ return isElementOfLikeTlc(element, set.elements);
1574
+ }
1575
+ case "MixedSet":
1576
+ return isElementOfLikeTlc(element, set.elements);
677
1577
  case "Interval":
678
1578
  if (typeof element === "number") return element >= set.low && element <= set.high;
679
- if (set.low <= set.high && !isModelValue(element))
680
- throw new TlaEvaluationError(describeMembership(element, set));
1579
+ if (set.low <= set.high && !isModelValue(element)) {
1580
+ throw new TlaEvaluationError(
1581
+ `Attempted to check if the value:\n${formatValue(element)}\nis in the integer interval ${formatValue(set)}`,
1582
+ );
1583
+ }
681
1584
  return false;
682
1585
  case "InfiniteSet": {
683
1586
  if (set.name === "Any") return true;
684
1587
  if (isModelValue(element)) return false;
685
1588
  const isMemberType =
686
1589
  set.name === "STRING" ? typeof element === "string" : typeof element === "number";
687
- if (!isMemberType) throw new TlaEvaluationError(describeMembership(element, set));
1590
+ if (!isMemberType) {
1591
+ throw new TlaEvaluationError(
1592
+ `Attempted to check if the value:\n${formatValue(element)}\nis an element of ${set.name}.`,
1593
+ );
1594
+ }
688
1595
  return set.name !== "Nat" || (typeof element === "number" && element >= 0);
689
1596
  }
690
1597
  case "Subset":
@@ -695,40 +1602,58 @@ export const isElementOf = (element: Value, set: Value): boolean => {
695
1602
  if (isModelValue(element)) return false;
696
1603
  throw new TlaEvaluationError(describeMembership(element, set));
697
1604
  }
698
- const fn = toFiniteFunction(element);
699
- return (
700
- valuesEqual(createSortedSet(fn.domain), set.domain) &&
701
- fn.range.every((item) => isElementOf(item, set.range))
702
- );
1605
+ const functionValue = toFiniteFunction(element);
1606
+ const low = getIntervalLow(functionValue.domain);
1607
+ const isDomain =
1608
+ low === undefined
1609
+ ? isEqualLikeTlc(set.domain, createSortedSet(functionValue.domain))
1610
+ : isEqualLikeTlc(
1611
+ { kind: "Interval", low, high: low + functionValue.domain.length - 1 },
1612
+ set.domain,
1613
+ );
1614
+ return isDomain && functionValue.range.every((item) => isElementOf(item, set.range));
703
1615
  }
704
1616
  case "RecordSet": {
705
- if (!isFunctionValue(element)) {
1617
+ const functionValue = isFunctionValue(element) ? toFiniteFunction(element) : undefined;
1618
+ if (!functionValue || !isRecordDomain(functionValue.domain)) {
706
1619
  if (isModelValue(element)) return false;
707
- throw new TlaEvaluationError(describeMembership(element, set));
1620
+ throw new TlaEvaluationError(
1621
+ `Attempted to check if non-record\n${formatValue(element)}\nis in the set of records:\n${formatValue(set)}`,
1622
+ );
708
1623
  }
709
- const fn = toFiniteFunction(element);
710
1624
  return (
711
- fn.domain.length === set.names.length &&
1625
+ functionValue.domain.length === set.names.length &&
712
1626
  set.names.every((name, index) => {
713
- const position = findIndex(fn.domain, name);
714
- return position !== -1 && isElementOf(fn.range[position], set.sets[index]);
1627
+ const position = findIndex(functionValue.domain, name);
1628
+ return position !== -1 && isElementOf(functionValue.range[position], set.sets[index]);
715
1629
  })
716
1630
  );
717
1631
  }
718
1632
  case "ProductSet": {
719
- if (!isFunctionValue(element)) {
720
- if (isModelValue(element)) return false;
721
- throw new TlaEvaluationError(describeMembership(element, set));
1633
+ const elements = isFunctionValue(element) ? getSequenceElements(element) : undefined;
1634
+ if (elements === undefined) {
1635
+ const isFunction =
1636
+ isFunctionValue(element) &&
1637
+ toFiniteFunction(element).domain.every((key) => typeof key === "number");
1638
+ if (isFunction || isModelValue(element)) return false;
1639
+ throw new TlaEvaluationError(
1640
+ `Attempted to check if non-tuple\n${formatValue(element)}\nis in the set of tuples:\n${formatValue(set)}`,
1641
+ );
722
1642
  }
723
- const elements = getSequenceElements(element);
724
1643
  return (
725
- elements !== undefined &&
726
1644
  elements.length === set.factors.length &&
727
1645
  elements.every((item, index) => isElementOf(item, set.factors[index]))
728
1646
  );
729
1647
  }
730
1648
  case "FilteredSet":
731
1649
  return isElementOf(element, set.base) && set.test(element);
1650
+ case "Union":
1651
+ if (!isTlcEnumerable(set.base)) {
1652
+ throw new TlaEvaluationError(
1653
+ `Attempted to check if:\n ${formatValue(element)}\nis an element of the non-enumerable set:\n ${formatUnion(set)}`,
1654
+ );
1655
+ }
1656
+ return toFiniteSet(set.base).elements.some((inner) => isElementOf(element, inner));
732
1657
  case "SetOperation":
733
1658
  if (set.operator === "\\cup")
734
1659
  return isElementOf(element, set.left) || isElementOf(element, set.right);
@@ -745,14 +1670,34 @@ export const isElementOf = (element: Value, set: Value): boolean => {
745
1670
  return elements.every((item) => isElementOf(item, set.base));
746
1671
  }
747
1672
  default:
748
- throw new TlaEvaluationError(`Expected a set but found ${formatValue(set)}`);
1673
+ throw new TlaEvaluationError(describeNonSetMembership(element, set));
749
1674
  }
750
1675
  };
751
1676
 
1677
+ // A function that agrees with a member of a function set outside some entries is a member when
1678
+ // those entries are.
1679
+ export const isUpdatedElementOf = (element: Value, member: Value, set: Value): boolean => {
1680
+ if (element === member) return true;
1681
+ if (
1682
+ typeof element !== "object" ||
1683
+ typeof member !== "object" ||
1684
+ typeof set !== "object" ||
1685
+ element.kind !== "Function" ||
1686
+ member.kind !== "Function" ||
1687
+ set.kind !== "FunctionSet" ||
1688
+ element.domain !== member.domain
1689
+ )
1690
+ return isElementOf(element, set);
1691
+ return element.range.every((item, index) =>
1692
+ isUpdatedElementOf(item, member.range[index], set.range),
1693
+ );
1694
+ };
1695
+
752
1696
  export const toFiniteFunction = (value: Value): FunctionValue => {
753
1697
  if (typeof value === "object" && value.kind === "Function") return value;
754
1698
  if (typeof value === "object" && value.kind === "LazyFunction") {
755
- const domain = toFiniteSet(value.domain).elements;
1699
+ if (!isEnumerable(value.domain)) getCardinality(value.domain);
1700
+ const domain = getFunctionDomain(value.domain);
756
1701
  return createSortedFunction(
757
1702
  domain,
758
1703
  domain.map((key) => value.apply(key)),
@@ -761,64 +1706,103 @@ export const toFiniteFunction = (value: Value): FunctionValue => {
761
1706
  throw new TlaEvaluationError(`Expected a function but found ${formatValue(value)}`);
762
1707
  };
763
1708
 
764
- export const applyFunction = (fn: Value, argument: Value): Value => {
765
- if (typeof fn === "object" && fn.kind === "LazyFunction") {
766
- if (!isElementOf(argument, fn.domain)) {
767
- throw new TlaEvaluationError(
768
- `Attempted to apply a function to ${formatValue(argument)}, which is not in its domain`,
769
- );
770
- }
771
- return fn.apply(argument);
772
- }
773
- if (typeof fn !== "object" || fn.kind !== "Function") {
774
- throw new TlaEvaluationError(`Attempted to apply ${formatValue(fn)}, which is not a function`);
775
- }
776
- const domain = fn.domain;
1709
+ const lambdaFunctions = new WeakSet<ObjectValue>();
1710
+
1711
+ export const markLambdaFunction = <FunctionLike extends FunctionValue | LazyFunctionValue>(
1712
+ functionValue: FunctionLike,
1713
+ ) => {
1714
+ lambdaFunctions.add(functionValue);
1715
+ return functionValue;
1716
+ };
1717
+
1718
+ const describeMissingArgument = (functionValue: Value, argument: Value) =>
1719
+ typeof functionValue === "object" && lambdaFunctions.has(functionValue)
1720
+ ? `In applying the function\n${prettyPrint(formatValue(functionValue))},\nthe first argument is:\n${prettyPrint(formatValue(argument))}\nwhich is not in its domain.\n`
1721
+ : `Attempted to apply function ${formatValue(functionValue)} to ${formatValue(argument)}, which is not in its domain`;
1722
+
1723
+ const findKeyIndex = (domain: Value[], argument: Value) => {
777
1724
  const first = domain[0];
778
1725
  if (typeof argument === "number" && typeof first === "number") {
779
1726
  const offset = argument - first;
780
- if (domain[offset] === argument) return fn.range[offset];
1727
+ if (domain[offset] === argument) return offset;
781
1728
  }
782
1729
  if (domain.length <= 8) {
783
1730
  for (let index = 0; index < domain.length; index++)
784
- if (domain[index] === argument) return fn.range[index];
1731
+ if (domain[index] === argument) return index;
785
1732
  }
786
- const position = findIndex(domain, argument);
1733
+ return findIndex(domain, argument);
1734
+ };
1735
+
1736
+ export const applyFunction = (
1737
+ functionValue: Value,
1738
+ argument: Value,
1739
+ describeMissing?: (functionValue: FunctionValue) => string,
1740
+ ): Value => {
1741
+ if (typeof functionValue === "object" && functionValue.kind === "LazyFunction") {
1742
+ if (!isElementOf(argument, functionValue.domain)) {
1743
+ throw new TlaEvaluationError(
1744
+ lambdaFunctions.has(functionValue)
1745
+ ? describeMissingArgument(functionValue, argument)
1746
+ : `Attempted to apply a function to ${formatValue(argument)}, which is not in its domain`,
1747
+ );
1748
+ }
1749
+ return functionValue.apply(argument);
1750
+ }
1751
+ if (typeof functionValue !== "object" || functionValue.kind !== "Function") {
1752
+ throw new TlaEvaluationError(
1753
+ `Attempted to apply ${formatValue(functionValue)}, which is not a function`,
1754
+ );
1755
+ }
1756
+ const position = findKeyIndex(functionValue.domain, argument);
787
1757
  if (position === -1) {
788
1758
  throw new TlaEvaluationError(
789
- `Attempted to apply function ${formatValue(fn)} to ${formatValue(argument)}, which is not in its domain`,
1759
+ describeMissing?.(functionValue) ?? describeMissingArgument(functionValue, argument),
790
1760
  );
791
1761
  }
792
- return fn.range[position];
1762
+ return functionValue.range[position];
793
1763
  };
794
1764
 
795
- export const getDomain = (fn: Value): Value => {
796
- if (typeof fn === "object" && fn.kind === "LazyFunction") return fn.domain;
797
- return createSortedSet(toFiniteFunction(fn).domain);
1765
+ export const getDomainInterval = (domain: Value[]) =>
1766
+ isIntervalDomain(domain)
1767
+ ? (intervalArrays.get(domain) ?? getIntervalValue(domain.length))
1768
+ : undefined;
1769
+
1770
+ export const getDomain = (functionValue: Value): Value => {
1771
+ if (typeof functionValue === "object" && functionValue.kind === "LazyFunction")
1772
+ return functionValue.domain;
1773
+ const domain = toFiniteFunction(functionValue).domain;
1774
+ return getDomainInterval(domain) ?? createSortedSet(domain);
798
1775
  };
799
1776
 
800
1777
  export const updateFunction = (
801
- fn: Value,
1778
+ functionValue: Value,
802
1779
  argument: Value,
803
1780
  update: (previous: Value) => Value,
1781
+ isKeptLazy = false,
804
1782
  ): Value => {
805
- if (typeof fn === "object" && fn.kind === "LazyFunction" && !isEnumerable(fn.domain)) {
806
- if (!isElementOf(argument, fn.domain)) return fn;
807
- const updated = update(fn.apply(argument));
1783
+ const isLazy =
1784
+ typeof functionValue === "object" &&
1785
+ functionValue.kind === "LazyFunction" &&
1786
+ (isKeptLazy || !isEnumerable(functionValue.domain));
1787
+ if (isLazy) {
1788
+ if (!isElementOf(argument, functionValue.domain)) return functionValue;
1789
+ const updated = update(functionValue.apply(argument));
808
1790
  return {
809
1791
  kind: "LazyFunction",
810
- domain: fn.domain,
811
- apply: (key) => (valuesEqual(key, argument) ? updated : fn.apply(key)),
1792
+ domain: functionValue.domain,
1793
+ apply: (key) => (isEqualLikeTlc(argument, key) ? updated : functionValue.apply(key)),
1794
+ source: functionValue.source,
812
1795
  };
813
1796
  }
814
- const finite = toFiniteFunction(fn);
815
- const position = findIndex(finite.domain, argument);
1797
+ const finite = toFiniteFunction(functionValue);
1798
+ const position = findKeyIndex(finite.domain, argument);
816
1799
  if (position === -1) return finite;
817
1800
  const range = [...finite.range];
818
1801
  const previous = finite.range[position];
819
1802
  const next = update(previous);
820
1803
  range[position] = next;
821
1804
  const updated = createSortedFunction(finite.domain, range);
1805
+ updated.writtenKeys = finite.writtenKeys;
822
1806
  if (finite.hash !== 0) {
823
1807
  const key = finite.domain[position];
824
1808
  updated.hash =
@@ -867,7 +1851,7 @@ const getElementContribution = (element: Value) =>
867
1851
  const getEntryContribution = (key: Value, value: Value) =>
868
1852
  finalizeHash((Math.imul(getValueHash(key), 0x27d4eb2d) + getValueHash(value)) | 0);
869
1853
 
870
- const computeValueHash = (value: ObjectValue): number => {
1854
+ const computeValueHash = (value: ObjectValue) => {
871
1855
  if (value.kind === "ModelValue") return finalizeHash(combineHash(hashString(value.name), 0x6d));
872
1856
  if (isSetValue(value)) {
873
1857
  if (!isEnumerable(value)) return hashString(getValueKey(value));
@@ -878,10 +1862,11 @@ const computeValueHash = (value: ObjectValue): number => {
878
1862
  }
879
1863
  return hash;
880
1864
  }
881
- const fn = toFiniteFunction(value);
1865
+ const functionValue = toFiniteFunction(value);
882
1866
  let hash = FUNCTION_HASH_SEED;
883
- for (let index = 0; index < fn.domain.length; index++) {
884
- hash = (hash + getEntryContribution(fn.domain[index], fn.range[index])) | 0;
1867
+ for (let index = 0; index < functionValue.domain.length; index++) {
1868
+ hash =
1869
+ (hash + getEntryContribution(functionValue.domain[index], functionValue.range[index])) | 0;
885
1870
  }
886
1871
  return hash;
887
1872
  };
@@ -924,7 +1909,7 @@ const getEntryFingerprint = (key: Value, value: Value) =>
924
1909
  (Math.imul(getValueFingerprint(key), 0x165667b1) + getValueFingerprint(value)) | 0,
925
1910
  );
926
1911
 
927
- const computeValueFingerprint = (value: ObjectValue): number => {
1912
+ const computeValueFingerprint = (value: ObjectValue) => {
928
1913
  if (value.kind === "ModelValue") return finalizeFingerprint(fingerprintString(value.name) ^ 0x3c);
929
1914
  if (isSetValue(value)) {
930
1915
  if (!isEnumerable(value)) return fingerprintString(getValueKey(value));
@@ -935,10 +1920,12 @@ const computeValueFingerprint = (value: ObjectValue): number => {
935
1920
  }
936
1921
  return fingerprint;
937
1922
  }
938
- const fn = toFiniteFunction(value);
1923
+ const functionValue = toFiniteFunction(value);
939
1924
  let fingerprint = 0x4cf5ad43;
940
- for (let index = 0; index < fn.domain.length; index++) {
941
- fingerprint = (fingerprint + getEntryFingerprint(fn.domain[index], fn.range[index])) | 0;
1925
+ for (let index = 0; index < functionValue.domain.length; index++) {
1926
+ fingerprint =
1927
+ (fingerprint + getEntryFingerprint(functionValue.domain[index], functionValue.range[index])) |
1928
+ 0;
942
1929
  }
943
1930
  return fingerprint;
944
1931
  };
@@ -971,7 +1958,7 @@ export const getValueKey = (value: Value): string => {
971
1958
  return key;
972
1959
  };
973
1960
 
974
- const computeValueKey = (value: ObjectValue): string => {
1961
+ const computeValueKey = (value: ObjectValue) => {
975
1962
  if (value.kind === "ModelValue") return `@${value.name}`;
976
1963
  if (value.kind === "InfiniteSet") return `<${value.name}>`;
977
1964
  if (value.kind === "SequenceSet") return `<Seq ${getValueKey(value.base)}>`;
@@ -984,42 +1971,95 @@ const computeValueKey = (value: ObjectValue): string => {
984
1971
  if (!isEnumerable(value)) return `<${value.kind} ${formatValue(value)}>`;
985
1972
  return `{${toFiniteSet(value).elements.map(getValueKey).join(",")}}`;
986
1973
  }
987
- const fn = toFiniteFunction(value);
988
- return `(${fn.domain.map((key, index) => `${getValueKey(key)}:${getValueKey(fn.range[index])}`).join(",")})`;
1974
+ const functionValue = toFiniteFunction(value);
1975
+ return `(${functionValue.domain.map((key, index) => `${getValueKey(key)}:${getValueKey(functionValue.range[index])}`).join(",")})`;
989
1976
  };
990
1977
 
991
1978
  const formatString = (value: string) =>
992
1979
  `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t")}"`;
993
1980
 
994
- const formatLazySet = (
995
- value:
996
- | SubsetValue
997
- | FunctionSetValue
998
- | RecordSetValue
999
- | ProductSetValue
1000
- | SetOperationValue
1001
- | FilteredSetValue,
1002
- isExpanded: boolean,
1003
- ): string => {
1004
- const format = (inner: Value) => formatValue(inner, isExpanded);
1981
+ type LazySetValue =
1982
+ | SubsetValue
1983
+ | FunctionSetValue
1984
+ | RecordSetValue
1985
+ | ProductSetValue
1986
+ | SetOperationValue
1987
+ | FilteredSetValue
1988
+ | UnionValue;
1989
+
1990
+ const MAXIMUM_EXPANDED_SUBSET_BASE = 6;
1991
+ const MAXIMUM_EXPANDED_PRODUCT = 1999;
1992
+
1993
+ const getProductSize = (sets: Value[]) =>
1994
+ sets.reduce<number>((size, set) => (size === 0 ? 0 : size * getCardinality(set)), 1);
1995
+
1996
+ const expandLazySet = (value: LazySetValue) => {
1997
+ if (value.kind === "Subset") {
1998
+ return getCardinality(value.base) <= MAXIMUM_EXPANDED_SUBSET_BASE
1999
+ ? toFiniteSet(value)
2000
+ : undefined;
2001
+ }
2002
+ if (value.kind === "SetOperation" || value.kind === "FilteredSet" || value.kind === "Union") {
2003
+ return toFiniteSet(value);
2004
+ }
2005
+ if (
2006
+ value.kind === "FunctionSet" &&
2007
+ isEnumerable(value.domain) &&
2008
+ getCardinality(value.domain) === 0
2009
+ ) {
2010
+ return createSortedSet([createSortedFunction([], [])]);
2011
+ }
2012
+ const size =
2013
+ value.kind === "FunctionSet"
2014
+ ? getCardinality(value.range) ** getCardinality(value.domain)
2015
+ : getProductSize(value.kind === "RecordSet" ? value.sets : value.factors);
2016
+ if (size === 0) return EMPTY_SET;
2017
+ return size <= MAXIMUM_EXPANDED_PRODUCT ? toFiniteSet(value) : undefined;
2018
+ };
2019
+
2020
+ const getExpandedLazySet = (value: LazySetValue, isStrict: boolean) => {
2021
+ if (isStrict) return expandLazySet(value);
2022
+ // TLC prints a set it can't enumerate symbolically, except in Print, PrintT, and ToString.
2023
+ if (!isEnumerable(value)) return undefined;
2024
+ try {
2025
+ return expandLazySet(value);
2026
+ } catch (error) {
2027
+ if (error instanceof TlaEvaluationError) return undefined;
2028
+ throw error;
2029
+ }
2030
+ };
2031
+
2032
+ const formatLazySet = (value: LazySetValue, isStrict: boolean): string => {
2033
+ const format = (inner: Value) => formatValue(inner, isStrict);
1005
2034
  switch (value.kind) {
1006
2035
  case "Subset":
1007
- return `SUBSET ${format(value.base)}`;
2036
+ return typeof value.base === "object" && value.base.kind === "Interval"
2037
+ ? `SUBSET (${format(value.base)})`
2038
+ : `SUBSET ${format(value.base)}`;
1008
2039
  case "FunctionSet":
1009
2040
  return `[${format(value.domain)} -> ${format(value.range)}]`;
1010
2041
  case "RecordSet":
1011
- return `[${value.names.map((name, index) => `${name} : ${format(value.sets[index])}`).join(", ")}]`;
2042
+ return `[${value.names.map((name, index) => `${name}: ${format(value.sets[index])}`).join(", ")}]`;
1012
2043
  case "ProductSet":
1013
- return value.factors.map(format).join(" \\X ");
2044
+ return `(${value.factors.map(format).join(" \\X ")})`;
1014
2045
  case "SetOperation":
1015
2046
  return `${format(value.left)} ${value.operator} ${format(value.right)}`;
1016
2047
  case "FilteredSet":
1017
2048
  return `{x \\in ${format(value.base)} : ...}`;
2049
+ case "Union":
2050
+ return formatUnion(value, isStrict);
1018
2051
  }
1019
2052
  };
1020
2053
 
1021
- export const formatValue = (value: Value, isExpanded = false): string => {
1022
- const format = (inner: Value) => formatValue(inner, isExpanded);
2054
+ // FcnRcdValue#isName: TLC prints a function as a record when every key consists of letters,
2055
+ // decimal digits, and underscores, with at least one letter.
2056
+ const isFieldName = (name: string) =>
2057
+ /^[\p{L}\p{Nd}_]*$/u.test(name) &&
2058
+ /\p{L}/u.test(name) &&
2059
+ (name.length < 4 || !/^[WS]F_/.test(name));
2060
+
2061
+ export const formatValue = (value: Value, isStrict = false, isNormalized = false): string => {
2062
+ const format = (inner: Value) => formatValue(inner, isStrict, isNormalized);
1023
2063
  if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
1024
2064
  if (typeof value === "number") return String(value);
1025
2065
  if (typeof value === "string") return formatString(value);
@@ -1028,8 +2068,10 @@ export const formatValue = (value: Value, isExpanded = false): string => {
1028
2068
  return value.name;
1029
2069
  case "Set":
1030
2070
  return `{${value.elements.map(format).join(", ")}}`;
2071
+ case "MixedSet":
2072
+ return format(toFiniteSet(value));
1031
2073
  case "Interval":
1032
- return `${value.low}..${value.high}`;
2074
+ return value.low <= value.high ? `${value.low}..${value.high}` : "{}";
1033
2075
  case "InfiniteSet":
1034
2076
  return value.name === "Any" ? "ANY" : value.name;
1035
2077
  case "Subset":
@@ -1038,23 +2080,36 @@ export const formatValue = (value: Value, isExpanded = false): string => {
1038
2080
  case "ProductSet":
1039
2081
  case "SetOperation":
1040
2082
  case "FilteredSet":
1041
- return isExpanded && isEnumerable(value)
1042
- ? format(toFiniteSet(value))
1043
- : formatLazySet(value, isExpanded);
2083
+ case "Union": {
2084
+ const expanded = getExpandedLazySet(value, isStrict);
2085
+ return expanded ? format(expanded) : formatLazySet(value, isStrict);
2086
+ }
1044
2087
 
1045
2088
  case "SequenceSet":
1046
2089
  return `Seq(${format(value.base)})`;
1047
2090
  case "Function":
1048
2091
  case "LazyFunction": {
1049
- if (value.kind === "LazyFunction" && !isEnumerable(value.domain)) return "<function>";
1050
- const fn = toFiniteFunction(value);
1051
- if (fn.domain.every((key, index) => key === index + 1)) {
1052
- return `<<${fn.range.map(format).join(", ")}>>`;
2092
+ if (value.kind === "LazyFunction" && !isEnumerable(value.domain)) {
2093
+ const source = value.source;
2094
+ if (!source) return "<function>";
2095
+ const parameters = source.parameters
2096
+ .map((parameter) => `${parameter.name} \\in ${formatValue(parameter.domain)}`)
2097
+ .join(", ");
2098
+ return `[${parameters} |-> <expression ${source.body}>]`;
2099
+ }
2100
+ const functionValue = toFiniteFunction(value);
2101
+ if (functionValue.domain.every((key, index) => key === index + 1)) {
2102
+ return `<<${functionValue.range.map(format).join(", ")}>>`;
1053
2103
  }
1054
- if (fn.domain.every((key) => typeof key === "string" && key.length > 0)) {
1055
- return `[${fn.domain.map((key, index) => `${String(key)} |-> ${format(fn.range[index])}`).join(", ")}]`;
2104
+ if (functionValue.domain.every((key) => typeof key === "string" && isFieldName(key))) {
2105
+ const isSorted =
2106
+ isNormalized || functionValue.hash !== 0 || functionValue.fingerprint !== 0;
2107
+ const pairs = isSorted
2108
+ ? functionValue.domain.map((key, index) => [key, functionValue.range[index]])
2109
+ : getWrittenPairs(functionValue);
2110
+ return `[${pairs.map((pair) => `${String(pair[0])} |-> ${format(pair[1])}`).join(", ")}]`;
1056
2111
  }
1057
- return `(${fn.domain.map((key, index) => `${format(key)} :> ${format(fn.range[index])}`).join(" @@ ")})`;
2112
+ return `(${functionValue.domain.map((key, index) => `${format(key)} :> ${format(functionValue.range[index])}`).join(" @@ ")})`;
1058
2113
  }
1059
2114
  }
1060
2115
  };