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/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/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,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 = { kind: "ModelValue", name };
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 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);
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
- if (!isIntervalDomain(domain)) return createSortedSet(domain);
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 = findIndex(finite.domain, argument);
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];