tla-ts 0.0.2 → 0.0.3-dev.a50fc89
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 +56 -10
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +50 -1
- package/dist/index.d.ts +50 -1
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +1 -1
- 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 +12 -12
- package/dist/transfer.js +12 -12
- 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/lexer.ts +6 -1
- 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/parser.ts +285 -52
- package/src/run-cli.ts +295 -27
- package/src/standard-levels.ts +3 -3
- package/src/transfer.ts +22 -1
- package/src/values.ts +53 -20
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/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,7 @@ import { prettyPrint } from "./pretty-print";
|
|
|
4
4
|
export interface ModelValue {
|
|
5
5
|
kind: "ModelValue";
|
|
6
6
|
name: string;
|
|
7
|
+
type: string | undefined;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export interface SetValue {
|
|
@@ -233,8 +234,7 @@ const compareSets = (left: SetValue, right: SetValue) => {
|
|
|
233
234
|
return 0;
|
|
234
235
|
};
|
|
235
236
|
|
|
236
|
-
const getModelValueType = (value: Value) =>
|
|
237
|
-
isModelValue(value) && value.name.length > 2 && value.name[1] === "_" ? value.name[0] : undefined;
|
|
237
|
+
const getModelValueType = (value: Value) => (isModelValue(value) ? value.type : undefined);
|
|
238
238
|
|
|
239
239
|
const checkModelValueTypes = (left: Value, right: Value, isEquality: boolean) => {
|
|
240
240
|
const leftType = getModelValueType(left);
|
|
@@ -349,11 +349,14 @@ export const compareValues = (left: Value, right: Value): number => {
|
|
|
349
349
|
const otherSize = Math.max(0, right.high - right.low + 1);
|
|
350
350
|
return size !== otherSize || size === 0 ? size - otherSize : left.low - right.low;
|
|
351
351
|
}
|
|
352
|
+
if (leftKind === "ModelValue" && rightKind === "ModelValue") {
|
|
353
|
+
if (left.type !== undefined && right.type !== undefined) {
|
|
354
|
+
checkModelValueTypes(left, right, false);
|
|
355
|
+
}
|
|
356
|
+
return compareStrings(left.name, right.name);
|
|
357
|
+
}
|
|
352
358
|
if (leftKind === "ModelValue" || rightKind === "ModelValue") {
|
|
353
359
|
checkModelValueTypes(left, right, false);
|
|
354
|
-
if (leftKind === "ModelValue" && rightKind === "ModelValue") {
|
|
355
|
-
return compareStrings(left.name, right.name);
|
|
356
|
-
}
|
|
357
360
|
return leftKind === "ModelValue" ? -1 : 1;
|
|
358
361
|
}
|
|
359
362
|
if (left === ANY_SET || right === ANY_SET) return compareAny(left === ANY_SET ? right : left);
|
|
@@ -386,7 +389,11 @@ export const createModelValue = (name: string): ModelValue => {
|
|
|
386
389
|
rankString(name);
|
|
387
390
|
const known = modelValues.get(name);
|
|
388
391
|
if (known) return known;
|
|
389
|
-
const value: ModelValue = {
|
|
392
|
+
const value: ModelValue = {
|
|
393
|
+
kind: "ModelValue",
|
|
394
|
+
name,
|
|
395
|
+
type: name.length > 2 && name[1] === "_" ? name[0] : undefined,
|
|
396
|
+
};
|
|
390
397
|
modelValues.set(name, value);
|
|
391
398
|
return value;
|
|
392
399
|
};
|
|
@@ -1667,6 +1674,25 @@ export const isElementOf = (element: Value, set: Value): boolean => {
|
|
|
1667
1674
|
}
|
|
1668
1675
|
};
|
|
1669
1676
|
|
|
1677
|
+
// A function that agrees with a member of a function set outside some entries is a member when
|
|
1678
|
+
// those entries are.
|
|
1679
|
+
export const isUpdatedElementOf = (element: Value, member: Value, set: Value): boolean => {
|
|
1680
|
+
if (element === member) return true;
|
|
1681
|
+
if (
|
|
1682
|
+
typeof element !== "object" ||
|
|
1683
|
+
typeof member !== "object" ||
|
|
1684
|
+
typeof set !== "object" ||
|
|
1685
|
+
element.kind !== "Function" ||
|
|
1686
|
+
member.kind !== "Function" ||
|
|
1687
|
+
set.kind !== "FunctionSet" ||
|
|
1688
|
+
element.domain !== member.domain
|
|
1689
|
+
)
|
|
1690
|
+
return isElementOf(element, set);
|
|
1691
|
+
return element.range.every((item, index) =>
|
|
1692
|
+
isUpdatedElementOf(item, member.range[index], set.range),
|
|
1693
|
+
);
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1670
1696
|
export const toFiniteFunction = (value: Value): FunctionValue => {
|
|
1671
1697
|
if (typeof value === "object" && value.kind === "Function") return value;
|
|
1672
1698
|
if (typeof value === "object" && value.kind === "LazyFunction") {
|
|
@@ -1694,6 +1720,19 @@ const describeMissingArgument = (functionValue: Value, argument: Value) =>
|
|
|
1694
1720
|
? `In applying the function\n${prettyPrint(formatValue(functionValue))},\nthe first argument is:\n${prettyPrint(formatValue(argument))}\nwhich is not in its domain.\n`
|
|
1695
1721
|
: `Attempted to apply function ${formatValue(functionValue)} to ${formatValue(argument)}, which is not in its domain`;
|
|
1696
1722
|
|
|
1723
|
+
const findKeyIndex = (domain: Value[], argument: Value) => {
|
|
1724
|
+
const first = domain[0];
|
|
1725
|
+
if (typeof argument === "number" && typeof first === "number") {
|
|
1726
|
+
const offset = argument - first;
|
|
1727
|
+
if (domain[offset] === argument) return offset;
|
|
1728
|
+
}
|
|
1729
|
+
if (domain.length <= 8) {
|
|
1730
|
+
for (let index = 0; index < domain.length; index++)
|
|
1731
|
+
if (domain[index] === argument) return index;
|
|
1732
|
+
}
|
|
1733
|
+
return findIndex(domain, argument);
|
|
1734
|
+
};
|
|
1735
|
+
|
|
1697
1736
|
export const applyFunction = (
|
|
1698
1737
|
functionValue: Value,
|
|
1699
1738
|
argument: Value,
|
|
@@ -1714,17 +1753,7 @@ export const applyFunction = (
|
|
|
1714
1753
|
`Attempted to apply ${formatValue(functionValue)}, which is not a function`,
|
|
1715
1754
|
);
|
|
1716
1755
|
}
|
|
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);
|
|
1756
|
+
const position = findKeyIndex(functionValue.domain, argument);
|
|
1728
1757
|
if (position === -1) {
|
|
1729
1758
|
throw new TlaEvaluationError(
|
|
1730
1759
|
describeMissing?.(functionValue) ?? describeMissingArgument(functionValue, argument),
|
|
@@ -1733,12 +1762,16 @@ export const applyFunction = (
|
|
|
1733
1762
|
return functionValue.range[position];
|
|
1734
1763
|
};
|
|
1735
1764
|
|
|
1765
|
+
export const getDomainInterval = (domain: Value[]) =>
|
|
1766
|
+
isIntervalDomain(domain)
|
|
1767
|
+
? (intervalArrays.get(domain) ?? getIntervalValue(domain.length))
|
|
1768
|
+
: undefined;
|
|
1769
|
+
|
|
1736
1770
|
export const getDomain = (functionValue: Value): Value => {
|
|
1737
1771
|
if (typeof functionValue === "object" && functionValue.kind === "LazyFunction")
|
|
1738
1772
|
return functionValue.domain;
|
|
1739
1773
|
const domain = toFiniteFunction(functionValue).domain;
|
|
1740
|
-
|
|
1741
|
-
return intervalArrays.get(domain) ?? getIntervalValue(domain.length);
|
|
1774
|
+
return getDomainInterval(domain) ?? createSortedSet(domain);
|
|
1742
1775
|
};
|
|
1743
1776
|
|
|
1744
1777
|
export const updateFunction = (
|
|
@@ -1762,7 +1795,7 @@ export const updateFunction = (
|
|
|
1762
1795
|
};
|
|
1763
1796
|
}
|
|
1764
1797
|
const finite = toFiniteFunction(functionValue);
|
|
1765
|
-
const position =
|
|
1798
|
+
const position = findKeyIndex(finite.domain, argument);
|
|
1766
1799
|
if (position === -1) return finite;
|
|
1767
1800
|
const range = [...finite.range];
|
|
1768
1801
|
const previous = finite.range[position];
|