tla-ts 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +34 -41
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +166 -30
  4. package/dist/index.d.ts +166 -30
  5. package/dist/index.js +1 -1
  6. package/dist/parallel-worker.cjs +2 -2
  7. package/dist/parallel-worker.js +2 -2
  8. package/dist/parallel.cjs +1 -1
  9. package/dist/parallel.js +2 -2
  10. package/dist/run-cli.cjs +2 -1
  11. package/dist/run-cli.js +2 -1
  12. package/dist/transfer.cjs +388 -39
  13. package/dist/transfer.js +388 -39
  14. package/package.json +4 -2
  15. package/src/ast.ts +82 -5
  16. package/src/bags.ts +161 -71
  17. package/src/builtins.ts +203 -23
  18. package/src/bundled-modules.ts +63 -32
  19. package/src/check.ts +904 -230
  20. package/src/codegen.ts +52 -18
  21. package/src/config.ts +39 -11
  22. package/src/environment.ts +485 -140
  23. package/src/errors.ts +22 -0
  24. package/src/evaluate.ts +486 -136
  25. package/src/exit-codes.ts +75 -0
  26. package/src/fingerprints.ts +2 -2
  27. package/src/index.ts +9 -4
  28. package/src/instances.ts +221 -66
  29. package/src/java-overrides.ts +241 -0
  30. package/src/json.ts +153 -72
  31. package/src/levels.ts +850 -0
  32. package/src/lexer.ts +97 -27
  33. package/src/liveness.ts +47 -91
  34. package/src/parallel-worker.ts +57 -28
  35. package/src/parallel.ts +116 -62
  36. package/src/parser.ts +703 -183
  37. package/src/pretty-print.ts +259 -0
  38. package/src/randomization.ts +24 -113
  39. package/src/resolve.ts +472 -41
  40. package/src/run-cli.ts +142 -44
  41. package/src/runtime.ts +63 -28
  42. package/src/spec-description.ts +509 -0
  43. package/src/standard-levels.ts +105 -0
  44. package/src/standard-modules.ts +223 -92
  45. package/src/subexpressions.ts +21 -35
  46. package/src/substitution-levels.ts +184 -0
  47. package/src/successors.ts +261 -46
  48. package/src/symmetry.ts +65 -21
  49. package/src/tableau.ts +43 -37
  50. package/src/tlc-ext.ts +120 -33
  51. package/src/tlc-module.ts +71 -47
  52. package/src/tlc-modules.ts +1022 -0
  53. package/src/tlc-order.ts +497 -0
  54. package/src/tlc-random.ts +411 -0
  55. package/src/tlc-summary.ts +109 -0
  56. package/src/transfer.ts +4 -4
  57. package/src/values.ts +1186 -164
package/src/values.ts CHANGED
@@ -1,4 +1,5 @@
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";
@@ -12,12 +13,20 @@ export interface SetValue {
12
13
  fingerprint: number;
13
14
  }
14
15
 
16
+ // A set enumeration whose elements TLC can't sort, such as {1, "a"}. TLC sorts a set enumeration
17
+ // only when it has to, so it still checks membership in one element by element.
18
+ export interface MixedSetValue {
19
+ kind: "MixedSet";
20
+ elements: Value[];
21
+ }
22
+
15
23
  export interface FunctionValue {
16
24
  kind: "Function";
17
25
  domain: Value[];
18
26
  range: Value[];
19
27
  hash: number;
20
28
  fingerprint: number;
29
+ writtenKeys: Value[] | undefined;
21
30
  }
22
31
 
23
32
  export interface IntervalValue {
@@ -71,10 +80,27 @@ export interface FilteredSetValue {
71
80
  test: (element: Value) => boolean;
72
81
  }
73
82
 
83
+ // UNION S when some element of S isn't a set enumeration. TLC enumerates it only when it has to.
84
+ export interface UnionValue {
85
+ kind: "Union";
86
+ base: Value;
87
+ }
88
+
89
+ interface LazyFunctionParameter {
90
+ name: string;
91
+ domain: Value;
92
+ }
93
+
94
+ export interface LazyFunctionSource {
95
+ parameters: LazyFunctionParameter[];
96
+ body: string;
97
+ }
98
+
74
99
  export interface LazyFunctionValue {
75
100
  kind: "LazyFunction";
76
101
  domain: Value;
77
102
  apply: (argument: Value) => Value;
103
+ source?: LazyFunctionSource;
78
104
  }
79
105
 
80
106
  export type Value =
@@ -83,6 +109,7 @@ export type Value =
83
109
  | string
84
110
  | ModelValue
85
111
  | SetValue
112
+ | MixedSetValue
86
113
  | FunctionValue
87
114
  | IntervalValue
88
115
  | InfiniteSetValue
@@ -93,12 +120,13 @@ export type Value =
93
120
  | SequenceSetValue
94
121
  | SetOperationValue
95
122
  | FilteredSetValue
123
+ | UnionValue
96
124
  | LazyFunctionValue;
97
125
 
98
126
  export type ObjectValue = Exclude<Value, boolean | number | string>;
99
127
 
100
- const MAXIMUM_INTEGER = 2147483647;
101
- const MINIMUM_INTEGER = -2147483648;
128
+ export const MAXIMUM_INTEGER = 2147483647;
129
+ export const MINIMUM_INTEGER = -2147483648;
102
130
  const MAXIMUM_ENUMERATED_SUBSET_BASE = 30;
103
131
 
104
132
  export const isSetValue = (value: Value) =>
@@ -113,7 +141,7 @@ export const isFunctionValue = (value: Value): value is FunctionValue | LazyFunc
113
141
  export const isModelValue = (value: Value): value is ModelValue =>
114
142
  typeof value === "object" && value.kind === "ModelValue";
115
143
 
116
- const describeKind = (value: Value) => {
144
+ export const describeKind = (value: Value) => {
117
145
  if (typeof value === "boolean") return "boolean";
118
146
  if (typeof value === "number") return "integer";
119
147
  if (typeof value === "string") return "string";
@@ -123,15 +151,59 @@ const describeKind = (value: Value) => {
123
151
 
124
152
  export const checkInteger = (value: number) => {
125
153
  if (!Number.isInteger(value) || value > MAXIMUM_INTEGER || value < MINIMUM_INTEGER) {
126
- throw new TlaEvaluationError(`Integer overflow: ${value} is outside TLC's 32-bit range`);
154
+ throw new TlaModuleError(`Integer overflow: ${value} is outside TLC's 32-bit range`);
127
155
  }
128
156
  return value;
129
157
  };
130
158
 
131
- const compareFunctions = (left: FunctionValue, right: FunctionValue): number => {
159
+ // TLC keeps a record, or a function with string keys that @@ builds, in the order the spec writes
160
+ // it until it sorts the value to compare or fingerprint it. ToString, Assert, and error messages
161
+ // show that order.
162
+ const writtenOrderUse = { isUsed: false };
163
+
164
+ const setWrittenOrder = (functionValue: FunctionValue, keys: Value[]) => {
165
+ writtenOrderUse.isUsed = true;
166
+ functionValue.writtenKeys = keys;
167
+ return functionValue;
168
+ };
169
+
170
+ export const getWrittenPairs = (functionValue: FunctionValue): [Value, Value][] => {
171
+ const keys = functionValue.writtenKeys ?? functionValue.domain;
172
+ return keys.map((key) => [key, functionValue.range[findIndex(functionValue.domain, key)]]);
173
+ };
174
+
175
+ export const createWrittenRecord = (names: Value[], range: Value[], writtenNames: Value[]) =>
176
+ setWrittenOrder(createSortedFunction(names, range), writtenNames);
177
+
178
+ // TLC sorts a value it compares even with itself, and every value inside it, so a value it has
179
+ // hashed or fingerprinted is already sorted.
180
+ const forgetWrittenOrder = (value: Value): void => {
181
+ if (typeof value !== "object" || (value.kind !== "Function" && value.kind !== "Set")) return;
182
+ if (value.hash !== 0 || value.fingerprint !== 0) return;
183
+ if (value.kind === "Set") return value.elements.forEach(forgetWrittenOrder);
184
+ value.writtenKeys = undefined;
185
+ value.range.forEach(forgetWrittenOrder);
186
+ };
187
+
188
+ const isRecordDomain = (domain: Value[]) => domain.every((key) => typeof key === "string");
189
+
190
+ // TLC compares a record only with another record, so comparing one with a function of another size
191
+ // can fail before the sizes decide.
192
+ const isRecordMismatch = (left: Value[], right: Value[]) =>
193
+ left.length > 0 && right.length > 0 && isRecordDomain(left) !== isRecordDomain(right);
194
+
195
+ const compareFunctions = (left: FunctionValue, right: FunctionValue) => {
196
+ if (writtenOrderUse.isUsed) {
197
+ left.writtenKeys = undefined;
198
+ right.writtenKeys = undefined;
199
+ }
132
200
  const leftDomain = left.domain;
133
201
  const rightDomain = right.domain;
134
- if (leftDomain.length !== rightDomain.length) return leftDomain.length - rightDomain.length;
202
+ if (leftDomain.length !== rightDomain.length) {
203
+ return isRecordMismatch(leftDomain, rightDomain)
204
+ ? compareLikeTlc(left, right)
205
+ : leftDomain.length - rightDomain.length;
206
+ }
135
207
  if (leftDomain !== rightDomain) {
136
208
  for (let index = 0; index < leftDomain.length; index++) {
137
209
  const keyComparison = compareValues(leftDomain[index], rightDomain[index]);
@@ -148,7 +220,7 @@ const compareFunctions = (left: FunctionValue, right: FunctionValue): number =>
148
220
  return 0;
149
221
  };
150
222
 
151
- const compareSets = (left: SetValue, right: SetValue): number => {
223
+ const compareSets = (left: SetValue, right: SetValue) => {
152
224
  const leftElements = left.elements;
153
225
  const rightElements = right.elements;
154
226
  if (leftElements.length !== rightElements.length)
@@ -185,41 +257,120 @@ 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
+ }
195
352
  if (leftKind === "ModelValue" || rightKind === "ModelValue") {
196
353
  checkModelValueTypes(left, right, false);
197
354
  if (leftKind === "ModelValue" && rightKind === "ModelValue") {
198
- return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
355
+ return compareStrings(left.name, right.name);
199
356
  }
200
357
  return leftKind === "ModelValue" ? -1 : 1;
201
358
  }
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;
359
+ if (left === ANY_SET || right === ANY_SET) return compareAny(left === ANY_SET ? right : left);
360
+ if (isSetValue(left) && isSetValue(right) && isOverriddenComparison(left, right)) {
361
+ return compareOverriddenSets(left, right, "compare");
211
362
  }
212
- const leftIsSet = isSetValue(left);
213
- if (leftIsSet !== isSetValue(right)) {
363
+ const isLeftSet = isSetValue(left);
364
+ if (isLeftSet !== isSetValue(right)) {
214
365
  throw new TlaEvaluationError(
215
366
  `Attempted to compare ${describeKind(left)} ${formatValue(left)} with ${describeKind(right)} ${formatValue(right)}`,
216
367
  );
217
368
  }
218
- if (leftIsSet) return compareSets(toFiniteSet(left), toFiniteSet(right));
369
+ if (isLeftSet) return compareSets(toFiniteSet(left), toFiniteSet(right));
219
370
  return compareFunctions(toFiniteFunction(left), toFiniteFunction(right));
220
371
  }
221
372
  if (typeof left === "number" && typeof right === "number") return left - right;
222
- if (typeof left === "string" && typeof right === "string") return left < right ? -1 : 1;
373
+ if (typeof left === "string" && typeof right === "string") return compareStrings(left, right);
223
374
  if (typeof left === "boolean" && typeof right === "boolean") return left < right ? -1 : 1;
224
375
  checkModelValueTypes(left, right, false);
225
376
  if (isModelValue(left)) return -1;
@@ -232,6 +383,7 @@ export const compareValues = (left: Value, right: Value): number => {
232
383
  const modelValues = new Map<string, ModelValue>();
233
384
 
234
385
  export const createModelValue = (name: string): ModelValue => {
386
+ rankString(name);
235
387
  const known = modelValues.get(name);
236
388
  if (known) return known;
237
389
  const value: ModelValue = { kind: "ModelValue", name };
@@ -252,10 +404,14 @@ export const createSortedFunction = (domain: Value[], range: Value[]): FunctionV
252
404
  range,
253
405
  hash: 0,
254
406
  fingerprint: 0,
407
+ writtenKeys: undefined,
255
408
  });
256
409
 
257
410
  export const valuesEqual = (left: Value, right: Value): boolean => {
258
- if (left === right) return true;
411
+ if (left === right) {
412
+ if (writtenOrderUse.isUsed) forgetWrittenOrder(left);
413
+ return left !== ANY_SET || compareAny(left);
414
+ }
259
415
  if (typeof left !== "object" || typeof right !== "object") {
260
416
  if (typeof left === typeof right) return false;
261
417
  if (typeof left === "object" || typeof right === "object") {
@@ -276,9 +432,52 @@ export const valuesEqual = (left: Value, right: Value): boolean => {
276
432
  checkModelValueTypes(left, right, true);
277
433
  return isModelValue(left) && isModelValue(right) && left.name === right.name;
278
434
  }
435
+ if (left.kind === "Subset" && right.kind === "Subset") return valuesEqual(left.base, right.base);
436
+ if (left.kind === "FunctionSet" && right.kind === "FunctionSet") {
437
+ const isUnit = isEmptySet(left.domain);
438
+ const isOtherUnit = isEmptySet(right.domain);
439
+ if (isUnit || isOtherUnit) return isUnit === isOtherUnit;
440
+ const isEmpty = isEmptySet(left.range);
441
+ const isOtherEmpty = isEmptySet(right.range);
442
+ if (isEmpty || isOtherEmpty) return isEmpty === isOtherEmpty;
443
+ return valuesEqual(left.domain, right.domain) && valuesEqual(left.range, right.range);
444
+ }
445
+ if (left.kind === "RecordSet" && right.kind === "RecordSet") {
446
+ if (isEmptySet(left)) return isEmptySet(right);
447
+ if (isEmptySet(right)) return false;
448
+ const getFields = (set: RecordSetValue) =>
449
+ set.names
450
+ .map((name, index): [string, Value] => [name, set.sets[index]])
451
+ .sort((first, second) => (first[0] < second[0] ? -1 : 1));
452
+ const otherFields = getFields(right);
453
+ return (
454
+ left.names.length === right.names.length &&
455
+ getFields(left).every(
456
+ (field, index) =>
457
+ field[0] === otherFields[index][0] && valuesEqual(field[1], otherFields[index][1]),
458
+ )
459
+ );
460
+ }
461
+ if (left.kind === "ProductSet" && right.kind === "ProductSet") {
462
+ if (isEmptySet(left)) return isEmptySet(right);
463
+ if (isEmptySet(right)) return false;
464
+ return (
465
+ left.factors.length === right.factors.length &&
466
+ left.factors.every((factor, index) => valuesEqual(factor, right.factors[index]))
467
+ );
468
+ }
279
469
  const isInfiniteComparison =
280
470
  isSetValue(left) && isSetValue(right) && !(isEnumerable(left) && isEnumerable(right));
281
- if (isInfiniteComparison) return getValueKey(left) === getValueKey(right);
471
+ if (isInfiniteComparison && isOverriddenComparison(left, right)) {
472
+ return compareOverriddenSets(left, right, "check equality of") === 0;
473
+ }
474
+ if (isInfiniteComparison) {
475
+ if (!isOverriddenSet(left)) {
476
+ const elements = toFiniteSet(left);
477
+ if (!isOverriddenSet(right)) return compareSets(elements, toFiniteSet(right)) === 0;
478
+ }
479
+ return getValueKey(left) === getValueKey(right);
480
+ }
282
481
  return compareValues(left, right) === 0;
283
482
  };
284
483
 
@@ -304,6 +503,357 @@ export const isSameValue = (left: Value, right: Value): boolean => {
304
503
  return getValueKey(left) === getValueKey(right);
305
504
  };
306
505
 
506
+ // The comparisons above fail with their own messages. These follow the compareTo and equals
507
+ // methods of TLC's Java value classes, which word a failure by the class of the left operand, so a
508
+ // failed comparison runs again through them to fail as TLC does.
509
+ interface TlcFunction {
510
+ domain: Value[];
511
+ range: Value[];
512
+ low: number | undefined;
513
+ }
514
+
515
+ const getIntervalLow = (domain: Value[]) => {
516
+ const first = domain.length === 0 ? 1 : domain[0];
517
+ return typeof first === "number" && domain.every((key, index) => key === first + index)
518
+ ? first
519
+ : undefined;
520
+ };
521
+
522
+ const toTlcFunction = (value: Value) => {
523
+ if (!isFunctionValue(value)) return undefined;
524
+ const functionValue = toFiniteFunction(value);
525
+ return {
526
+ domain: functionValue.domain,
527
+ range: functionValue.range,
528
+ low: getIntervalLow(functionValue.domain),
529
+ };
530
+ };
531
+
532
+ const toTlcRecord = (value: Value) => {
533
+ if (!isFunctionValue(value)) return undefined;
534
+ const functionValue = toFiniteFunction(value);
535
+ return functionValue.domain.every((key) => typeof key === "string") ? functionValue : undefined;
536
+ };
537
+
538
+ const compareModelValueWith = (modelValue: ModelValue, other: Value) => {
539
+ if (getModelValueType(modelValue) !== undefined) {
540
+ throw new TlaEvaluationError(
541
+ `Attempted to compare the typed model value ${formatValue(modelValue)} and the non-model value\n${formatValue(other)}`,
542
+ );
543
+ }
544
+ return 1;
545
+ };
546
+
547
+ const equalsModelValueWith = (modelValue: ModelValue, other: Value) => {
548
+ if (getModelValueType(modelValue) !== undefined) {
549
+ throw new TlaEvaluationError(
550
+ `Attempted to check equality of the typed model value ${formatValue(modelValue)} and the non-model value\n${formatValue(other)}`,
551
+ );
552
+ }
553
+ return false;
554
+ };
555
+
556
+ const compareModelValue = (left: ModelValue, right: Value) => {
557
+ const type = getModelValueType(left);
558
+ if (type === undefined) return isModelValue(right) ? compareStrings(left.name, right.name) : -1;
559
+ if (!isModelValue(right)) {
560
+ throw new TlaEvaluationError(
561
+ `Attempted to compare the typed model value ${formatValue(left)} and non-model value\n${formatValue(right)}`,
562
+ );
563
+ }
564
+ const otherType = getModelValueType(right);
565
+ if (otherType !== undefined && otherType !== type) {
566
+ throw new TlaEvaluationError(
567
+ `Attempted to compare the differently-typed model values ${formatValue(left)} and ${formatValue(right)}`,
568
+ );
569
+ }
570
+ return compareStrings(left.name, right.name);
571
+ };
572
+
573
+ const equalsModelValue = (left: ModelValue, right: Value) => {
574
+ const type = getModelValueType(left);
575
+ if (type === undefined) return isModelValue(right) && left.name === right.name;
576
+ if (!isModelValue(right)) {
577
+ throw new TlaEvaluationError(
578
+ `Attempted to check equality of typed model value ${formatValue(left)} and non-model value\n${formatValue(right)}`,
579
+ );
580
+ }
581
+ const otherType = getModelValueType(right);
582
+ if (otherType !== undefined && otherType !== type) {
583
+ throw new TlaEvaluationError(
584
+ `Attempted to check equality of the differently-typed model values ${formatValue(left)} and ${formatValue(right)}`,
585
+ );
586
+ }
587
+ return left.name === right.name;
588
+ };
589
+
590
+ const SCALAR_KINDS = {
591
+ number: { name: "integer", other: "non-integer", equality: "check equality of" },
592
+ string: { name: "string", other: "non-string", equality: "check equality of" },
593
+ boolean: { name: "boolean", other: "non-boolean", equality: "compare equality of" },
594
+ };
595
+
596
+ const describeScalarMismatch = (
597
+ left: number | string | boolean,
598
+ right: Value,
599
+ isEquality: boolean,
600
+ ) => {
601
+ const kind =
602
+ SCALAR_KINDS[
603
+ typeof left === "number" ? "number" : typeof left === "string" ? "string" : "boolean"
604
+ ];
605
+ return `Attempted to ${isEquality ? kind.equality : "compare"} ${kind.name} ${formatValue(left)} with ${kind.other}:\n${formatValue(right)}`;
606
+ };
607
+
608
+ const compareTlcFunctions = (functionValue: TlcFunction, left: Value, right: Value) => {
609
+ const other = toTlcFunction(right);
610
+ if (!other) {
611
+ if (isModelValue(right)) return compareModelValueWith(right, left);
612
+ throw new TlaEvaluationError(
613
+ `Attempted to compare the function ${formatValue(left)} with the value:\n${formatValue(right)}`,
614
+ );
615
+ }
616
+ if (functionValue.range.length !== other.range.length)
617
+ return functionValue.range.length - other.range.length;
618
+ const compareRanges = () => compareElementsLikeTlc(functionValue.range, other.range);
619
+ if (functionValue.low !== undefined && functionValue.range.length === 0) return 0;
620
+ if (functionValue.low !== undefined && other.low !== undefined) {
621
+ return functionValue.low === other.low ? compareRanges() : functionValue.low - other.low;
622
+ }
623
+ const intervalLow = functionValue.low ?? other.low;
624
+ if (intervalLow === undefined) {
625
+ return compareElementsLikeTlc(functionValue.domain, other.domain) || compareRanges();
626
+ }
627
+ const keys = functionValue.low === undefined ? functionValue.domain : other.domain;
628
+ for (let index = 0; index < keys.length; index++) {
629
+ const key = keys[index];
630
+ if (typeof key !== "number") {
631
+ throw new TlaEvaluationError(
632
+ `Attempted to compare integer with non-integer${functionValue.low === undefined ? "" : ":"}\n${formatValue(key)}.`,
633
+ );
634
+ }
635
+ const difference =
636
+ functionValue.low === undefined ? key - (intervalLow + index) : intervalLow + index - key;
637
+ if (difference !== 0) return difference;
638
+ }
639
+ return compareRanges();
640
+ };
641
+
642
+ const compareElementsLikeTlc = (left: Value[], right: Value[]) => {
643
+ for (let index = 0; index < left.length; index++) {
644
+ const comparison = compareLikeTlc(left[index], right[index]);
645
+ if (comparison !== 0) return comparison;
646
+ }
647
+ return 0;
648
+ };
649
+
650
+ const compareFunctionLikeTlc = (left: FunctionValue | LazyFunctionValue, right: Value) => {
651
+ const functionValue = toFiniteFunction(left);
652
+ const isTuple = left.kind === "Function" && getSequenceElements(functionValue) !== undefined;
653
+ if (isTuple) {
654
+ const tuple = isFunctionValue(right) ? getSequenceElements(right) : undefined;
655
+ if (!tuple) return compareTlcFunctions({ ...functionValue, low: 1 }, left, right);
656
+ return (
657
+ functionValue.range.length - tuple.length ||
658
+ compareElementsLikeTlc(functionValue.range, tuple)
659
+ );
660
+ }
661
+ if (left.kind === "Function" && functionValue.domain.every((key) => typeof key === "string")) {
662
+ const record = toTlcRecord(right);
663
+ if (!record) {
664
+ if (isModelValue(right)) return compareModelValueWith(right, left);
665
+ throw new TlaEvaluationError(
666
+ `Attempted to compare record:\n${formatValue(left)}\nwith non-record\n${formatValue(right)}`,
667
+ );
668
+ }
669
+ return (
670
+ functionValue.domain.length - record.domain.length ||
671
+ compareElementsLikeTlc(functionValue.domain, record.domain) ||
672
+ compareElementsLikeTlc(functionValue.range, record.range)
673
+ );
674
+ }
675
+ return compareTlcFunctions(
676
+ { ...functionValue, low: getIntervalLow(functionValue.domain) },
677
+ left,
678
+ right,
679
+ );
680
+ };
681
+
682
+ export const isEnumeratedSet = (value: Value): value is SetValue | MixedSetValue =>
683
+ typeof value === "object" && (value.kind === "Set" || value.kind === "MixedSet");
684
+
685
+ // TLC compares a lazy set, or a set it overrides in Java, through its own class before it compares
686
+ // elements. The comparisons above already fail as those classes do.
687
+ const isComparedByClass = (left: ObjectValue, right: Value) =>
688
+ isOverriddenSet(left) ||
689
+ isOverriddenSet(right) ||
690
+ (typeof right === "object" &&
691
+ left.kind === right.kind &&
692
+ (left.kind === "Interval" ||
693
+ left.kind === "Subset" ||
694
+ left.kind === "FunctionSet" ||
695
+ left.kind === "RecordSet" ||
696
+ left.kind === "ProductSet"));
697
+
698
+ const getSetElementsLikeTlc = (left: SetValue | MixedSetValue, right: Value) => {
699
+ const other = isEnumeratedSet(right) ? right : toFiniteSet(right);
700
+ return { elements: toFiniteSet(left).elements, otherElements: toFiniteSet(other).elements };
701
+ };
702
+
703
+ const compareSetLikeTlc = (left: ObjectValue, right: Value): number => {
704
+ if (isComparedByClass(left, right)) return compareValues(left, right);
705
+ if (!isEnumeratedSet(left)) return compareSetLikeTlc(toFiniteSet(left), right);
706
+ if (!isSetValue(right)) {
707
+ if (isModelValue(right)) return compareModelValueWith(right, left);
708
+ throw new TlaEvaluationError(
709
+ `Attempted to compare the set ${formatValue(left)} with the value:\n${formatValue(right)}`,
710
+ );
711
+ }
712
+ const sets = getSetElementsLikeTlc(left, right);
713
+ return (
714
+ sets.elements.length - sets.otherElements.length ||
715
+ compareElementsLikeTlc(sets.elements, sets.otherElements)
716
+ );
717
+ };
718
+
719
+ const compareLikeTlc = (left: Value, right: Value): number => {
720
+ if (typeof left !== "object") {
721
+ if (typeof right === typeof left) return compareValues(left, right);
722
+ if (isModelValue(right)) return compareModelValueWith(right, left);
723
+ throw new TlaEvaluationError(describeScalarMismatch(left, right, false));
724
+ }
725
+ if (left.kind === "ModelValue") return compareModelValue(left, right);
726
+ if (isFunctionValue(left)) return compareFunctionLikeTlc(left, right);
727
+ return compareSetLikeTlc(left, right);
728
+ };
729
+
730
+ const equalsElementsLikeTlc = (left: Value[], right: Value[]) =>
731
+ left.every((element, index) => equalsLikeTlc(element, right[index]));
732
+
733
+ const equalsTlcFunctions = (functionValue: TlcFunction, left: Value, right: Value) => {
734
+ const other = toTlcFunction(right);
735
+ if (!other) {
736
+ if (isModelValue(right)) return equalsModelValueWith(right, left);
737
+ throw new TlaEvaluationError(
738
+ `Attempted to check equality of the function ${formatValue(left)} with the value:\n${formatValue(right)}`,
739
+ );
740
+ }
741
+ if (functionValue.range.length !== other.range.length) return false;
742
+ if (functionValue.low !== undefined && other.low !== undefined) {
743
+ return (
744
+ (functionValue.range.length === 0 || functionValue.low === other.low) &&
745
+ equalsElementsLikeTlc(functionValue.range, other.range)
746
+ );
747
+ }
748
+ const intervalLow = functionValue.low ?? other.low;
749
+ if (intervalLow === undefined) {
750
+ return (
751
+ equalsElementsLikeTlc(functionValue.domain, other.domain) &&
752
+ equalsElementsLikeTlc(functionValue.range, other.range)
753
+ );
754
+ }
755
+ const keys = functionValue.low === undefined ? functionValue.domain : other.domain;
756
+ return (
757
+ keys.every((key, index) => {
758
+ if (typeof key !== "number") {
759
+ throw new TlaEvaluationError(
760
+ `Attempted to compare an integer with non-integer:\n${formatValue(key)}.`,
761
+ );
762
+ }
763
+ return key === intervalLow + index;
764
+ }) && equalsElementsLikeTlc(functionValue.range, other.range)
765
+ );
766
+ };
767
+
768
+ const equalsFunctionLikeTlc = (left: FunctionValue | LazyFunctionValue, right: Value) => {
769
+ const functionValue = toFiniteFunction(left);
770
+ const isTuple = left.kind === "Function" && getSequenceElements(functionValue) !== undefined;
771
+ if (isTuple) {
772
+ const tuple = isFunctionValue(right) ? getSequenceElements(right) : undefined;
773
+ if (!tuple) return equalsTlcFunctions({ ...functionValue, low: 1 }, left, right);
774
+ return (
775
+ functionValue.range.length === tuple.length &&
776
+ equalsElementsLikeTlc(functionValue.range, tuple)
777
+ );
778
+ }
779
+ if (left.kind === "Function" && functionValue.domain.every((key) => typeof key === "string")) {
780
+ const record = toTlcRecord(right);
781
+ if (!record) {
782
+ if (isModelValue(right)) return equalsModelValueWith(right, left);
783
+ throw new TlaEvaluationError(
784
+ `Attempted to check equality of record:\n${formatValue(left)}\nwith non-record\n${formatValue(right)}`,
785
+ );
786
+ }
787
+ return (
788
+ functionValue.domain.length === record.domain.length &&
789
+ equalsElementsLikeTlc(functionValue.domain, record.domain) &&
790
+ equalsElementsLikeTlc(functionValue.range, record.range)
791
+ );
792
+ }
793
+ return equalsTlcFunctions(
794
+ { ...functionValue, low: getIntervalLow(functionValue.domain) },
795
+ left,
796
+ right,
797
+ );
798
+ };
799
+
800
+ const equalsSetLikeTlc = (left: ObjectValue, right: Value): boolean => {
801
+ if (isComparedByClass(left, right)) return valuesEqual(left, right);
802
+ if (!isEnumeratedSet(left)) return equalsSetLikeTlc(toFiniteSet(left), right);
803
+ if (!isSetValue(right)) {
804
+ if (isModelValue(right)) return equalsModelValueWith(right, left);
805
+ throw new TlaEvaluationError(
806
+ `Attempted to check equality of the set ${formatValue(left)} with the value:\n${formatValue(right)}`,
807
+ );
808
+ }
809
+ const sets = getSetElementsLikeTlc(left, right);
810
+ return (
811
+ sets.elements.length === sets.otherElements.length &&
812
+ equalsElementsLikeTlc(sets.elements, sets.otherElements)
813
+ );
814
+ };
815
+
816
+ const equalsLikeTlc = (left: Value, right: Value): boolean => {
817
+ if (typeof left !== "object") {
818
+ if (typeof right === typeof left) return left === right;
819
+ if (isModelValue(right)) return equalsModelValueWith(right, left);
820
+ throw new TlaEvaluationError(describeScalarMismatch(left, right, true));
821
+ }
822
+ if (left.kind === "ModelValue") return equalsModelValue(left, right);
823
+ if (isFunctionValue(left)) return equalsFunctionLikeTlc(left, right);
824
+ return equalsSetLikeTlc(left, right);
825
+ };
826
+
827
+ // TLC's ValueVec.sort, which inserts each element into the sorted prefix by binary search.
828
+ const sortLikeTlc = (elements: Value[]) => {
829
+ const sorted: Value[] = [];
830
+ for (const element of elements) {
831
+ const bounds = { low: 0, high: sorted.length, comparison: 1 };
832
+ while (bounds.low < bounds.high) {
833
+ const middle = bounds.low + ((bounds.high - bounds.low) >> 1);
834
+ bounds.comparison = compareLikeTlc(element, sorted[middle]);
835
+ if (bounds.comparison === 0) break;
836
+ if (bounds.comparison < 0) bounds.high = middle;
837
+ else bounds.low = middle + 1;
838
+ }
839
+ if (bounds.comparison !== 0) sorted.splice(bounds.low, 0, element);
840
+ }
841
+ return sorted;
842
+ };
843
+
844
+ // TLC checks membership in a set enumeration it hasn't sorted element by element.
845
+ const isElementOfLikeTlc = (element: Value, elements: Value[]) =>
846
+ elements.some((member) => equalsLikeTlc(member, element));
847
+
848
+ export const isEqualLikeTlc = (left: Value, right: Value) => {
849
+ try {
850
+ return valuesEqual(left, right);
851
+ } catch (error) {
852
+ if (!(error instanceof TlaEvaluationError)) throw error;
853
+ return equalsLikeTlc(left, right);
854
+ }
855
+ };
856
+
307
857
  const isStrictlySorted = (elements: Value[]) => {
308
858
  for (let index = 1; index < elements.length; index++) {
309
859
  if (compareValues(elements[index - 1], elements[index]) >= 0) return false;
@@ -321,6 +871,17 @@ export const createSet = (elements: Value[]): SetValue => {
321
871
  return createSortedSet(unique);
322
872
  };
323
873
 
874
+ // TLC builds a set enumeration without sorting it, so one whose elements it can't compare still
875
+ // evaluates.
876
+ export const createEnumeratedSet = (elements: Value[]): SetValue | MixedSetValue => {
877
+ try {
878
+ return createSet(elements);
879
+ } catch (error) {
880
+ if (!(error instanceof TlaEvaluationError)) throw error;
881
+ return { kind: "MixedSet", elements };
882
+ }
883
+ };
884
+
324
885
  export const EMPTY_SET = createSortedSet([]);
325
886
 
326
887
  export const differenceSorted = (left: SetValue, right: SetValue): SetValue => {
@@ -428,18 +989,99 @@ export const createFunction = (pairs: [Value, Value][]): FunctionValue => {
428
989
  return createSortedFunction(domain, range);
429
990
  };
430
991
 
992
+ export const createWrittenFunction = (pairs: [Value, Value][]) => {
993
+ const functionValue = createFunction(pairs);
994
+ const isWrittenSorted =
995
+ functionValue.domain.length !== pairs.length ||
996
+ pairs.some((pair) => typeof pair[0] !== "string") ||
997
+ pairs.every((pair, index) => pair[0] === functionValue.domain[index]);
998
+ return isWrittenSorted
999
+ ? functionValue
1000
+ : setWrittenOrder(
1001
+ functionValue,
1002
+ pairs.map((pair) => pair[0]),
1003
+ );
1004
+ };
1005
+
1006
+ const intervalDomains: Value[][] = [];
1007
+ const intervalValues: IntervalValue[] = [];
1008
+ const intervalArrays = new WeakMap<Value[], IntervalValue>();
1009
+
1010
+ // TLC keeps the domain of a tuple, or of a function built over an interval, as an interval. Every
1011
+ // function over 1..n shares one domain array per length, and the elements of any other interval
1012
+ // map to it, so the array's identity records it. Callers must not mutate the returned array.
1013
+ const getIntervalDomain = (length: number): Value[] => {
1014
+ const cached = intervalDomains[length];
1015
+ if (cached) return cached;
1016
+ const domain = Array.from({ length }, (_, index) => index + 1);
1017
+ intervalDomains[length] = domain;
1018
+ return domain;
1019
+ };
1020
+
1021
+ // The elements of an interval for a caller that only iterates them, so they need not record
1022
+ // the interval for DOMAIN.
1023
+ export const getIntervalElements = (value: IntervalValue): Value[] => {
1024
+ getCardinality(value);
1025
+ if (value.low === 1 && value.high >= 1) return getIntervalDomain(value.high);
1026
+ const elements: number[] = [];
1027
+ for (let element = value.low; element <= value.high; element++) elements.push(element);
1028
+ return elements;
1029
+ };
1030
+
1031
+ const isIntervalDomain = (domain: Value[]) =>
1032
+ domain.length > 0 && (domain === intervalDomains[domain.length] || intervalArrays.has(domain));
1033
+
1034
+ // TLC keeps an interval domain only for a function built over an interval itself, so a set
1035
+ // such as (1..2) \cap S, whose elements are the shared array, gets a copy.
1036
+ export const getFunctionDomain = (set: Value): Value[] => {
1037
+ const elements = toFiniteSet(set).elements;
1038
+ return isIntervalDomain(elements) && !(typeof set === "object" && set.kind === "Interval")
1039
+ ? [...elements]
1040
+ : elements;
1041
+ };
1042
+
1043
+ const getIntervalValue = (high: number): IntervalValue => {
1044
+ const cached = intervalValues[high];
1045
+ if (cached) return cached;
1046
+ const interval: IntervalValue = { kind: "Interval", low: 1, high };
1047
+ intervalValues[high] = interval;
1048
+ return interval;
1049
+ };
1050
+
431
1051
  export const createTuple = (elements: Value[]): FunctionValue =>
432
- createSortedFunction(
433
- elements.map((_, index) => index + 1),
434
- elements,
435
- );
1052
+ createSortedFunction(getIntervalDomain(elements.length), elements);
436
1053
 
437
1054
  export const createRecord = (fields: [string, Value][]): FunctionValue => createFunction(fields);
438
1055
 
1056
+ // TLC's Enumerable classes: every set except the ones it implements in Java.
1057
+ const isTlcEnumerable = (value: Value) => isSetValue(value) && !isOverriddenSet(value);
1058
+
1059
+ const formatUnion = (value: UnionValue, isStrict = false) =>
1060
+ `UNION(${formatValue(value.base, isStrict)})`;
1061
+
1062
+ const getUnionSets = (value: UnionValue) => {
1063
+ if (!isTlcEnumerable(value.base)) {
1064
+ throw new TlaEvaluationError(
1065
+ `Attempted to enumerate the nonenumerable set:\n${formatUnion(value)}`,
1066
+ );
1067
+ }
1068
+ return toFiniteSet(value.base).elements;
1069
+ };
1070
+
1071
+ const getUnionSet = (set: Value, index: number, union: UnionValue) => {
1072
+ if (isTlcEnumerable(set)) return set;
1073
+ throw new TlaEvaluationError(
1074
+ index === 0
1075
+ ? "Attempted to enumerate UNION(s), but some element of s is nonenumerable."
1076
+ : `Attempted to enumerate the nonenumerable set:\n${formatValue(set)}\nwhen enumerating:\n${formatUnion(union)}`,
1077
+ );
1078
+ };
1079
+
439
1080
  export const isEnumerable = (value: Value): boolean => {
440
1081
  if (typeof value !== "object") return false;
441
1082
  switch (value.kind) {
442
1083
  case "Set":
1084
+ case "MixedSet":
443
1085
  case "Interval":
444
1086
  return true;
445
1087
  case "InfiniteSet":
@@ -449,11 +1091,17 @@ export const isEnumerable = (value: Value): boolean => {
449
1091
  case "FilteredSet":
450
1092
  return isEnumerable(value.base);
451
1093
  case "FunctionSet":
452
- return isEnumerable(value.domain) && isEnumerable(value.range);
1094
+ return (
1095
+ (isEnumerable(value.domain) && isEnumerable(value.range)) ||
1096
+ getEmptiness(value.domain) === true ||
1097
+ getEmptiness(value) === true
1098
+ );
453
1099
  case "RecordSet":
454
- return value.sets.every(isEnumerable);
1100
+ return value.sets.every(isEnumerable) || getEmptiness(value) === true;
455
1101
  case "ProductSet":
456
- return value.factors.every(isEnumerable);
1102
+ return value.factors.every(isEnumerable) || getEmptiness(value) === true;
1103
+ case "Union":
1104
+ return isEnumerable(value.base) && toFiniteSet(value.base).elements.every(isEnumerable);
457
1105
  case "SetOperation":
458
1106
  if (value.operator === "\\cup") return isEnumerable(value.left) && isEnumerable(value.right);
459
1107
  if (value.operator === "\\cap") return isEnumerable(value.left) || isEnumerable(value.right);
@@ -463,7 +1111,42 @@ export const isEnumerable = (value: Value): boolean => {
463
1111
  }
464
1112
  };
465
1113
 
466
- const getCartesianProduct = (lists: Value[][]): Value[][] => {
1114
+ const getEmptiness = (value: Value): boolean | undefined => {
1115
+ if (typeof value !== "object") return undefined;
1116
+ switch (value.kind) {
1117
+ case "Set":
1118
+ case "MixedSet":
1119
+ return value.elements.length === 0;
1120
+ case "Interval":
1121
+ return value.low > value.high;
1122
+ case "InfiniteSet":
1123
+ return value.name === "Any" ? undefined : false;
1124
+ case "Subset":
1125
+ case "SequenceSet":
1126
+ return false;
1127
+ case "FunctionSet": {
1128
+ const isUnit = getEmptiness(value.domain);
1129
+ if (isUnit === undefined) return undefined;
1130
+ return isUnit ? false : getEmptiness(value.range);
1131
+ }
1132
+ case "RecordSet":
1133
+ case "ProductSet": {
1134
+ for (const part of value.kind === "RecordSet" ? value.sets : value.factors) {
1135
+ const isEmpty = getEmptiness(part);
1136
+ if (isEmpty !== false) return isEmpty;
1137
+ }
1138
+ return false;
1139
+ }
1140
+ case "SetOperation":
1141
+ case "FilteredSet":
1142
+ case "Union":
1143
+ return isEnumerable(value) ? toFiniteSet(value).elements.length === 0 : undefined;
1144
+ default:
1145
+ return undefined;
1146
+ }
1147
+ };
1148
+
1149
+ const getCartesianProduct = (lists: Value[][]) => {
467
1150
  const total = lists.reduce((product, list) => product * list.length, 1);
468
1151
  const combinations: Value[][] = [];
469
1152
  const positions = lists.map(() => 0);
@@ -491,7 +1174,7 @@ const advanceCombination = (indices: number[], length: number) => {
491
1174
  return false;
492
1175
  };
493
1176
 
494
- const getSortedSubsets = (base: Value[]): Value[] => {
1177
+ const getSortedSubsets = (base: Value[]) => {
495
1178
  const subsets: Value[] = [];
496
1179
  for (let size = 0; size <= base.length; size++) {
497
1180
  const indices = Array.from({ length: size }, (_, index) => index);
@@ -508,12 +1191,15 @@ const enumerateSet = (value: Value): Value[] => {
508
1191
  switch (value.kind) {
509
1192
  case "Set":
510
1193
  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
- );
1194
+ case "MixedSet":
1195
+ return sortLikeTlc(value.elements);
1196
+ case "Interval": {
1197
+ const elements = getIntervalElements(value);
1198
+ if (elements !== intervalDomains[elements.length]) intervalArrays.set(elements, value);
1199
+ return elements;
1200
+ }
516
1201
  case "Subset": {
1202
+ if (isOverriddenSet(value.base)) getCardinality(value.base);
517
1203
  const base = toFiniteSet(value.base).elements;
518
1204
  if (base.length > MAXIMUM_ENUMERATED_SUBSET_BASE) {
519
1205
  throw new TlaEvaluationError(
@@ -523,29 +1209,83 @@ const enumerateSet = (value: Value): Value[] => {
523
1209
  return getSortedSubsets(base);
524
1210
  }
525
1211
  case "FunctionSet": {
526
- const domain = toFiniteSet(value.domain).elements;
527
- const range = toFiniteSet(value.range).elements;
1212
+ if (isEmptySet(value)) return [];
1213
+ if (isOverriddenSet(value.domain)) {
1214
+ throw new TlaEvaluationError(
1215
+ `Attempted to enumerate a set of the form [D -> R],but the domain D:\n${formatValue(value.domain)}\ncannot be enumerated.`,
1216
+ );
1217
+ }
1218
+ const domain = getFunctionDomain(value.domain);
1219
+ if (domain.length > 0 && isOverriddenSet(value.range)) {
1220
+ throw new TlaEvaluationError(
1221
+ `Attempted to enumerate a set of the form [D -> R],but the range R:\n${formatValue(value.range)}\ncannot be enumerated.`,
1222
+ );
1223
+ }
1224
+ const range = domain.length === 0 ? [] : toFiniteSet(value.range).elements;
528
1225
  return getCartesianProduct(domain.map(() => range)).map((combination): Value =>
529
1226
  createSortedFunction(domain, combination),
530
1227
  );
531
1228
  }
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,
1229
+ case "RecordSet": {
1230
+ if (isEmptySet(value)) return [];
1231
+ const fields = value.sets.map((set, index) => {
1232
+ if (isOverriddenSet(set)) {
1233
+ throw new TlaEvaluationError(
1234
+ `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)}`,
1235
+ );
1236
+ }
1237
+ return toFiniteSet(set).elements;
1238
+ });
1239
+ return getCartesianProduct(fields).map((combination) =>
1240
+ createRecord(value.names.map((name, index): [string, Value] => [name, combination[index]])),
542
1241
  );
1242
+ }
1243
+ case "ProductSet": {
1244
+ if (isEmptySet(value)) return [];
1245
+ const factors = value.factors.map((factor, index) => {
1246
+ if (isOverriddenSet(factor)) {
1247
+ throw new TlaEvaluationError(
1248
+ `Attempted to enumerate a set of the form s1 \\X s2 ... \\X sn,\nbut can't enumerate s${index}:\n${formatValue(factor)}`,
1249
+ );
1250
+ }
1251
+ return toFiniteSet(factor).elements;
1252
+ });
1253
+ return getCartesianProduct(factors).map(createTuple);
1254
+ }
543
1255
  case "FilteredSet":
1256
+ if (isOverriddenSet(value.base)) {
1257
+ throw new TlaEvaluationError(
1258
+ `Attempted to enumerate { x \\in S : p(x) } when S:\n${formatValue(value.base)}\nis not enumerable`,
1259
+ );
1260
+ }
544
1261
  return toFiniteSet(value.base).elements.filter(value.test);
1262
+ case "Union": {
1263
+ const elements = getUnionSets(value).flatMap(
1264
+ (set, index) => toFiniteSet(getUnionSet(set, index, value)).elements,
1265
+ );
1266
+ return toFiniteSet(createEnumeratedSet(elements)).elements;
1267
+ }
545
1268
  case "SetOperation": {
546
- if (!isEnumerable(value)) {
1269
+ if (
1270
+ value.operator === "\\cup" &&
1271
+ (isOverriddenSet(value.left) || isOverriddenSet(value.right))
1272
+ ) {
1273
+ throw new TlaEvaluationError(
1274
+ `Attempted to enumerate S \\cup T when S:\n${formatValue(value.left)}\nand T:\n${formatValue(value.right)}\nare not both enumerable`,
1275
+ );
1276
+ }
1277
+ if (
1278
+ value.operator === "\\cap" &&
1279
+ isOverriddenSet(value.left) &&
1280
+ isOverriddenSet(value.right)
1281
+ ) {
547
1282
  throw new TlaEvaluationError(
548
- `Attempted to enumerate the non-enumerable set ${formatValue(value)}`,
1283
+ `Attempted to enumerate S \\cap T when neither S:\n${formatValue(value.left)}\nnor T:\n${formatValue(value.right)}\nis enumerable`,
1284
+ );
1285
+ }
1286
+ if (value.operator === "\\" && isOverriddenSet(value.left)) {
1287
+ throw new TlaEvaluationError(
1288
+ `Attempted to enumerate S \\ T when S:\n${formatValue(value.left)}\nis not enumerable.`,
549
1289
  );
550
1290
  }
551
1291
  if (value.operator === "\\cup")
@@ -571,12 +1311,16 @@ const finiteSets = new WeakMap<ObjectValue, SetValue>();
571
1311
 
572
1312
  export const toFiniteSet = (value: Value): SetValue => {
573
1313
  if (typeof value === "object" && value.kind === "Set") return value;
1314
+ if (typeof value === "object" && value.kind === "Interval") {
1315
+ return createSortedSet(enumerateSet(value));
1316
+ }
574
1317
  const cached = typeof value === "object" ? finiteSets.get(value) : undefined;
575
1318
  if (cached) return cached;
576
1319
  const elements = enumerateSet(value);
577
1320
  const isSorted =
578
1321
  typeof value === "object" &&
579
- (value.kind === "Interval" ||
1322
+ (value.kind === "MixedSet" ||
1323
+ value.kind === "Union" ||
580
1324
  value.kind === "SetOperation" ||
581
1325
  value.kind === "FilteredSet" ||
582
1326
  value.kind === "ProductSet" ||
@@ -587,23 +1331,48 @@ export const toFiniteSet = (value: Value): SetValue => {
587
1331
  return set;
588
1332
  };
589
1333
 
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)}`,
1334
+ // TLC's Enumerator for a lazy set streams the sets it combines until the set caches its elements,
1335
+ // so an element that two of them share comes up once for each.
1336
+ export const getStreamedElements = (value: Value): Value[] => {
1337
+ if (typeof value !== "object" || finiteSets.has(value)) return toFiniteSet(value).elements;
1338
+ if (value.kind === "Union") {
1339
+ return getUnionSets(value).flatMap((set, index) =>
1340
+ getStreamedElements(getUnionSet(set, index, value)),
594
1341
  );
595
1342
  }
596
- return cardinality;
1343
+ if (value.kind === "FilteredSet" && !isOverriddenSet(value.base)) {
1344
+ return getStreamedElements(value.base).filter(value.test);
1345
+ }
1346
+ if (value.kind !== "SetOperation") return toFiniteSet(value).elements;
1347
+ if (value.operator === "\\cup") {
1348
+ return isOverriddenSet(value.left) || isOverriddenSet(value.right)
1349
+ ? toFiniteSet(value).elements
1350
+ : [...getStreamedElements(value.left), ...getStreamedElements(value.right)];
1351
+ }
1352
+ const isLeftEnumerated = value.operator === "\\" || isEnumerable(value.left);
1353
+ const enumerated = isLeftEnumerated ? value.left : value.right;
1354
+ const other = isLeftEnumerated ? value.right : value.left;
1355
+ if (isOverriddenSet(enumerated)) return toFiniteSet(value).elements;
1356
+ return getStreamedElements(enumerated).filter(
1357
+ (element) => isElementOf(element, other) === (value.operator === "\\cap"),
1358
+ );
597
1359
  };
598
1360
 
599
1361
  export const isEmptySet = (value: Value): boolean => {
600
- if (typeof value !== "object") return toFiniteSet(value).elements.length === 0;
1362
+ if (typeof value !== "object" || !isSetValue(value)) {
1363
+ throw new TlaEvaluationError(`Shouldn't call isEmpty() on value ${formatValue(value)}`);
1364
+ }
601
1365
  switch (value.kind) {
602
1366
  case "Set":
1367
+ case "MixedSet":
603
1368
  return value.elements.length === 0;
604
1369
  case "Interval":
605
1370
  return value.low > value.high;
606
1371
  case "InfiniteSet":
1372
+ if (value.name === "Any") {
1373
+ throw new TlaEvaluationError("Shouldn't call isEmpty() on value ANY");
1374
+ }
1375
+ return false;
607
1376
  case "Subset":
608
1377
  case "SequenceSet":
609
1378
  return false;
@@ -612,41 +1381,143 @@ export const isEmptySet = (value: Value): boolean => {
612
1381
  case "RecordSet":
613
1382
  return value.sets.some(isEmptySet);
614
1383
  case "FunctionSet":
615
- return isEmptySet(value.range) && !isEmptySet(value.domain);
1384
+ return !isEmptySet(value.domain) && isEmptySet(value.range);
1385
+ case "Union":
1386
+ return getUnionSets(value).every((set, index) => isEmptySet(getUnionSet(set, index, value)));
616
1387
  default:
617
1388
  return toFiniteSet(value).elements.length === 0;
618
1389
  }
619
1390
  };
620
1391
 
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
- };
1392
+ const getProductCardinality = (sets: Value[], describe: () => string) =>
1393
+ sets.reduce<number>((product, set) => {
1394
+ const size = product * getCardinality(set);
1395
+ if (size > MAXIMUM_INTEGER) throw new TlaEvaluationError(describe());
1396
+ return size;
1397
+ }, 1);
1398
+
1399
+ const describeOverflow = (value: Value) =>
1400
+ `Overflow when computing the number of elements in:\n${formatValue(value)}`;
627
1401
 
628
1402
  export const getCardinality = (value: Value): number => {
629
1403
  if (typeof value !== "object") return toFiniteSet(value).elements.length;
630
1404
  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,
1405
+ case "Interval": {
1406
+ const size = Math.max(0, value.high - value.low + 1);
1407
+ if (size > MAXIMUM_INTEGER) {
1408
+ throw new TlaEvaluationError(
1409
+ `Size of interval value exceeds the maximum representable size (32bits): ${value.low}..${value.high}.`,
1410
+ );
1411
+ }
1412
+ return size;
1413
+ }
1414
+ case "InfiniteSet":
1415
+ case "SequenceSet":
1416
+ throw new TlaEvaluationError(
1417
+ `Attempted to compute the number of elements in the overridden value ${formatValue(value)}.`,
640
1418
  );
1419
+ case "Subset": {
1420
+ const size = getCardinality(value.base);
1421
+ if (size >= 31) throw new TlaEvaluationError(describeOverflow(value));
1422
+ return 2 ** size;
1423
+ }
1424
+ case "FunctionSet": {
1425
+ if (isEmptySet(value.domain)) return 1;
1426
+ if (isEmptySet(value.range)) return 0;
1427
+ const rangeSize = getCardinality(value.range);
1428
+ if (rangeSize === 1) return 1;
1429
+ const domainSize = getCardinality(value.domain);
1430
+ const size = { value: 1 };
1431
+ for (let index = 0; index < domainSize; index++) {
1432
+ size.value *= rangeSize;
1433
+ if (size.value > MAXIMUM_INTEGER) throw new TlaEvaluationError(describeOverflow(value));
1434
+ }
1435
+ return size.value;
1436
+ }
641
1437
  case "RecordSet":
642
- return getProductCardinality(value.sets, value);
1438
+ if (isEmptySet(value)) return 0;
1439
+ return getProductCardinality(value.sets, () => describeOverflow(value));
643
1440
  case "ProductSet":
644
- return getProductCardinality(value.factors, value);
1441
+ if (isEmptySet(value)) return 0;
1442
+ return getProductCardinality(
1443
+ value.factors,
1444
+ () => `Overflow when computing the number of elements in ${formatValue(value)}`,
1445
+ );
645
1446
  default:
646
1447
  return toFiniteSet(value).elements.length;
647
1448
  }
648
1449
  };
649
1450
 
1451
+ const describeInfiniteCheck = (value: Value) =>
1452
+ `Attempted to check if the set ${formatValue(value)}is finite.`;
1453
+
1454
+ export const isFiniteSet = (value: Value): boolean => {
1455
+ if (typeof value !== "object" || value.kind === "ModelValue") {
1456
+ throw new TlaEvaluationError(
1457
+ `Attempted to check if the ${describeKind(value)} ${formatValue(value)} is a finite set.`,
1458
+ );
1459
+ }
1460
+ switch (value.kind) {
1461
+ case "Set":
1462
+ case "MixedSet":
1463
+ case "Interval":
1464
+ case "Function":
1465
+ return true;
1466
+ case "LazyFunction":
1467
+ throw new TlaEvaluationError(
1468
+ `Attempted to check if the function:\n${formatValue(value)}\nis a finite set.`,
1469
+ );
1470
+ case "InfiniteSet":
1471
+ return false;
1472
+ case "SequenceSet":
1473
+ return isFiniteSet(value.base) && isEmptySet(value.base);
1474
+ case "Subset":
1475
+ return isFiniteSet(value.base);
1476
+ case "FunctionSet": {
1477
+ const isFiniteDomain = isFiniteSet(value.domain);
1478
+ if (isFiniteDomain && isEmptySet(value.domain)) return true;
1479
+ const isFiniteRange = isFiniteSet(value.range);
1480
+ if (isFiniteRange && isEmptySet(value.range)) return true;
1481
+ return isFiniteRange && (isFiniteDomain || getCardinality(value.range) === 1);
1482
+ }
1483
+ case "RecordSet":
1484
+ case "ProductSet": {
1485
+ const parts = value.kind === "RecordSet" ? value.sets : value.factors;
1486
+ const finiteness = { isAllFinite: true };
1487
+ for (const part of parts) {
1488
+ if (!isFiniteSet(part)) finiteness.isAllFinite = false;
1489
+ else if (isEmptySet(part)) return true;
1490
+ }
1491
+ return finiteness.isAllFinite;
1492
+ }
1493
+ case "Union":
1494
+ if (!isTlcEnumerable(value.base)) {
1495
+ throw new TlaEvaluationError(
1496
+ `Attempted to check if the nonenumerable set:\n${formatUnion(value)}\nis a finite set.`,
1497
+ );
1498
+ }
1499
+ return toFiniteSet(value.base).elements.every(isFiniteSet);
1500
+ case "SetOperation":
1501
+ if (value.operator === "\\cup") return isFiniteSet(value.left) && isFiniteSet(value.right);
1502
+ if (value.operator === "\\cap") {
1503
+ if (!isFiniteSet(value.left) && !isFiniteSet(value.right)) {
1504
+ throw new TlaEvaluationError(describeInfiniteCheck(value));
1505
+ }
1506
+ return true;
1507
+ }
1508
+ if (isFiniteSet(value.left)) return true;
1509
+ if (!isFiniteSet(value.right)) throw new TlaEvaluationError(describeInfiniteCheck(value));
1510
+ return false;
1511
+ case "FilteredSet":
1512
+ if (!isFiniteSet(value.base)) {
1513
+ throw new TlaEvaluationError(
1514
+ `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.`,
1515
+ );
1516
+ }
1517
+ return true;
1518
+ }
1519
+ };
1520
+
650
1521
  const findIndex = (sortedValues: Value[], target: Value) => {
651
1522
  const bounds = { low: 0, high: sortedValues.length - 1 };
652
1523
  while (bounds.low <= bounds.high) {
@@ -661,30 +1532,59 @@ const findIndex = (sortedValues: Value[], target: Value) => {
661
1532
 
662
1533
  export const getSequenceElements = (value: Value): Value[] | undefined => {
663
1534
  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;
1535
+ const functionValue = toFiniteFunction(value);
1536
+ return functionValue.domain.every((key, index) => key === index + 1)
1537
+ ? functionValue.range
1538
+ : undefined;
666
1539
  };
667
1540
 
668
1541
  const describeMembership = (element: Value, set: Value) =>
669
1542
  `Attempted to check if the value ${formatValue(element)} is an element of ${formatValue(set)}`;
670
1543
 
1544
+ const describeNonSetMembership = (element: Value, value: Value) => {
1545
+ const prefix = `Attempted to check if the value:\n${formatValue(element)}\nis an element of the`;
1546
+ if (!isFunctionValue(value)) return `${prefix} ${describeKind(value)} ${formatValue(value)}`;
1547
+ const domain =
1548
+ value.kind === "Function" || isEnumerable(value.domain) ? toFiniteFunction(value).domain : [];
1549
+ if (domain.length > 0 && domain.every((key) => typeof key === "string")) {
1550
+ return `Attempted to check if element:\n${formatValue(element)}\nis in the record:\n${formatValue(value)}`;
1551
+ }
1552
+ if (domain.every((key, index) => key === index + 1)) {
1553
+ return "Attempted to check set membership in a tuple value.";
1554
+ }
1555
+ return `${prefix} function ${formatValue(value)}`;
1556
+ };
1557
+
671
1558
  export const isElementOf = (element: Value, set: Value): boolean => {
672
- if (typeof set !== "object")
673
- throw new TlaEvaluationError(`Expected a set but found ${formatValue(set)}`);
1559
+ if (!isSetValue(set)) throw new TlaEvaluationError(describeNonSetMembership(element, set));
674
1560
  switch (set.kind) {
675
1561
  case "Set":
676
- return findIndex(set.elements, element) !== -1;
1562
+ try {
1563
+ return findIndex(set.elements, element) !== -1;
1564
+ } catch (error) {
1565
+ if (!(error instanceof TlaEvaluationError)) throw error;
1566
+ return isElementOfLikeTlc(element, set.elements);
1567
+ }
1568
+ case "MixedSet":
1569
+ return isElementOfLikeTlc(element, set.elements);
677
1570
  case "Interval":
678
1571
  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));
1572
+ if (set.low <= set.high && !isModelValue(element)) {
1573
+ throw new TlaEvaluationError(
1574
+ `Attempted to check if the value:\n${formatValue(element)}\nis in the integer interval ${formatValue(set)}`,
1575
+ );
1576
+ }
681
1577
  return false;
682
1578
  case "InfiniteSet": {
683
1579
  if (set.name === "Any") return true;
684
1580
  if (isModelValue(element)) return false;
685
1581
  const isMemberType =
686
1582
  set.name === "STRING" ? typeof element === "string" : typeof element === "number";
687
- if (!isMemberType) throw new TlaEvaluationError(describeMembership(element, set));
1583
+ if (!isMemberType) {
1584
+ throw new TlaEvaluationError(
1585
+ `Attempted to check if the value:\n${formatValue(element)}\nis an element of ${set.name}.`,
1586
+ );
1587
+ }
688
1588
  return set.name !== "Nat" || (typeof element === "number" && element >= 0);
689
1589
  }
690
1590
  case "Subset":
@@ -695,40 +1595,58 @@ export const isElementOf = (element: Value, set: Value): boolean => {
695
1595
  if (isModelValue(element)) return false;
696
1596
  throw new TlaEvaluationError(describeMembership(element, set));
697
1597
  }
698
- const fn = toFiniteFunction(element);
699
- return (
700
- valuesEqual(createSortedSet(fn.domain), set.domain) &&
701
- fn.range.every((item) => isElementOf(item, set.range))
702
- );
1598
+ const functionValue = toFiniteFunction(element);
1599
+ const low = getIntervalLow(functionValue.domain);
1600
+ const isDomain =
1601
+ low === undefined
1602
+ ? isEqualLikeTlc(set.domain, createSortedSet(functionValue.domain))
1603
+ : isEqualLikeTlc(
1604
+ { kind: "Interval", low, high: low + functionValue.domain.length - 1 },
1605
+ set.domain,
1606
+ );
1607
+ return isDomain && functionValue.range.every((item) => isElementOf(item, set.range));
703
1608
  }
704
1609
  case "RecordSet": {
705
- if (!isFunctionValue(element)) {
1610
+ const functionValue = isFunctionValue(element) ? toFiniteFunction(element) : undefined;
1611
+ if (!functionValue || !isRecordDomain(functionValue.domain)) {
706
1612
  if (isModelValue(element)) return false;
707
- throw new TlaEvaluationError(describeMembership(element, set));
1613
+ throw new TlaEvaluationError(
1614
+ `Attempted to check if non-record\n${formatValue(element)}\nis in the set of records:\n${formatValue(set)}`,
1615
+ );
708
1616
  }
709
- const fn = toFiniteFunction(element);
710
1617
  return (
711
- fn.domain.length === set.names.length &&
1618
+ functionValue.domain.length === set.names.length &&
712
1619
  set.names.every((name, index) => {
713
- const position = findIndex(fn.domain, name);
714
- return position !== -1 && isElementOf(fn.range[position], set.sets[index]);
1620
+ const position = findIndex(functionValue.domain, name);
1621
+ return position !== -1 && isElementOf(functionValue.range[position], set.sets[index]);
715
1622
  })
716
1623
  );
717
1624
  }
718
1625
  case "ProductSet": {
719
- if (!isFunctionValue(element)) {
720
- if (isModelValue(element)) return false;
721
- throw new TlaEvaluationError(describeMembership(element, set));
1626
+ const elements = isFunctionValue(element) ? getSequenceElements(element) : undefined;
1627
+ if (elements === undefined) {
1628
+ const isFunction =
1629
+ isFunctionValue(element) &&
1630
+ toFiniteFunction(element).domain.every((key) => typeof key === "number");
1631
+ if (isFunction || isModelValue(element)) return false;
1632
+ throw new TlaEvaluationError(
1633
+ `Attempted to check if non-tuple\n${formatValue(element)}\nis in the set of tuples:\n${formatValue(set)}`,
1634
+ );
722
1635
  }
723
- const elements = getSequenceElements(element);
724
1636
  return (
725
- elements !== undefined &&
726
1637
  elements.length === set.factors.length &&
727
1638
  elements.every((item, index) => isElementOf(item, set.factors[index]))
728
1639
  );
729
1640
  }
730
1641
  case "FilteredSet":
731
1642
  return isElementOf(element, set.base) && set.test(element);
1643
+ case "Union":
1644
+ if (!isTlcEnumerable(set.base)) {
1645
+ throw new TlaEvaluationError(
1646
+ `Attempted to check if:\n ${formatValue(element)}\nis an element of the non-enumerable set:\n ${formatUnion(set)}`,
1647
+ );
1648
+ }
1649
+ return toFiniteSet(set.base).elements.some((inner) => isElementOf(element, inner));
732
1650
  case "SetOperation":
733
1651
  if (set.operator === "\\cup")
734
1652
  return isElementOf(element, set.left) || isElementOf(element, set.right);
@@ -745,14 +1663,15 @@ export const isElementOf = (element: Value, set: Value): boolean => {
745
1663
  return elements.every((item) => isElementOf(item, set.base));
746
1664
  }
747
1665
  default:
748
- throw new TlaEvaluationError(`Expected a set but found ${formatValue(set)}`);
1666
+ throw new TlaEvaluationError(describeNonSetMembership(element, set));
749
1667
  }
750
1668
  };
751
1669
 
752
1670
  export const toFiniteFunction = (value: Value): FunctionValue => {
753
1671
  if (typeof value === "object" && value.kind === "Function") return value;
754
1672
  if (typeof value === "object" && value.kind === "LazyFunction") {
755
- const domain = toFiniteSet(value.domain).elements;
1673
+ if (!isEnumerable(value.domain)) getCardinality(value.domain);
1674
+ const domain = getFunctionDomain(value.domain);
756
1675
  return createSortedFunction(
757
1676
  domain,
758
1677
  domain.map((key) => value.apply(key)),
@@ -761,57 +1680,88 @@ export const toFiniteFunction = (value: Value): FunctionValue => {
761
1680
  throw new TlaEvaluationError(`Expected a function but found ${formatValue(value)}`);
762
1681
  };
763
1682
 
764
- export const applyFunction = (fn: Value, argument: Value): Value => {
765
- if (typeof fn === "object" && fn.kind === "LazyFunction") {
766
- if (!isElementOf(argument, fn.domain)) {
1683
+ const lambdaFunctions = new WeakSet<ObjectValue>();
1684
+
1685
+ export const markLambdaFunction = <FunctionLike extends FunctionValue | LazyFunctionValue>(
1686
+ functionValue: FunctionLike,
1687
+ ) => {
1688
+ lambdaFunctions.add(functionValue);
1689
+ return functionValue;
1690
+ };
1691
+
1692
+ const describeMissingArgument = (functionValue: Value, argument: Value) =>
1693
+ typeof functionValue === "object" && lambdaFunctions.has(functionValue)
1694
+ ? `In applying the function\n${prettyPrint(formatValue(functionValue))},\nthe first argument is:\n${prettyPrint(formatValue(argument))}\nwhich is not in its domain.\n`
1695
+ : `Attempted to apply function ${formatValue(functionValue)} to ${formatValue(argument)}, which is not in its domain`;
1696
+
1697
+ export const applyFunction = (
1698
+ functionValue: Value,
1699
+ argument: Value,
1700
+ describeMissing?: (functionValue: FunctionValue) => string,
1701
+ ): Value => {
1702
+ if (typeof functionValue === "object" && functionValue.kind === "LazyFunction") {
1703
+ if (!isElementOf(argument, functionValue.domain)) {
767
1704
  throw new TlaEvaluationError(
768
- `Attempted to apply a function to ${formatValue(argument)}, which is not in its domain`,
1705
+ lambdaFunctions.has(functionValue)
1706
+ ? describeMissingArgument(functionValue, argument)
1707
+ : `Attempted to apply a function to ${formatValue(argument)}, which is not in its domain`,
769
1708
  );
770
1709
  }
771
- return fn.apply(argument);
1710
+ return functionValue.apply(argument);
772
1711
  }
773
- if (typeof fn !== "object" || fn.kind !== "Function") {
774
- throw new TlaEvaluationError(`Attempted to apply ${formatValue(fn)}, which is not a function`);
1712
+ if (typeof functionValue !== "object" || functionValue.kind !== "Function") {
1713
+ throw new TlaEvaluationError(
1714
+ `Attempted to apply ${formatValue(functionValue)}, which is not a function`,
1715
+ );
775
1716
  }
776
- const domain = fn.domain;
1717
+ const domain = functionValue.domain;
777
1718
  const first = domain[0];
778
1719
  if (typeof argument === "number" && typeof first === "number") {
779
1720
  const offset = argument - first;
780
- if (domain[offset] === argument) return fn.range[offset];
1721
+ if (domain[offset] === argument) return functionValue.range[offset];
781
1722
  }
782
1723
  if (domain.length <= 8) {
783
1724
  for (let index = 0; index < domain.length; index++)
784
- if (domain[index] === argument) return fn.range[index];
1725
+ if (domain[index] === argument) return functionValue.range[index];
785
1726
  }
786
1727
  const position = findIndex(domain, argument);
787
1728
  if (position === -1) {
788
1729
  throw new TlaEvaluationError(
789
- `Attempted to apply function ${formatValue(fn)} to ${formatValue(argument)}, which is not in its domain`,
1730
+ describeMissing?.(functionValue) ?? describeMissingArgument(functionValue, argument),
790
1731
  );
791
1732
  }
792
- return fn.range[position];
1733
+ return functionValue.range[position];
793
1734
  };
794
1735
 
795
- export const getDomain = (fn: Value): Value => {
796
- if (typeof fn === "object" && fn.kind === "LazyFunction") return fn.domain;
797
- return createSortedSet(toFiniteFunction(fn).domain);
1736
+ export const getDomain = (functionValue: Value): Value => {
1737
+ if (typeof functionValue === "object" && functionValue.kind === "LazyFunction")
1738
+ return functionValue.domain;
1739
+ const domain = toFiniteFunction(functionValue).domain;
1740
+ if (!isIntervalDomain(domain)) return createSortedSet(domain);
1741
+ return intervalArrays.get(domain) ?? getIntervalValue(domain.length);
798
1742
  };
799
1743
 
800
1744
  export const updateFunction = (
801
- fn: Value,
1745
+ functionValue: Value,
802
1746
  argument: Value,
803
1747
  update: (previous: Value) => Value,
1748
+ isKeptLazy = false,
804
1749
  ): 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));
1750
+ const isLazy =
1751
+ typeof functionValue === "object" &&
1752
+ functionValue.kind === "LazyFunction" &&
1753
+ (isKeptLazy || !isEnumerable(functionValue.domain));
1754
+ if (isLazy) {
1755
+ if (!isElementOf(argument, functionValue.domain)) return functionValue;
1756
+ const updated = update(functionValue.apply(argument));
808
1757
  return {
809
1758
  kind: "LazyFunction",
810
- domain: fn.domain,
811
- apply: (key) => (valuesEqual(key, argument) ? updated : fn.apply(key)),
1759
+ domain: functionValue.domain,
1760
+ apply: (key) => (isEqualLikeTlc(argument, key) ? updated : functionValue.apply(key)),
1761
+ source: functionValue.source,
812
1762
  };
813
1763
  }
814
- const finite = toFiniteFunction(fn);
1764
+ const finite = toFiniteFunction(functionValue);
815
1765
  const position = findIndex(finite.domain, argument);
816
1766
  if (position === -1) return finite;
817
1767
  const range = [...finite.range];
@@ -819,6 +1769,7 @@ export const updateFunction = (
819
1769
  const next = update(previous);
820
1770
  range[position] = next;
821
1771
  const updated = createSortedFunction(finite.domain, range);
1772
+ updated.writtenKeys = finite.writtenKeys;
822
1773
  if (finite.hash !== 0) {
823
1774
  const key = finite.domain[position];
824
1775
  updated.hash =
@@ -867,7 +1818,7 @@ const getElementContribution = (element: Value) =>
867
1818
  const getEntryContribution = (key: Value, value: Value) =>
868
1819
  finalizeHash((Math.imul(getValueHash(key), 0x27d4eb2d) + getValueHash(value)) | 0);
869
1820
 
870
- const computeValueHash = (value: ObjectValue): number => {
1821
+ const computeValueHash = (value: ObjectValue) => {
871
1822
  if (value.kind === "ModelValue") return finalizeHash(combineHash(hashString(value.name), 0x6d));
872
1823
  if (isSetValue(value)) {
873
1824
  if (!isEnumerable(value)) return hashString(getValueKey(value));
@@ -878,10 +1829,11 @@ const computeValueHash = (value: ObjectValue): number => {
878
1829
  }
879
1830
  return hash;
880
1831
  }
881
- const fn = toFiniteFunction(value);
1832
+ const functionValue = toFiniteFunction(value);
882
1833
  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;
1834
+ for (let index = 0; index < functionValue.domain.length; index++) {
1835
+ hash =
1836
+ (hash + getEntryContribution(functionValue.domain[index], functionValue.range[index])) | 0;
885
1837
  }
886
1838
  return hash;
887
1839
  };
@@ -924,7 +1876,7 @@ const getEntryFingerprint = (key: Value, value: Value) =>
924
1876
  (Math.imul(getValueFingerprint(key), 0x165667b1) + getValueFingerprint(value)) | 0,
925
1877
  );
926
1878
 
927
- const computeValueFingerprint = (value: ObjectValue): number => {
1879
+ const computeValueFingerprint = (value: ObjectValue) => {
928
1880
  if (value.kind === "ModelValue") return finalizeFingerprint(fingerprintString(value.name) ^ 0x3c);
929
1881
  if (isSetValue(value)) {
930
1882
  if (!isEnumerable(value)) return fingerprintString(getValueKey(value));
@@ -935,10 +1887,12 @@ const computeValueFingerprint = (value: ObjectValue): number => {
935
1887
  }
936
1888
  return fingerprint;
937
1889
  }
938
- const fn = toFiniteFunction(value);
1890
+ const functionValue = toFiniteFunction(value);
939
1891
  let fingerprint = 0x4cf5ad43;
940
- for (let index = 0; index < fn.domain.length; index++) {
941
- fingerprint = (fingerprint + getEntryFingerprint(fn.domain[index], fn.range[index])) | 0;
1892
+ for (let index = 0; index < functionValue.domain.length; index++) {
1893
+ fingerprint =
1894
+ (fingerprint + getEntryFingerprint(functionValue.domain[index], functionValue.range[index])) |
1895
+ 0;
942
1896
  }
943
1897
  return fingerprint;
944
1898
  };
@@ -971,7 +1925,7 @@ export const getValueKey = (value: Value): string => {
971
1925
  return key;
972
1926
  };
973
1927
 
974
- const computeValueKey = (value: ObjectValue): string => {
1928
+ const computeValueKey = (value: ObjectValue) => {
975
1929
  if (value.kind === "ModelValue") return `@${value.name}`;
976
1930
  if (value.kind === "InfiniteSet") return `<${value.name}>`;
977
1931
  if (value.kind === "SequenceSet") return `<Seq ${getValueKey(value.base)}>`;
@@ -984,42 +1938,95 @@ const computeValueKey = (value: ObjectValue): string => {
984
1938
  if (!isEnumerable(value)) return `<${value.kind} ${formatValue(value)}>`;
985
1939
  return `{${toFiniteSet(value).elements.map(getValueKey).join(",")}}`;
986
1940
  }
987
- const fn = toFiniteFunction(value);
988
- return `(${fn.domain.map((key, index) => `${getValueKey(key)}:${getValueKey(fn.range[index])}`).join(",")})`;
1941
+ const functionValue = toFiniteFunction(value);
1942
+ return `(${functionValue.domain.map((key, index) => `${getValueKey(key)}:${getValueKey(functionValue.range[index])}`).join(",")})`;
989
1943
  };
990
1944
 
991
1945
  const formatString = (value: string) =>
992
1946
  `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t")}"`;
993
1947
 
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);
1948
+ type LazySetValue =
1949
+ | SubsetValue
1950
+ | FunctionSetValue
1951
+ | RecordSetValue
1952
+ | ProductSetValue
1953
+ | SetOperationValue
1954
+ | FilteredSetValue
1955
+ | UnionValue;
1956
+
1957
+ const MAXIMUM_EXPANDED_SUBSET_BASE = 6;
1958
+ const MAXIMUM_EXPANDED_PRODUCT = 1999;
1959
+
1960
+ const getProductSize = (sets: Value[]) =>
1961
+ sets.reduce<number>((size, set) => (size === 0 ? 0 : size * getCardinality(set)), 1);
1962
+
1963
+ const expandLazySet = (value: LazySetValue) => {
1964
+ if (value.kind === "Subset") {
1965
+ return getCardinality(value.base) <= MAXIMUM_EXPANDED_SUBSET_BASE
1966
+ ? toFiniteSet(value)
1967
+ : undefined;
1968
+ }
1969
+ if (value.kind === "SetOperation" || value.kind === "FilteredSet" || value.kind === "Union") {
1970
+ return toFiniteSet(value);
1971
+ }
1972
+ if (
1973
+ value.kind === "FunctionSet" &&
1974
+ isEnumerable(value.domain) &&
1975
+ getCardinality(value.domain) === 0
1976
+ ) {
1977
+ return createSortedSet([createSortedFunction([], [])]);
1978
+ }
1979
+ const size =
1980
+ value.kind === "FunctionSet"
1981
+ ? getCardinality(value.range) ** getCardinality(value.domain)
1982
+ : getProductSize(value.kind === "RecordSet" ? value.sets : value.factors);
1983
+ if (size === 0) return EMPTY_SET;
1984
+ return size <= MAXIMUM_EXPANDED_PRODUCT ? toFiniteSet(value) : undefined;
1985
+ };
1986
+
1987
+ const getExpandedLazySet = (value: LazySetValue, isStrict: boolean) => {
1988
+ if (isStrict) return expandLazySet(value);
1989
+ // TLC prints a set it can't enumerate symbolically, except in Print, PrintT, and ToString.
1990
+ if (!isEnumerable(value)) return undefined;
1991
+ try {
1992
+ return expandLazySet(value);
1993
+ } catch (error) {
1994
+ if (error instanceof TlaEvaluationError) return undefined;
1995
+ throw error;
1996
+ }
1997
+ };
1998
+
1999
+ const formatLazySet = (value: LazySetValue, isStrict: boolean): string => {
2000
+ const format = (inner: Value) => formatValue(inner, isStrict);
1005
2001
  switch (value.kind) {
1006
2002
  case "Subset":
1007
- return `SUBSET ${format(value.base)}`;
2003
+ return typeof value.base === "object" && value.base.kind === "Interval"
2004
+ ? `SUBSET (${format(value.base)})`
2005
+ : `SUBSET ${format(value.base)}`;
1008
2006
  case "FunctionSet":
1009
2007
  return `[${format(value.domain)} -> ${format(value.range)}]`;
1010
2008
  case "RecordSet":
1011
- return `[${value.names.map((name, index) => `${name} : ${format(value.sets[index])}`).join(", ")}]`;
2009
+ return `[${value.names.map((name, index) => `${name}: ${format(value.sets[index])}`).join(", ")}]`;
1012
2010
  case "ProductSet":
1013
- return value.factors.map(format).join(" \\X ");
2011
+ return `(${value.factors.map(format).join(" \\X ")})`;
1014
2012
  case "SetOperation":
1015
2013
  return `${format(value.left)} ${value.operator} ${format(value.right)}`;
1016
2014
  case "FilteredSet":
1017
2015
  return `{x \\in ${format(value.base)} : ...}`;
2016
+ case "Union":
2017
+ return formatUnion(value, isStrict);
1018
2018
  }
1019
2019
  };
1020
2020
 
1021
- export const formatValue = (value: Value, isExpanded = false): string => {
1022
- const format = (inner: Value) => formatValue(inner, isExpanded);
2021
+ // FcnRcdValue#isName: TLC prints a function as a record when every key consists of letters,
2022
+ // decimal digits, and underscores, with at least one letter.
2023
+ const isFieldName = (name: string) =>
2024
+ /^[\p{L}\p{Nd}_]*$/u.test(name) &&
2025
+ /\p{L}/u.test(name) &&
2026
+ (name.length < 4 || !/^[WS]F_/.test(name));
2027
+
2028
+ export const formatValue = (value: Value, isStrict = false, isNormalized = false): string => {
2029
+ const format = (inner: Value) => formatValue(inner, isStrict, isNormalized);
1023
2030
  if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
1024
2031
  if (typeof value === "number") return String(value);
1025
2032
  if (typeof value === "string") return formatString(value);
@@ -1028,8 +2035,10 @@ export const formatValue = (value: Value, isExpanded = false): string => {
1028
2035
  return value.name;
1029
2036
  case "Set":
1030
2037
  return `{${value.elements.map(format).join(", ")}}`;
2038
+ case "MixedSet":
2039
+ return format(toFiniteSet(value));
1031
2040
  case "Interval":
1032
- return `${value.low}..${value.high}`;
2041
+ return value.low <= value.high ? `${value.low}..${value.high}` : "{}";
1033
2042
  case "InfiniteSet":
1034
2043
  return value.name === "Any" ? "ANY" : value.name;
1035
2044
  case "Subset":
@@ -1038,23 +2047,36 @@ export const formatValue = (value: Value, isExpanded = false): string => {
1038
2047
  case "ProductSet":
1039
2048
  case "SetOperation":
1040
2049
  case "FilteredSet":
1041
- return isExpanded && isEnumerable(value)
1042
- ? format(toFiniteSet(value))
1043
- : formatLazySet(value, isExpanded);
2050
+ case "Union": {
2051
+ const expanded = getExpandedLazySet(value, isStrict);
2052
+ return expanded ? format(expanded) : formatLazySet(value, isStrict);
2053
+ }
1044
2054
 
1045
2055
  case "SequenceSet":
1046
2056
  return `Seq(${format(value.base)})`;
1047
2057
  case "Function":
1048
2058
  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(", ")}>>`;
2059
+ if (value.kind === "LazyFunction" && !isEnumerable(value.domain)) {
2060
+ const source = value.source;
2061
+ if (!source) return "<function>";
2062
+ const parameters = source.parameters
2063
+ .map((parameter) => `${parameter.name} \\in ${formatValue(parameter.domain)}`)
2064
+ .join(", ");
2065
+ return `[${parameters} |-> <expression ${source.body}>]`;
2066
+ }
2067
+ const functionValue = toFiniteFunction(value);
2068
+ if (functionValue.domain.every((key, index) => key === index + 1)) {
2069
+ return `<<${functionValue.range.map(format).join(", ")}>>`;
1053
2070
  }
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(", ")}]`;
2071
+ if (functionValue.domain.every((key) => typeof key === "string" && isFieldName(key))) {
2072
+ const isSorted =
2073
+ isNormalized || functionValue.hash !== 0 || functionValue.fingerprint !== 0;
2074
+ const pairs = isSorted
2075
+ ? functionValue.domain.map((key, index) => [key, functionValue.range[index]])
2076
+ : getWrittenPairs(functionValue);
2077
+ return `[${pairs.map((pair) => `${String(pair[0])} |-> ${format(pair[1])}`).join(", ")}]`;
1056
2078
  }
1057
- return `(${fn.domain.map((key, index) => `${format(key)} :> ${format(fn.range[index])}`).join(" @@ ")})`;
2079
+ return `(${functionValue.domain.map((key, index) => `${format(key)} :> ${format(functionValue.range[index])}`).join(" @@ ")})`;
1058
2080
  }
1059
2081
  }
1060
2082
  };