tla-ts 0.0.1 → 0.0.3-dev.6979965
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 +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 +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 +121 -123
- package/src/parallel.ts +167 -83
- 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/symmetry.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createFunction,
|
|
6
6
|
createSet,
|
|
7
7
|
createSortedFunction,
|
|
8
|
+
createSortedSet,
|
|
8
9
|
isEnumerable,
|
|
9
10
|
isFunctionValue,
|
|
10
11
|
isModelValue,
|
|
@@ -18,6 +19,9 @@ import {
|
|
|
18
19
|
|
|
19
20
|
type Images = Map<string, ModelValue>;
|
|
20
21
|
|
|
22
|
+
const CACHE_LIMIT = 1 << 22;
|
|
23
|
+
const CACHED_SIZE_LIMIT = 64;
|
|
24
|
+
|
|
21
25
|
interface Permutation {
|
|
22
26
|
images: Images;
|
|
23
27
|
cache: WeakMap<ObjectValue, Value>;
|
|
@@ -31,7 +35,7 @@ const getPermutationKey = (permutation: Images) =>
|
|
|
31
35
|
.sort()
|
|
32
36
|
.join(",");
|
|
33
37
|
|
|
34
|
-
const composePermutations = (first: Images, second: Images)
|
|
38
|
+
const composePermutations = (first: Images, second: Images) => {
|
|
35
39
|
const composed: Images = new Map();
|
|
36
40
|
const names = new Set([...first.keys(), ...second.keys()]);
|
|
37
41
|
names.forEach((name) => {
|
|
@@ -42,14 +46,14 @@ const composePermutations = (first: Images, second: Images): Images => {
|
|
|
42
46
|
return composed;
|
|
43
47
|
};
|
|
44
48
|
|
|
45
|
-
const toImages = (value: Value, name: string)
|
|
49
|
+
const toImages = (value: Value, name: string) => {
|
|
46
50
|
if (!isFunctionValue(value)) {
|
|
47
51
|
throw new TlaConfigError(`The symmetry values of ${name} must be functions`);
|
|
48
52
|
}
|
|
49
|
-
const
|
|
53
|
+
const functionValue = toFiniteFunction(value);
|
|
50
54
|
const permutation: Images = new Map();
|
|
51
|
-
|
|
52
|
-
const image =
|
|
55
|
+
functionValue.domain.forEach((key, index) => {
|
|
56
|
+
const image = functionValue.range[index];
|
|
53
57
|
if (!isModelValue(key) || !isModelValue(image)) {
|
|
54
58
|
throw new TlaConfigError("Symmetry function must have model values as domain and range");
|
|
55
59
|
}
|
|
@@ -103,7 +107,7 @@ export const getSymmetryGroup = (name: string, environment: Environment): Permut
|
|
|
103
107
|
}));
|
|
104
108
|
};
|
|
105
109
|
|
|
106
|
-
const permuteElements = (elements: Value[], permutation: Permutation)
|
|
110
|
+
const permuteElements = (elements: Value[], permutation: Permutation) => {
|
|
107
111
|
for (let index = 0; index < elements.length; index++) {
|
|
108
112
|
const element = elements[index];
|
|
109
113
|
const permuted = permuteValue(element, permutation);
|
|
@@ -118,8 +122,32 @@ const permuteElements = (elements: Value[], permutation: Permutation): Value[] |
|
|
|
118
122
|
return undefined;
|
|
119
123
|
};
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
// A permutation maps distinct elements to distinct elements and keeps the elements it doesn't move
|
|
126
|
+
// in order, so it sorts only the moved ones and merges them back.
|
|
127
|
+
const permuteSortedElements = (elements: Value[], permutation: Permutation) => {
|
|
128
|
+
const kept: Value[] = [];
|
|
129
|
+
const moved: Value[] = [];
|
|
130
|
+
for (const element of elements) {
|
|
131
|
+
const permuted = permuteValue(element, permutation);
|
|
132
|
+
if (permuted === element) kept.push(element);
|
|
133
|
+
else moved.push(permuted);
|
|
134
|
+
}
|
|
135
|
+
if (moved.length === 0) return undefined;
|
|
136
|
+
moved.sort(compareValues);
|
|
137
|
+
const merged: Value[] = [];
|
|
138
|
+
let keptIndex = 0;
|
|
139
|
+
let movedIndex = 0;
|
|
140
|
+
while (keptIndex < kept.length && movedIndex < moved.length) {
|
|
141
|
+
merged.push(
|
|
142
|
+
compareValues(kept[keptIndex], moved[movedIndex]) < 0
|
|
143
|
+
? kept[keptIndex++]
|
|
144
|
+
: moved[movedIndex++],
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
while (keptIndex < kept.length) merged.push(kept[keptIndex++]);
|
|
148
|
+
while (movedIndex < moved.length) merged.push(moved[movedIndex++]);
|
|
149
|
+
return merged;
|
|
150
|
+
};
|
|
123
151
|
|
|
124
152
|
const getSize = (value: ObjectValue) =>
|
|
125
153
|
value.kind === "Set"
|
|
@@ -145,20 +173,22 @@ const permuteValue = (value: Value, permutation: Permutation): Value => {
|
|
|
145
173
|
return permuted;
|
|
146
174
|
};
|
|
147
175
|
|
|
148
|
-
const computePermutedValue = (value: ObjectValue, permutation: Permutation)
|
|
176
|
+
const computePermutedValue = (value: ObjectValue, permutation: Permutation) => {
|
|
149
177
|
const kind = value.kind;
|
|
150
178
|
if (kind === "Set") {
|
|
151
|
-
const elements =
|
|
152
|
-
return elements ?
|
|
179
|
+
const elements = permuteSortedElements(value.elements, permutation);
|
|
180
|
+
return elements ? createSortedSet(elements) : value;
|
|
153
181
|
}
|
|
154
182
|
if (kind === "Function" || (kind === "LazyFunction" && isEnumerable(value.domain))) {
|
|
155
|
-
const
|
|
156
|
-
const domain = permuteElements(
|
|
157
|
-
const range = permuteElements(
|
|
183
|
+
const functionValue = toFiniteFunction(value);
|
|
184
|
+
const domain = permuteElements(functionValue.domain, permutation);
|
|
185
|
+
const range = permuteElements(functionValue.range, permutation) ?? functionValue.range;
|
|
158
186
|
if (domain) {
|
|
159
187
|
return createFunction(domain.map((key, index): [Value, Value] => [key, range[index]]));
|
|
160
188
|
}
|
|
161
|
-
return range ===
|
|
189
|
+
return range === functionValue.range
|
|
190
|
+
? functionValue
|
|
191
|
+
: createSortedFunction(functionValue.domain, range);
|
|
162
192
|
}
|
|
163
193
|
if (kind === "Interval" || !isSetValue(value) || !isEnumerable(value)) return value;
|
|
164
194
|
const set = toFiniteSet(value);
|
|
@@ -173,20 +203,34 @@ const permuteRemaining = (state: Value[], permuted: Value[], permutation: Permut
|
|
|
173
203
|
return permuted;
|
|
174
204
|
};
|
|
175
205
|
|
|
176
|
-
const getSmallerPermutation = (
|
|
206
|
+
const getSmallerPermutation = (
|
|
207
|
+
state: Value[],
|
|
208
|
+
minimum: Value[],
|
|
209
|
+
permutation: Permutation,
|
|
210
|
+
order: number[] | undefined,
|
|
211
|
+
) => {
|
|
177
212
|
const permuted: Value[] = [];
|
|
178
|
-
for (let
|
|
213
|
+
for (let position = 0; position < state.length; position++) {
|
|
214
|
+
const index = order ? order[position] : position;
|
|
179
215
|
const value = permuteValue(state[index], permutation);
|
|
180
216
|
const comparison = compareValues(value, minimum[index]);
|
|
181
217
|
if (comparison > 0) return undefined;
|
|
182
|
-
permuted.push(value);
|
|
183
|
-
if (comparison < 0)
|
|
218
|
+
if (!order) permuted.push(value);
|
|
219
|
+
if (comparison < 0) {
|
|
220
|
+
return order
|
|
221
|
+
? state.map((item) => permuteValue(item, permutation))
|
|
222
|
+
: permuteRemaining(state, permuted, permutation);
|
|
223
|
+
}
|
|
184
224
|
}
|
|
185
225
|
return undefined;
|
|
186
226
|
};
|
|
187
227
|
|
|
188
|
-
export const getRepresentative = (
|
|
228
|
+
export const getRepresentative = (
|
|
229
|
+
state: Value[],
|
|
230
|
+
group: Permutation[],
|
|
231
|
+
order?: number[],
|
|
232
|
+
): Value[] =>
|
|
189
233
|
group.reduce(
|
|
190
|
-
(minimum, permutation) => getSmallerPermutation(state, minimum, permutation) ?? minimum,
|
|
234
|
+
(minimum, permutation) => getSmallerPermutation(state, minimum, permutation, order) ?? minimum,
|
|
191
235
|
state,
|
|
192
236
|
);
|
package/src/tableau.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { Expression } from "./ast";
|
|
2
2
|
import { TlaEvaluationError } from "./errors";
|
|
3
3
|
import type { Scope } from "./evaluate";
|
|
4
|
+
import { ACTION_LEVEL, TEMPORAL_LEVEL } from "./levels";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
interface LiveBoolean {
|
|
6
7
|
kind: "Boolean";
|
|
7
8
|
value: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
interface LiveState {
|
|
11
12
|
kind: "State";
|
|
12
13
|
expression: Expression;
|
|
13
14
|
scope: Scope | undefined;
|
|
@@ -19,7 +20,7 @@ export interface LiveEnabled {
|
|
|
19
20
|
scope: Scope | undefined;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
interface LiveAction {
|
|
23
24
|
kind: "Action";
|
|
24
25
|
expression: Expression;
|
|
25
26
|
unchanged: Expression | undefined;
|
|
@@ -27,12 +28,12 @@ export interface LiveAction {
|
|
|
27
28
|
enabled?: LiveEnabled;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
interface LiveUnary {
|
|
31
32
|
kind: "Not" | "Always" | "Eventually" | "Next";
|
|
32
33
|
body: LiveExpression;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
interface LiveJunction {
|
|
36
37
|
kind: "And" | "Or";
|
|
37
38
|
operands: LiveExpression[];
|
|
38
39
|
}
|
|
@@ -45,7 +46,7 @@ export type LiveExpression =
|
|
|
45
46
|
| LiveUnary
|
|
46
47
|
| LiveJunction;
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
interface TableauNode {
|
|
49
50
|
particle: LiveExpression[];
|
|
50
51
|
successors: number[];
|
|
51
52
|
statePredicates: LiveExpression[];
|
|
@@ -56,7 +57,7 @@ export interface Tableau {
|
|
|
56
57
|
initialCount: number;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
interface ErrorModel {
|
|
60
61
|
eventuallyAlways: number[];
|
|
61
62
|
infinitelyOftenStates: number[];
|
|
62
63
|
infinitelyOftenActions: number[];
|
|
@@ -84,14 +85,12 @@ interface Triple {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const STATE_LEVEL = 1;
|
|
87
|
-
const ACTION_LEVEL = 2;
|
|
88
|
-
const TEMPORAL_LEVEL = 3;
|
|
89
88
|
const MAXIMUM_DISJUNCTS = 2147483647;
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
const TRUE: LiveBoolean = { kind: "Boolean", value: true };
|
|
91
|
+
const FALSE: LiveBoolean = { kind: "Boolean", value: false };
|
|
93
92
|
|
|
94
|
-
const createBoolean = (value: boolean) => (value ? TRUE : FALSE);
|
|
93
|
+
export const createBoolean = (value: boolean) => (value ? TRUE : FALSE);
|
|
95
94
|
|
|
96
95
|
const isAtom = (expression: LiveExpression) =>
|
|
97
96
|
expression.kind === "State" || expression.kind === "Enabled" || expression.kind === "Action";
|
|
@@ -265,6 +264,10 @@ const toDisjunctiveNormalForm = (expression: LiveExpression): LiveExpression =>
|
|
|
265
264
|
const total = { value: 1 };
|
|
266
265
|
for (const operand of expression.operands) {
|
|
267
266
|
const normal = toDisjunctiveNormalForm(operand);
|
|
267
|
+
if (normal.kind === "And") {
|
|
268
|
+
items.push(...normal.operands);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
268
271
|
if (normal.kind === "Or") {
|
|
269
272
|
total.value *= normal.operands.length;
|
|
270
273
|
if (total.value > MAXIMUM_DISJUNCTS) {
|
|
@@ -272,12 +275,8 @@ const toDisjunctiveNormalForm = (expression: LiveExpression): LiveExpression =>
|
|
|
272
275
|
"TLC cannot handle the temporal formula because it exceeds the maximum supported size in disjunctive normal form",
|
|
273
276
|
);
|
|
274
277
|
}
|
|
275
|
-
items.push(normal);
|
|
276
|
-
} else if (normal.kind === "And") {
|
|
277
|
-
items.push(...normal.operands);
|
|
278
|
-
} else {
|
|
279
|
-
items.push(normal);
|
|
280
278
|
}
|
|
279
|
+
items.push(normal);
|
|
281
280
|
}
|
|
282
281
|
if (total.value === 1) return { kind: "And", operands: items };
|
|
283
282
|
const conjunctions = Array.from({ length: total.value }, (): LiveExpression[] => []);
|
|
@@ -427,9 +426,13 @@ const getExpansion = (
|
|
|
427
426
|
term: LiveExpression,
|
|
428
427
|
repeated: "Always" | "Eventually",
|
|
429
428
|
junction: "And" | "Or",
|
|
430
|
-
)
|
|
431
|
-
if (term.kind === repeated && "body" in term)
|
|
432
|
-
|
|
429
|
+
) => {
|
|
430
|
+
if (term.kind === repeated && "body" in term) {
|
|
431
|
+
return { first: term.body, second: createUnary("Next", term) };
|
|
432
|
+
}
|
|
433
|
+
if (term.kind === junction && "operands" in term) {
|
|
434
|
+
return { first: term.operands[0], second: term.operands[1] };
|
|
435
|
+
}
|
|
433
436
|
return undefined;
|
|
434
437
|
};
|
|
435
438
|
|
|
@@ -437,10 +440,10 @@ const getTriples = (
|
|
|
437
440
|
closure: LiveExpression[],
|
|
438
441
|
repeated: "Always" | "Eventually",
|
|
439
442
|
junction: "And" | "Or",
|
|
440
|
-
)
|
|
443
|
+
) =>
|
|
441
444
|
closure.flatMap((term): Triple[] => {
|
|
442
445
|
const parts = getExpansion(term, repeated, junction);
|
|
443
|
-
return parts ? [{ whole: term, first: parts
|
|
446
|
+
return parts ? [{ whole: term, first: parts.first, second: parts.second }] : [];
|
|
444
447
|
});
|
|
445
448
|
|
|
446
449
|
const isSameParticle = (left: LiveExpression[], right: LiveExpression[]) =>
|
|
@@ -451,7 +454,7 @@ const unionParticles = (left: LiveExpression[][], right: LiveExpression[][]) =>
|
|
|
451
454
|
...right,
|
|
452
455
|
];
|
|
453
456
|
|
|
454
|
-
const getParticleClosure = (terms: LiveExpression[])
|
|
457
|
+
const getParticleClosure = (terms: LiveExpression[]) => {
|
|
455
458
|
const closure = getPositiveClosure(terms);
|
|
456
459
|
const alphas = getTriples(closure, "Always", "And");
|
|
457
460
|
const betas = getTriples(closure, "Eventually", "Or");
|
|
@@ -460,9 +463,11 @@ const getParticleClosure = (terms: LiveExpression[]): LiveExpression[][] => {
|
|
|
460
463
|
for (const term of particle) {
|
|
461
464
|
const parts = getExpansion(term, "Eventually", "Or");
|
|
462
465
|
if (!parts) continue;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
+
if (isMember(particle, parts.first) || isMember(particle, parts.second)) continue;
|
|
467
|
+
return unionParticles(
|
|
468
|
+
expand([...particle, parts.first]),
|
|
469
|
+
expand([...particle, parts.second]),
|
|
470
|
+
);
|
|
466
471
|
}
|
|
467
472
|
const beta = betas.find(
|
|
468
473
|
(triple) =>
|
|
@@ -478,9 +483,8 @@ const getParticleClosure = (terms: LiveExpression[]): LiveExpression[][] => {
|
|
|
478
483
|
for (let index = 0; index < expanded.length; index++) {
|
|
479
484
|
const parts = getExpansion(expanded[index], "Always", "And");
|
|
480
485
|
if (!parts) continue;
|
|
481
|
-
|
|
482
|
-
if (!isMember(expanded,
|
|
483
|
-
if (!isMember(expanded, second)) expanded.push(second);
|
|
486
|
+
if (!isMember(expanded, parts.first)) expanded.push(parts.first);
|
|
487
|
+
if (!isMember(expanded, parts.second)) expanded.push(parts.second);
|
|
484
488
|
}
|
|
485
489
|
const state = { isChanged: true };
|
|
486
490
|
while (state.isChanged) {
|
|
@@ -509,7 +513,7 @@ const createTableauNode = (particle: LiveExpression[]): TableauNode => ({
|
|
|
509
513
|
statePredicates: particle.filter((term) => getLiveLevel(term) <= STATE_LEVEL),
|
|
510
514
|
});
|
|
511
515
|
|
|
512
|
-
const createTableau = (formula: LiveExpression)
|
|
516
|
+
const createTableau = (formula: LiveExpression) => {
|
|
513
517
|
const nodes = getParticleClosure([formula]).map(createTableauNode);
|
|
514
518
|
const initialCount = nodes.length;
|
|
515
519
|
for (let index = 0; index < nodes.length; index++) {
|
|
@@ -529,11 +533,13 @@ export const isFulfilling = (particle: LiveExpression[], promise: LiveUnary) =>
|
|
|
529
533
|
!isMember(particle, promise) || isMember(particle, promise.body);
|
|
530
534
|
|
|
531
535
|
const classify = (expression: LiveExpression, classification: Classification) => {
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
536
|
+
if (
|
|
537
|
+
expression.kind === "Eventually" &&
|
|
538
|
+
expression.body.kind === "Always" &&
|
|
539
|
+
getLiveLevel(expression.body.body) < TEMPORAL_LEVEL
|
|
540
|
+
) {
|
|
541
|
+
classification.eventuallyAlways.push(expression.body.body);
|
|
542
|
+
return;
|
|
537
543
|
}
|
|
538
544
|
const infinitelyOften = getInfinitelyOftenBody(expression);
|
|
539
545
|
if (infinitelyOften) {
|
|
@@ -592,13 +598,13 @@ export const getSolutions = (formula: LiveExpression): Solution[] => {
|
|
|
592
598
|
if (group) group.classifications.push(classification);
|
|
593
599
|
else groups.push({ formula: temporal, classifications: [classification] });
|
|
594
600
|
}
|
|
595
|
-
return groups.map((group)
|
|
601
|
+
return groups.map((group) => {
|
|
596
602
|
const binary = group.formula && makeBinary(group.formula);
|
|
597
603
|
const promises: LiveUnary[] = [];
|
|
598
604
|
if (binary) extractPromises(binary, promises);
|
|
599
605
|
const stateChecks: LiveExpression[] = [];
|
|
600
606
|
const actionChecks: LiveExpression[] = [];
|
|
601
|
-
const models = group.classifications.map((classification)
|
|
607
|
+
const models = group.classifications.map((classification) => ({
|
|
602
608
|
infinitelyOftenActions: classification.infinitelyOftenActions.map((check) =>
|
|
603
609
|
addToBin(actionChecks, check),
|
|
604
610
|
),
|
package/src/tlc-ext.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
createJavaOverrideError,
|
|
3
3
|
defineHigherOrderOperator,
|
|
4
4
|
defineLazyOperator,
|
|
5
5
|
defineOperator,
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
getThunk,
|
|
9
9
|
getValue,
|
|
10
10
|
} from "./builtins";
|
|
11
|
-
import { TlaEvaluationError } from "./errors";
|
|
11
|
+
import { TlaEvaluationError, TlaModuleError, TlaUnsupportedError } from "./errors";
|
|
12
|
+
import { getTlcFingerprint } from "./tlc-random";
|
|
12
13
|
import {
|
|
13
14
|
evaluate,
|
|
15
|
+
expectBoolean,
|
|
14
16
|
getChildren,
|
|
15
17
|
getDefinition,
|
|
16
18
|
isLocallyResolved,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
type Environment,
|
|
19
21
|
} from "./evaluate";
|
|
20
22
|
import type { Definition, Expression } from "./ast";
|
|
23
|
+
import type { TraceStep } from "./check";
|
|
21
24
|
import {
|
|
22
25
|
getNamedRegisters,
|
|
23
26
|
getPossibleCounts,
|
|
@@ -31,18 +34,74 @@ import {
|
|
|
31
34
|
createSet,
|
|
32
35
|
createTuple,
|
|
33
36
|
formatValue,
|
|
37
|
+
getDomain,
|
|
34
38
|
getSequenceElements,
|
|
35
|
-
getValueHash,
|
|
36
39
|
getValueKey,
|
|
40
|
+
isFunctionValue,
|
|
37
41
|
toFiniteSet,
|
|
38
42
|
valuesEqual,
|
|
39
43
|
type Value,
|
|
40
44
|
} from "./values";
|
|
41
45
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
const OVERRIDE_PARAMETERS =
|
|
47
|
+
"tlc2.tool.impl.Tool,tla2sany.semantic.ExprOrOpArgNode[],tlc2.util.Context,tlc2.tool.TLCState,tlc2.tool.TLCState,int,tlc2.tool.coverage.CostModel";
|
|
48
|
+
|
|
49
|
+
const ASSERT_ERROR_METHOD = `public static synchronized tlc2.value.impl.Value tlc2.module.TLCExt.assertError(${OVERRIDE_PARAMETERS})`;
|
|
50
|
+
|
|
51
|
+
const TRACE_METHOD = `public static tlc2.value.impl.TupleValue tlc2.module.TLCExt.getTrace(${OVERRIDE_PARAMETERS}) throws java.io.IOException`;
|
|
52
|
+
|
|
53
|
+
const PICK_SUCCESSOR_METHOD = `public static synchronized tlc2.value.impl.Value tlc2.module.TLCExt.pickSuccessor(${OVERRIDE_PARAMETERS})`;
|
|
54
|
+
|
|
55
|
+
const createEmptyCounterExample = () =>
|
|
56
|
+
createRecord([
|
|
57
|
+
["action", createSet([])],
|
|
58
|
+
["state", createSet([])],
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const createBuiltinLocation = () =>
|
|
62
|
+
createRecord([
|
|
63
|
+
["beginLine", 0],
|
|
64
|
+
["beginColumn", 0],
|
|
65
|
+
["endLine", 0],
|
|
66
|
+
["endColumn", 0],
|
|
67
|
+
["module", "--TLA+ BUILTINS--"],
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
// TLC's record for an action it doesn't store, such as every action in breadth-first search.
|
|
71
|
+
export const createActionRecord = (action: string | undefined) =>
|
|
72
|
+
createRecord([
|
|
73
|
+
["name", action ?? "UnnamedAction"],
|
|
74
|
+
["location", createBuiltinLocation()],
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// TLC's CounterExample is a graph of the violating behavior: the states numbered from 1, an edge
|
|
78
|
+
// for each step, and for a lasso an edge from the last state back to where the loop starts.
|
|
79
|
+
export const createCounterExample = (
|
|
80
|
+
trace: TraceStep[],
|
|
81
|
+
loop: { start: number; action: string | undefined } | undefined,
|
|
82
|
+
) => {
|
|
83
|
+
const states = trace.map((step, index) =>
|
|
84
|
+
createTuple([index + 1, createRecord(Object.entries(step.state))]),
|
|
85
|
+
);
|
|
86
|
+
const steps = states
|
|
87
|
+
.slice(1)
|
|
88
|
+
.map((state, index) =>
|
|
89
|
+
createTuple([states[index], createActionRecord(trace[index + 1].action), state]),
|
|
90
|
+
);
|
|
91
|
+
const closing = loop
|
|
92
|
+
? [
|
|
93
|
+
createTuple([
|
|
94
|
+
states[states.length - 1],
|
|
95
|
+
createActionRecord(loop.action),
|
|
96
|
+
states[loop.start],
|
|
97
|
+
]),
|
|
98
|
+
]
|
|
99
|
+
: [];
|
|
100
|
+
return createRecord([
|
|
101
|
+
["action", createSet([...steps, ...closing])],
|
|
102
|
+
["state", createSet(states)],
|
|
103
|
+
]);
|
|
104
|
+
};
|
|
46
105
|
|
|
47
106
|
const getRegister = (runtime: CheckerRuntime, key: Value) => {
|
|
48
107
|
if (typeof key === "number") return runtime.registers.get(key);
|
|
@@ -61,7 +120,7 @@ const setRegister = (runtime: CheckerRuntime, key: Value, value: Value) => {
|
|
|
61
120
|
runtime.namedRegisters.set(key, value);
|
|
62
121
|
return;
|
|
63
122
|
}
|
|
64
|
-
throw new
|
|
123
|
+
throw new TlaModuleError(
|
|
65
124
|
`The first argument of TLCSet should be a nonnegative integer, but instead it is:\n${formatValue(key)}`,
|
|
66
125
|
);
|
|
67
126
|
};
|
|
@@ -112,13 +171,20 @@ export const createTlcExtOperators = (
|
|
|
112
171
|
const right = getValue(args, 1, "AssertEq");
|
|
113
172
|
const isEqual = valuesEqual(left, right);
|
|
114
173
|
if (!isEqual) {
|
|
115
|
-
print("Assertion failed");
|
|
174
|
+
print(formatValue("Assertion failed", true));
|
|
116
175
|
print(formatValue(left, true));
|
|
117
176
|
print(formatValue(right, true));
|
|
118
177
|
}
|
|
119
178
|
return isEqual;
|
|
120
179
|
}),
|
|
121
180
|
defineLazyOperator("AssertError", 2, (args) => {
|
|
181
|
+
const message = getOperator(args, 0, "AssertError").expression;
|
|
182
|
+
if (message && message.kind !== "String") {
|
|
183
|
+
throw createJavaOverrideError(
|
|
184
|
+
ASSERT_ERROR_METHOD,
|
|
185
|
+
`In computing AssertError, a non-string expression (line ${message.location.line}, col ${message.location.column}) was used as the err of an AssertError(err, exp).`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
122
188
|
const expected = getThunk(args, 0, "AssertError")();
|
|
123
189
|
try {
|
|
124
190
|
getThunk(args, 1, "AssertError")();
|
|
@@ -144,46 +210,68 @@ export const createTlcExtOperators = (
|
|
|
144
210
|
},
|
|
145
211
|
defineStateOperator("Trace", 0, (_, context) => {
|
|
146
212
|
const state = context?.state;
|
|
147
|
-
if (!state || !isCompleteState(state)) {
|
|
148
|
-
|
|
149
|
-
|
|
213
|
+
if (!context || !state || !isCompleteState(state)) {
|
|
214
|
+
const unassigned = (context?.environment.variables ?? []).filter(
|
|
215
|
+
(_, index) => state?.[index] === undefined,
|
|
216
|
+
);
|
|
217
|
+
throw createJavaOverrideError(
|
|
218
|
+
TRACE_METHOD,
|
|
219
|
+
`In evaluating TLCExt!Trace, the state is not completely specified yet (variable${unassigned.length > 1 ? "s" : ""} ${unassigned.join(", ")} undefined).`,
|
|
150
220
|
);
|
|
151
221
|
}
|
|
152
222
|
return createTuple(
|
|
153
223
|
runtime.getBehavior(state).map((step) => toStateRecord(step, context.environment)),
|
|
154
224
|
);
|
|
155
225
|
}),
|
|
156
|
-
|
|
226
|
+
defineStateOperator(
|
|
227
|
+
"CounterExample",
|
|
228
|
+
0,
|
|
229
|
+
() => runtime.counterExample ?? createEmptyCounterExample(),
|
|
230
|
+
),
|
|
157
231
|
defineOperator("ToTrace", 1, (args) => {
|
|
158
232
|
const counterExample = getValue(args, 0, "ToTrace");
|
|
233
|
+
if (
|
|
234
|
+
!isFunctionValue(counterExample) ||
|
|
235
|
+
!valuesEqual(getDomain(counterExample), createSet(["action", "state"]))
|
|
236
|
+
) {
|
|
237
|
+
throw new TlaEvaluationError(
|
|
238
|
+
`The argument of ToTrace should be a CounterExample, but instead it is:\n${formatValue(counterExample)}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
159
241
|
const states = toFiniteSet(applyFunction(counterExample, "state")).elements.map(
|
|
160
242
|
(pair) => getSequenceElements(pair) ?? [],
|
|
161
243
|
);
|
|
162
244
|
return createTuple(
|
|
163
|
-
[
|
|
164
|
-
.sort((left, right) => Number(left[0]) - Number(right[0]))
|
|
165
|
-
.map((pair) => pair[1]),
|
|
245
|
+
states.sort((left, right) => Number(left[0]) - Number(right[0])).map((pair) => pair[1]),
|
|
166
246
|
);
|
|
167
247
|
}),
|
|
168
248
|
defineOperator("TLCModelValue", 1, (args) => {
|
|
169
249
|
const name = getValue(args, 0, "TLCModelValue");
|
|
170
250
|
if (typeof name !== "string") {
|
|
171
|
-
throw new
|
|
251
|
+
throw new TlaModuleError(
|
|
172
252
|
`The argument of ModelValue should be a string, but instead it is:\n${formatValue(name)}`,
|
|
173
253
|
);
|
|
174
254
|
}
|
|
175
255
|
return createModelValue(name);
|
|
176
256
|
}),
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return true;
|
|
180
|
-
}),
|
|
257
|
+
// TLC runs the deferred expression only in simulation, when it extends a behavior.
|
|
258
|
+
defineLazyOperator("TLCDefer", 1, () => true),
|
|
181
259
|
defineOperator("TLCNoOp", 1, (args) => getValue(args, 0, "TLCNoOp")),
|
|
182
260
|
defineLazyOperator("PickSuccessor", 1, (args, context) => {
|
|
183
|
-
if (getThunk(args, 0, "PickSuccessor")() === true) return true;
|
|
184
261
|
const next = context?.next;
|
|
185
|
-
if (!next || !isCompleteState(next))
|
|
186
|
-
|
|
262
|
+
if (!next || !isCompleteState(next)) {
|
|
263
|
+
throw createJavaOverrideError(
|
|
264
|
+
PICK_SUCCESSOR_METHOD,
|
|
265
|
+
'Cannot invoke "tlc2.value.IValue.fingerPrint(long)" because "minVals[i]" is null',
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (runtime.isSeen(next)) return true;
|
|
269
|
+
if (expectBoolean(getThunk(args, 0, "PickSuccessor")(), "the condition of PickSuccessor")) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
throw new TlaUnsupportedError(
|
|
273
|
+
"PickSuccessor needs interactive input to extend the behavior with a new state",
|
|
274
|
+
);
|
|
187
275
|
}),
|
|
188
276
|
defineLazyOperator("TLCCache", 2, (args, context) => {
|
|
189
277
|
const expression = getOperator(args, 0, "TLCCache");
|
|
@@ -200,18 +288,20 @@ export const createTlcExtOperators = (
|
|
|
200
288
|
cache.set(closure, value);
|
|
201
289
|
return value;
|
|
202
290
|
}),
|
|
203
|
-
defineOperator("TLCFP", 1, (args) =>
|
|
291
|
+
defineOperator("TLCFP", 1, (args) =>
|
|
292
|
+
Number(BigInt.asIntN(32, getTlcFingerprint([getValue(args, 0, "TLCFP")]))),
|
|
293
|
+
),
|
|
204
294
|
defineStateOperator("TLCEvalDefinition", 1, (args, context) => {
|
|
205
295
|
const name = getValue(args, 0, "TLCEvalDefinition");
|
|
206
296
|
const definition =
|
|
207
297
|
typeof name === "string" && context ? getDefinition(name, context.environment) : undefined;
|
|
208
298
|
if (!definition || typeof name !== "string" || !context) {
|
|
209
|
-
throw new
|
|
299
|
+
throw new TlaModuleError(
|
|
210
300
|
`The argument of TLCEvalDefinition should be a name of a definition reachable from the root module, but instead it is:\n${formatValue(name)}`,
|
|
211
301
|
);
|
|
212
302
|
}
|
|
213
303
|
if (definition.kind === "Operator" && definition.params.length > 0) {
|
|
214
|
-
throw new
|
|
304
|
+
throw new TlaModuleError(
|
|
215
305
|
`The argument of TLCEvalDefinition should be a zero-arity definition, but instead it is:\n${name}`,
|
|
216
306
|
);
|
|
217
307
|
}
|
|
@@ -225,12 +315,9 @@ export const createPossibleOperators = (
|
|
|
225
315
|
runtime: CheckerRuntime,
|
|
226
316
|
): Builtin[] => [
|
|
227
317
|
defineStateOperator("_Track", 2, (args) => {
|
|
228
|
-
const name = getValue(args, 1, "_Track");
|
|
229
|
-
const count = runtime.possibleCounts.get(
|
|
230
|
-
runtime.possibleCounts.set(
|
|
231
|
-
String(name),
|
|
232
|
-
getValue(args, 0, "_Track") === true ? count + 1 : count,
|
|
233
|
-
);
|
|
318
|
+
const name = String(getValue(args, 1, "_Track"));
|
|
319
|
+
const count = runtime.possibleCounts.get(name) ?? 0;
|
|
320
|
+
runtime.possibleCounts.set(name, getValue(args, 0, "_Track") === true ? count + 1 : count);
|
|
234
321
|
return true;
|
|
235
322
|
}),
|
|
236
323
|
defineStateOperator("_Counts", 0, () => getPossibleCounts(runtime)),
|