tla-ts 0.0.2 → 0.0.3-dev.4c8c06c
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 +63 -11
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +51 -1
- package/dist/index.d.ts +51 -1
- 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 -2
- package/dist/run-cli.js +20 -2
- package/dist/transfer.cjs +14 -14
- package/dist/transfer.js +14 -14
- package/package.json +3 -1
- package/src/ast.ts +43 -3
- package/src/check.ts +43 -10
- package/src/cli-options.ts +58 -0
- package/src/errors.ts +37 -1
- package/src/evaluate.ts +3 -1
- package/src/fingerprints.ts +46 -11
- package/src/lexer.ts +11 -2
- 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 +163 -211
- package/src/parallel.ts +58 -33
- package/src/parser.ts +285 -52
- package/src/run-cli.ts +295 -27
- package/src/standard-levels.ts +3 -3
- package/src/symmetry.ts +14 -2
- package/src/transfer.ts +22 -1
- package/src/values.ts +59 -23
package/src/run-cli.ts
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
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
13
|
import type { CheckResult, CheckStatus, TraceStep } from "./check";
|
|
14
|
+
import { defineCommand, type ParsedArgs } from "./cli-options";
|
|
5
15
|
import { parseConfig } from "./config";
|
|
6
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
getErrorExitCode,
|
|
18
|
+
TlaConfigError,
|
|
19
|
+
TlaError,
|
|
20
|
+
TlaEvaluationError,
|
|
21
|
+
TlaProofError,
|
|
22
|
+
} from "./errors";
|
|
7
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";
|
|
8
27
|
import { check } from "./parallel";
|
|
9
28
|
import { prettyPrint } from "./pretty-print";
|
|
10
29
|
import {
|
|
@@ -26,17 +45,54 @@ const EXIT_CODES: Record<CheckStatus, number> = {
|
|
|
26
45
|
error: EXIT_STATUS.error,
|
|
27
46
|
};
|
|
28
47
|
|
|
29
|
-
const
|
|
30
|
-
|
|
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
|
+
);
|
|
31
87
|
|
|
32
88
|
const isJavaLong = (text: string) =>
|
|
33
89
|
/^[+-]?\d+$/.test(text) && BigInt.asIntN(64, BigInt(text)) === BigInt(text);
|
|
34
90
|
|
|
35
|
-
const getWorkerCount = (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
91
|
+
const getWorkerCount = (values: string[] | undefined) => {
|
|
92
|
+
if (!values) return 1;
|
|
93
|
+
return values[0] === "auto"
|
|
94
|
+
? availableParallelism()
|
|
95
|
+
: Math.max(1, Number.parseInt(values[0], 10) || 1);
|
|
40
96
|
};
|
|
41
97
|
|
|
42
98
|
const printState = (state: Record<string, Value>) => {
|
|
@@ -130,26 +186,21 @@ const printResult = (result: CheckResult) => {
|
|
|
130
186
|
if (outDegree) console.log(outDegree);
|
|
131
187
|
};
|
|
132
188
|
|
|
133
|
-
const
|
|
134
|
-
const specPath = args.find((arg, index) => arg.endsWith(".tla") && args[index - 1] !== "-config");
|
|
135
|
-
if (!specPath) {
|
|
136
|
-
console.error(USAGE);
|
|
137
|
-
return 1;
|
|
138
|
-
}
|
|
189
|
+
const checkSpec = async (specPath: string, args: ParsedArgs) => {
|
|
139
190
|
if (!existsSync(specPath)) {
|
|
140
191
|
console.error(`Cannot find the specification file ${specPath}`);
|
|
141
192
|
return EXIT_STATUS.configParseError;
|
|
142
193
|
}
|
|
143
|
-
const
|
|
144
|
-
|
|
194
|
+
const configPath =
|
|
195
|
+
args.get("-config")?.[0] ??
|
|
196
|
+
(args.has("-config") ? undefined : specPath.replace(/\.tla$/, ".cfg"));
|
|
145
197
|
if (!configPath || !existsSync(configPath)) {
|
|
146
198
|
console.error(`Cannot find the configuration file ${configPath ?? ""}`);
|
|
147
199
|
return EXIT_STATUS.configParseError;
|
|
148
200
|
}
|
|
149
201
|
const config = parseConfig(readFileSync(configPath, "utf8"));
|
|
150
|
-
if (args.
|
|
151
|
-
const
|
|
152
|
-
const seed = seedFlag === -1 ? "0" : args[seedFlag + 1];
|
|
202
|
+
if (args.has("-deadlock")) config.checkDeadlock = false;
|
|
203
|
+
const seed = args.has("-seed") ? args.get("-seed")?.[0] : "0";
|
|
153
204
|
if (seed === undefined) {
|
|
154
205
|
console.error("Error: seed required.");
|
|
155
206
|
return 1;
|
|
@@ -158,8 +209,7 @@ const run = async (args: string[]) => {
|
|
|
158
209
|
console.error(`Error: An integer for seed required. But encountered ${seed}`);
|
|
159
210
|
return 1;
|
|
160
211
|
}
|
|
161
|
-
const
|
|
162
|
-
const livenessCheck = livenessFlag === -1 ? "default" : args[livenessFlag + 1]?.toLowerCase();
|
|
212
|
+
const livenessCheck = args.has("-lncheck") ? args.get("-lncheck")?.[0]?.toLowerCase() : "default";
|
|
163
213
|
if (livenessCheck === undefined) {
|
|
164
214
|
console.error("Error: expect a strategy such as final for -lncheck option.");
|
|
165
215
|
return 1;
|
|
@@ -167,7 +217,7 @@ const run = async (args: string[]) => {
|
|
|
167
217
|
const directory = dirname(specPath);
|
|
168
218
|
console.log(`Checking ${basename(specPath)} with ${basename(configPath)}`);
|
|
169
219
|
const result = await check({
|
|
170
|
-
workers: getWorkerCount(args),
|
|
220
|
+
workers: getWorkerCount(args.get("-workers")),
|
|
171
221
|
livenessCheck: ["final", "seqfinal", "off"].includes(livenessCheck) ? "final" : "default",
|
|
172
222
|
seed: BigInt(seed),
|
|
173
223
|
source: readFileSync(specPath, "utf8"),
|
|
@@ -199,12 +249,230 @@ const printError = (error: TlaError) => {
|
|
|
199
249
|
console.error(`${error.name}: ${error.message}`);
|
|
200
250
|
};
|
|
201
251
|
|
|
202
|
-
|
|
252
|
+
// Prints a TlaError and returns its exit code.
|
|
253
|
+
const reportErrors = async (run: () => number | Promise<number>) => {
|
|
203
254
|
try {
|
|
204
|
-
|
|
255
|
+
return await run();
|
|
205
256
|
} catch (error) {
|
|
206
257
|
if (!(error instanceof TlaError)) throw error;
|
|
207
258
|
printError(error);
|
|
208
|
-
|
|
259
|
+
return getErrorExitCode(error);
|
|
260
|
+
}
|
|
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
|
+
);
|
|
209
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));
|
|
210
478
|
};
|
package/src/standard-levels.ts
CHANGED
|
@@ -28,7 +28,7 @@ const SIGNATURES: Signature[] = [
|
|
|
28
28
|
["Head", 0, [0], [2], [1]],
|
|
29
29
|
["Tail", 0, [0], [2], [1]],
|
|
30
30
|
["SubSeq", 0, [0, 0, 0], [2, 2, 2], [1, 1, 1]],
|
|
31
|
-
["SelectSeq", 0, [0, 1], [2, 2], [1, 1], [], ["1,
|
|
31
|
+
["SelectSeq", 0, [0, 1], [2, 2], [1, 1], [], ["1,0,0"]],
|
|
32
32
|
["IsFiniteSet", 0, [0], [2], [1]],
|
|
33
33
|
["Cardinality", 0, [0], [2], [1]],
|
|
34
34
|
["IsABag", 0, [0], [2], [1]],
|
|
@@ -53,7 +53,7 @@ const SIGNATURES: Signature[] = [
|
|
|
53
53
|
[":>", 0, [0, 0], [2, 3], [1, 1]],
|
|
54
54
|
["@@", 0, [0, 0], [2, 2], [1, 1]],
|
|
55
55
|
["Permutations", 0, [0], [2], [1]],
|
|
56
|
-
["SortSeq", 0, [0, 2], [2, 2], [1, 1], [], ["1,
|
|
56
|
+
["SortSeq", 0, [0, 2], [2, 2], [1, 1], [], ["1,0,0", "1,0,1"]],
|
|
57
57
|
["RandomElement", 0, [0], [3], [1]],
|
|
58
58
|
["Any", 0, [], [], []],
|
|
59
59
|
["ToString", 0, [0], [2], [1]],
|
|
@@ -61,7 +61,7 @@ const SIGNATURES: Signature[] = [
|
|
|
61
61
|
["AssertEq", 0, [0, 0], [2, 2], [1, 1]],
|
|
62
62
|
["AssertError", 0, [0, 0], [2, 3], [1, 0]],
|
|
63
63
|
["TLCGetOrDefault", 0, [0, 0], [3, 3], [0, 0]],
|
|
64
|
-
["TLCGetAndSet", 0, [0, 2, 0, 0], [3, 3, 3, 3], [0, 0, 0, 0], [], ["1,
|
|
64
|
+
["TLCGetAndSet", 0, [0, 2, 0, 0], [3, 3, 3, 3], [0, 0, 0, 0], [], ["1,2,1"]],
|
|
65
65
|
["Trace", 0, [], [], []],
|
|
66
66
|
["CounterExample", 0, [], [], []],
|
|
67
67
|
["ToTrace", 0, [0], [3], [0]],
|
package/src/symmetry.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isEnumerable,
|
|
10
10
|
isFunctionValue,
|
|
11
11
|
isModelValue,
|
|
12
|
+
isSameValue,
|
|
12
13
|
isSetValue,
|
|
13
14
|
toFiniteFunction,
|
|
14
15
|
toFiniteSet,
|
|
@@ -163,7 +164,10 @@ const permuteValue = (value: Value, permutation: Permutation): Value => {
|
|
|
163
164
|
if (size === 0 || size > CACHED_SIZE_LIMIT) return computePermutedValue(value, permutation);
|
|
164
165
|
const cached = permutation.cache.get(value);
|
|
165
166
|
if (cached !== undefined) return cached;
|
|
166
|
-
const
|
|
167
|
+
const computed = computePermutedValue(value, permutation);
|
|
168
|
+
// A permutation often maps a value to an equal one. Keeping the original lets the comparison
|
|
169
|
+
// with the smallest state so far stop at identity.
|
|
170
|
+
const permuted = computed !== value && isSameValue(computed, value) ? value : computed;
|
|
167
171
|
if (permutation.cacheSize >= permutation.cacheLimit) {
|
|
168
172
|
permutation.cache = new WeakMap();
|
|
169
173
|
permutation.cacheSize = 0;
|
|
@@ -184,7 +188,15 @@ const computePermutedValue = (value: ObjectValue, permutation: Permutation) => {
|
|
|
184
188
|
const domain = permuteElements(functionValue.domain, permutation);
|
|
185
189
|
const range = permuteElements(functionValue.range, permutation) ?? functionValue.range;
|
|
186
190
|
if (domain) {
|
|
187
|
-
|
|
191
|
+
const permuted = createFunction(
|
|
192
|
+
domain.map((key, index): [Value, Value] => [key, range[index]]),
|
|
193
|
+
);
|
|
194
|
+
// A permutation often maps the domain to itself, such as a function over the symmetric set.
|
|
195
|
+
// Sharing the original domain array lets comparisons with other images skip the domain.
|
|
196
|
+
const isSameDomain = permuted.domain.every(
|
|
197
|
+
(key, index) => key === functionValue.domain[index],
|
|
198
|
+
);
|
|
199
|
+
return isSameDomain ? createSortedFunction(functionValue.domain, permuted.range) : permuted;
|
|
188
200
|
}
|
|
189
201
|
return range === functionValue.range
|
|
190
202
|
? functionValue
|
package/src/transfer.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createModelValue,
|
|
3
|
+
createSortedFunction,
|
|
4
|
+
createSortedSet,
|
|
5
|
+
getDomainInterval,
|
|
6
|
+
getFunctionDomain,
|
|
7
|
+
type Value,
|
|
8
|
+
} from "./values";
|
|
2
9
|
|
|
3
10
|
const FALSE = 0;
|
|
4
11
|
const TRUE = 1;
|
|
@@ -7,6 +14,7 @@ const STRING = 3;
|
|
|
7
14
|
const MODEL_VALUE = 4;
|
|
8
15
|
const SET = 5;
|
|
9
16
|
const FUNCTION = 6;
|
|
17
|
+
const INTERVAL_FUNCTION = 7;
|
|
10
18
|
|
|
11
19
|
export interface EncodedStates {
|
|
12
20
|
words: Int32Array;
|
|
@@ -65,6 +73,13 @@ export const createStateEncoder = (): StateEncoder => {
|
|
|
65
73
|
return value.elements.every(encodeValue);
|
|
66
74
|
}
|
|
67
75
|
if (value.kind === "Function") {
|
|
76
|
+
const interval = getDomainInterval(value.domain);
|
|
77
|
+
if (interval) {
|
|
78
|
+
push(INTERVAL_FUNCTION);
|
|
79
|
+
push(interval.low);
|
|
80
|
+
push(value.range.length);
|
|
81
|
+
return value.range.every(encodeValue);
|
|
82
|
+
}
|
|
68
83
|
push(FUNCTION);
|
|
69
84
|
push(value.domain.length);
|
|
70
85
|
return value.domain.every(encodeValue) && value.range.every(encodeValue);
|
|
@@ -102,6 +117,12 @@ export const createStateDecoder =
|
|
|
102
117
|
if (tag === INTEGER) return words[cursor.index++];
|
|
103
118
|
if (tag === STRING) return encoded.strings[words[cursor.index++]];
|
|
104
119
|
if (tag === MODEL_VALUE) return createModelValue(encoded.strings[words[cursor.index++]]);
|
|
120
|
+
if (tag === INTERVAL_FUNCTION) {
|
|
121
|
+
const low = words[cursor.index++];
|
|
122
|
+
const length = words[cursor.index++];
|
|
123
|
+
const domain = getFunctionDomain({ kind: "Interval", low, high: low + length - 1 });
|
|
124
|
+
return createSortedFunction(domain, decodeElements(length));
|
|
125
|
+
}
|
|
105
126
|
const length = words[cursor.index++];
|
|
106
127
|
if (tag === SET) return createSortedSet(decodeElements(length));
|
|
107
128
|
const domain = decodeElements(length);
|
package/src/values.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { prettyPrint } from "./pretty-print";
|
|
|
4
4
|
export interface ModelValue {
|
|
5
5
|
kind: "ModelValue";
|
|
6
6
|
name: string;
|
|
7
|
+
type: string | undefined;
|
|
8
|
+
rank: number;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export interface SetValue {
|
|
@@ -233,8 +235,7 @@ const compareSets = (left: SetValue, right: SetValue) => {
|
|
|
233
235
|
return 0;
|
|
234
236
|
};
|
|
235
237
|
|
|
236
|
-
const getModelValueType = (value: Value) =>
|
|
237
|
-
isModelValue(value) && value.name.length > 2 && value.name[1] === "_" ? value.name[0] : undefined;
|
|
238
|
+
const getModelValueType = (value: Value) => (isModelValue(value) ? value.type : undefined);
|
|
238
239
|
|
|
239
240
|
const checkModelValueTypes = (left: Value, right: Value, isEquality: boolean) => {
|
|
240
241
|
const leftType = getModelValueType(left);
|
|
@@ -264,6 +265,7 @@ const stringOrder = { ranks: Object.create(null) as Record<string, number>, size
|
|
|
264
265
|
export const setStringOrder = (texts: Iterable<string>) => {
|
|
265
266
|
stringOrder.ranks = Object.create(null);
|
|
266
267
|
stringOrder.size = 0;
|
|
268
|
+
modelValues.clear();
|
|
267
269
|
for (const text of texts) rankString(text);
|
|
268
270
|
};
|
|
269
271
|
|
|
@@ -349,11 +351,14 @@ export const compareValues = (left: Value, right: Value): number => {
|
|
|
349
351
|
const otherSize = Math.max(0, right.high - right.low + 1);
|
|
350
352
|
return size !== otherSize || size === 0 ? size - otherSize : left.low - right.low;
|
|
351
353
|
}
|
|
354
|
+
if (leftKind === "ModelValue" && rightKind === "ModelValue") {
|
|
355
|
+
if (left.type !== undefined && right.type !== undefined) {
|
|
356
|
+
checkModelValueTypes(left, right, false);
|
|
357
|
+
}
|
|
358
|
+
return left.rank - right.rank;
|
|
359
|
+
}
|
|
352
360
|
if (leftKind === "ModelValue" || rightKind === "ModelValue") {
|
|
353
361
|
checkModelValueTypes(left, right, false);
|
|
354
|
-
if (leftKind === "ModelValue" && rightKind === "ModelValue") {
|
|
355
|
-
return compareStrings(left.name, right.name);
|
|
356
|
-
}
|
|
357
362
|
return leftKind === "ModelValue" ? -1 : 1;
|
|
358
363
|
}
|
|
359
364
|
if (left === ANY_SET || right === ANY_SET) return compareAny(left === ANY_SET ? right : left);
|
|
@@ -382,11 +387,16 @@ export const compareValues = (left: Value, right: Value): number => {
|
|
|
382
387
|
|
|
383
388
|
const modelValues = new Map<string, ModelValue>();
|
|
384
389
|
|
|
390
|
+
// A check ranks every model value's name when it creates the value, and creates its own values.
|
|
385
391
|
export const createModelValue = (name: string): ModelValue => {
|
|
386
|
-
rankString(name);
|
|
387
392
|
const known = modelValues.get(name);
|
|
388
393
|
if (known) return known;
|
|
389
|
-
const value: ModelValue = {
|
|
394
|
+
const value: ModelValue = {
|
|
395
|
+
kind: "ModelValue",
|
|
396
|
+
name,
|
|
397
|
+
type: name.length > 2 && name[1] === "_" ? name[0] : undefined,
|
|
398
|
+
rank: getStringRank(name),
|
|
399
|
+
};
|
|
390
400
|
modelValues.set(name, value);
|
|
391
401
|
return value;
|
|
392
402
|
};
|
|
@@ -555,7 +565,7 @@ const equalsModelValueWith = (modelValue: ModelValue, other: Value) => {
|
|
|
555
565
|
|
|
556
566
|
const compareModelValue = (left: ModelValue, right: Value) => {
|
|
557
567
|
const type = getModelValueType(left);
|
|
558
|
-
if (type === undefined) return isModelValue(right) ?
|
|
568
|
+
if (type === undefined) return isModelValue(right) ? left.rank - right.rank : -1;
|
|
559
569
|
if (!isModelValue(right)) {
|
|
560
570
|
throw new TlaEvaluationError(
|
|
561
571
|
`Attempted to compare the typed model value ${formatValue(left)} and non-model value\n${formatValue(right)}`,
|
|
@@ -567,7 +577,7 @@ const compareModelValue = (left: ModelValue, right: Value) => {
|
|
|
567
577
|
`Attempted to compare the differently-typed model values ${formatValue(left)} and ${formatValue(right)}`,
|
|
568
578
|
);
|
|
569
579
|
}
|
|
570
|
-
return
|
|
580
|
+
return left.rank - right.rank;
|
|
571
581
|
};
|
|
572
582
|
|
|
573
583
|
const equalsModelValue = (left: ModelValue, right: Value) => {
|
|
@@ -1667,6 +1677,25 @@ export const isElementOf = (element: Value, set: Value): boolean => {
|
|
|
1667
1677
|
}
|
|
1668
1678
|
};
|
|
1669
1679
|
|
|
1680
|
+
// A function that agrees with a member of a function set outside some entries is a member when
|
|
1681
|
+
// those entries are.
|
|
1682
|
+
export const isUpdatedElementOf = (element: Value, member: Value, set: Value): boolean => {
|
|
1683
|
+
if (element === member) return true;
|
|
1684
|
+
if (
|
|
1685
|
+
typeof element !== "object" ||
|
|
1686
|
+
typeof member !== "object" ||
|
|
1687
|
+
typeof set !== "object" ||
|
|
1688
|
+
element.kind !== "Function" ||
|
|
1689
|
+
member.kind !== "Function" ||
|
|
1690
|
+
set.kind !== "FunctionSet" ||
|
|
1691
|
+
element.domain !== member.domain
|
|
1692
|
+
)
|
|
1693
|
+
return isElementOf(element, set);
|
|
1694
|
+
return element.range.every((item, index) =>
|
|
1695
|
+
isUpdatedElementOf(item, member.range[index], set.range),
|
|
1696
|
+
);
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1670
1699
|
export const toFiniteFunction = (value: Value): FunctionValue => {
|
|
1671
1700
|
if (typeof value === "object" && value.kind === "Function") return value;
|
|
1672
1701
|
if (typeof value === "object" && value.kind === "LazyFunction") {
|
|
@@ -1694,6 +1723,19 @@ const describeMissingArgument = (functionValue: Value, argument: Value) =>
|
|
|
1694
1723
|
? `In applying the function\n${prettyPrint(formatValue(functionValue))},\nthe first argument is:\n${prettyPrint(formatValue(argument))}\nwhich is not in its domain.\n`
|
|
1695
1724
|
: `Attempted to apply function ${formatValue(functionValue)} to ${formatValue(argument)}, which is not in its domain`;
|
|
1696
1725
|
|
|
1726
|
+
const findKeyIndex = (domain: Value[], argument: Value) => {
|
|
1727
|
+
const first = domain[0];
|
|
1728
|
+
if (typeof argument === "number" && typeof first === "number") {
|
|
1729
|
+
const offset = argument - first;
|
|
1730
|
+
if (domain[offset] === argument) return offset;
|
|
1731
|
+
}
|
|
1732
|
+
if (domain.length <= 8) {
|
|
1733
|
+
for (let index = 0; index < domain.length; index++)
|
|
1734
|
+
if (domain[index] === argument) return index;
|
|
1735
|
+
}
|
|
1736
|
+
return findIndex(domain, argument);
|
|
1737
|
+
};
|
|
1738
|
+
|
|
1697
1739
|
export const applyFunction = (
|
|
1698
1740
|
functionValue: Value,
|
|
1699
1741
|
argument: Value,
|
|
@@ -1714,17 +1756,7 @@ export const applyFunction = (
|
|
|
1714
1756
|
`Attempted to apply ${formatValue(functionValue)}, which is not a function`,
|
|
1715
1757
|
);
|
|
1716
1758
|
}
|
|
1717
|
-
const
|
|
1718
|
-
const first = domain[0];
|
|
1719
|
-
if (typeof argument === "number" && typeof first === "number") {
|
|
1720
|
-
const offset = argument - first;
|
|
1721
|
-
if (domain[offset] === argument) return functionValue.range[offset];
|
|
1722
|
-
}
|
|
1723
|
-
if (domain.length <= 8) {
|
|
1724
|
-
for (let index = 0; index < domain.length; index++)
|
|
1725
|
-
if (domain[index] === argument) return functionValue.range[index];
|
|
1726
|
-
}
|
|
1727
|
-
const position = findIndex(domain, argument);
|
|
1759
|
+
const position = findKeyIndex(functionValue.domain, argument);
|
|
1728
1760
|
if (position === -1) {
|
|
1729
1761
|
throw new TlaEvaluationError(
|
|
1730
1762
|
describeMissing?.(functionValue) ?? describeMissingArgument(functionValue, argument),
|
|
@@ -1733,12 +1765,16 @@ export const applyFunction = (
|
|
|
1733
1765
|
return functionValue.range[position];
|
|
1734
1766
|
};
|
|
1735
1767
|
|
|
1768
|
+
export const getDomainInterval = (domain: Value[]) =>
|
|
1769
|
+
isIntervalDomain(domain)
|
|
1770
|
+
? (intervalArrays.get(domain) ?? getIntervalValue(domain.length))
|
|
1771
|
+
: undefined;
|
|
1772
|
+
|
|
1736
1773
|
export const getDomain = (functionValue: Value): Value => {
|
|
1737
1774
|
if (typeof functionValue === "object" && functionValue.kind === "LazyFunction")
|
|
1738
1775
|
return functionValue.domain;
|
|
1739
1776
|
const domain = toFiniteFunction(functionValue).domain;
|
|
1740
|
-
|
|
1741
|
-
return intervalArrays.get(domain) ?? getIntervalValue(domain.length);
|
|
1777
|
+
return getDomainInterval(domain) ?? createSortedSet(domain);
|
|
1742
1778
|
};
|
|
1743
1779
|
|
|
1744
1780
|
export const updateFunction = (
|
|
@@ -1762,7 +1798,7 @@ export const updateFunction = (
|
|
|
1762
1798
|
};
|
|
1763
1799
|
}
|
|
1764
1800
|
const finite = toFiniteFunction(functionValue);
|
|
1765
|
-
const position =
|
|
1801
|
+
const position = findKeyIndex(finite.domain, argument);
|
|
1766
1802
|
if (position === -1) return finite;
|
|
1767
1803
|
const range = [...finite.range];
|
|
1768
1804
|
const previous = finite.range[position];
|