tla-ts 0.0.1 → 0.0.3-dev.3cf2ad0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -40
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +216 -30
- package/dist/index.d.ts +216 -30
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +2 -2
- package/dist/parallel-worker.js +2 -2
- package/dist/parallel.cjs +1 -1
- package/dist/parallel.js +2 -2
- package/dist/run-cli.cjs +20 -1
- package/dist/run-cli.js +20 -1
- package/dist/transfer.cjs +388 -39
- package/dist/transfer.js +388 -39
- package/package.json +6 -2
- package/src/ast.ts +122 -5
- package/src/bags.ts +161 -71
- package/src/builtins.ts +203 -23
- package/src/bundled-modules.ts +63 -32
- package/src/check.ts +945 -238
- package/src/cli-options.ts +58 -0
- package/src/codegen.ts +52 -18
- package/src/config.ts +39 -11
- package/src/environment.ts +485 -140
- package/src/errors.ts +59 -1
- package/src/evaluate.ts +489 -137
- package/src/exit-codes.ts +75 -0
- package/src/fingerprints.ts +48 -13
- package/src/index.ts +9 -4
- package/src/instances.ts +221 -66
- package/src/java-overrides.ts +241 -0
- package/src/json.ts +153 -72
- package/src/levels.ts +850 -0
- package/src/lexer.ts +103 -28
- package/src/liveness.ts +47 -91
- package/src/obligation-action.ts +384 -0
- package/src/obligation-anon.ts +575 -0
- package/src/obligation-deref.ts +637 -0
- package/src/obligation-expression.ts +1060 -0
- package/src/obligation-format.ts +406 -0
- package/src/obligation-generate.ts +1074 -0
- package/src/obligation-levels.ts +329 -0
- package/src/obligation-module.ts +1042 -0
- package/src/obligation-printer.ts +889 -0
- package/src/obligation-proof.ts +76 -0
- package/src/obligation-source.ts +1084 -0
- package/src/obligation-tuples.ts +184 -0
- package/src/obligations.ts +316 -0
- package/src/parallel-worker.ts +183 -202
- package/src/parallel.ts +163 -84
- package/src/parser.ts +958 -205
- package/src/pretty-print.ts +259 -0
- package/src/randomization.ts +24 -113
- package/src/resolve.ts +472 -41
- package/src/run-cli.ts +427 -61
- package/src/runtime.ts +63 -28
- package/src/spec-description.ts +509 -0
- package/src/standard-levels.ts +105 -0
- package/src/standard-modules.ts +223 -92
- package/src/subexpressions.ts +21 -35
- package/src/substitution-levels.ts +184 -0
- package/src/successors.ts +261 -46
- package/src/symmetry.ts +70 -22
- package/src/tableau.ts +43 -37
- package/src/tlc-ext.ts +120 -33
- package/src/tlc-module.ts +71 -47
- package/src/tlc-modules.ts +1022 -0
- package/src/tlc-order.ts +497 -0
- package/src/tlc-random.ts +411 -0
- package/src/tlc-summary.ts +109 -0
- package/src/transfer.ts +26 -5
- package/src/values.ts +1234 -176
package/src/run-cli.ts
CHANGED
|
@@ -1,95 +1,236 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
globSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
3
12
|
import { availableParallelism } from "node:os";
|
|
4
|
-
import {
|
|
13
|
+
import type { CheckResult, CheckStatus, TraceStep } from "./check";
|
|
14
|
+
import { defineCommand, type ParsedArgs } from "./cli-options";
|
|
5
15
|
import { parseConfig } from "./config";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
16
|
+
import {
|
|
17
|
+
getErrorExitCode,
|
|
18
|
+
TlaConfigError,
|
|
19
|
+
TlaError,
|
|
20
|
+
TlaEvaluationError,
|
|
21
|
+
TlaProofError,
|
|
22
|
+
} from "./errors";
|
|
23
|
+
import { EXIT_STATUS } from "./exit-codes";
|
|
24
|
+
import type { LineRange } from "./obligation-generate";
|
|
25
|
+
import { getAtPrefix, getLocusText } from "./obligation-expression";
|
|
26
|
+
import { formatSummary, getObligations, WHOLE_MODULE, type ModuleObligations } from "./obligations";
|
|
27
|
+
import { check } from "./parallel";
|
|
28
|
+
import { prettyPrint } from "./pretty-print";
|
|
29
|
+
import {
|
|
30
|
+
formatProbability,
|
|
31
|
+
getActualProbability,
|
|
32
|
+
getOptimisticProbability,
|
|
33
|
+
getOutDegreeSummary,
|
|
34
|
+
} from "./tlc-summary";
|
|
35
|
+
import { formatValue, type Value } from "./values";
|
|
9
36
|
|
|
10
37
|
const EXIT_CODES: Record<CheckStatus, number> = {
|
|
11
|
-
success:
|
|
12
|
-
"assumption-violation":
|
|
13
|
-
deadlock:
|
|
14
|
-
"invariant-violation":
|
|
15
|
-
"property-violation":
|
|
16
|
-
"liveness-violation":
|
|
17
|
-
"postcondition-violation":
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
success: EXIT_STATUS.success,
|
|
39
|
+
"assumption-violation": EXIT_STATUS.assumptionViolation,
|
|
40
|
+
deadlock: EXIT_STATUS.deadlock,
|
|
41
|
+
"invariant-violation": EXIT_STATUS.safetyViolation,
|
|
42
|
+
"property-violation": EXIT_STATUS.safetyViolation,
|
|
43
|
+
"liveness-violation": EXIT_STATUS.livenessViolation,
|
|
44
|
+
"postcondition-violation": EXIT_STATUS.assumptionViolation,
|
|
45
|
+
error: EXIT_STATUS.error,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const CHECK_COMMAND = defineCommand("tla-ts <Spec.tla|Spec.cfg|glob>... {options}", [
|
|
49
|
+
{ flag: "-config <Spec.cfg>" },
|
|
50
|
+
{ flag: "-deadlock" },
|
|
51
|
+
{ flag: "-lncheck <final|default>" },
|
|
52
|
+
{ flag: "-seed <num>" },
|
|
53
|
+
{ flag: "-workers <count|auto>" },
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const PROVE_COMMAND = defineCommand("tla-ts prove {options} <Module.tla|glob>...", [
|
|
57
|
+
{ flag: "-N", isRequired: true },
|
|
58
|
+
{ flag: "--toolbox <start> <end>" },
|
|
59
|
+
{ flag: "--line <num>" },
|
|
60
|
+
{ flag: "--printallobs" },
|
|
61
|
+
{ flag: "--summary" },
|
|
62
|
+
{ flag: "--strict" },
|
|
63
|
+
{ flag: "-I <directory>", isRepeatable: true },
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
const USAGE = [
|
|
67
|
+
`Usage: ${CHECK_COMMAND.usage}`,
|
|
68
|
+
` ${PROVE_COMMAND.usage}`,
|
|
69
|
+
" tla-ts prove [<tlapm option>]... <Module.tla|glob>...",
|
|
70
|
+
].join("\n");
|
|
71
|
+
|
|
72
|
+
const GLOB_CHARACTERS = /[*?[{]/;
|
|
73
|
+
|
|
74
|
+
interface InputPath {
|
|
75
|
+
path: string;
|
|
76
|
+
isGlobbed: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const getInputPaths = (paths: string[]): InputPath[] =>
|
|
80
|
+
paths.flatMap((arg): InputPath[] =>
|
|
81
|
+
GLOB_CHARACTERS.test(arg)
|
|
82
|
+
? globSync(arg)
|
|
83
|
+
.sort()
|
|
84
|
+
.map((path) => ({ path, isGlobbed: true }))
|
|
85
|
+
: [{ path: arg, isGlobbed: false }],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const isJavaLong = (text: string) =>
|
|
89
|
+
/^[+-]?\d+$/.test(text) && BigInt.asIntN(64, BigInt(text)) === BigInt(text);
|
|
90
|
+
|
|
91
|
+
const getWorkerCount = (values: string[] | undefined) => {
|
|
92
|
+
if (!values) return 1;
|
|
93
|
+
return values[0] === "auto"
|
|
27
94
|
? availableParallelism()
|
|
28
|
-
: Math.max(1, Number.parseInt(
|
|
95
|
+
: Math.max(1, Number.parseInt(values[0], 10) || 1);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const printState = (state: Record<string, Value>) => {
|
|
99
|
+
const entries = Object.entries(state);
|
|
100
|
+
entries.forEach(([name, value]) =>
|
|
101
|
+
console.log(
|
|
102
|
+
`${entries.length === 1 ? "" : "/\\ "}${name} = ${prettyPrint(formatValue(value))}`,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
console.log("");
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const printTrace = (trace: TraceStep[], heading = "Error: The behavior up to this point is:") => {
|
|
109
|
+
if (trace.length > 0) console.log(heading);
|
|
110
|
+
trace.forEach((step, index) => {
|
|
111
|
+
console.log(`State ${index + 1}: <${step.label ?? step.action ?? "Initial predicate"}>`);
|
|
112
|
+
printState(step.state);
|
|
113
|
+
});
|
|
29
114
|
};
|
|
30
115
|
|
|
31
116
|
const printResult = (result: CheckResult) => {
|
|
32
|
-
if (result.
|
|
117
|
+
if (result.failure) {
|
|
118
|
+
console.log(`Error: ${result.failure.message}`);
|
|
119
|
+
if (result.failure.initialState) {
|
|
120
|
+
console.log("While working on the initial state:");
|
|
121
|
+
printState(result.failure.initialState);
|
|
122
|
+
}
|
|
123
|
+
printTrace(result.failure.trace);
|
|
124
|
+
} else if (result.violation) {
|
|
125
|
+
const violation = result.violation;
|
|
126
|
+
const name = violation.name;
|
|
127
|
+
const loopStart = violation.loopStart;
|
|
128
|
+
// TLC prints a violation by an initial state without the behavior or the statistics.
|
|
129
|
+
const isInitial =
|
|
130
|
+
loopStart === undefined &&
|
|
131
|
+
violation.trace.length === 1 &&
|
|
132
|
+
(result.status === "invariant-violation" || result.status === "liveness-violation");
|
|
33
133
|
const headline = {
|
|
34
134
|
success: "",
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
135
|
+
error: "",
|
|
136
|
+
"assumption-violation": violation.message
|
|
137
|
+
? `Error: Evaluating assumption ${name} failed.\n${violation.message}`
|
|
138
|
+
: `Error: Assumption ${name} is false.`,
|
|
38
139
|
deadlock: "Error: Deadlock reached.",
|
|
39
|
-
"invariant-violation":
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
140
|
+
"invariant-violation": isInitial
|
|
141
|
+
? `Error: Invariant ${name} is violated by the initial state:`
|
|
142
|
+
: `Error: Invariant ${name} is violated.`,
|
|
143
|
+
"property-violation": `Error: Property ${name} is violated.`,
|
|
144
|
+
"liveness-violation":
|
|
145
|
+
loopStart !== undefined
|
|
146
|
+
? `Error: Temporal property ${name} was violated.\n`
|
|
147
|
+
: isInitial
|
|
148
|
+
? `Error: Property ${name} is violated by the initial state:`
|
|
149
|
+
: `Error: Action property ${name} is violated.`,
|
|
150
|
+
"postcondition-violation": `Error: ${violation.message ?? `Postcondition ${name} is false.`}`,
|
|
43
151
|
}[result.status];
|
|
152
|
+
if (violation.counts) console.log(violation.counts);
|
|
44
153
|
console.log(headline);
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
51
|
-
console.log("");
|
|
52
|
-
});
|
|
53
|
-
const loopStart = result.violation.loopStart;
|
|
54
|
-
if (loopStart !== undefined) {
|
|
55
|
-
const isStuttering = loopStart === result.violation.trace.length - 1;
|
|
154
|
+
if (isInitial) printState(violation.trace[0].state);
|
|
155
|
+
else if (loopStart === undefined) printTrace(violation.trace);
|
|
156
|
+
else {
|
|
157
|
+
printTrace(violation.trace, "Error: The following behavior constitutes a counter-example:\n");
|
|
158
|
+
const isStuttering = loopStart === violation.trace.length - 1;
|
|
56
159
|
console.log(
|
|
57
|
-
isStuttering
|
|
160
|
+
isStuttering
|
|
161
|
+
? `State ${loopStart + 2}: Stuttering`
|
|
162
|
+
: `Back to state ${loopStart + 1}${violation.loopLabel ? `: <${violation.loopLabel}>\n` : ""}`,
|
|
58
163
|
);
|
|
59
164
|
}
|
|
165
|
+
const unwitnessed = violation.unwitnessed;
|
|
166
|
+
if (unwitnessed?.counts) console.log(unwitnessed.counts);
|
|
167
|
+
if (unwitnessed) console.log(`Error: ${unwitnessed.message}`);
|
|
168
|
+
// TLC checks assumptions before it starts the search, so it has no statistics to print.
|
|
169
|
+
if (isInitial || result.status === "assumption-violation") return;
|
|
60
170
|
} else {
|
|
171
|
+
const optimistic = getOptimisticProbability(result.distinctStates, result.generatedStates);
|
|
61
172
|
console.log("Model checking completed. No error has been found.");
|
|
173
|
+
console.log(" Estimates of the probability that TLC did not check all reachable states");
|
|
174
|
+
console.log(" because two distinct states had the same fingerprint:");
|
|
175
|
+
console.log(` calculated (optimistic): val = ${formatProbability(optimistic)}`);
|
|
176
|
+
if (result.fingerprintDistance !== undefined) {
|
|
177
|
+
const actual = getActualProbability(result.fingerprintDistance);
|
|
178
|
+
console.log(` based on the actual fingerprints: val = ${formatProbability(actual)}`);
|
|
179
|
+
}
|
|
62
180
|
}
|
|
63
181
|
console.log(
|
|
64
|
-
`${result.generatedStates} states generated, ${result.distinctStates} distinct states found,
|
|
182
|
+
`${result.generatedStates} states generated, ${result.distinctStates} distinct states found, ${result.queuedStates} states left on queue.`,
|
|
65
183
|
);
|
|
66
184
|
console.log(`The depth of the complete state graph search is ${result.depth}.`);
|
|
185
|
+
const outDegree = result.outDegrees && getOutDegreeSummary(result.outDegrees);
|
|
186
|
+
if (outDegree) console.log(outDegree);
|
|
67
187
|
};
|
|
68
188
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return 1;
|
|
189
|
+
const checkSpec = async (specPath: string, args: ParsedArgs) => {
|
|
190
|
+
if (!existsSync(specPath)) {
|
|
191
|
+
console.error(`Cannot find the specification file ${specPath}`);
|
|
192
|
+
return EXIT_STATUS.configParseError;
|
|
74
193
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
194
|
+
const configPath =
|
|
195
|
+
args.get("-config")?.[0] ??
|
|
196
|
+
(args.has("-config") ? undefined : specPath.replace(/\.tla$/, ".cfg"));
|
|
77
197
|
if (!configPath || !existsSync(configPath)) {
|
|
78
198
|
console.error(`Cannot find the configuration file ${configPath ?? ""}`);
|
|
79
|
-
return
|
|
199
|
+
return EXIT_STATUS.configParseError;
|
|
80
200
|
}
|
|
81
201
|
const config = parseConfig(readFileSync(configPath, "utf8"));
|
|
82
|
-
if (args.
|
|
202
|
+
if (args.has("-deadlock")) config.checkDeadlock = false;
|
|
203
|
+
const seed = args.has("-seed") ? args.get("-seed")?.[0] : "0";
|
|
204
|
+
if (seed === undefined) {
|
|
205
|
+
console.error("Error: seed required.");
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
if (!isJavaLong(seed)) {
|
|
209
|
+
console.error(`Error: An integer for seed required. But encountered ${seed}`);
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
212
|
+
const livenessCheck = args.has("-lncheck") ? args.get("-lncheck")?.[0]?.toLowerCase() : "default";
|
|
213
|
+
if (livenessCheck === undefined) {
|
|
214
|
+
console.error("Error: expect a strategy such as final for -lncheck option.");
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
83
217
|
const directory = dirname(specPath);
|
|
84
218
|
console.log(`Checking ${basename(specPath)} with ${basename(configPath)}`);
|
|
85
|
-
const result = await
|
|
86
|
-
workers: getWorkerCount(args),
|
|
219
|
+
const result = await check({
|
|
220
|
+
workers: getWorkerCount(args.get("-workers")),
|
|
221
|
+
livenessCheck: ["final", "seqfinal", "off"].includes(livenessCheck) ? "final" : "default",
|
|
222
|
+
seed: BigInt(seed),
|
|
87
223
|
source: readFileSync(specPath, "utf8"),
|
|
88
224
|
config,
|
|
225
|
+
onPause: () => readSync(0, Buffer.alloc(1)),
|
|
89
226
|
loadModule: (name) => {
|
|
90
227
|
const modulePath = join(directory, `${name}.tla`);
|
|
91
228
|
return existsSync(modulePath) ? readFileSync(modulePath, "utf8") : undefined;
|
|
92
229
|
},
|
|
230
|
+
loadClass: (name) => {
|
|
231
|
+
const classPath = join(directory, `${name}.class`);
|
|
232
|
+
return existsSync(classPath) ? readFileSync(classPath) : undefined;
|
|
233
|
+
},
|
|
93
234
|
readFile: (path) => (existsSync(path) ? readFileSync(path, "utf8") : undefined),
|
|
94
235
|
writeFile: (path, content) => {
|
|
95
236
|
mkdirSync(dirname(resolve(path)), { recursive: true });
|
|
@@ -97,16 +238,241 @@ const run = async (args: string[]) => {
|
|
|
97
238
|
},
|
|
98
239
|
});
|
|
99
240
|
printResult(result);
|
|
100
|
-
return EXIT_CODES[result.status];
|
|
241
|
+
return result.failure?.exitCode ?? EXIT_CODES[result.status];
|
|
101
242
|
};
|
|
102
243
|
|
|
103
|
-
|
|
104
|
-
|
|
244
|
+
// TLC prints the errors it raises itself. tla-ts names the others, such as a module SANY rejects.
|
|
245
|
+
const printError = (error: TlaError) => {
|
|
246
|
+
if (error instanceof TlaConfigError || error instanceof TlaEvaluationError) {
|
|
247
|
+
return console.log(`Error: ${error.message}`);
|
|
248
|
+
}
|
|
249
|
+
console.error(`${error.name}: ${error.message}`);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// Prints a TlaError and returns its exit code.
|
|
253
|
+
const reportErrors = async (run: () => number | Promise<number>) => {
|
|
105
254
|
try {
|
|
106
|
-
|
|
255
|
+
return await run();
|
|
107
256
|
} catch (error) {
|
|
108
257
|
if (!(error instanceof TlaError)) throw error;
|
|
109
|
-
|
|
258
|
+
printError(error);
|
|
259
|
+
return getErrorExitCode(error);
|
|
110
260
|
}
|
|
111
|
-
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Runs each path in turn, so an error in one leaves the others to report. The exit code is the
|
|
264
|
+
// first that is not success.
|
|
265
|
+
const runEach = async (paths: string[], runPath: (path: string) => number | Promise<number>) => {
|
|
266
|
+
const exitCodes: number[] = [];
|
|
267
|
+
for (const [index, path] of paths.entries()) {
|
|
268
|
+
if (index > 0) console.log("");
|
|
269
|
+
exitCodes.push(await reportErrors(() => runPath(path)));
|
|
270
|
+
}
|
|
271
|
+
return exitCodes.find((exitCode) => exitCode !== EXIT_STATUS.success) ?? EXIT_STATUS.success;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// tlapm installs its standard library, with modules such as TLAPS, at lib/tlapm/stdlib beside
|
|
275
|
+
// bin/tlapm.
|
|
276
|
+
const getTlapmPaths = () =>
|
|
277
|
+
(process.env.PATH ?? "")
|
|
278
|
+
.split(delimiter)
|
|
279
|
+
.map((directory) => join(directory, "tlapm"))
|
|
280
|
+
.filter((path) => existsSync(path));
|
|
281
|
+
|
|
282
|
+
const getTlapmLibrary = () =>
|
|
283
|
+
getTlapmPaths()
|
|
284
|
+
.map((path) => join(dirname(dirname(realpathSync(path))), "lib/tlapm/stdlib"))
|
|
285
|
+
.find((path) => existsSync(path));
|
|
286
|
+
|
|
287
|
+
const expandGlobs = (paths: string[]) =>
|
|
288
|
+
paths.flatMap((path) => (GLOB_CHARACTERS.test(path) ? globSync(path).sort() : [path]));
|
|
289
|
+
|
|
290
|
+
const proveWithTlapm = (args: string[]) => {
|
|
291
|
+
const tlapm = getTlapmPaths()[0];
|
|
292
|
+
if (tlapm === undefined) {
|
|
293
|
+
console.error(
|
|
294
|
+
"Error: tla-ts prove runs tlapm to prove obligations, but tlapm isn't on your PATH. Install tlapm, or pass -N to only generate the obligations.",
|
|
295
|
+
);
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
return spawnSync(tlapm, expandGlobs(args), { stdio: "inherit" }).status ?? 1;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const readModule = (path: string) =>
|
|
302
|
+
existsSync(path) ? { file: path, source: readFileSync(path, "utf8") } : undefined;
|
|
303
|
+
|
|
304
|
+
// A message of tlapm's toolbox protocol, which it writes to stderr.
|
|
305
|
+
const printToolboxMessage = (fields: string[]) =>
|
|
306
|
+
console.error(`@!!BEGIN\n${fields.map((field) => `@!!${field}\n`).join("")}@!!END\n`);
|
|
307
|
+
|
|
308
|
+
const TLAPM_VERSION = "5f5037f";
|
|
309
|
+
|
|
310
|
+
const quoteArgument = (arg: string) =>
|
|
311
|
+
/^[+,\-./0-9:=@A-Z_a-z]+$/.test(arg) ? arg : `'${arg.replaceAll("'", "'\\''")}'`;
|
|
312
|
+
|
|
313
|
+
const printToolboxHeader = (args: string[]) => {
|
|
314
|
+
const now = new Date();
|
|
315
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
316
|
+
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
317
|
+
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
318
|
+
console.log(
|
|
319
|
+
`\n\\* TLAPM version ${TLAPM_VERSION}\n\\* launched at ${date} ${time} with command line:\n\\* ${["tla-ts", "prove", ...args].map(quoteArgument).join(" ")}\n`,
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const STRICT_EXIT_STATUS = { incomplete: 11, emptyTarget: 12 };
|
|
324
|
+
|
|
325
|
+
const checkStrictly = (result: ModuleObligations, isTargeted: boolean, isSummary: boolean) => {
|
|
326
|
+
const summary = result.summary;
|
|
327
|
+
if (!isSummary) {
|
|
328
|
+
summary.absent.forEach((locus) => console.error(`Missing proof at ${getLocusText(locus)}`));
|
|
329
|
+
summary.omitted.forEach((locus) => console.error(`Omitted proof at ${getLocusText(locus)}`));
|
|
330
|
+
}
|
|
331
|
+
const isIncomplete = summary.absent.length + summary.omitted.length > 0;
|
|
332
|
+
if (isIncomplete) {
|
|
333
|
+
console.error(
|
|
334
|
+
`${getAtPrefix(result.location)}[ERROR]: Proof incomplete in module ${JSON.stringify(result.name)}: ${summary.absent.length} missing, ${summary.omitted.length} omitted proof step(s).`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const isEmpty = isTargeted && result.obligations.length === 0;
|
|
338
|
+
if (isEmpty) {
|
|
339
|
+
console.error(
|
|
340
|
+
`${getAtPrefix(result.location)}[ERROR]: No proof obligation found for the selected target.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return [
|
|
344
|
+
...(isIncomplete ? [STRICT_EXIT_STATUS.incomplete] : []),
|
|
345
|
+
...(isEmpty ? [STRICT_EXIT_STATUS.emptyTarget] : []),
|
|
346
|
+
];
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const TLAPM_FAILURE_STATUS = 3;
|
|
350
|
+
|
|
351
|
+
const generateObligations = (generate: () => ModuleObligations[]) => {
|
|
352
|
+
try {
|
|
353
|
+
return generate();
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (!(error instanceof TlaProofError)) throw error;
|
|
356
|
+
console.error(` tlapm ending abnormally with ${error.exception}\n`);
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const prove = (args: string[]) => {
|
|
362
|
+
if (!args.includes("-N") && !args.includes("--summary")) return proveWithTlapm(args);
|
|
363
|
+
const parsed = PROVE_COMMAND.parse(args);
|
|
364
|
+
const unsupported = parsed.unknown[0];
|
|
365
|
+
if (unsupported !== undefined) {
|
|
366
|
+
console.error(`Error: tla-ts prove does not support the tlapm option ${unsupported}.`);
|
|
367
|
+
return 1;
|
|
368
|
+
}
|
|
369
|
+
if (parsed.incomplete) {
|
|
370
|
+
console.error(`Error: option '${parsed.incomplete.name}' needs an argument.`);
|
|
371
|
+
return 1;
|
|
372
|
+
}
|
|
373
|
+
const nonInteger = parsed.options.find(
|
|
374
|
+
(option) => option.name !== "-I" && option.values.some((value) => !/^-?\d+$/.test(value)),
|
|
375
|
+
);
|
|
376
|
+
if (nonInteger) {
|
|
377
|
+
console.error(`Error: option '${nonInteger.name}' expects an integer.`);
|
|
378
|
+
return 1;
|
|
379
|
+
}
|
|
380
|
+
const files = expandGlobs(parsed.positionals);
|
|
381
|
+
const modulePaths = files.map((path) => (path.endsWith(".tla") ? path : `${path}.tla`));
|
|
382
|
+
if (modulePaths.length === 0) {
|
|
383
|
+
console.error(USAGE);
|
|
384
|
+
return 1;
|
|
385
|
+
}
|
|
386
|
+
const isToolbox = parsed.has("--toolbox") || parsed.has("--line");
|
|
387
|
+
if (isToolbox) printToolboxHeader(args);
|
|
388
|
+
// Like tlapm, the last of --toolbox and --line wins, and a 0 means the whole module.
|
|
389
|
+
const lines = parsed.options.reduce<LineRange>((range, option) => {
|
|
390
|
+
const [start, end] = option.values.map(Number);
|
|
391
|
+
if (option.name === "--toolbox") return { start, end: end === 0 ? Infinity : end };
|
|
392
|
+
if (option.name === "--line") return start === 0 ? WHOLE_MODULE : { start, end: start };
|
|
393
|
+
return range;
|
|
394
|
+
}, WHOLE_MODULE);
|
|
395
|
+
// tlapm searches the current directory and each module's, then its library, then each -I
|
|
396
|
+
// directory, the last given first.
|
|
397
|
+
const tlapmLibrary = getTlapmLibrary();
|
|
398
|
+
const includes = parsed.options.flatMap((option) => (option.name === "-I" ? option.values : []));
|
|
399
|
+
const directories = modulePaths.reduce(
|
|
400
|
+
(found, path) => (found.includes(dirname(path)) ? found : [dirname(path), ...found]),
|
|
401
|
+
[".", ...(tlapmLibrary ? [tlapmLibrary] : []), ...includes.toReversed()],
|
|
402
|
+
);
|
|
403
|
+
// tlapm names each file it reads by joining a search directory and the name, as in ./Module.tla.
|
|
404
|
+
const results = generateObligations(() => {
|
|
405
|
+
const missingIndex = modulePaths.findIndex((path) => !existsSync(path));
|
|
406
|
+
if (missingIndex !== -1) {
|
|
407
|
+
const missing = files[missingIndex];
|
|
408
|
+
console.error(`File "<unknown>":\nError: File ${JSON.stringify(missing)} not found`);
|
|
409
|
+
throw new TlaProofError(`File ${missing} not found`, { failure: "Module.Parser.parse_file" });
|
|
410
|
+
}
|
|
411
|
+
return getObligations(
|
|
412
|
+
modulePaths.map((path) => ({ file: `./${path}`, source: readFileSync(path, "utf8") })),
|
|
413
|
+
(name) =>
|
|
414
|
+
directories.map((directory) => readModule(`${directory}/${name}.tla`)).find(Boolean),
|
|
415
|
+
{
|
|
416
|
+
lines,
|
|
417
|
+
onWarning: (warning) => {
|
|
418
|
+
if (warning.channel === "stdout") return console.log(warning.text);
|
|
419
|
+
const isToolboxMessage =
|
|
420
|
+
warning.channel === "toolbox" || warning.channel === "toolboxOnly";
|
|
421
|
+
if (isToolbox && isToolboxMessage)
|
|
422
|
+
return printToolboxMessage(["type:warning", `msg:${warning.text}`]);
|
|
423
|
+
if (warning.channel !== "toolboxOnly") console.error(warning.text);
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
if (!results) return TLAPM_FAILURE_STATUS;
|
|
429
|
+
const failures = results.flatMap((result) => {
|
|
430
|
+
if (parsed.has("--summary")) console.log(formatSummary(result));
|
|
431
|
+
if (isToolbox) {
|
|
432
|
+
result.obligations.forEach((obligation) =>
|
|
433
|
+
printToolboxMessage([
|
|
434
|
+
"type:obligation",
|
|
435
|
+
`id:${obligation.id}`,
|
|
436
|
+
`loc:${obligation.location}`,
|
|
437
|
+
"status:to be proved",
|
|
438
|
+
...(parsed.has("--printallobs") ? [`obl:${obligation.text}`] : []),
|
|
439
|
+
]),
|
|
440
|
+
);
|
|
441
|
+
printToolboxMessage(["type:obligationsnumber", `count:${result.obligations.length}`]);
|
|
442
|
+
}
|
|
443
|
+
if (!parsed.has("--strict")) return [];
|
|
444
|
+
return checkStrictly(result, lines.start > 0 || lines.end < Infinity, parsed.has("--summary"));
|
|
445
|
+
});
|
|
446
|
+
return failures.includes(STRICT_EXIT_STATUS.incomplete)
|
|
447
|
+
? STRICT_EXIT_STATUS.incomplete
|
|
448
|
+
: (failures[0] ?? EXIT_STATUS.success);
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const run = async (args: string[]) => {
|
|
452
|
+
if (args[0] === "prove") return reportErrors(() => prove(args.slice(1)));
|
|
453
|
+
const parsed = CHECK_COMMAND.parse(args);
|
|
454
|
+
const inputs = getInputPaths(parsed.positionals);
|
|
455
|
+
const isConfigured = parsed.has("-config");
|
|
456
|
+
// A glob also matches modules that only other specs import, which have no configuration.
|
|
457
|
+
const specPaths = inputs
|
|
458
|
+
.map((input) => ({ ...input, path: input.path.replace(/\.cfg$/, ".tla") }))
|
|
459
|
+
.filter(
|
|
460
|
+
(input) =>
|
|
461
|
+
input.path.endsWith(".tla") &&
|
|
462
|
+
(!input.isGlobbed || isConfigured || existsSync(input.path.replace(/\.tla$/, ".cfg"))),
|
|
463
|
+
)
|
|
464
|
+
.map((input) => input.path);
|
|
465
|
+
if (specPaths.length === 0) {
|
|
466
|
+
console.error(USAGE);
|
|
467
|
+
return 1;
|
|
468
|
+
}
|
|
469
|
+
if (isConfigured && specPaths.length > 1) {
|
|
470
|
+
console.error("Error: -config applies to one specification, but several were given.");
|
|
471
|
+
return 1;
|
|
472
|
+
}
|
|
473
|
+
return runEach([...new Set(specPaths)], (path) => checkSpec(path, parsed));
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
export const runCli = async () => {
|
|
477
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
112
478
|
};
|
package/src/runtime.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createJavaRandom, getTlcFingerprint, type JavaRandom } from "./tlc-random";
|
|
2
|
+
import { createFunction, createRecord, type Value } from "./values";
|
|
2
3
|
|
|
3
4
|
export interface CheckerRuntime {
|
|
4
5
|
getLevel: (state: (Value | undefined)[]) => number;
|
|
5
6
|
getBehavior: (state: Value[]) => Value[][];
|
|
6
|
-
|
|
7
|
+
getSpecification: () => Value;
|
|
8
|
+
counterExample: Value | undefined;
|
|
9
|
+
setLivenessSource: (index: number) => void;
|
|
10
|
+
getCurrentState: () => Value[] | undefined;
|
|
11
|
+
getFingerprint: (state: Value[]) => bigint;
|
|
12
|
+
getRandom: () => JavaRandom;
|
|
13
|
+
evaluateAtStartup: <Result>(evaluate: () => Result) => Result;
|
|
14
|
+
pause: () => void;
|
|
15
|
+
seed: bigint;
|
|
16
|
+
isSeen: (state: Value[]) => boolean;
|
|
7
17
|
generatedStates: number;
|
|
8
18
|
distinctStates: number;
|
|
9
19
|
initialStates: number;
|
|
@@ -18,37 +28,62 @@ export interface CheckerRuntime {
|
|
|
18
28
|
startedAt: number;
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
// TLC's RandomEnumerableValues keeps one java.util.Random per thread, seeded with -seed. The
|
|
32
|
+
// main thread evaluates constants, assumptions, and initial states with it. A worker reseeds
|
|
33
|
+
// its Random with the fingerprint of each state it expands, XORed with the seed, so a state's
|
|
34
|
+
// successors are the same whatever order the workers expand states in.
|
|
35
|
+
export const createRuntime = (checkDeadlock: boolean, seed = 0n): CheckerRuntime => {
|
|
36
|
+
const startup = createJavaRandom(seed);
|
|
37
|
+
const worker = { random: createJavaRandom(seed), state: undefined as Value[] | undefined };
|
|
38
|
+
const phase = { isStartup: false };
|
|
39
|
+
const runtime: CheckerRuntime = {
|
|
40
|
+
getLevel: () => 0,
|
|
41
|
+
getBehavior: () => [],
|
|
42
|
+
getSpecification: () => createRecord([]),
|
|
43
|
+
counterExample: undefined,
|
|
44
|
+
isSeen: () => false,
|
|
45
|
+
setLivenessSource: () => {},
|
|
46
|
+
getCurrentState: () => undefined,
|
|
47
|
+
getFingerprint: getTlcFingerprint,
|
|
48
|
+
getRandom: () => {
|
|
49
|
+
const state = phase.isStartup ? undefined : runtime.getCurrentState();
|
|
50
|
+
if (state === undefined) return startup;
|
|
51
|
+
if (state !== worker.state) {
|
|
52
|
+
worker.random.setSeed(runtime.getFingerprint(state) ^ BigInt.asUintN(64, seed));
|
|
53
|
+
worker.state = state;
|
|
54
|
+
}
|
|
55
|
+
return worker.random;
|
|
56
|
+
},
|
|
57
|
+
evaluateAtStartup: (evaluate) => {
|
|
58
|
+
const wasStartup = phase.isStartup;
|
|
59
|
+
phase.isStartup = true;
|
|
60
|
+
try {
|
|
61
|
+
return evaluate();
|
|
62
|
+
} finally {
|
|
63
|
+
phase.isStartup = wasStartup;
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
pause: () => {},
|
|
67
|
+
seed,
|
|
68
|
+
generatedStates: 0,
|
|
69
|
+
distinctStates: 0,
|
|
70
|
+
initialStates: 0,
|
|
71
|
+
queueSize: 0,
|
|
72
|
+
diameter: 1,
|
|
73
|
+
registers: new Map(),
|
|
74
|
+
namedRegisters: new Map(),
|
|
75
|
+
properties: new Map(),
|
|
76
|
+
possibleCounts: new Map(),
|
|
77
|
+
isStopped: false,
|
|
78
|
+
checkDeadlock,
|
|
79
|
+
startedAt: Date.now(),
|
|
28
80
|
};
|
|
81
|
+
return runtime;
|
|
29
82
|
};
|
|
30
83
|
|
|
31
|
-
export const createRuntime = (checkDeadlock: boolean): CheckerRuntime => ({
|
|
32
|
-
getLevel: () => 0,
|
|
33
|
-
getBehavior: () => [],
|
|
34
|
-
random: createRandom(0),
|
|
35
|
-
generatedStates: 0,
|
|
36
|
-
distinctStates: 0,
|
|
37
|
-
initialStates: 0,
|
|
38
|
-
queueSize: 0,
|
|
39
|
-
diameter: 1,
|
|
40
|
-
registers: new Map(),
|
|
41
|
-
namedRegisters: new Map(),
|
|
42
|
-
properties: new Map(),
|
|
43
|
-
possibleCounts: new Map(),
|
|
44
|
-
isStopped: false,
|
|
45
|
-
checkDeadlock,
|
|
46
|
-
startedAt: Date.now(),
|
|
47
|
-
});
|
|
48
|
-
|
|
49
84
|
export const NAMED_REGISTER_PREFIX = "s:";
|
|
50
85
|
|
|
51
|
-
|
|
86
|
+
const POSSIBLE_REGISTER = "s:_possible";
|
|
52
87
|
|
|
53
88
|
export const getPossibleCounts = (runtime: CheckerRuntime) =>
|
|
54
89
|
createFunction([...runtime.possibleCounts].map(([name, count]): [Value, Value] => [name, count]));
|