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/src/run-cli.ts CHANGED
@@ -1,10 +1,29 @@
1
- import { existsSync, mkdirSync, readFileSync, readSync, 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
13
  import type { CheckResult, CheckStatus, TraceStep } from "./check";
14
+ import { defineCommand, type ParsedArgs } from "./cli-options";
5
15
  import { parseConfig } from "./config";
6
- import { getErrorExitCode, TlaConfigError, TlaError, TlaEvaluationError } from "./errors";
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 USAGE =
30
- "Usage: tla-ts <Spec.tla> [-config <Spec.cfg>] [-deadlock] [-lncheck <final|default>] [-seed <num>] [-workers <count|auto>]";
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 = (args: string[]) => {
36
- const flag = args.indexOf("-workers");
37
- if (flag === -1) return 1;
38
- const value = args[flag + 1];
39
- return value === "auto" ? availableParallelism() : Math.max(1, Number.parseInt(value, 10) || 1);
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 run = async (args: string[]) => {
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 configFlag = args.indexOf("-config");
144
- 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"));
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.includes("-deadlock")) config.checkDeadlock = false;
151
- const seedFlag = args.indexOf("-seed");
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 livenessFlag = args.indexOf("-lncheck");
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
- export const runCli = async () => {
252
+ // Prints a TlaError and returns its exit code.
253
+ const reportErrors = async (run: () => number | Promise<number>) => {
203
254
  try {
204
- process.exitCode = await run(process.argv.slice(2));
255
+ return await run();
205
256
  } catch (error) {
206
257
  if (!(error instanceof TlaError)) throw error;
207
258
  printError(error);
208
- process.exitCode = getErrorExitCode(error);
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
  };
@@ -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, 0, 0"]],
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, 0, 0", "1, 0, 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, 2, 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 permuted = computePermutedValue(value, permutation);
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
- return createFunction(domain.map((key, index): [Value, Value] => [key, range[index]]));
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 { createModelValue, createSortedFunction, createSortedSet, type Value } from "./values";
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 = { kind: "ModelValue", name };
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) ? compareStrings(left.name, right.name) : -1;
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 compareStrings(left.name, right.name);
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 domain = functionValue.domain;
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
- if (!isIntervalDomain(domain)) return createSortedSet(domain);
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 = findIndex(finite.domain, argument);
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];