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.
Files changed (71) hide show
  1. package/README.md +85 -40
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +216 -30
  4. package/dist/index.d.ts +216 -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 +20 -1
  11. package/dist/run-cli.js +20 -1
  12. package/dist/transfer.cjs +388 -39
  13. package/dist/transfer.js +388 -39
  14. package/package.json +6 -2
  15. package/src/ast.ts +122 -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 +945 -238
  20. package/src/cli-options.ts +58 -0
  21. package/src/codegen.ts +52 -18
  22. package/src/config.ts +39 -11
  23. package/src/environment.ts +485 -140
  24. package/src/errors.ts +59 -1
  25. package/src/evaluate.ts +489 -137
  26. package/src/exit-codes.ts +75 -0
  27. package/src/fingerprints.ts +48 -13
  28. package/src/index.ts +9 -4
  29. package/src/instances.ts +221 -66
  30. package/src/java-overrides.ts +241 -0
  31. package/src/json.ts +153 -72
  32. package/src/levels.ts +850 -0
  33. package/src/lexer.ts +103 -28
  34. package/src/liveness.ts +47 -91
  35. package/src/obligation-action.ts +384 -0
  36. package/src/obligation-anon.ts +575 -0
  37. package/src/obligation-deref.ts +637 -0
  38. package/src/obligation-expression.ts +1060 -0
  39. package/src/obligation-format.ts +406 -0
  40. package/src/obligation-generate.ts +1074 -0
  41. package/src/obligation-levels.ts +329 -0
  42. package/src/obligation-module.ts +1042 -0
  43. package/src/obligation-printer.ts +889 -0
  44. package/src/obligation-proof.ts +76 -0
  45. package/src/obligation-source.ts +1084 -0
  46. package/src/obligation-tuples.ts +184 -0
  47. package/src/obligations.ts +316 -0
  48. package/src/parallel-worker.ts +183 -202
  49. package/src/parallel.ts +163 -84
  50. package/src/parser.ts +958 -205
  51. package/src/pretty-print.ts +259 -0
  52. package/src/randomization.ts +24 -113
  53. package/src/resolve.ts +472 -41
  54. package/src/run-cli.ts +427 -61
  55. package/src/runtime.ts +63 -28
  56. package/src/spec-description.ts +509 -0
  57. package/src/standard-levels.ts +105 -0
  58. package/src/standard-modules.ts +223 -92
  59. package/src/subexpressions.ts +21 -35
  60. package/src/substitution-levels.ts +184 -0
  61. package/src/successors.ts +261 -46
  62. package/src/symmetry.ts +70 -22
  63. package/src/tableau.ts +43 -37
  64. package/src/tlc-ext.ts +120 -33
  65. package/src/tlc-module.ts +71 -47
  66. package/src/tlc-modules.ts +1022 -0
  67. package/src/tlc-order.ts +497 -0
  68. package/src/tlc-random.ts +411 -0
  69. package/src/tlc-summary.ts +109 -0
  70. package/src/transfer.ts +26 -5
  71. package/src/values.ts +1234 -176
package/src/run-cli.ts CHANGED
@@ -1,95 +1,236 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { basename, dirname, join, resolve } from "node:path";
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 { type CheckResult, type CheckStatus } from "./check";
13
+ import type { CheckResult, CheckStatus, TraceStep } from "./check";
14
+ import { defineCommand, type ParsedArgs } from "./cli-options";
5
15
  import { parseConfig } from "./config";
6
- import { TlaError } from "./errors";
7
- import { checkInParallel } from "./parallel";
8
- import { formatValue } from "./values";
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: 0,
12
- "assumption-violation": 10,
13
- deadlock: 11,
14
- "invariant-violation": 12,
15
- "property-violation": 12,
16
- "liveness-violation": 13,
17
- "postcondition-violation": 10,
18
- };
19
-
20
- const USAGE = "Usage: tla-ts <Spec.tla> [-config <Spec.cfg>] [-deadlock] [-workers <count|auto>]";
21
-
22
- const getWorkerCount = (args: string[]) => {
23
- const flag = args.indexOf("-workers");
24
- if (flag === -1) return 1;
25
- const value = args[flag + 1];
26
- return value === "auto"
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(value ?? "1", 10) || 1);
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.violation) {
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
- "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.`,
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": `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.`}`,
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 (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;
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 ? `State ${loopStart + 2}: Stuttering` : `Back to state ${loopStart + 1}`,
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, 0 states left on queue.`,
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 run = async (args: string[]) => {
70
- const specPath = args.find((arg, index) => arg.endsWith(".tla") && args[index - 1] !== "-config");
71
- if (!specPath) {
72
- console.error(USAGE);
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 configFlag = args.indexOf("-config");
76
- const configPath = configFlag === -1 ? specPath.replace(/\.tla$/, ".cfg") : args[configFlag + 1];
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 1;
199
+ return EXIT_STATUS.configParseError;
80
200
  }
81
201
  const config = parseConfig(readFileSync(configPath, "utf8"));
82
- if (args.includes("-deadlock")) config.checkDeadlock = false;
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 checkInParallel({
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
- export const runCli = async () => {
104
- const outcome = { code: 1 };
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
- outcome.code = await run(process.argv.slice(2));
255
+ return await run();
107
256
  } catch (error) {
108
257
  if (!(error instanceof TlaError)) throw error;
109
- console.error(`${error.name}: ${error.message}`);
258
+ printError(error);
259
+ return getErrorExitCode(error);
110
260
  }
111
- process.exitCode = outcome.code;
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 { 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]));