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