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