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