tla-ts 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -41
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +166 -30
- package/dist/index.d.ts +166 -30
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +2 -2
- package/dist/parallel-worker.js +2 -2
- package/dist/parallel.cjs +1 -1
- package/dist/parallel.js +2 -2
- package/dist/run-cli.cjs +2 -1
- package/dist/run-cli.js +2 -1
- package/dist/transfer.cjs +388 -39
- package/dist/transfer.js +388 -39
- package/package.json +4 -2
- package/src/ast.ts +82 -5
- package/src/bags.ts +161 -71
- package/src/builtins.ts +203 -23
- package/src/bundled-modules.ts +63 -32
- package/src/check.ts +904 -230
- package/src/codegen.ts +52 -18
- package/src/config.ts +39 -11
- package/src/environment.ts +485 -140
- package/src/errors.ts +22 -0
- package/src/evaluate.ts +486 -136
- package/src/exit-codes.ts +75 -0
- package/src/fingerprints.ts +2 -2
- package/src/index.ts +9 -4
- package/src/instances.ts +221 -66
- package/src/java-overrides.ts +241 -0
- package/src/json.ts +153 -72
- package/src/levels.ts +850 -0
- package/src/lexer.ts +97 -27
- package/src/liveness.ts +47 -91
- package/src/parallel-worker.ts +57 -28
- package/src/parallel.ts +116 -62
- package/src/parser.ts +703 -183
- package/src/pretty-print.ts +259 -0
- package/src/randomization.ts +24 -113
- package/src/resolve.ts +472 -41
- package/src/run-cli.ts +142 -44
- package/src/runtime.ts +63 -28
- package/src/spec-description.ts +509 -0
- package/src/standard-levels.ts +105 -0
- package/src/standard-modules.ts +223 -92
- package/src/subexpressions.ts +21 -35
- package/src/substitution-levels.ts +184 -0
- package/src/successors.ts +261 -46
- package/src/symmetry.ts +65 -21
- package/src/tableau.ts +43 -37
- package/src/tlc-ext.ts +120 -33
- package/src/tlc-module.ts +71 -47
- package/src/tlc-modules.ts +1022 -0
- package/src/tlc-order.ts +497 -0
- package/src/tlc-random.ts +411 -0
- package/src/tlc-summary.ts +109 -0
- package/src/transfer.ts +4 -4
- package/src/values.ts +1186 -164
package/src/run-cli.ts
CHANGED
|
@@ -1,69 +1,133 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { availableParallelism } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import type { CheckResult, CheckStatus, TraceStep } from "./check";
|
|
5
5
|
import { parseConfig } from "./config";
|
|
6
|
-
import { TlaError } from "./errors";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
6
|
+
import { getErrorExitCode, TlaConfigError, TlaError, TlaEvaluationError } from "./errors";
|
|
7
|
+
import { EXIT_STATUS } from "./exit-codes";
|
|
8
|
+
import { check } from "./parallel";
|
|
9
|
+
import { prettyPrint } from "./pretty-print";
|
|
10
|
+
import {
|
|
11
|
+
formatProbability,
|
|
12
|
+
getActualProbability,
|
|
13
|
+
getOptimisticProbability,
|
|
14
|
+
getOutDegreeSummary,
|
|
15
|
+
} from "./tlc-summary";
|
|
16
|
+
import { formatValue, type Value } from "./values";
|
|
9
17
|
|
|
10
18
|
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":
|
|
19
|
+
success: EXIT_STATUS.success,
|
|
20
|
+
"assumption-violation": EXIT_STATUS.assumptionViolation,
|
|
21
|
+
deadlock: EXIT_STATUS.deadlock,
|
|
22
|
+
"invariant-violation": EXIT_STATUS.safetyViolation,
|
|
23
|
+
"property-violation": EXIT_STATUS.safetyViolation,
|
|
24
|
+
"liveness-violation": EXIT_STATUS.livenessViolation,
|
|
25
|
+
"postcondition-violation": EXIT_STATUS.assumptionViolation,
|
|
26
|
+
error: EXIT_STATUS.error,
|
|
18
27
|
};
|
|
19
28
|
|
|
20
|
-
const USAGE =
|
|
29
|
+
const USAGE =
|
|
30
|
+
"Usage: tla-ts <Spec.tla> [-config <Spec.cfg>] [-deadlock] [-lncheck <final|default>] [-seed <num>] [-workers <count|auto>]";
|
|
31
|
+
|
|
32
|
+
const isJavaLong = (text: string) =>
|
|
33
|
+
/^[+-]?\d+$/.test(text) && BigInt.asIntN(64, BigInt(text)) === BigInt(text);
|
|
21
34
|
|
|
22
35
|
const getWorkerCount = (args: string[]) => {
|
|
23
36
|
const flag = args.indexOf("-workers");
|
|
24
37
|
if (flag === -1) return 1;
|
|
25
38
|
const value = args[flag + 1];
|
|
26
|
-
return value === "auto"
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
return value === "auto" ? availableParallelism() : Math.max(1, Number.parseInt(value, 10) || 1);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const printState = (state: Record<string, Value>) => {
|
|
43
|
+
const entries = Object.entries(state);
|
|
44
|
+
entries.forEach(([name, value]) =>
|
|
45
|
+
console.log(
|
|
46
|
+
`${entries.length === 1 ? "" : "/\\ "}${name} = ${prettyPrint(formatValue(value))}`,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
console.log("");
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const printTrace = (trace: TraceStep[], heading = "Error: The behavior up to this point is:") => {
|
|
53
|
+
if (trace.length > 0) console.log(heading);
|
|
54
|
+
trace.forEach((step, index) => {
|
|
55
|
+
console.log(`State ${index + 1}: <${step.label ?? step.action ?? "Initial predicate"}>`);
|
|
56
|
+
printState(step.state);
|
|
57
|
+
});
|
|
29
58
|
};
|
|
30
59
|
|
|
31
60
|
const printResult = (result: CheckResult) => {
|
|
32
|
-
if (result.
|
|
61
|
+
if (result.failure) {
|
|
62
|
+
console.log(`Error: ${result.failure.message}`);
|
|
63
|
+
if (result.failure.initialState) {
|
|
64
|
+
console.log("While working on the initial state:");
|
|
65
|
+
printState(result.failure.initialState);
|
|
66
|
+
}
|
|
67
|
+
printTrace(result.failure.trace);
|
|
68
|
+
} else if (result.violation) {
|
|
69
|
+
const violation = result.violation;
|
|
70
|
+
const name = violation.name;
|
|
71
|
+
const loopStart = violation.loopStart;
|
|
72
|
+
// TLC prints a violation by an initial state without the behavior or the statistics.
|
|
73
|
+
const isInitial =
|
|
74
|
+
loopStart === undefined &&
|
|
75
|
+
violation.trace.length === 1 &&
|
|
76
|
+
(result.status === "invariant-violation" || result.status === "liveness-violation");
|
|
33
77
|
const headline = {
|
|
34
78
|
success: "",
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
79
|
+
error: "",
|
|
80
|
+
"assumption-violation": violation.message
|
|
81
|
+
? `Error: Evaluating assumption ${name} failed.\n${violation.message}`
|
|
82
|
+
: `Error: Assumption ${name} is false.`,
|
|
38
83
|
deadlock: "Error: Deadlock reached.",
|
|
39
|
-
"invariant-violation":
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
84
|
+
"invariant-violation": isInitial
|
|
85
|
+
? `Error: Invariant ${name} is violated by the initial state:`
|
|
86
|
+
: `Error: Invariant ${name} is violated.`,
|
|
87
|
+
"property-violation": `Error: Property ${name} is violated.`,
|
|
88
|
+
"liveness-violation":
|
|
89
|
+
loopStart !== undefined
|
|
90
|
+
? `Error: Temporal property ${name} was violated.\n`
|
|
91
|
+
: isInitial
|
|
92
|
+
? `Error: Property ${name} is violated by the initial state:`
|
|
93
|
+
: `Error: Action property ${name} is violated.`,
|
|
94
|
+
"postcondition-violation": `Error: ${violation.message ?? `Postcondition ${name} is false.`}`,
|
|
43
95
|
}[result.status];
|
|
96
|
+
if (violation.counts) console.log(violation.counts);
|
|
44
97
|
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;
|
|
98
|
+
if (isInitial) printState(violation.trace[0].state);
|
|
99
|
+
else if (loopStart === undefined) printTrace(violation.trace);
|
|
100
|
+
else {
|
|
101
|
+
printTrace(violation.trace, "Error: The following behavior constitutes a counter-example:\n");
|
|
102
|
+
const isStuttering = loopStart === violation.trace.length - 1;
|
|
56
103
|
console.log(
|
|
57
|
-
isStuttering
|
|
104
|
+
isStuttering
|
|
105
|
+
? `State ${loopStart + 2}: Stuttering`
|
|
106
|
+
: `Back to state ${loopStart + 1}${violation.loopLabel ? `: <${violation.loopLabel}>\n` : ""}`,
|
|
58
107
|
);
|
|
59
108
|
}
|
|
109
|
+
const unwitnessed = violation.unwitnessed;
|
|
110
|
+
if (unwitnessed?.counts) console.log(unwitnessed.counts);
|
|
111
|
+
if (unwitnessed) console.log(`Error: ${unwitnessed.message}`);
|
|
112
|
+
// TLC checks assumptions before it starts the search, so it has no statistics to print.
|
|
113
|
+
if (isInitial || result.status === "assumption-violation") return;
|
|
60
114
|
} else {
|
|
115
|
+
const optimistic = getOptimisticProbability(result.distinctStates, result.generatedStates);
|
|
61
116
|
console.log("Model checking completed. No error has been found.");
|
|
117
|
+
console.log(" Estimates of the probability that TLC did not check all reachable states");
|
|
118
|
+
console.log(" because two distinct states had the same fingerprint:");
|
|
119
|
+
console.log(` calculated (optimistic): val = ${formatProbability(optimistic)}`);
|
|
120
|
+
if (result.fingerprintDistance !== undefined) {
|
|
121
|
+
const actual = getActualProbability(result.fingerprintDistance);
|
|
122
|
+
console.log(` based on the actual fingerprints: val = ${formatProbability(actual)}`);
|
|
123
|
+
}
|
|
62
124
|
}
|
|
63
125
|
console.log(
|
|
64
|
-
`${result.generatedStates} states generated, ${result.distinctStates} distinct states found,
|
|
126
|
+
`${result.generatedStates} states generated, ${result.distinctStates} distinct states found, ${result.queuedStates} states left on queue.`,
|
|
65
127
|
);
|
|
66
128
|
console.log(`The depth of the complete state graph search is ${result.depth}.`);
|
|
129
|
+
const outDegree = result.outDegrees && getOutDegreeSummary(result.outDegrees);
|
|
130
|
+
if (outDegree) console.log(outDegree);
|
|
67
131
|
};
|
|
68
132
|
|
|
69
133
|
const run = async (args: string[]) => {
|
|
@@ -72,24 +136,51 @@ const run = async (args: string[]) => {
|
|
|
72
136
|
console.error(USAGE);
|
|
73
137
|
return 1;
|
|
74
138
|
}
|
|
139
|
+
if (!existsSync(specPath)) {
|
|
140
|
+
console.error(`Cannot find the specification file ${specPath}`);
|
|
141
|
+
return EXIT_STATUS.configParseError;
|
|
142
|
+
}
|
|
75
143
|
const configFlag = args.indexOf("-config");
|
|
76
144
|
const configPath = configFlag === -1 ? specPath.replace(/\.tla$/, ".cfg") : args[configFlag + 1];
|
|
77
145
|
if (!configPath || !existsSync(configPath)) {
|
|
78
146
|
console.error(`Cannot find the configuration file ${configPath ?? ""}`);
|
|
79
|
-
return
|
|
147
|
+
return EXIT_STATUS.configParseError;
|
|
80
148
|
}
|
|
81
149
|
const config = parseConfig(readFileSync(configPath, "utf8"));
|
|
82
150
|
if (args.includes("-deadlock")) config.checkDeadlock = false;
|
|
151
|
+
const seedFlag = args.indexOf("-seed");
|
|
152
|
+
const seed = seedFlag === -1 ? "0" : args[seedFlag + 1];
|
|
153
|
+
if (seed === undefined) {
|
|
154
|
+
console.error("Error: seed required.");
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
if (!isJavaLong(seed)) {
|
|
158
|
+
console.error(`Error: An integer for seed required. But encountered ${seed}`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
const livenessFlag = args.indexOf("-lncheck");
|
|
162
|
+
const livenessCheck = livenessFlag === -1 ? "default" : args[livenessFlag + 1]?.toLowerCase();
|
|
163
|
+
if (livenessCheck === undefined) {
|
|
164
|
+
console.error("Error: expect a strategy such as final for -lncheck option.");
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
83
167
|
const directory = dirname(specPath);
|
|
84
168
|
console.log(`Checking ${basename(specPath)} with ${basename(configPath)}`);
|
|
85
|
-
const result = await
|
|
169
|
+
const result = await check({
|
|
86
170
|
workers: getWorkerCount(args),
|
|
171
|
+
livenessCheck: ["final", "seqfinal", "off"].includes(livenessCheck) ? "final" : "default",
|
|
172
|
+
seed: BigInt(seed),
|
|
87
173
|
source: readFileSync(specPath, "utf8"),
|
|
88
174
|
config,
|
|
175
|
+
onPause: () => readSync(0, Buffer.alloc(1)),
|
|
89
176
|
loadModule: (name) => {
|
|
90
177
|
const modulePath = join(directory, `${name}.tla`);
|
|
91
178
|
return existsSync(modulePath) ? readFileSync(modulePath, "utf8") : undefined;
|
|
92
179
|
},
|
|
180
|
+
loadClass: (name) => {
|
|
181
|
+
const classPath = join(directory, `${name}.class`);
|
|
182
|
+
return existsSync(classPath) ? readFileSync(classPath) : undefined;
|
|
183
|
+
},
|
|
93
184
|
readFile: (path) => (existsSync(path) ? readFileSync(path, "utf8") : undefined),
|
|
94
185
|
writeFile: (path, content) => {
|
|
95
186
|
mkdirSync(dirname(resolve(path)), { recursive: true });
|
|
@@ -97,16 +188,23 @@ const run = async (args: string[]) => {
|
|
|
97
188
|
},
|
|
98
189
|
});
|
|
99
190
|
printResult(result);
|
|
100
|
-
return EXIT_CODES[result.status];
|
|
191
|
+
return result.failure?.exitCode ?? EXIT_CODES[result.status];
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// TLC prints the errors it raises itself. tla-ts names the others, such as a module SANY rejects.
|
|
195
|
+
const printError = (error: TlaError) => {
|
|
196
|
+
if (error instanceof TlaConfigError || error instanceof TlaEvaluationError) {
|
|
197
|
+
return console.log(`Error: ${error.message}`);
|
|
198
|
+
}
|
|
199
|
+
console.error(`${error.name}: ${error.message}`);
|
|
101
200
|
};
|
|
102
201
|
|
|
103
202
|
export const runCli = async () => {
|
|
104
|
-
const outcome = { code: 1 };
|
|
105
203
|
try {
|
|
106
|
-
|
|
204
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
107
205
|
} catch (error) {
|
|
108
206
|
if (!(error instanceof TlaError)) throw error;
|
|
109
|
-
|
|
207
|
+
printError(error);
|
|
208
|
+
process.exitCode = getErrorExitCode(error);
|
|
110
209
|
}
|
|
111
|
-
process.exitCode = outcome.code;
|
|
112
210
|
};
|
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]));
|