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.
- package/README.md +34 -41
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +166 -30
- package/dist/index.d.ts +166 -30
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +2 -2
- package/dist/parallel-worker.js +2 -2
- package/dist/parallel.cjs +1 -1
- package/dist/parallel.js +2 -2
- package/dist/run-cli.cjs +2 -1
- package/dist/run-cli.js +2 -1
- package/dist/transfer.cjs +388 -39
- package/dist/transfer.js +388 -39
- package/package.json +4 -2
- package/src/ast.ts +82 -5
- package/src/bags.ts +161 -71
- package/src/builtins.ts +203 -23
- package/src/bundled-modules.ts +63 -32
- package/src/check.ts +904 -230
- package/src/codegen.ts +52 -18
- package/src/config.ts +39 -11
- package/src/environment.ts +485 -140
- package/src/errors.ts +22 -0
- package/src/evaluate.ts +486 -136
- package/src/exit-codes.ts +75 -0
- package/src/fingerprints.ts +2 -2
- package/src/index.ts +9 -4
- package/src/instances.ts +221 -66
- package/src/java-overrides.ts +241 -0
- package/src/json.ts +153 -72
- package/src/levels.ts +850 -0
- package/src/lexer.ts +97 -27
- package/src/liveness.ts +47 -91
- package/src/parallel-worker.ts +57 -28
- package/src/parallel.ts +116 -62
- package/src/parser.ts +703 -183
- package/src/pretty-print.ts +259 -0
- package/src/randomization.ts +24 -113
- package/src/resolve.ts +472 -41
- package/src/run-cli.ts +142 -44
- package/src/runtime.ts +63 -28
- package/src/spec-description.ts +509 -0
- package/src/standard-levels.ts +105 -0
- package/src/standard-modules.ts +223 -92
- package/src/subexpressions.ts +21 -35
- package/src/substitution-levels.ts +184 -0
- package/src/successors.ts +261 -46
- package/src/symmetry.ts +65 -21
- package/src/tableau.ts +43 -37
- package/src/tlc-ext.ts +120 -33
- package/src/tlc-module.ts +71 -47
- package/src/tlc-modules.ts +1022 -0
- package/src/tlc-order.ts +497 -0
- package/src/tlc-random.ts +411 -0
- package/src/tlc-summary.ts +109 -0
- package/src/transfer.ts +4 -4
- 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
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
|
355
|
+
return compareStrings(left.name, right.name);
|
|
199
356
|
}
|
|
200
357
|
return leftKind === "ModelValue" ? -1 : 1;
|
|
201
358
|
}
|
|
202
|
-
|
|
203
|
-
if (
|
|
204
|
-
|
|
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
|
|
213
|
-
if (
|
|
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 (
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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[])
|
|
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 "
|
|
512
|
-
return
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
527
|
-
|
|
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
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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 (
|
|
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
|
|
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 === "
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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"
|
|
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.
|
|
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[],
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
1438
|
+
if (isEmptySet(value)) return 0;
|
|
1439
|
+
return getProductCardinality(value.sets, () => describeOverflow(value));
|
|
643
1440
|
case "ProductSet":
|
|
644
|
-
|
|
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
|
|
665
|
-
return
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1618
|
+
functionValue.domain.length === set.names.length &&
|
|
712
1619
|
set.names.every((name, index) => {
|
|
713
|
-
const position = findIndex(
|
|
714
|
-
return position !== -1 && isElementOf(
|
|
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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
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
|
|
1710
|
+
return functionValue.apply(argument);
|
|
772
1711
|
}
|
|
773
|
-
if (typeof
|
|
774
|
-
throw new TlaEvaluationError(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
1730
|
+
describeMissing?.(functionValue) ?? describeMissingArgument(functionValue, argument),
|
|
790
1731
|
);
|
|
791
1732
|
}
|
|
792
|
-
return
|
|
1733
|
+
return functionValue.range[position];
|
|
793
1734
|
};
|
|
794
1735
|
|
|
795
|
-
export const getDomain = (
|
|
796
|
-
if (typeof
|
|
797
|
-
|
|
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
|
-
|
|
1745
|
+
functionValue: Value,
|
|
802
1746
|
argument: Value,
|
|
803
1747
|
update: (previous: Value) => Value,
|
|
1748
|
+
isKeptLazy = false,
|
|
804
1749
|
): Value => {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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:
|
|
811
|
-
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(
|
|
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)
|
|
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
|
|
1832
|
+
const functionValue = toFiniteFunction(value);
|
|
882
1833
|
let hash = FUNCTION_HASH_SEED;
|
|
883
|
-
for (let index = 0; index <
|
|
884
|
-
hash =
|
|
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)
|
|
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
|
|
1890
|
+
const functionValue = toFiniteFunction(value);
|
|
939
1891
|
let fingerprint = 0x4cf5ad43;
|
|
940
|
-
for (let index = 0; index <
|
|
941
|
-
fingerprint =
|
|
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)
|
|
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
|
|
988
|
-
return `(${
|
|
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
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
1022
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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))
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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 (
|
|
1055
|
-
|
|
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 `(${
|
|
2079
|
+
return `(${functionValue.domain.map((key, index) => `${format(key)} :> ${format(functionValue.range[index])}`).join(" @@ ")})`;
|
|
1058
2080
|
}
|
|
1059
2081
|
}
|
|
1060
2082
|
};
|