tla-ts 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -41
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +166 -30
- package/dist/index.d.ts +166 -30
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +2 -2
- package/dist/parallel-worker.js +2 -2
- package/dist/parallel.cjs +1 -1
- package/dist/parallel.js +2 -2
- package/dist/run-cli.cjs +2 -1
- package/dist/run-cli.js +2 -1
- package/dist/transfer.cjs +388 -39
- package/dist/transfer.js +388 -39
- package/package.json +4 -2
- package/src/ast.ts +82 -5
- package/src/bags.ts +161 -71
- package/src/builtins.ts +203 -23
- package/src/bundled-modules.ts +63 -32
- package/src/check.ts +904 -230
- package/src/codegen.ts +52 -18
- package/src/config.ts +39 -11
- package/src/environment.ts +485 -140
- package/src/errors.ts +22 -0
- package/src/evaluate.ts +486 -136
- package/src/exit-codes.ts +75 -0
- package/src/fingerprints.ts +2 -2
- package/src/index.ts +9 -4
- package/src/instances.ts +221 -66
- package/src/java-overrides.ts +241 -0
- package/src/json.ts +153 -72
- package/src/levels.ts +850 -0
- package/src/lexer.ts +97 -27
- package/src/liveness.ts +47 -91
- package/src/parallel-worker.ts +57 -28
- package/src/parallel.ts +116 -62
- package/src/parser.ts +703 -183
- package/src/pretty-print.ts +259 -0
- package/src/randomization.ts +24 -113
- package/src/resolve.ts +472 -41
- package/src/run-cli.ts +142 -44
- package/src/runtime.ts +63 -28
- package/src/spec-description.ts +509 -0
- package/src/standard-levels.ts +105 -0
- package/src/standard-modules.ts +223 -92
- package/src/subexpressions.ts +21 -35
- package/src/substitution-levels.ts +184 -0
- package/src/successors.ts +261 -46
- package/src/symmetry.ts +65 -21
- package/src/tableau.ts +43 -37
- package/src/tlc-ext.ts +120 -33
- package/src/tlc-module.ts +71 -47
- package/src/tlc-modules.ts +1022 -0
- package/src/tlc-order.ts +497 -0
- package/src/tlc-random.ts +411 -0
- package/src/tlc-summary.ts +109 -0
- package/src/transfer.ts +4 -4
- package/src/values.ts +1186 -164
package/src/check.ts
CHANGED
|
@@ -1,41 +1,77 @@
|
|
|
1
|
-
import type { Assumption, Expression, SourceLocation } from "./ast";
|
|
2
|
-
import { parseConfig, type ModelConfig } from "./config";
|
|
1
|
+
import type { Assumption, Definition, Expression, SourceLocation } from "./ast";
|
|
2
|
+
import { parseConfig, type ConfigValue, type ModelConfig } from "./config";
|
|
3
3
|
import { getReadVariables } from "./dependencies";
|
|
4
4
|
import { createEnvironment } from "./environment";
|
|
5
|
-
import { TlaConfigError, TlaEvaluationError,
|
|
5
|
+
import { getErrorExitCode, TlaConfigError, TlaEvaluationError, TlaModuleError } from "./errors";
|
|
6
|
+
import { EXIT_STATUS, setExitCode, setNextStatePhase, withLivenessExitCode } from "./exit-codes";
|
|
6
7
|
import {
|
|
8
|
+
describeRange,
|
|
7
9
|
evaluate,
|
|
8
10
|
expectBoolean,
|
|
11
|
+
formatRange,
|
|
12
|
+
getChildren,
|
|
9
13
|
getDefinition,
|
|
14
|
+
isConstantLevel,
|
|
15
|
+
isImpure,
|
|
10
16
|
type Context,
|
|
11
17
|
type Environment,
|
|
12
18
|
type Scope,
|
|
13
19
|
} from "./evaluate";
|
|
14
20
|
import {
|
|
15
|
-
ACTION_LEVEL,
|
|
16
21
|
createLivenessChecker,
|
|
17
22
|
expandTemporal,
|
|
18
23
|
getLevel,
|
|
19
|
-
|
|
20
|
-
toUnscopedExpression,
|
|
24
|
+
type Lasso,
|
|
21
25
|
type LiveSuccessor,
|
|
22
26
|
type ScopedExpression,
|
|
23
27
|
type TemporalProperty,
|
|
24
28
|
} from "./liveness";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
29
|
+
import { ACTION_LEVEL, createLevelAnalyzer, TEMPORAL_LEVEL } from "./levels";
|
|
30
|
+
import { prettyPrint } from "./pretty-print";
|
|
31
|
+
import {
|
|
32
|
+
forEachInitialState,
|
|
33
|
+
forEachSuccessor,
|
|
34
|
+
getInitialStates,
|
|
35
|
+
getSuccessors,
|
|
36
|
+
type Successor,
|
|
37
|
+
} from "./successors";
|
|
38
|
+
import {
|
|
39
|
+
getActionLabel,
|
|
40
|
+
getNextActions,
|
|
41
|
+
getSpecificationValue,
|
|
42
|
+
type SpecAction,
|
|
43
|
+
} from "./spec-description";
|
|
27
44
|
import { getRepresentative, getSymmetryGroup } from "./symmetry";
|
|
45
|
+
import { getTlcStringOrder, getTlcVariableOrder } from "./tlc-order";
|
|
46
|
+
import { createTlcFingerprinter, getTlcFingerprint } from "./tlc-random";
|
|
28
47
|
import { createCanonicalizer } from "./canonical";
|
|
29
48
|
import { getPossibleCounts, type CheckerRuntime } from "./runtime";
|
|
30
|
-
import {
|
|
49
|
+
import { createCounterExample } from "./tlc-ext";
|
|
50
|
+
import { getFingerprintDistance, OUT_DEGREE_BUCKETS } from "./tlc-summary";
|
|
51
|
+
import {
|
|
52
|
+
combineHash,
|
|
53
|
+
formatValue,
|
|
54
|
+
getValueHash,
|
|
55
|
+
isFunctionValue,
|
|
56
|
+
isSameValue,
|
|
57
|
+
setStringOrder,
|
|
58
|
+
toFiniteFunction,
|
|
59
|
+
type Value,
|
|
60
|
+
} from "./values";
|
|
31
61
|
|
|
32
62
|
export interface CheckOptions {
|
|
33
63
|
source: string;
|
|
34
64
|
config: string | ModelConfig;
|
|
35
65
|
loadModule?: (name: string) => string | undefined;
|
|
66
|
+
loadClass?: (name: string) => Uint8Array | undefined;
|
|
36
67
|
onPrint?: (text: string) => void;
|
|
68
|
+
onPause?: () => void;
|
|
37
69
|
readFile?: (path: string) => string | undefined;
|
|
38
70
|
writeFile?: (path: string, content: string) => void;
|
|
71
|
+
seed?: bigint;
|
|
72
|
+
workers?: number;
|
|
73
|
+
// TLC's -lncheck: "final" checks liveness only on the complete state graph.
|
|
74
|
+
livenessCheck?: "default" | "final";
|
|
39
75
|
}
|
|
40
76
|
|
|
41
77
|
export type CheckStatus =
|
|
@@ -45,10 +81,13 @@ export type CheckStatus =
|
|
|
45
81
|
| "property-violation"
|
|
46
82
|
| "liveness-violation"
|
|
47
83
|
| "postcondition-violation"
|
|
48
|
-
| "deadlock"
|
|
84
|
+
| "deadlock"
|
|
85
|
+
| "error";
|
|
49
86
|
|
|
50
87
|
export interface TraceStep {
|
|
51
88
|
action: string | undefined;
|
|
89
|
+
// TLC's name, parameters and location for the action that took this step.
|
|
90
|
+
label: string | undefined;
|
|
52
91
|
state: Record<string, Value>;
|
|
53
92
|
}
|
|
54
93
|
|
|
@@ -56,41 +95,68 @@ export interface Violation {
|
|
|
56
95
|
name: string;
|
|
57
96
|
trace: TraceStep[];
|
|
58
97
|
loopStart: number | undefined;
|
|
98
|
+
loopLabel?: string;
|
|
59
99
|
message?: string;
|
|
100
|
+
counts?: string;
|
|
101
|
+
unwitnessed?: Postcondition;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface Postcondition {
|
|
105
|
+
name: string;
|
|
106
|
+
message: string;
|
|
107
|
+
counts?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// An evaluation that failed during the search, with the behavior up to the state TLC was working on.
|
|
111
|
+
export interface EvaluationFailure {
|
|
112
|
+
message: string;
|
|
113
|
+
trace: TraceStep[];
|
|
114
|
+
initialState: Record<string, Value> | undefined;
|
|
115
|
+
exitCode: number;
|
|
60
116
|
}
|
|
61
117
|
|
|
62
118
|
export interface CheckResult {
|
|
63
119
|
status: CheckStatus;
|
|
64
120
|
violation: Violation | undefined;
|
|
121
|
+
failure: EvaluationFailure | undefined;
|
|
65
122
|
generatedStates: number;
|
|
66
123
|
distinctStates: number;
|
|
124
|
+
queuedStates: number;
|
|
67
125
|
depth: number;
|
|
126
|
+
// How many states that TLC counts as expanded found each number of new states, when TLC prints
|
|
127
|
+
// the outdegree of the state graph.
|
|
128
|
+
outDegrees: number[] | undefined;
|
|
129
|
+
// The smallest distance between the fingerprints of two distinct states, when TLC estimates a
|
|
130
|
+
// collision from the actual fingerprints.
|
|
131
|
+
fingerprintDistance: bigint | undefined;
|
|
68
132
|
}
|
|
69
133
|
|
|
70
|
-
|
|
134
|
+
interface NamedExpression {
|
|
71
135
|
name: string;
|
|
72
136
|
expression: Expression;
|
|
73
137
|
scope: Scope | undefined;
|
|
74
138
|
}
|
|
75
139
|
|
|
76
|
-
|
|
140
|
+
interface StateCheck extends NamedExpression {
|
|
77
141
|
variables: number[] | undefined;
|
|
78
142
|
}
|
|
79
143
|
|
|
80
|
-
|
|
144
|
+
interface ActionProperty {
|
|
81
145
|
name: string;
|
|
146
|
+
box: Expression;
|
|
82
147
|
action: Expression;
|
|
83
148
|
subscript: Expression;
|
|
84
149
|
scope: Scope | undefined;
|
|
85
150
|
}
|
|
86
151
|
|
|
87
|
-
|
|
88
|
-
init: Expression;
|
|
89
|
-
next: Expression;
|
|
152
|
+
interface Specification {
|
|
153
|
+
init: Expression | undefined;
|
|
154
|
+
next: Expression | undefined;
|
|
155
|
+
scope: Scope | undefined;
|
|
90
156
|
temporals: ScopedExpression[];
|
|
91
157
|
}
|
|
92
158
|
|
|
93
|
-
|
|
159
|
+
interface Properties {
|
|
94
160
|
initialPredicates: NamedExpression[];
|
|
95
161
|
invariants: NamedExpression[];
|
|
96
162
|
actionProperties: ActionProperty[];
|
|
@@ -99,18 +165,106 @@ export interface Properties {
|
|
|
99
165
|
|
|
100
166
|
const START: SourceLocation = { line: 0, column: 0 };
|
|
101
167
|
|
|
168
|
+
const INITIAL_RECOVERY_FAILURE = "Failed to recover the initial state from its fingerprint.";
|
|
169
|
+
const NEXT_RECOVERY_FAILURE = "Failed to recover the next state from its fingerprint.";
|
|
170
|
+
|
|
102
171
|
const createIdentifier = (name: string): Expression => ({
|
|
103
172
|
kind: "Identifier",
|
|
104
173
|
name,
|
|
105
174
|
location: START,
|
|
106
175
|
});
|
|
107
176
|
|
|
108
|
-
|
|
177
|
+
// TLC leaves the kind out when it rejects arguments to the specification or a property.
|
|
178
|
+
const UNNAMED_ARGUMENT_KINDS = new Set(["name", "property"]);
|
|
179
|
+
|
|
180
|
+
// TLC checks liveness on the graph so far 3 seconds into the search and then every minute, once
|
|
181
|
+
// the graph has grown by a tenth and liveness checking has taken at most a fifth of the time.
|
|
182
|
+
const PERIODIC_START = 3000;
|
|
183
|
+
const PROGRESS_INTERVAL = 60_000;
|
|
184
|
+
const LIVENESS_GROWTH = 0.1;
|
|
185
|
+
const LIVENESS_RATIO = 0.2;
|
|
186
|
+
|
|
187
|
+
const getConfigOperator = (kind: string, name: string, environment: Environment) => {
|
|
109
188
|
const definition = getDefinition(name, environment);
|
|
110
|
-
if (definition
|
|
111
|
-
throw new TlaConfigError(
|
|
189
|
+
if (!definition) {
|
|
190
|
+
throw new TlaConfigError(
|
|
191
|
+
`The ${kind} ${name} specified in the configuration file\nis not defined in the specification.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (definition.kind === "Operator" && definition.params.length > 0) {
|
|
195
|
+
throw new TlaConfigError(
|
|
196
|
+
UNNAMED_ARGUMENT_KINDS.has(kind)
|
|
197
|
+
? `TLC requires ${name} not to take any argument.`
|
|
198
|
+
: `TLC requires ${kind} not to take any argument, but one was given: ${name}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
return definition;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const getConfigDefinition = (kind: string, name: string, environment: Environment) => {
|
|
205
|
+
const definition = getConfigOperator(kind, name, environment);
|
|
206
|
+
if (isConstantLevel(definition, environment)) {
|
|
207
|
+
throw new TlaConfigError(`The ${kind} ${name} cannot be a constant.`);
|
|
208
|
+
}
|
|
209
|
+
return definition;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// TLC evaluates every constant definition before it reads the configuration. It keeps a literal
|
|
213
|
+
// number or string, and a definition that fails to evaluate, as an operator.
|
|
214
|
+
const getConstantValue = (name: string, definition: Definition, environment: Environment) => {
|
|
215
|
+
if (!isConstantLevel(definition, environment)) return undefined;
|
|
216
|
+
if (definition.body.kind === "Number" || definition.body.kind === "String") return undefined;
|
|
217
|
+
try {
|
|
218
|
+
return evaluate(createIdentifier(name), getContext(environment, undefined, undefined));
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof TlaEvaluationError) return undefined;
|
|
221
|
+
throw error;
|
|
112
222
|
}
|
|
113
|
-
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const getConfigPredicate = (kind: string, name: string, environment: Environment) => {
|
|
226
|
+
const definition = getConfigOperator(kind, name, environment);
|
|
227
|
+
const value = getConstantValue(name, definition, environment);
|
|
228
|
+
if (value !== undefined && value !== true) {
|
|
229
|
+
throw new TlaConfigError(`The ${kind} of ${name} is equal to ${formatValue(value)}`);
|
|
230
|
+
}
|
|
231
|
+
return value === undefined ? definition : undefined;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const getInvariants = (names: string[], environment: Environment) => {
|
|
235
|
+
const analyzer = createLevelAnalyzer({
|
|
236
|
+
constants: new Set(environment.constants.keys()),
|
|
237
|
+
variables: new Set(environment.variables),
|
|
238
|
+
getDefinition: (name) => getDefinition(name, environment),
|
|
239
|
+
});
|
|
240
|
+
return names.flatMap((name) => {
|
|
241
|
+
const definition = getConfigPredicate("invariant", name, environment);
|
|
242
|
+
if (!definition) return [];
|
|
243
|
+
const levels = analyzer.analyzeDefinition(definition, new Map());
|
|
244
|
+
if (levels.level >= ACTION_LEVEL) {
|
|
245
|
+
const note =
|
|
246
|
+
levels.params.size === 0
|
|
247
|
+
? ""
|
|
248
|
+
: "\nNote that a bug can cause TLC to incorrectly report this error.\nIf you believe your TLA+ or PlusCal specification to be correct,\nplease check if this bug described in LevelNode.java starting at line 590ff affects you.";
|
|
249
|
+
throw new TlaConfigError(
|
|
250
|
+
`The invariant ${name} is not a state predicate (one with no primes or temporal operators).${note}`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return [{ name, expression: createIdentifier(name), scope: undefined }];
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const toAliasRecord = (value: Value, fieldOrder: string[]) => {
|
|
258
|
+
if (!isFunctionValue(value)) return undefined;
|
|
259
|
+
const record = toFiniteFunction(value);
|
|
260
|
+
if (!record.domain.every((key) => typeof key === "string")) return undefined;
|
|
261
|
+
const entries = record.domain.map((key, index): [string, Value] => [
|
|
262
|
+
String(key),
|
|
263
|
+
record.range[index],
|
|
264
|
+
]);
|
|
265
|
+
return Object.fromEntries(
|
|
266
|
+
entries.sort((left, right) => fieldOrder.indexOf(left[0]) - fieldOrder.indexOf(right[0])),
|
|
267
|
+
);
|
|
114
268
|
};
|
|
115
269
|
|
|
116
270
|
const getConjuncts = (expression: Expression, environment: Environment): Expression[] => {
|
|
@@ -125,39 +279,66 @@ const getConjuncts = (expression: Expression, environment: Environment): Express
|
|
|
125
279
|
return [expression];
|
|
126
280
|
};
|
|
127
281
|
|
|
128
|
-
|
|
282
|
+
const isConstantTrue = (item: ScopedExpression, environment: Environment, usage: string) => {
|
|
283
|
+
const conjunct = item.expression;
|
|
284
|
+
if (conjunct.kind !== "Identifier" || getLevel(conjunct, item.scope, environment) > 0) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const value = evaluate(conjunct, getContext(environment, undefined, undefined, item.scope));
|
|
288
|
+
if (value === false) {
|
|
289
|
+
throw new TlaConfigError(`The spec is trivially false because ${conjunct.name} is false.`);
|
|
290
|
+
}
|
|
291
|
+
if (value !== true) {
|
|
292
|
+
throw new TlaConfigError(
|
|
293
|
+
`The operator ${conjunct.name}, which equals ${formatValue(value)},\ncannot be used as a ${usage}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return true;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const getSpecification = (config: ModelConfig, environment: Environment): Specification => {
|
|
129
300
|
if (config.specification === undefined) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
301
|
+
const getOperator = (name: string | undefined, description: string) => {
|
|
302
|
+
if (name === undefined) return undefined;
|
|
303
|
+
const body = getConfigOperator(description, name, environment).body;
|
|
304
|
+
const identifier = createIdentifier(name);
|
|
305
|
+
// TLC evaluates a literal body later and reports that it is not a boolean.
|
|
306
|
+
const isLiteral = body.kind === "Number" || body.kind === "String";
|
|
307
|
+
if (!isLiteral && getLevel(identifier, undefined, environment) === 0) {
|
|
308
|
+
throw new TlaConfigError(`The ${description} ${name} cannot be a constant.`);
|
|
309
|
+
}
|
|
310
|
+
return identifier;
|
|
311
|
+
};
|
|
138
312
|
return {
|
|
139
|
-
init:
|
|
140
|
-
next:
|
|
313
|
+
init: getOperator(config.init, "initial predicate"),
|
|
314
|
+
next: getOperator(config.next, "next state action"),
|
|
315
|
+
scope: undefined,
|
|
141
316
|
temporals: [],
|
|
142
317
|
};
|
|
143
318
|
}
|
|
144
|
-
const initConjuncts:
|
|
145
|
-
const nextActions:
|
|
319
|
+
const initConjuncts: ScopedExpression[] = [];
|
|
320
|
+
const nextActions: ScopedExpression[] = [];
|
|
146
321
|
const temporals: ScopedExpression[] = [];
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
322
|
+
const definition = getConfigOperator("name", config.specification, environment);
|
|
323
|
+
const value = getConstantValue(config.specification, definition, environment);
|
|
324
|
+
if (value !== undefined) {
|
|
325
|
+
throw new TlaConfigError(
|
|
326
|
+
`The value of ${config.specification} is equal to ${formatValue(value)}`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
expandTemporal(definition.body, undefined, environment, false).forEach((item) => {
|
|
330
|
+
const conjunct = item.expression;
|
|
331
|
+
if (isConstantTrue(item, environment, "spec")) return;
|
|
332
|
+
if (conjunct.kind === "UnboundedQuantifier" && conjunct.isTemporal) {
|
|
333
|
+
throw setExitCode(
|
|
334
|
+
new TlaConfigError(
|
|
335
|
+
"TLC does not support temporal existential, nor universal, quantification over state variables.",
|
|
336
|
+
),
|
|
337
|
+
EXIT_STATUS.error,
|
|
153
338
|
);
|
|
154
339
|
}
|
|
155
|
-
return unscoped;
|
|
156
|
-
};
|
|
157
|
-
expandTemporal(body, undefined, environment).forEach((item) => {
|
|
158
|
-
const conjunct = item.expression;
|
|
159
340
|
if (conjunct.kind === "Always" && conjunct.expression.kind === "BoxAction") {
|
|
160
|
-
nextActions.push(
|
|
341
|
+
nextActions.push({ expression: conjunct.expression.action, scope: item.scope });
|
|
161
342
|
return;
|
|
162
343
|
}
|
|
163
344
|
if (
|
|
@@ -178,21 +359,54 @@ export const getSpecification = (config: ModelConfig, environment: Environment):
|
|
|
178
359
|
`TLC cannot handle the action-level conjunct of the specification ${config.specification}`,
|
|
179
360
|
);
|
|
180
361
|
}
|
|
181
|
-
initConjuncts.push(
|
|
362
|
+
initConjuncts.push(item);
|
|
182
363
|
});
|
|
183
|
-
if (nextActions.length
|
|
364
|
+
if (nextActions.length > 1) {
|
|
184
365
|
throw new TlaConfigError(
|
|
185
|
-
`The specification ${config.specification} must contain
|
|
366
|
+
`The specification ${config.specification} must contain at most one [][Next]_vars`,
|
|
186
367
|
);
|
|
187
368
|
}
|
|
369
|
+
const items = [...initConjuncts, ...nextActions];
|
|
370
|
+
const getName = (index: number) => `$specification${index}`;
|
|
371
|
+
const scope = items.reduce<Scope | undefined>(
|
|
372
|
+
(parent, item, index) =>
|
|
373
|
+
item.scope === undefined
|
|
374
|
+
? parent
|
|
375
|
+
: {
|
|
376
|
+
name: getName(index),
|
|
377
|
+
binding: {
|
|
378
|
+
kind: "Thunk",
|
|
379
|
+
expression: item.expression,
|
|
380
|
+
scope: item.scope,
|
|
381
|
+
state: undefined,
|
|
382
|
+
next: undefined,
|
|
383
|
+
cache: undefined,
|
|
384
|
+
},
|
|
385
|
+
parent,
|
|
386
|
+
},
|
|
387
|
+
undefined,
|
|
388
|
+
);
|
|
389
|
+
const getReference = (item: ScopedExpression, index: number): Expression =>
|
|
390
|
+
item.scope === undefined
|
|
391
|
+
? item.expression
|
|
392
|
+
: {
|
|
393
|
+
kind: "Identifier",
|
|
394
|
+
name: getName(index),
|
|
395
|
+
location: item.expression.location,
|
|
396
|
+
resolution: { kind: "Local" },
|
|
397
|
+
};
|
|
188
398
|
return {
|
|
189
|
-
init:
|
|
190
|
-
|
|
399
|
+
init:
|
|
400
|
+
initConjuncts.length === 0
|
|
401
|
+
? undefined
|
|
402
|
+
: { kind: "And", operands: initConjuncts.map(getReference), location: START },
|
|
403
|
+
next: nextActions.length === 0 ? undefined : getReference(nextActions[0], initConjuncts.length),
|
|
404
|
+
scope,
|
|
191
405
|
temporals,
|
|
192
406
|
};
|
|
193
407
|
};
|
|
194
408
|
|
|
195
|
-
|
|
409
|
+
const getProperties = (config: ModelConfig, environment: Environment): Properties => {
|
|
196
410
|
const properties: Properties = {
|
|
197
411
|
initialPredicates: [],
|
|
198
412
|
invariants: [],
|
|
@@ -200,12 +414,15 @@ export const getProperties = (config: ModelConfig, environment: Environment): Pr
|
|
|
200
414
|
liveness: [],
|
|
201
415
|
};
|
|
202
416
|
config.properties.forEach((name) => {
|
|
203
|
-
const
|
|
204
|
-
|
|
417
|
+
const definition = getConfigPredicate("property", name, environment);
|
|
418
|
+
if (!definition) return;
|
|
419
|
+
expandTemporal(definition.body, undefined, environment, true).forEach((item) => {
|
|
205
420
|
const conjunct = item.expression;
|
|
421
|
+
if (isConstantTrue(item, environment, "property")) return;
|
|
206
422
|
if (conjunct.kind === "Always" && conjunct.expression.kind === "BoxAction") {
|
|
207
423
|
properties.actionProperties.push({
|
|
208
424
|
name,
|
|
425
|
+
box: conjunct.expression,
|
|
209
426
|
action: conjunct.expression.action,
|
|
210
427
|
subscript: conjunct.expression.subscript,
|
|
211
428
|
scope: item.scope,
|
|
@@ -233,13 +450,38 @@ export const getProperties = (config: ModelConfig, environment: Environment): Pr
|
|
|
233
450
|
return properties;
|
|
234
451
|
};
|
|
235
452
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
453
|
+
const getNamedExpressions = (
|
|
454
|
+
kind: string,
|
|
455
|
+
names: string[],
|
|
456
|
+
environment: Environment,
|
|
457
|
+
): NamedExpression[] =>
|
|
458
|
+
names.flatMap((name) =>
|
|
459
|
+
getConfigPredicate(kind, name, environment)
|
|
460
|
+
? [{ name, expression: createIdentifier(name), scope: undefined }]
|
|
461
|
+
: [],
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
const getBodyRange = (name: string, environment: Environment) => {
|
|
465
|
+
const definition = getDefinition(name, environment);
|
|
466
|
+
return definition?.kind === "Operator" ? definition.bodyRange : undefined;
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const getPossiblePredicates = (names: string[], environment: Environment) =>
|
|
470
|
+
names.map((name, index) => {
|
|
471
|
+
if (names.indexOf(name) !== index) {
|
|
472
|
+
throw new TlaConfigError(`_POSSIBLE lists ${name} more than once`);
|
|
473
|
+
}
|
|
474
|
+
getConfigDefinition("possible", name, environment);
|
|
475
|
+
const expression = createIdentifier(name);
|
|
476
|
+
if (getLevel(expression, undefined, environment) === TEMPORAL_LEVEL) {
|
|
477
|
+
throw new TlaConfigError(
|
|
478
|
+
`The possible of ${name} is equal to a temporal formula; only state- and action-level predicates are supported`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return { name, expression, scope: undefined };
|
|
240
482
|
});
|
|
241
483
|
|
|
242
|
-
|
|
484
|
+
const getStateChecks = (named: NamedExpression[], environment: Environment): StateCheck[] =>
|
|
243
485
|
named.flatMap((item) =>
|
|
244
486
|
getConjuncts(item.expression, environment).map((expression) => ({
|
|
245
487
|
name: item.name,
|
|
@@ -249,11 +491,7 @@ export const getStateChecks = (named: NamedExpression[], environment: Environmen
|
|
|
249
491
|
})),
|
|
250
492
|
);
|
|
251
493
|
|
|
252
|
-
|
|
253
|
-
stateCheck: StateCheck,
|
|
254
|
-
state: Value[],
|
|
255
|
-
parent: Value[] | undefined,
|
|
256
|
-
) => {
|
|
494
|
+
const isUnchangedFor = (stateCheck: StateCheck, state: Value[], parent: Value[] | undefined) => {
|
|
257
495
|
const variables = stateCheck.variables;
|
|
258
496
|
if (!parent || !variables) return false;
|
|
259
497
|
for (let index = 0; index < variables.length; index++) {
|
|
@@ -262,10 +500,40 @@ export const isUnchangedFor = (
|
|
|
262
500
|
return true;
|
|
263
501
|
};
|
|
264
502
|
|
|
265
|
-
|
|
503
|
+
const WIDE_CHARACTER = /[\u0100-\uffff]/;
|
|
504
|
+
|
|
505
|
+
const DESERIALIZERS = new Set(["JsonDeserialize", "ndJsonDeserialize"]);
|
|
506
|
+
|
|
507
|
+
const isWideConfigValue = (value: ConfigValue): boolean =>
|
|
508
|
+
value.kind === "String"
|
|
509
|
+
? WIDE_CHARACTER.test(value.value)
|
|
510
|
+
: value.kind === "Set" && value.elements.some(isWideConfigValue);
|
|
511
|
+
|
|
512
|
+
const isWideExpression = (expression: Expression): boolean =>
|
|
513
|
+
(expression.kind === "String" && WIDE_CHARACTER.test(expression.value)) ||
|
|
514
|
+
(expression.kind === "Apply" && DESERIALIZERS.has(expression.operator)) ||
|
|
515
|
+
getChildren(expression).some(isWideExpression);
|
|
516
|
+
|
|
517
|
+
// TLC fingerprints a string by the low byte of each character, so it merges states whose
|
|
518
|
+
// strings differ only above U+00FF. Strings reach a state from literals, the config, or files.
|
|
519
|
+
const hasWideString = (config: ModelConfig, environment: Environment) =>
|
|
520
|
+
config.constants.some(
|
|
521
|
+
(constant) =>
|
|
522
|
+
isWideConfigValue(constant.value) || (constant.args ?? []).some(isWideConfigValue),
|
|
523
|
+
) ||
|
|
524
|
+
[...environment.definitions.values()].some(
|
|
525
|
+
(definition) =>
|
|
526
|
+
isWideExpression(definition.body) ||
|
|
527
|
+
(definition.kind === "Function" &&
|
|
528
|
+
definition.bounds.some((bound) => isWideExpression(bound.set))),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
interface Search {
|
|
266
532
|
properties: Properties;
|
|
267
533
|
spec: Specification;
|
|
268
534
|
getFingerprintState: (state: Value[]) => Value[];
|
|
535
|
+
getStoredFingerprints: (states: Value[][], fingerprintStates: Value[][]) => BigInt64Array;
|
|
536
|
+
getTlcOrder: () => number[];
|
|
269
537
|
isSymmetric: boolean;
|
|
270
538
|
findViolatedInvariant: (state: Value[], parent?: Value[]) => StateCheck | undefined;
|
|
271
539
|
satisfiesConstraints: (state: Value[], parent?: Value[]) => boolean;
|
|
@@ -274,9 +542,8 @@ export interface Search {
|
|
|
274
542
|
satisfiesActionConstraints: (state: Value[], next: Value[]) => boolean;
|
|
275
543
|
trackStatePossible: (state: Value[]) => void;
|
|
276
544
|
trackActionPossible: (state: Value[], next: Value[]) => void;
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
) => Omit<Violation, "loopStart"> | undefined;
|
|
545
|
+
findUnwitnessedPossible: () => Postcondition | undefined;
|
|
546
|
+
findPostconditionViolation: () => Postcondition | undefined;
|
|
280
547
|
}
|
|
281
548
|
|
|
282
549
|
const getContext = (
|
|
@@ -284,11 +551,39 @@ const getContext = (
|
|
|
284
551
|
state: Value[] | undefined,
|
|
285
552
|
next: Value[] | undefined,
|
|
286
553
|
scope?: Scope,
|
|
287
|
-
)
|
|
554
|
+
) => ({ environment, scope, state, next });
|
|
288
555
|
|
|
289
556
|
const holds = (expression: Expression, context: Context, description: string) =>
|
|
290
557
|
expectBoolean(evaluate(expression, context), description);
|
|
291
558
|
|
|
559
|
+
// TLC names a check that is not a boolean by the location of the predicate it evaluates.
|
|
560
|
+
const holdsLikeTlc = (
|
|
561
|
+
expression: Expression,
|
|
562
|
+
context: Context,
|
|
563
|
+
getMessage = getExpectedBoolean,
|
|
564
|
+
) => {
|
|
565
|
+
const value = evaluate(expression, context);
|
|
566
|
+
if (typeof value !== "boolean") {
|
|
567
|
+
throw new TlaEvaluationError(getMessage(expression, context.environment));
|
|
568
|
+
}
|
|
569
|
+
return value;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// SANY prints a number by its digits and any other expression by its location.
|
|
573
|
+
const getExpectedBoolean = (expression: Expression, environment: Environment) => {
|
|
574
|
+
const definition =
|
|
575
|
+
expression.kind === "Identifier" ? getDefinition(expression.name, environment) : undefined;
|
|
576
|
+
const body = definition?.kind === "Operator" ? definition.body : expression;
|
|
577
|
+
const range = definition?.kind === "Operator" ? definition.bodyRange : undefined;
|
|
578
|
+
const location =
|
|
579
|
+
body.kind === "Number"
|
|
580
|
+
? (body.text ?? String(body.value))
|
|
581
|
+
: range
|
|
582
|
+
? formatRange(range)
|
|
583
|
+
: describeRange(body);
|
|
584
|
+
return `TLC expected a boolean value, but did not find one. ${location}`;
|
|
585
|
+
};
|
|
586
|
+
|
|
292
587
|
const ignorePossible = () => {};
|
|
293
588
|
|
|
294
589
|
const trackPossible = (items: NamedExpression[], runtime: CheckerRuntime, context: Context) =>
|
|
@@ -320,36 +615,56 @@ export const findAssumptionViolation = (
|
|
|
320
615
|
for (const assumption of assumptions.filter((candidate) => !candidate.isAxiom)) {
|
|
321
616
|
const failure = getConstantFailure(assumption.expression, environment, "an assumption");
|
|
322
617
|
if (failure) {
|
|
323
|
-
|
|
324
|
-
assumption.name ?? `line ${assumption.location.line}, column ${assumption.location.column}`;
|
|
325
|
-
return { name, trace: [], message: failure.message };
|
|
618
|
+
return { name: describeRange(assumption.expression), trace: [], message: failure.message };
|
|
326
619
|
}
|
|
327
620
|
}
|
|
328
621
|
return undefined;
|
|
329
622
|
};
|
|
330
623
|
|
|
331
|
-
export const createSearch = (
|
|
624
|
+
export const createSearch = (
|
|
625
|
+
config: ModelConfig,
|
|
626
|
+
environment: Environment,
|
|
627
|
+
getVariableOrder: () => string[],
|
|
628
|
+
): Search | undefined => {
|
|
629
|
+
const configInvariants = getInvariants(config.invariants, environment);
|
|
630
|
+
const spec = getSpecification(config, environment);
|
|
332
631
|
const properties = getProperties(config, environment);
|
|
333
|
-
const invariants = getStateChecks(
|
|
334
|
-
|
|
632
|
+
const invariants = getStateChecks([...configInvariants, ...properties.invariants], environment);
|
|
633
|
+
const isCheckingBehaviors =
|
|
634
|
+
invariants.length > 0 ||
|
|
635
|
+
properties.actionProperties.length > 0 ||
|
|
636
|
+
properties.liveness.length > 0;
|
|
637
|
+
if (
|
|
638
|
+
spec.init === undefined &&
|
|
639
|
+
(isCheckingBehaviors ||
|
|
640
|
+
properties.initialPredicates.length > 0 ||
|
|
641
|
+
environment.variables.length > 0)
|
|
642
|
+
) {
|
|
643
|
+
throw new TlaConfigError("The configuration file did not specify the initial state predicate.");
|
|
644
|
+
}
|
|
645
|
+
if (spec.next === undefined && isCheckingBehaviors) {
|
|
646
|
+
throw new TlaConfigError("The configuration file did not specify the next state predicate.");
|
|
647
|
+
}
|
|
648
|
+
const constraints = getStateChecks(
|
|
649
|
+
getNamedExpressions("constraint", config.constraints, environment),
|
|
335
650
|
environment,
|
|
336
651
|
);
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
config.
|
|
340
|
-
environment
|
|
341
|
-
|
|
342
|
-
|
|
652
|
+
const actionConstraints = getNamedExpressions(
|
|
653
|
+
"action constraint",
|
|
654
|
+
config.actionConstraints,
|
|
655
|
+
environment,
|
|
656
|
+
);
|
|
657
|
+
if (config.rlReward !== undefined) getConfigOperator("rlreward", config.rlReward, environment);
|
|
658
|
+
if (config.periodic !== undefined) getConfigOperator("periodic", config.periodic, environment);
|
|
659
|
+
const possible = getPossiblePredicates(config.possible, environment);
|
|
343
660
|
if (environment.variables.length === 0) return undefined;
|
|
344
661
|
const symmetryGroup =
|
|
345
662
|
config.symmetry === undefined ? [] : getSymmetryGroup(config.symmetry, environment);
|
|
663
|
+
if (config.view !== undefined) getConfigOperator("view function", config.view, environment);
|
|
346
664
|
const view =
|
|
347
|
-
config.view === undefined
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
environment,
|
|
351
|
-
);
|
|
352
|
-
const actionConstraints = getNamedExpressions(config.actionConstraints, environment);
|
|
665
|
+
config.view === undefined
|
|
666
|
+
? undefined
|
|
667
|
+
: { name: config.view, expression: createIdentifier(config.view), scope: undefined };
|
|
353
668
|
const actionProperties = properties.actionProperties.map((property) => {
|
|
354
669
|
const unchanged: Expression = {
|
|
355
670
|
kind: "Unchanged",
|
|
@@ -358,72 +673,130 @@ export const createSearch = (config: ModelConfig, environment: Environment): Sea
|
|
|
358
673
|
};
|
|
359
674
|
return { ...property, unchanged };
|
|
360
675
|
});
|
|
361
|
-
const possible = getNamedExpressions(config.possible, environment);
|
|
362
676
|
const isActionPossible = (item: NamedExpression) =>
|
|
363
677
|
getLevel(item.expression, undefined, environment) >= ACTION_LEVEL;
|
|
364
678
|
const statePossible = possible.filter((item) => !isActionPossible(item));
|
|
365
679
|
const actionPossible = possible.filter(isActionPossible);
|
|
366
680
|
const runtime = environment.runtime;
|
|
681
|
+
const tlcOrder = { indices: undefined as number[] | undefined };
|
|
682
|
+
const getTlcOrder = () => {
|
|
683
|
+
if (tlcOrder.indices) return tlcOrder.indices;
|
|
684
|
+
const variableOrder = getVariableOrder();
|
|
685
|
+
const getTlcPosition = (index: number) => {
|
|
686
|
+
const position = variableOrder.indexOf(environment.variables[index]);
|
|
687
|
+
return position === -1 ? environment.variables.length : position;
|
|
688
|
+
};
|
|
689
|
+
tlcOrder.indices = environment.variables
|
|
690
|
+
.map((_, index) => index)
|
|
691
|
+
.sort((left, right) => getTlcPosition(left) - getTlcPosition(right));
|
|
692
|
+
return tlcOrder.indices;
|
|
693
|
+
};
|
|
694
|
+
// A representative in TLC's variable order is the one whose fingerprint TLC reports.
|
|
695
|
+
const order = symmetryGroup.length > 0 ? getTlcOrder() : undefined;
|
|
696
|
+
const getViewValue = (expression: Expression, state: Value[]) =>
|
|
697
|
+
evaluate(expression, getContext(environment, state, undefined));
|
|
698
|
+
const getTlcStateFingerprint = (state: Value[]) => {
|
|
699
|
+
const indices = getTlcOrder();
|
|
700
|
+
const representative =
|
|
701
|
+
symmetryGroup.length === 0 ? state : getRepresentative(state, symmetryGroup, indices);
|
|
702
|
+
return getTlcFingerprint(
|
|
703
|
+
view === undefined
|
|
704
|
+
? indices.map((index) => representative[index])
|
|
705
|
+
: [getViewValue(view.expression, representative)],
|
|
706
|
+
);
|
|
707
|
+
};
|
|
708
|
+
const isFingerprintedLikeTlc = hasWideString(config, environment);
|
|
709
|
+
runtime.getFingerprint = getTlcStateFingerprint;
|
|
710
|
+
const findUnwitnessedPossible = () => {
|
|
711
|
+
const unwitnessed = possible.find((item) => !runtime.possibleCounts.get(item.name));
|
|
712
|
+
if (!unwitnessed) return undefined;
|
|
713
|
+
const range = getBodyRange(unwitnessed.name, environment);
|
|
714
|
+
const location = range ? ` at ${formatRange(range)}` : "";
|
|
715
|
+
return {
|
|
716
|
+
name: unwitnessed.name,
|
|
717
|
+
message: `The _POSSIBLE predicate ${unwitnessed.name}${location} was never witnessed.\nOthers may be unwitnessed too; only witnessed predicates appear with a\nnon-zero count in the record printed above.\nSee https://explain.tlapl.us/possible-conditions for additional details.`,
|
|
718
|
+
counts: prettyPrint(formatValue(getPossibleCounts(runtime), true)),
|
|
719
|
+
};
|
|
720
|
+
};
|
|
367
721
|
return {
|
|
368
722
|
properties,
|
|
369
723
|
spec,
|
|
370
724
|
isSymmetric: symmetryGroup.length > 0,
|
|
725
|
+
getTlcOrder,
|
|
726
|
+
getStoredFingerprints: (states, fingerprintStates) => {
|
|
727
|
+
if (view !== undefined || isFingerprintedLikeTlc) {
|
|
728
|
+
return BigInt64Array.from(states, getTlcStateFingerprint);
|
|
729
|
+
}
|
|
730
|
+
const indices = getTlcOrder();
|
|
731
|
+
const getFingerprint = createTlcFingerprinter();
|
|
732
|
+
return BigInt64Array.from(fingerprintStates, (state) =>
|
|
733
|
+
getFingerprint(indices.map((index) => state[index])),
|
|
734
|
+
);
|
|
735
|
+
},
|
|
371
736
|
getFingerprintState: (state) => {
|
|
737
|
+
if (isFingerprintedLikeTlc) return [String(getTlcStateFingerprint(state))];
|
|
372
738
|
const representative =
|
|
373
|
-
symmetryGroup.length === 0 ? state : getRepresentative(state, symmetryGroup);
|
|
374
|
-
return view === undefined
|
|
375
|
-
? representative
|
|
376
|
-
: [evaluate(view.expression, getContext(environment, representative, undefined))];
|
|
739
|
+
symmetryGroup.length === 0 ? state : getRepresentative(state, symmetryGroup, order);
|
|
740
|
+
return view === undefined ? representative : [getViewValue(view.expression, representative)];
|
|
377
741
|
},
|
|
378
742
|
findViolatedInvariant: (state, parent) =>
|
|
379
|
-
invariants.find(
|
|
380
|
-
(invariant)
|
|
381
|
-
|
|
382
|
-
!
|
|
743
|
+
invariants.find((invariant) => {
|
|
744
|
+
if (isUnchangedFor(invariant, state, parent)) return false;
|
|
745
|
+
try {
|
|
746
|
+
return !holdsLikeTlc(
|
|
383
747
|
invariant.expression,
|
|
384
748
|
getContext(environment, state, undefined, invariant.scope),
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
749
|
+
);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
// TLC reports a failure only on a state after the initial states as an invariant failure.
|
|
752
|
+
if (!parent) throw error;
|
|
753
|
+
throw withHeadline(
|
|
754
|
+
setExitCode(error, EXIT_STATUS.safetyEvaluationFailure),
|
|
755
|
+
`Evaluating invariant ${invariant.name} failed.`,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}),
|
|
388
759
|
satisfiesConstraints: (state, parent) =>
|
|
389
760
|
constraints.every(
|
|
390
761
|
(constraint) =>
|
|
391
762
|
isUnchangedFor(constraint, state, parent) ||
|
|
392
|
-
|
|
393
|
-
constraint.expression,
|
|
394
|
-
getContext(environment, state, undefined),
|
|
395
|
-
`constraint ${constraint.name}`,
|
|
396
|
-
),
|
|
763
|
+
holdsLikeTlc(constraint.expression, getContext(environment, state, undefined)),
|
|
397
764
|
),
|
|
398
765
|
findFailedInitialPredicate: (state) =>
|
|
399
766
|
properties.initialPredicates.find(
|
|
400
767
|
(predicate) =>
|
|
401
|
-
!
|
|
768
|
+
!holdsLikeTlc(
|
|
402
769
|
predicate.expression,
|
|
403
770
|
getContext(environment, state, undefined, predicate.scope),
|
|
404
|
-
`property ${predicate.name}`,
|
|
405
771
|
),
|
|
406
772
|
),
|
|
407
773
|
findViolatedActionProperty: (state, next) =>
|
|
408
|
-
actionProperties.find(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
774
|
+
actionProperties.find((property) => {
|
|
775
|
+
try {
|
|
776
|
+
return (
|
|
777
|
+
!holdsLikeTlc(
|
|
778
|
+
property.action,
|
|
779
|
+
getContext(environment, state, next, property.scope),
|
|
780
|
+
() =>
|
|
781
|
+
`Attempted to evaluate an expression of form [A]_e, but A was not a boolean.\n${describeRange(property.box)}`,
|
|
782
|
+
) &&
|
|
783
|
+
!holds(
|
|
784
|
+
property.unchanged,
|
|
785
|
+
getContext(environment, state, next, property.scope),
|
|
786
|
+
"a subscript",
|
|
787
|
+
)
|
|
788
|
+
);
|
|
789
|
+
} catch (error) {
|
|
790
|
+
throw withHeadline(error, `Evaluating action property ${property.name} failed.`);
|
|
791
|
+
}
|
|
792
|
+
}),
|
|
421
793
|
satisfiesActionConstraints: (state, next) =>
|
|
422
794
|
actionConstraints.every((constraint) =>
|
|
423
|
-
|
|
795
|
+
holdsLikeTlc(
|
|
424
796
|
constraint.expression,
|
|
425
797
|
getContext(environment, state, next),
|
|
426
|
-
|
|
798
|
+
(expression) =>
|
|
799
|
+
`TLC threw an unexpected exception.\nThis was probably caused by an error in the spec or model.\nSee the User Output or TLC Console for clues to what happened.\nThe exception was a java.lang.RuntimeException\n: ${getExpectedBoolean(expression, environment)}`,
|
|
427
800
|
),
|
|
428
801
|
),
|
|
429
802
|
trackStatePossible:
|
|
@@ -436,20 +809,14 @@ export const createSearch = (config: ModelConfig, environment: Environment): Sea
|
|
|
436
809
|
? ignorePossible
|
|
437
810
|
: (state, next) =>
|
|
438
811
|
trackPossible(actionPossible, runtime, getContext(environment, state, next)),
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return {
|
|
444
|
-
name: unwitnessed.name,
|
|
445
|
-
trace: [],
|
|
446
|
-
message: `The _POSSIBLE predicate ${unwitnessed.name} was never witnessed.`,
|
|
447
|
-
};
|
|
448
|
-
}
|
|
812
|
+
findUnwitnessedPossible,
|
|
813
|
+
findPostconditionViolation: () => {
|
|
814
|
+
const unwitnessed = findUnwitnessedPossible();
|
|
815
|
+
if (unwitnessed) return unwitnessed;
|
|
449
816
|
for (const name of config.postconditions) {
|
|
450
817
|
const failure = getConstantFailure(createIdentifier(name), environment, "a postcondition");
|
|
451
818
|
if (failure) {
|
|
452
|
-
return { name,
|
|
819
|
+
return { name, message: failure.message ?? `Postcondition ${name} is false.` };
|
|
453
820
|
}
|
|
454
821
|
}
|
|
455
822
|
return undefined;
|
|
@@ -457,37 +824,114 @@ export const createSearch = (config: ModelConfig, environment: Environment): Sea
|
|
|
457
824
|
};
|
|
458
825
|
};
|
|
459
826
|
|
|
460
|
-
|
|
827
|
+
const withHeadline = (error: unknown, headline: string) =>
|
|
828
|
+
error instanceof TlaEvaluationError
|
|
829
|
+
? setExitCode(
|
|
830
|
+
new TlaEvaluationError(`${headline}\n${error.message}`, error),
|
|
831
|
+
getErrorExitCode(error),
|
|
832
|
+
)
|
|
833
|
+
: error;
|
|
834
|
+
|
|
835
|
+
export const checkSequentially = (options: CheckOptions): CheckResult => {
|
|
836
|
+
setNextStatePhase(false);
|
|
461
837
|
const config = typeof options.config === "string" ? parseConfig(options.config) : options.config;
|
|
462
|
-
const
|
|
463
|
-
|
|
838
|
+
const onPrint = options.onPrint ?? console.log;
|
|
839
|
+
// TLC prints again while it rebuilds a trace, between the states it prints. Printing those lines
|
|
840
|
+
// before the trace would misplace them, so rebuilding a trace prints nothing.
|
|
841
|
+
const output = { isMuted: false };
|
|
842
|
+
const print = (text: string) => {
|
|
843
|
+
if (!output.isMuted) onPrint(text);
|
|
844
|
+
};
|
|
845
|
+
const withoutOutput = <Result>(rebuild: () => Result) => {
|
|
846
|
+
output.isMuted = true;
|
|
847
|
+
const result = rebuild();
|
|
848
|
+
output.isMuted = false;
|
|
849
|
+
return result;
|
|
850
|
+
};
|
|
851
|
+
setStringOrder(getTlcStringOrder(options.source, config, options.loadModule));
|
|
852
|
+
const specification = createEnvironment(config, {
|
|
853
|
+
source: options.source,
|
|
464
854
|
loadModule: options.loadModule,
|
|
855
|
+
loadClass: options.loadClass,
|
|
465
856
|
print,
|
|
466
857
|
files: { readFile: options.readFile, writeFile: options.writeFile },
|
|
858
|
+
seed: options.seed,
|
|
467
859
|
});
|
|
468
860
|
const environment = specification.environment;
|
|
469
861
|
const runtime = environment.runtime;
|
|
862
|
+
runtime.pause = options.onPause ?? (() => {});
|
|
863
|
+
runtime.getSpecification = () =>
|
|
864
|
+
getSpecificationValue(
|
|
865
|
+
config,
|
|
866
|
+
environment,
|
|
867
|
+
specification.variableLocations,
|
|
868
|
+
specification.instanceActions,
|
|
869
|
+
);
|
|
470
870
|
const statistics = { depth: 0 };
|
|
471
871
|
const createResult = (
|
|
472
872
|
status: CheckStatus,
|
|
473
873
|
violation?: Omit<Violation, "loopStart"> & { loopStart?: number },
|
|
874
|
+
failure?: EvaluationFailure,
|
|
474
875
|
): CheckResult => ({
|
|
475
876
|
status,
|
|
476
877
|
violation: violation && { ...violation, loopStart: violation.loopStart },
|
|
878
|
+
failure,
|
|
477
879
|
generatedStates: runtime.generatedStates,
|
|
478
880
|
distinctStates: runtime.distinctStates,
|
|
479
|
-
|
|
881
|
+
queuedStates: runtime.queueSize,
|
|
882
|
+
depth: runtime.generatedStates === 0 ? 0 : Math.max(statistics.depth, 1),
|
|
883
|
+
outDegrees: undefined,
|
|
884
|
+
fingerprintDistance: undefined,
|
|
885
|
+
});
|
|
886
|
+
const outDegrees = Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0);
|
|
887
|
+
const expansion = { newStates: 0 };
|
|
888
|
+
const getOutDegreeBucket = () => Math.min(expansion.newStates, OUT_DEGREE_BUCKETS - 1);
|
|
889
|
+
// TLC counts the state it was expanding when an evaluation failed.
|
|
890
|
+
const getPartialOutDegrees = () =>
|
|
891
|
+
outDegrees.map((count, degree) => (degree === getOutDegreeBucket() ? count + 1 : count));
|
|
892
|
+
const createSuccess = (): CheckResult => ({
|
|
893
|
+
...createResult("success"),
|
|
894
|
+
outDegrees: [...outDegrees],
|
|
895
|
+
fingerprintDistance: getFingerprintDistance(
|
|
896
|
+
runtime.distinctStates,
|
|
897
|
+
runtime.generatedStates,
|
|
898
|
+
() =>
|
|
899
|
+
search
|
|
900
|
+
? search.getStoredFingerprints(states, fingerprintStates)
|
|
901
|
+
: BigInt64Array.from(states, runtime.getFingerprint),
|
|
902
|
+
),
|
|
480
903
|
});
|
|
481
904
|
|
|
905
|
+
const search = createSearch(config, environment, () =>
|
|
906
|
+
getTlcVariableOrder(options.source, options.loadModule),
|
|
907
|
+
);
|
|
482
908
|
const assumptionViolation = findAssumptionViolation(specification.assumptions, environment);
|
|
483
909
|
if (assumptionViolation) return createResult("assumption-violation", assumptionViolation);
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
910
|
+
if (!search) return createSuccess();
|
|
911
|
+
const hasPostconditions = config.postconditions.length > 0 || config.possible.length > 0;
|
|
912
|
+
const reportViolation = (
|
|
913
|
+
status: CheckStatus,
|
|
914
|
+
violation: Omit<Violation, "loopStart"> & { loopStart?: number },
|
|
915
|
+
getLoopAction?: () => string | undefined,
|
|
916
|
+
) => {
|
|
917
|
+
if (hasPostconditions) {
|
|
918
|
+
const start = violation.loopStart;
|
|
919
|
+
runtime.counterExample = createCounterExample(
|
|
920
|
+
violation.trace,
|
|
921
|
+
start === undefined || !getLoopAction ? undefined : { start, action: getLoopAction() },
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
const unwitnessed = search.findUnwitnessedPossible();
|
|
925
|
+
if (!unwitnessed) search.findPostconditionViolation();
|
|
926
|
+
return createResult(status, unwitnessed ? { ...violation, unwitnessed } : violation);
|
|
927
|
+
};
|
|
488
928
|
const spec = search.spec;
|
|
489
929
|
const getFingerprintState = search.getFingerprintState;
|
|
490
|
-
const
|
|
930
|
+
const nextActions = { value: undefined as SpecAction[] | undefined };
|
|
931
|
+
const isReducing = config.view !== undefined || config.symmetry !== undefined;
|
|
932
|
+
const liveness = withLivenessExitCode(() =>
|
|
933
|
+
createLivenessChecker(search.properties.liveness, spec.temporals, environment),
|
|
934
|
+
);
|
|
491
935
|
const isCanonicalizedFirst = search.isSymmetric || liveness !== undefined;
|
|
492
936
|
|
|
493
937
|
const states: Value[][] = [];
|
|
@@ -498,30 +942,146 @@ export const check = (options: CheckOptions): CheckResult => {
|
|
|
498
942
|
const buckets = new Map<number, number | number[]>();
|
|
499
943
|
const canonicalizeState = createCanonicalizer();
|
|
500
944
|
|
|
501
|
-
const toRecord = (state: Value[]) =>
|
|
502
|
-
Object.fromEntries(
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
945
|
+
const toRecord = (state: Value[]): Record<string, Value> =>
|
|
946
|
+
Object.fromEntries(
|
|
947
|
+
search.getTlcOrder().map((index) => [environment.variables[index], state[index]]),
|
|
948
|
+
);
|
|
949
|
+
const getAliasState = (
|
|
950
|
+
alias: string,
|
|
951
|
+
steps: Successor[],
|
|
952
|
+
position: number,
|
|
953
|
+
loopStart: number | undefined,
|
|
954
|
+
) => {
|
|
955
|
+
const state = steps[position].state;
|
|
956
|
+
const next = (steps[position + 1] ?? steps[loopStart ?? position]).state;
|
|
957
|
+
// TLC evaluates TLCGet("level") and Trace in an alias at the state's position in the trace.
|
|
958
|
+
runtime.getLevel = () => position + 1;
|
|
959
|
+
runtime.getBehavior = () => steps.slice(0, position + 1).map((step) => step.state);
|
|
960
|
+
try {
|
|
961
|
+
const body = getConfigDefinition("alias", alias, environment).body;
|
|
962
|
+
const value = evaluate(createIdentifier(alias), getContext(environment, state, next));
|
|
963
|
+
const fieldOrder = body.kind === "Record" ? body.fields.map((field) => field.name) : [];
|
|
964
|
+
return toAliasRecord(value, fieldOrder) ?? toRecord(state);
|
|
965
|
+
} catch (error) {
|
|
966
|
+
if (!(error instanceof TlaConfigError || error instanceof TlaEvaluationError)) throw error;
|
|
967
|
+
return { ...toRecord(state), _ALIASEvalError: error.message };
|
|
510
968
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
969
|
+
};
|
|
970
|
+
// TLC labels a step with the first action that takes it, evaluated as when the search expanded
|
|
971
|
+
// the previous state.
|
|
972
|
+
const getLabel = (index: number, previous: Value[], state: Value[]) => {
|
|
973
|
+
nextActions.value ??= getNextActions(config, environment, specification.instanceActions);
|
|
974
|
+
const expanding = { ...current };
|
|
975
|
+
Object.assign(current, { index, isExpanding: true, candidate: undefined });
|
|
976
|
+
const fingerprintState = getFingerprintState(state);
|
|
977
|
+
const action = nextActions.value.find((item) =>
|
|
978
|
+
getSuccessors(item.expression, previous, environment, item.scope).some((successor) =>
|
|
979
|
+
isSameState(getFingerprintState(successor.state), fingerprintState),
|
|
980
|
+
),
|
|
981
|
+
);
|
|
982
|
+
Object.assign(current, expanding);
|
|
983
|
+
return action && getActionLabel(action);
|
|
984
|
+
};
|
|
985
|
+
const toTrace = (steps: Successor[], path: number[], loopStart?: number): TraceStep[] => {
|
|
986
|
+
const alias = config.alias;
|
|
987
|
+
const labels = withoutOutput(() => {
|
|
988
|
+
// TLC rebuilds a trace from the initial predicate, which can reset what TLCSet stored.
|
|
989
|
+
if (steps.length > 1 && isImpure(environment)) getSpecInitialStates();
|
|
990
|
+
// TLC finds the action that takes each step before it evaluates the alias.
|
|
991
|
+
return steps.map((step, position) =>
|
|
992
|
+
position === 0 || step.action === "Stuttering"
|
|
993
|
+
? undefined
|
|
994
|
+
: getLabel(path[position - 1], steps[position - 1].state, step.state),
|
|
995
|
+
);
|
|
524
996
|
});
|
|
997
|
+
return steps.map((step, position) => ({
|
|
998
|
+
action: step.action,
|
|
999
|
+
label: labels[position],
|
|
1000
|
+
state:
|
|
1001
|
+
alias === undefined
|
|
1002
|
+
? toRecord(step.state)
|
|
1003
|
+
: getAliasState(alias, steps, position, loopStart),
|
|
1004
|
+
}));
|
|
1005
|
+
};
|
|
1006
|
+
const getSpecInitialStates = () =>
|
|
1007
|
+
spec.init === undefined ? [] : getInitialStates(spec.init, environment, spec.scope);
|
|
1008
|
+
const getTraceSuccessors = (state: Value[]) =>
|
|
1009
|
+
spec.next === undefined ? [] : getSuccessors(spec.next, state, environment, spec.scope);
|
|
1010
|
+
const hasFingerprint = (state: Value[], index: number) =>
|
|
1011
|
+
isSameState(getFingerprintState(state), fingerprintStates[index]);
|
|
1012
|
+
const findSuccessorWithFingerprint = (state: Value[], index: number) =>
|
|
1013
|
+
getTraceSuccessors(state).find((successor) => hasFingerprint(successor.state, index));
|
|
1014
|
+
const getPathStates = (path: number[], nextFailure: string) => {
|
|
1015
|
+
const initial =
|
|
1016
|
+
isReducing || isImpure(environment)
|
|
1017
|
+
? getSpecInitialStates().find((state) => hasFingerprint(state, path[0]))
|
|
1018
|
+
: states[path[0]];
|
|
1019
|
+
if (!initial) throw new TlaEvaluationError(INITIAL_RECOVERY_FAILURE);
|
|
1020
|
+
const steps: Successor[] = [{ state: initial, action: undefined }];
|
|
1021
|
+
for (let position = 1; position < path.length; position++) {
|
|
1022
|
+
const previous = steps[position - 1].state;
|
|
1023
|
+
if (path[position - 1] === path[position]) {
|
|
1024
|
+
steps.push({ state: previous, action: "Stuttering" });
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
const successor = findSuccessorWithFingerprint(previous, path[position]);
|
|
1028
|
+
if (!successor) throw new TlaEvaluationError(nextFailure);
|
|
1029
|
+
steps.push(successor);
|
|
1030
|
+
}
|
|
1031
|
+
return steps;
|
|
1032
|
+
};
|
|
1033
|
+
const getTrace = (index: number, last?: Successor) => {
|
|
1034
|
+
const path: number[] = [];
|
|
1035
|
+
for (let position = index; position !== -1; position = parents[position]) {
|
|
1036
|
+
path.unshift(position);
|
|
1037
|
+
}
|
|
1038
|
+
const steps =
|
|
1039
|
+
isReducing && path.length > 1
|
|
1040
|
+
? getPathStates(path, INITIAL_RECOVERY_FAILURE)
|
|
1041
|
+
: path.map((position) => ({ state: states[position], action: actions[position] }));
|
|
1042
|
+
if (!last) return toTrace(steps, path);
|
|
1043
|
+
const action = isReducing
|
|
1044
|
+
? getTraceSuccessors(states[index]).find((successor) =>
|
|
1045
|
+
isSameState(successor.state, last.state),
|
|
1046
|
+
)?.action
|
|
1047
|
+
: last.action;
|
|
1048
|
+
return toTrace([...steps, { state: last.state, action }], path);
|
|
1049
|
+
};
|
|
1050
|
+
// TLC prints an initial state on its own without evaluating the alias.
|
|
1051
|
+
const getStateTrace = (index: number) =>
|
|
1052
|
+
parents[index] === -1
|
|
1053
|
+
? [{ action: undefined, label: undefined, state: toRecord(states[index]) }]
|
|
1054
|
+
: getTrace(index);
|
|
1055
|
+
// TLC names the initial state it was checking unless a standard module's Java code failed.
|
|
1056
|
+
const toFailure = (
|
|
1057
|
+
error: unknown,
|
|
1058
|
+
trace: TraceStep[],
|
|
1059
|
+
initialState?: Value[],
|
|
1060
|
+
failureOutDegrees?: number[],
|
|
1061
|
+
) => {
|
|
1062
|
+
if (!(error instanceof TlaEvaluationError)) throw error;
|
|
1063
|
+
return {
|
|
1064
|
+
...createResult("error", undefined, {
|
|
1065
|
+
message: error.message,
|
|
1066
|
+
trace,
|
|
1067
|
+
initialState:
|
|
1068
|
+
initialState && !(error instanceof TlaModuleError) ? toRecord(initialState) : undefined,
|
|
1069
|
+
exitCode: getErrorExitCode(error),
|
|
1070
|
+
}),
|
|
1071
|
+
outDegrees: failureOutDegrees,
|
|
1072
|
+
};
|
|
1073
|
+
};
|
|
1074
|
+
const getPathTrace = (path: number[], loopStart?: number) => {
|
|
1075
|
+
const steps = getPathStates(path, NEXT_RECOVERY_FAILURE);
|
|
1076
|
+
const last = steps[steps.length - 1].state;
|
|
1077
|
+
const isClosed =
|
|
1078
|
+
!isReducing ||
|
|
1079
|
+
loopStart === undefined ||
|
|
1080
|
+
loopStart === path.length - 1 ||
|
|
1081
|
+
findSuccessorWithFingerprint(last, path[loopStart]) !== undefined;
|
|
1082
|
+
if (!isClosed) throw new TlaEvaluationError(NEXT_RECOVERY_FAILURE);
|
|
1083
|
+
return toTrace(steps, path, loopStart);
|
|
1084
|
+
};
|
|
525
1085
|
const getStateHash = (state: Value[]) => {
|
|
526
1086
|
let hash = 0x811c9dc5;
|
|
527
1087
|
for (let index = 0; index < state.length; index++)
|
|
@@ -572,21 +1132,32 @@ export const check = (options: CheckOptions): CheckResult => {
|
|
|
572
1132
|
isExpanding: false,
|
|
573
1133
|
candidate: undefined as Value[] | undefined,
|
|
574
1134
|
candidateLevel: 0,
|
|
1135
|
+
livenessSource: -1,
|
|
575
1136
|
};
|
|
576
1137
|
const getPath = (index: number): Value[][] =>
|
|
577
1138
|
index === -1 ? [] : [...getPath(parents[index]), states[index]];
|
|
1139
|
+
runtime.isSeen = (state) => {
|
|
1140
|
+
const fingerprintState = getFingerprintState(state);
|
|
1141
|
+
return findStateIndex(getStateHash(fingerprintState), fingerprintState) !== -1;
|
|
1142
|
+
};
|
|
578
1143
|
runtime.getBehavior = (state) => {
|
|
579
1144
|
const index = state === current.candidate ? -1 : states.indexOf(state);
|
|
580
1145
|
return index === -1 ? [...getPath(current.index), state] : getPath(index);
|
|
581
1146
|
};
|
|
1147
|
+
runtime.getCurrentState = () => (current.index === -1 ? undefined : states[current.index]);
|
|
1148
|
+
runtime.setLivenessSource = (index) => {
|
|
1149
|
+
current.livenessSource = index;
|
|
1150
|
+
};
|
|
582
1151
|
runtime.getLevel = (state) => {
|
|
1152
|
+
const source = current.livenessSource;
|
|
1153
|
+
if (source !== -1) return state === states[source] ? depths[source] : depths[source] + 1;
|
|
583
1154
|
if (state === current.candidate) return current.candidateLevel;
|
|
584
1155
|
if (current.index === -1) return 0;
|
|
585
1156
|
if (state === states[current.index]) return depths[current.index];
|
|
586
1157
|
return current.isExpanding ? depths[current.index] + 1 : 0;
|
|
587
1158
|
};
|
|
588
1159
|
|
|
589
|
-
|
|
1160
|
+
const visitInitialState = (state: Value[]) => {
|
|
590
1161
|
canonicalizeState(state);
|
|
591
1162
|
runtime.generatedStates++;
|
|
592
1163
|
runtime.initialStates++;
|
|
@@ -594,10 +1165,10 @@ export const check = (options: CheckOptions): CheckResult => {
|
|
|
594
1165
|
current.candidateLevel = 1;
|
|
595
1166
|
const fingerprintState = getFingerprintState(state);
|
|
596
1167
|
const hash = getStateHash(fingerprintState);
|
|
597
|
-
const
|
|
598
|
-
const isInModel = isSeen || search.satisfiesConstraints(state);
|
|
1168
|
+
const isInModel = search.satisfiesConstraints(state);
|
|
599
1169
|
if (isInModel) search.trackStatePossible(state);
|
|
600
|
-
|
|
1170
|
+
const isSeen = isInModel && findStateIndex(hash, fingerprintState) !== -1;
|
|
1171
|
+
if (isSeen) return undefined;
|
|
601
1172
|
if (isInModel) {
|
|
602
1173
|
countDistinctState(1);
|
|
603
1174
|
runtime.queueSize++;
|
|
@@ -605,100 +1176,203 @@ export const check = (options: CheckOptions): CheckResult => {
|
|
|
605
1176
|
}
|
|
606
1177
|
const violated = search.findViolatedInvariant(state);
|
|
607
1178
|
if (violated) {
|
|
608
|
-
return
|
|
1179
|
+
return reportViolation("invariant-violation", {
|
|
609
1180
|
name: violated.name,
|
|
610
|
-
trace: [{ action: undefined, state
|
|
1181
|
+
trace: toTrace([{ action: undefined, state }], []),
|
|
611
1182
|
});
|
|
612
1183
|
}
|
|
613
1184
|
const failedPredicate = search.findFailedInitialPredicate(state);
|
|
614
1185
|
if (failedPredicate) {
|
|
615
|
-
return
|
|
1186
|
+
return reportViolation("liveness-violation", {
|
|
616
1187
|
name: failedPredicate.name,
|
|
617
|
-
trace: [{ action: undefined, state
|
|
1188
|
+
trace: toTrace([{ action: undefined, state }], []),
|
|
618
1189
|
});
|
|
619
1190
|
}
|
|
620
|
-
if (!isInModel)
|
|
1191
|
+
if (!isInModel) return undefined;
|
|
621
1192
|
markSeen(hash, states.length);
|
|
622
1193
|
addState(state, fingerprintState, -1, undefined);
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
1194
|
+
return undefined;
|
|
1195
|
+
};
|
|
1196
|
+
const visitSuccessor = (index: number, successor: Successor, liveSuccessors: LiveSuccessor[]) => {
|
|
626
1197
|
const state = states[index];
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
current.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1198
|
+
runtime.generatedStates++;
|
|
1199
|
+
current.candidate = successor.state;
|
|
1200
|
+
current.candidateLevel = depths[index] + 1;
|
|
1201
|
+
if (isCanonicalizedFirst) canonicalizeState(successor.state);
|
|
1202
|
+
const fingerprintState = getFingerprintState(successor.state);
|
|
1203
|
+
const hash = getStateHash(fingerprintState);
|
|
1204
|
+
const found = findStateIndex(hash, fingerprintState);
|
|
1205
|
+
const isStateInModel = search.satisfiesConstraints(successor.state, state);
|
|
1206
|
+
if (isStateInModel) search.trackStatePossible(successor.state);
|
|
1207
|
+
const isInModel = isStateInModel && search.satisfiesActionConstraints(state, successor.state);
|
|
1208
|
+
if (isInModel) search.trackActionPossible(state, successor.state);
|
|
1209
|
+
const existing = isInModel ? found : -1;
|
|
1210
|
+
if (existing !== -1 && liveness) {
|
|
1211
|
+
const stored = states[existing];
|
|
1212
|
+
const isStored = stored.every((value, position) => value === successor.state[position]);
|
|
1213
|
+
liveSuccessors.push({ index: existing, state: isStored ? stored : successor.state });
|
|
634
1214
|
}
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const hash = getStateHash(fingerprintState);
|
|
643
|
-
const found = findStateIndex(hash, fingerprintState);
|
|
644
|
-
const isStateInModel = found !== -1 || search.satisfiesConstraints(successor.state, state);
|
|
645
|
-
if (isStateInModel) search.trackStatePossible(successor.state);
|
|
646
|
-
const isInModel = isStateInModel && search.satisfiesActionConstraints(state, successor.state);
|
|
647
|
-
if (isInModel) search.trackActionPossible(state, successor.state);
|
|
648
|
-
const existing = isInModel ? found : -1;
|
|
649
|
-
if (existing !== -1 && liveness) {
|
|
650
|
-
const stored = states[existing];
|
|
651
|
-
const isStored = stored.every((value, position) => value === successor.state[position]);
|
|
652
|
-
liveSuccessors.push({ index: existing, state: isStored ? stored : successor.state });
|
|
653
|
-
}
|
|
654
|
-
const isNew = isInModel && existing === -1;
|
|
655
|
-
if (isNew) countDistinctState(depths[index] + 1);
|
|
656
|
-
if (existing === -1) {
|
|
1215
|
+
const isNew = isInModel && existing === -1;
|
|
1216
|
+
if (isNew) {
|
|
1217
|
+
countDistinctState(depths[index] + 1);
|
|
1218
|
+
expansion.newStates++;
|
|
1219
|
+
}
|
|
1220
|
+
if (existing === -1) {
|
|
1221
|
+
try {
|
|
657
1222
|
const violated = search.findViolatedInvariant(successor.state, state);
|
|
658
1223
|
if (violated) {
|
|
659
|
-
return
|
|
1224
|
+
return reportViolation("invariant-violation", {
|
|
660
1225
|
name: violated.name,
|
|
661
1226
|
trace: getTrace(index, successor),
|
|
662
1227
|
});
|
|
663
1228
|
}
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
// TLC prints the outdegree when it cannot reproduce the failure, as for an invariant here.
|
|
1231
|
+
return toFailure(error, getTrace(index, successor), undefined, getPartialOutDegrees());
|
|
664
1232
|
}
|
|
665
|
-
const violatedProperty = search.findViolatedActionProperty(state, successor.state);
|
|
666
|
-
if (violatedProperty) {
|
|
667
|
-
return createResult("liveness-violation", {
|
|
668
|
-
name: violatedProperty.name,
|
|
669
|
-
trace: getTrace(index, successor),
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
if (!isNew) continue;
|
|
673
|
-
if (!isCanonicalizedFirst) canonicalizeState(successor.state);
|
|
674
|
-
markSeen(hash, states.length);
|
|
675
|
-
if (liveness) liveSuccessors.push({ index: states.length, state: successor.state });
|
|
676
|
-
addState(successor.state, fingerprintState, index, successor.action);
|
|
677
|
-
runtime.queueSize++;
|
|
678
1233
|
}
|
|
679
|
-
const
|
|
680
|
-
if (
|
|
681
|
-
return
|
|
1234
|
+
const violatedProperty = search.findViolatedActionProperty(state, successor.state);
|
|
1235
|
+
if (violatedProperty) {
|
|
1236
|
+
return reportViolation("liveness-violation", {
|
|
1237
|
+
name: violatedProperty.name,
|
|
1238
|
+
trace: getTrace(index, successor),
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
if (!isNew) return undefined;
|
|
1242
|
+
if (!isCanonicalizedFirst) canonicalizeState(successor.state);
|
|
1243
|
+
markSeen(hash, states.length);
|
|
1244
|
+
if (liveness) liveSuccessors.push({ index: states.length, state: successor.state });
|
|
1245
|
+
addState(successor.state, fingerprintState, index, successor.action);
|
|
1246
|
+
runtime.queueSize++;
|
|
1247
|
+
return undefined;
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const outcome: { result: CheckResult | undefined } = { result: undefined };
|
|
1251
|
+
if (spec.init !== undefined) {
|
|
1252
|
+
try {
|
|
1253
|
+
forEachInitialState(
|
|
1254
|
+
spec.init,
|
|
1255
|
+
environment,
|
|
1256
|
+
(state) => {
|
|
1257
|
+
try {
|
|
1258
|
+
outcome.result = visitInitialState(state);
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
outcome.result = toFailure(error, [], state);
|
|
1261
|
+
}
|
|
1262
|
+
return outcome.result !== undefined;
|
|
1263
|
+
},
|
|
1264
|
+
spec.scope,
|
|
1265
|
+
);
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
return toFailure(error, []);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (outcome.result) return outcome.result;
|
|
1271
|
+
|
|
1272
|
+
const next = spec.next;
|
|
1273
|
+
if (next === undefined) {
|
|
1274
|
+
if (states.length > 0) {
|
|
1275
|
+
throw new TlaEvaluationError(
|
|
1276
|
+
"No next state actions defined to generate successor states from.",
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
return createSuccess();
|
|
1280
|
+
}
|
|
1281
|
+
const expandState = (index: number) => {
|
|
1282
|
+
const state = states[index];
|
|
1283
|
+
const liveSuccessors: LiveSuccessor[] = [];
|
|
1284
|
+
const successorCount = forEachSuccessor(
|
|
1285
|
+
next,
|
|
1286
|
+
state,
|
|
1287
|
+
environment,
|
|
1288
|
+
(successor) => {
|
|
1289
|
+
try {
|
|
1290
|
+
outcome.result = visitSuccessor(index, successor, liveSuccessors);
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
outcome.result = toFailure(error, getTrace(index, successor));
|
|
1293
|
+
}
|
|
1294
|
+
return outcome.result !== undefined;
|
|
1295
|
+
},
|
|
1296
|
+
spec.scope,
|
|
1297
|
+
);
|
|
1298
|
+
current.isExpanding = false;
|
|
1299
|
+
if (outcome.result) return outcome.result;
|
|
1300
|
+
if (successorCount === 0 && config.checkDeadlock) {
|
|
1301
|
+
return reportViolation("deadlock", { name: "deadlock", trace: getStateTrace(index) });
|
|
1302
|
+
}
|
|
1303
|
+
try {
|
|
1304
|
+
const accepted = liveness?.addState(index, state, liveSuccessors);
|
|
1305
|
+
current.livenessSource = -1;
|
|
1306
|
+
if (!accepted) return undefined;
|
|
1307
|
+
return reportViolation("property-violation", {
|
|
682
1308
|
name: accepted.name,
|
|
683
1309
|
trace: getPathTrace(accepted.path),
|
|
684
1310
|
});
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
return toFailure(error, getStateTrace(index), undefined, getPartialOutDegrees());
|
|
1313
|
+
}
|
|
1314
|
+
};
|
|
1315
|
+
const reportLasso = (lasso: Lasso) => {
|
|
1316
|
+
const path = [...lasso.prefix, ...lasso.cycle];
|
|
1317
|
+
const trace = getPathTrace(path, lasso.prefix.length);
|
|
1318
|
+
const back = path[lasso.prefix.length];
|
|
1319
|
+
const last = path[path.length - 1];
|
|
1320
|
+
const loopLabel =
|
|
1321
|
+
back === last ? undefined : withoutOutput(() => getLabel(last, states[last], states[back]));
|
|
1322
|
+
return reportViolation(
|
|
1323
|
+
"liveness-violation",
|
|
1324
|
+
{ name: lasso.name, trace, loopStart: lasso.prefix.length, loopLabel },
|
|
1325
|
+
() =>
|
|
1326
|
+
back === last
|
|
1327
|
+
? trace[trace.length - 1].action
|
|
1328
|
+
: getTraceSuccessors(states[last]).find((successor) =>
|
|
1329
|
+
isSameState(successor.state, states[back]),
|
|
1330
|
+
)?.action,
|
|
1331
|
+
);
|
|
1332
|
+
};
|
|
1333
|
+
const isPeriodic = liveness !== undefined && options.livenessCheck !== "final";
|
|
1334
|
+
const startedAt = performance.now();
|
|
1335
|
+
const periodic = { at: startedAt + PERIODIC_START, size: 1, time: 0 };
|
|
1336
|
+
const findPeriodicViolation = (expanded: number) => {
|
|
1337
|
+
const now = performance.now();
|
|
1338
|
+
periodic.at = now + PROGRESS_INTERVAL;
|
|
1339
|
+
const isGrown = (expanded - periodic.size) / periodic.size > LIVENESS_GROWTH;
|
|
1340
|
+
if (!isGrown || periodic.time > (now - startedAt) * LIVENESS_RATIO) return undefined;
|
|
1341
|
+
periodic.size = expanded;
|
|
1342
|
+
const lasso = liveness?.findViolation();
|
|
1343
|
+
current.livenessSource = -1;
|
|
1344
|
+
periodic.time += performance.now() - now;
|
|
1345
|
+
return lasso;
|
|
1346
|
+
};
|
|
1347
|
+
setNextStatePhase(true);
|
|
1348
|
+
for (let index = 0; index < states.length && !runtime.isStopped; index++) {
|
|
1349
|
+
current.index = index;
|
|
1350
|
+
current.candidate = undefined;
|
|
1351
|
+
runtime.queueSize = states.length - index - 1;
|
|
1352
|
+
current.isExpanding = true;
|
|
1353
|
+
try {
|
|
1354
|
+
outcome.result = expandState(index);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
outcome.result = toFailure(error, getStateTrace(index));
|
|
1357
|
+
}
|
|
1358
|
+
if (outcome.result) return outcome.result;
|
|
1359
|
+
outDegrees[getOutDegreeBucket()]++;
|
|
1360
|
+
expansion.newStates = 0;
|
|
1361
|
+
if (isPeriodic && performance.now() >= periodic.at) {
|
|
1362
|
+
const lasso = findPeriodicViolation(index + 1);
|
|
1363
|
+
if (lasso) return reportLasso(lasso);
|
|
685
1364
|
}
|
|
686
1365
|
}
|
|
687
1366
|
|
|
688
1367
|
current.index = -1;
|
|
689
1368
|
current.candidate = undefined;
|
|
690
1369
|
const lasso = liveness?.findViolation();
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
name: lasso.name,
|
|
694
|
-
trace: getPathTrace([...lasso.prefix, ...lasso.cycle]),
|
|
695
|
-
loopStart: lasso.prefix.length,
|
|
696
|
-
});
|
|
697
|
-
}
|
|
1370
|
+
current.livenessSource = -1;
|
|
1371
|
+
if (lasso) return reportLasso(lasso);
|
|
698
1372
|
|
|
699
|
-
const postconditionViolation = search.findPostconditionViolation(
|
|
1373
|
+
const postconditionViolation = search.findPostconditionViolation();
|
|
700
1374
|
if (postconditionViolation) {
|
|
701
|
-
return createResult("postcondition-violation", postconditionViolation);
|
|
1375
|
+
return createResult("postcondition-violation", { ...postconditionViolation, trace: [] });
|
|
702
1376
|
}
|
|
703
|
-
return
|
|
1377
|
+
return createSuccess();
|
|
704
1378
|
};
|