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