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.
Files changed (57) hide show
  1. package/README.md +34 -41
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +166 -30
  4. package/dist/index.d.ts +166 -30
  5. package/dist/index.js +1 -1
  6. package/dist/parallel-worker.cjs +2 -2
  7. package/dist/parallel-worker.js +2 -2
  8. package/dist/parallel.cjs +1 -1
  9. package/dist/parallel.js +2 -2
  10. package/dist/run-cli.cjs +2 -1
  11. package/dist/run-cli.js +2 -1
  12. package/dist/transfer.cjs +388 -39
  13. package/dist/transfer.js +388 -39
  14. package/package.json +4 -2
  15. package/src/ast.ts +82 -5
  16. package/src/bags.ts +161 -71
  17. package/src/builtins.ts +203 -23
  18. package/src/bundled-modules.ts +63 -32
  19. package/src/check.ts +904 -230
  20. package/src/codegen.ts +52 -18
  21. package/src/config.ts +39 -11
  22. package/src/environment.ts +485 -140
  23. package/src/errors.ts +22 -0
  24. package/src/evaluate.ts +486 -136
  25. package/src/exit-codes.ts +75 -0
  26. package/src/fingerprints.ts +2 -2
  27. package/src/index.ts +9 -4
  28. package/src/instances.ts +221 -66
  29. package/src/java-overrides.ts +241 -0
  30. package/src/json.ts +153 -72
  31. package/src/levels.ts +850 -0
  32. package/src/lexer.ts +97 -27
  33. package/src/liveness.ts +47 -91
  34. package/src/parallel-worker.ts +57 -28
  35. package/src/parallel.ts +116 -62
  36. package/src/parser.ts +703 -183
  37. package/src/pretty-print.ts +259 -0
  38. package/src/randomization.ts +24 -113
  39. package/src/resolve.ts +472 -41
  40. package/src/run-cli.ts +142 -44
  41. package/src/runtime.ts +63 -28
  42. package/src/spec-description.ts +509 -0
  43. package/src/standard-levels.ts +105 -0
  44. package/src/standard-modules.ts +223 -92
  45. package/src/subexpressions.ts +21 -35
  46. package/src/substitution-levels.ts +184 -0
  47. package/src/successors.ts +261 -46
  48. package/src/symmetry.ts +65 -21
  49. package/src/tableau.ts +43 -37
  50. package/src/tlc-ext.ts +120 -33
  51. package/src/tlc-module.ts +71 -47
  52. package/src/tlc-modules.ts +1022 -0
  53. package/src/tlc-order.ts +497 -0
  54. package/src/tlc-random.ts +411 -0
  55. package/src/tlc-summary.ts +109 -0
  56. package/src/transfer.ts +4 -4
  57. 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 { type CheckResult, type CheckStatus } from "./check";
4
+ import type { CheckResult, CheckStatus, TraceStep } from "./check";
5
5
  import { parseConfig } from "./config";
6
- import { TlaError } from "./errors";
7
- import { checkInParallel } from "./parallel";
8
- import { formatValue } from "./values";
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: 0,
12
- "assumption-violation": 10,
13
- deadlock: 11,
14
- "invariant-violation": 12,
15
- "property-violation": 12,
16
- "liveness-violation": 13,
17
- "postcondition-violation": 10,
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 = "Usage: tla-ts <Spec.tla> [-config <Spec.cfg>] [-deadlock] [-workers <count|auto>]";
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
- ? availableParallelism()
28
- : Math.max(1, Number.parseInt(value ?? "1", 10) || 1);
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.violation) {
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
- "assumption-violation": result.violation.message
36
- ? `Error: Evaluating assumption ${result.violation.name} failed.\n${result.violation.message}`
37
- : `Error: Assumption ${result.violation.name} is false.`,
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": `Error: Invariant ${result.violation.name} is violated.`,
40
- "property-violation": `Error: Property ${result.violation.name} is violated.`,
41
- "liveness-violation": `Error: Temporal property ${result.violation.name} was violated.`,
42
- "postcondition-violation": `Error: ${result.violation.message ?? `Postcondition ${result.violation.name} is false.`}`,
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 (result.violation.trace.length > 0) console.log("Error: The behavior up to this point is:");
46
- result.violation.trace.forEach((step, index) => {
47
- console.log(`State ${index + 1}: <${step.action ?? "Initial predicate"}>`);
48
- Object.entries(step.state).forEach(([name, value]) =>
49
- console.log(`/\\ ${name} = ${formatValue(value)}`),
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 ? `State ${loopStart + 2}: Stuttering` : `Back to state ${loopStart + 1}`,
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, 0 states left on queue.`,
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 1;
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 checkInParallel({
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
- outcome.code = await run(process.argv.slice(2));
204
+ process.exitCode = await run(process.argv.slice(2));
107
205
  } catch (error) {
108
206
  if (!(error instanceof TlaError)) throw error;
109
- console.error(`${error.name}: ${error.message}`);
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 { createFunction, type Value } from "./values";
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
- random: () => number;
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
- const createRandom = (seed: number) => {
22
- const state = { value: seed >>> 0 };
23
- return () => {
24
- state.value = (state.value + 0x6d2b79f5) >>> 0;
25
- const mixed = Math.imul(state.value ^ (state.value >>> 15), state.value | 1);
26
- const shifted = mixed ^ (mixed + Math.imul(mixed ^ (mixed >>> 7), mixed | 61));
27
- return ((shifted ^ (shifted >>> 14)) >>> 0) / 4294967296;
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
- export const POSSIBLE_REGISTER = "s:_possible";
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]));