tla-ts 0.0.1 → 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.
Files changed (71) hide show
  1. package/README.md +81 -42
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +215 -30
  4. package/dist/index.d.ts +215 -30
  5. package/dist/index.js +1 -1
  6. package/dist/parallel-worker.cjs +2 -2
  7. package/dist/parallel-worker.js +2 -2
  8. package/dist/parallel.cjs +1 -1
  9. package/dist/parallel.js +2 -2
  10. package/dist/run-cli.cjs +20 -1
  11. package/dist/run-cli.js +20 -1
  12. package/dist/transfer.cjs +388 -39
  13. package/dist/transfer.js +388 -39
  14. package/package.json +6 -2
  15. package/src/ast.ts +122 -5
  16. package/src/bags.ts +161 -71
  17. package/src/builtins.ts +203 -23
  18. package/src/bundled-modules.ts +63 -32
  19. package/src/check.ts +945 -238
  20. package/src/cli-options.ts +58 -0
  21. package/src/codegen.ts +52 -18
  22. package/src/config.ts +39 -11
  23. package/src/environment.ts +485 -140
  24. package/src/errors.ts +59 -1
  25. package/src/evaluate.ts +489 -137
  26. package/src/exit-codes.ts +75 -0
  27. package/src/fingerprints.ts +2 -2
  28. package/src/index.ts +9 -4
  29. package/src/instances.ts +221 -66
  30. package/src/java-overrides.ts +241 -0
  31. package/src/json.ts +153 -72
  32. package/src/levels.ts +850 -0
  33. package/src/lexer.ts +103 -28
  34. package/src/liveness.ts +47 -91
  35. package/src/obligation-action.ts +384 -0
  36. package/src/obligation-anon.ts +575 -0
  37. package/src/obligation-deref.ts +637 -0
  38. package/src/obligation-expression.ts +1060 -0
  39. package/src/obligation-format.ts +406 -0
  40. package/src/obligation-generate.ts +1074 -0
  41. package/src/obligation-levels.ts +329 -0
  42. package/src/obligation-module.ts +1042 -0
  43. package/src/obligation-printer.ts +889 -0
  44. package/src/obligation-proof.ts +76 -0
  45. package/src/obligation-source.ts +1084 -0
  46. package/src/obligation-tuples.ts +184 -0
  47. package/src/obligations.ts +316 -0
  48. package/src/parallel-worker.ts +57 -28
  49. package/src/parallel.ts +116 -62
  50. package/src/parser.ts +958 -205
  51. package/src/pretty-print.ts +259 -0
  52. package/src/randomization.ts +24 -113
  53. package/src/resolve.ts +472 -41
  54. package/src/run-cli.ts +427 -61
  55. package/src/runtime.ts +63 -28
  56. package/src/spec-description.ts +509 -0
  57. package/src/standard-levels.ts +105 -0
  58. package/src/standard-modules.ts +223 -92
  59. package/src/subexpressions.ts +21 -35
  60. package/src/substitution-levels.ts +184 -0
  61. package/src/successors.ts +261 -46
  62. package/src/symmetry.ts +65 -21
  63. package/src/tableau.ts +43 -37
  64. package/src/tlc-ext.ts +120 -33
  65. package/src/tlc-module.ts +71 -47
  66. package/src/tlc-modules.ts +1022 -0
  67. package/src/tlc-order.ts +497 -0
  68. package/src/tlc-random.ts +411 -0
  69. package/src/tlc-summary.ts +109 -0
  70. package/src/transfer.ts +26 -5
  71. package/src/values.ts +1231 -176
package/src/evaluate.ts CHANGED
@@ -1,35 +1,51 @@
1
1
  import type {
2
2
  Bound,
3
3
  BoundPattern,
4
+ DecimalExpression,
4
5
  Definition,
5
6
  Expression,
6
7
  FunctionDefinition,
8
+ NumberExpression,
7
9
  OperatorParam,
8
10
  RecordField,
11
+ SourceRange,
9
12
  } from "./ast";
13
+ import { getKindString } from "./builtins";
10
14
  import { TlaEvaluationError } from "./errors";
15
+ import { EXIT_STATUS, setExitCode } from "./exit-codes";
11
16
  import { generateEvaluator } from "./codegen";
12
17
  import type { CheckerRuntime } from "./runtime";
13
18
  import { getReadVariables } from "./dependencies";
14
- import { mapChildren, substitutedExpressions } from "./instances";
19
+ import { contextualApplications, mapChildren, substitutedExpressions } from "./instances";
15
20
  import { isEnabled } from "./successors";
16
21
  import {
17
22
  applyFunction,
18
- checkInteger,
23
+ combineHash,
24
+ compareValues,
19
25
  createFunction,
20
- createSet,
26
+ createEnumeratedSet,
21
27
  createSortedFunction,
22
28
  createSortedSet,
29
+ createWrittenRecord,
23
30
  createTuple,
24
31
  formatValue,
32
+ getDomain,
33
+ getFunctionDomain,
34
+ getIntervalElements,
25
35
  getSequenceElements,
26
36
  getValueHash,
27
37
  isEnumerable,
38
+ isFunctionValue,
39
+ isOverriddenSet,
28
40
  isSameValue,
41
+ markLambdaFunction,
29
42
  isSetValue,
43
+ MAXIMUM_INTEGER,
44
+ MINIMUM_INTEGER,
30
45
  toFiniteSet,
31
46
  updateFunction,
32
47
  valuesEqual,
48
+ type LazyFunctionSource,
33
49
  type LazyFunctionValue,
34
50
  type Value,
35
51
  } from "./values";
@@ -42,6 +58,7 @@ export interface OperatorValue {
42
58
  arity: number;
43
59
  call: (args: Value[]) => Value;
44
60
  expression?: Expression;
61
+ literalBody?: Value;
45
62
  }
46
63
 
47
64
  export type BuiltinArgument = Value | OperatorValue;
@@ -65,7 +82,7 @@ export interface Closure {
65
82
  scope: Scope | undefined;
66
83
  }
67
84
 
68
- export type Operator = Closure | Builtin | OperatorValue;
85
+ type Operator = Closure | Builtin | OperatorValue;
69
86
 
70
87
  export interface Thunk {
71
88
  kind: "Thunk";
@@ -76,10 +93,7 @@ export interface Thunk {
76
93
  cache: Value | undefined;
77
94
  }
78
95
 
79
- export type Binding =
80
- | { kind: "Value"; value: Value }
81
- | Thunk
82
- | { kind: "Operator"; operator: Operator };
96
+ type Binding = { kind: "Value"; value: Value } | Thunk | { kind: "Operator"; operator: Operator };
83
97
 
84
98
  export interface Scope {
85
99
  name: string;
@@ -111,6 +125,12 @@ export type Evaluator = (context: Context) => Value;
111
125
 
112
126
  type ArgumentBinder = (context: Context) => Binding;
113
127
 
128
+ interface CompiledStep {
129
+ kind: "Field" | "Index";
130
+ name: string;
131
+ args: Evaluator[];
132
+ }
133
+
114
134
  type GlobalName =
115
135
  | { kind: "Variable"; index: number }
116
136
  | { kind: "Value"; value: Value }
@@ -118,9 +138,87 @@ type GlobalName =
118
138
 
119
139
  const EXCEPT_AT = "@";
120
140
 
121
- const describeLocation = (expression: Expression) =>
141
+ export const describeLocation = (expression: Expression) =>
122
142
  `line ${expression.location.line}, column ${expression.location.column}`;
123
143
 
144
+ export const formatRange = (range: SourceRange) =>
145
+ `line ${range.start.line}, col ${range.start.column} to line ${range.end.line}, col ${range.end.column} of module ${range.module}`;
146
+
147
+ export const describeRange = (expression: Expression) => {
148
+ const range = expression.location.range;
149
+ return range ? formatRange(range) : describeLocation(expression);
150
+ };
151
+
152
+ export const getConditionError = (expression: Expression, value: Value) =>
153
+ new TlaEvaluationError(
154
+ `A non-boolean expression (${getKindString(value)}) was used as the condition of an IF.\n${describeRange(expression)}`,
155
+ );
156
+
157
+ // TLC parses an infix chain of /\ as nested binary conjunctions and a bulleted list as one
158
+ // conjunction list, and it words the error differently for each.
159
+ export const getConjunctError = (
160
+ expression: Expression & { kind: "And" | "Or" },
161
+ index: number,
162
+ value: Value,
163
+ ) => {
164
+ const range = expression.location.range;
165
+ const isBulleted =
166
+ range?.start.line === expression.location.line &&
167
+ range.start.column === expression.location.column;
168
+ if (isBulleted || !range) {
169
+ const operand = expression.operands[index];
170
+ // SANY prints a numeral as its value instead of its location.
171
+ const description = operand.kind === "Number" ? String(operand.value) : describeRange(operand);
172
+ return new TlaEvaluationError(
173
+ `A non-boolean expression (${getKindString(value)}) was used as a formula in a conjunction.\n${description}`,
174
+ );
175
+ }
176
+ const end = expression.operands[Math.max(index, 1)].location.range?.end ?? range.end;
177
+ return new TlaEvaluationError(
178
+ `Attempted to evaluate an expression of form P /\\ Q when ${index === 0 ? "P" : "Q"} was\n${getKindString(value)}.\n${describeRange({ ...expression, location: { ...expression.location, range: { ...range, end } } })}`,
179
+ );
180
+ };
181
+
182
+ export const getFieldError =
183
+ (expression: Expression & { kind: "FieldAccess" }) => (record: Value) =>
184
+ `Attempted to select nonexistent field "${expression.field}" from the record\n${formatValue(record)}\n${describeRange(expression)}`;
185
+
186
+ export const getDomainOperator = (expression: Expression) => (value: Value) => {
187
+ if (!isFunctionValue(value)) {
188
+ throw new TlaEvaluationError(
189
+ `Attempted to apply the operator DOMAIN to a non-function\n(${getKindString(value)})\n${describeRange(expression)}`,
190
+ );
191
+ }
192
+ return getDomain(value);
193
+ };
194
+
195
+ export const getSubsetOperator =
196
+ (expression: Expression, builtin: Builtin) => (left: Value, right: Value) => {
197
+ if (!isSetValue(left) || isOverriddenSet(left)) {
198
+ throw new TlaEvaluationError(
199
+ `Attempted to evaluate an expression of form S \\subseteq T, but S was not enumerable.\n${describeRange(expression)}`,
200
+ );
201
+ }
202
+ return builtin.evaluate([left, right]);
203
+ };
204
+
205
+ // TLC rejects a decimal, or an integer literal too big for it, while it processes the spec.
206
+ export const createRealNumberError = (expression: DecimalExpression) =>
207
+ setExitCode(
208
+ new TlaEvaluationError(
209
+ `TLC can't handle real numbers: ${expression.text} at ${describeLocation(expression)}`,
210
+ ),
211
+ EXIT_STATUS.configParseError,
212
+ );
213
+
214
+ export const createBigNumberError = (expression: NumberExpression) =>
215
+ setExitCode(
216
+ new TlaEvaluationError(
217
+ `TLC can't handle a number this big: ${expression.text ?? expression.value} at ${describeLocation(expression)}`,
218
+ ),
219
+ EXIT_STATUS.error,
220
+ );
221
+
124
222
  export const lookupScope = (scope: Scope | undefined, name: string): Binding | undefined => {
125
223
  for (let current = scope; current; current = current.parent) {
126
224
  if (current.name === name) return current.binding;
@@ -134,11 +232,7 @@ export const bindValue = (scope: Scope | undefined, name: string, value: Value):
134
232
  parent: scope,
135
233
  });
136
234
 
137
- export const bindPattern = (
138
- scope: Scope | undefined,
139
- pattern: BoundPattern,
140
- value: Value,
141
- ): Scope => {
235
+ const bindPattern = (scope: Scope | undefined, pattern: BoundPattern, value: Value) => {
142
236
  if (pattern.kind === "Name") return bindValue(scope, pattern.name, value);
143
237
  const elements = getSequenceElements(value);
144
238
  if (!elements || elements.length !== pattern.names.length) {
@@ -149,7 +243,7 @@ export const bindPattern = (
149
243
  return pattern.names.reduce<Scope | undefined>(
150
244
  (current, name, index) => bindValue(current, name, elements[index]),
151
245
  scope,
152
- ) as Scope;
246
+ );
153
247
  };
154
248
 
155
249
  const withScope = (context: Context, scope: Scope | undefined): Context => ({
@@ -171,6 +265,7 @@ export const expectBoolean = (value: Value, description: string) => {
171
265
  export const getChildren = (expression: Expression): Expression[] => {
172
266
  switch (expression.kind) {
173
267
  case "Number":
268
+ case "Decimal":
174
269
  case "String":
175
270
  case "Identifier":
176
271
  case "OperatorReference":
@@ -296,13 +391,37 @@ export const isConstantLevel = (definition: Definition, environment: Environment
296
391
  return isConstant;
297
392
  };
298
393
 
394
+ const getBoundExplanation = (set: Value, text: string) => {
395
+ const quantified = `universally (\\A) or existentially (\\E) quantify over ${text}, because this would require checking an\ninfinite number of cases.`;
396
+ if (typeof set === "object" && set.kind === "SequenceSet") {
397
+ return `In TLA+, Seq(S) represents the set of all finite sequences whose elements come from the set S. Even when S\nis a finite set, the number of possible sequences in Seq(S) is unbounded because sequences can have any\nfinite length (e.g., length 0, 1, 2, and so on). As a result, TLC cannot evaluate expressions that\n${quantified} Note that for a finite set of sequences s, TLC handles s \\subseteq Seq(S).\nSee https://explain.tlapl.us/seq-unenumerable for additional details.`;
398
+ }
399
+ if (
400
+ typeof set === "object" &&
401
+ set.kind === "InfiniteSet" &&
402
+ set.name !== "STRING" &&
403
+ set.name !== "Any"
404
+ ) {
405
+ return `The set ${set.name} contains infinitely many elements. As a result, TLC cannot evaluate expressions that\n${quantified} Note that TLC handles set membership like T \\subseteq ${set.name} for any finite set T.`;
406
+ }
407
+ return undefined;
408
+ };
409
+
410
+ // TLC enumerates every set except the ones it implements in Java, so any other set fails as its own
411
+ // class does.
299
412
  export const getEnumeratedElements = (set: Value, expression: Expression) => {
300
- if (!isEnumerable(set)) {
413
+ if (!isSetValue(set) || isOverriddenSet(set)) {
414
+ const text = formatValue(set);
415
+ const explanation = getBoundExplanation(set, text);
301
416
  throw new TlaEvaluationError(
302
- `TLC encountered a non-enumerable quantifier bound ${formatValue(set)} at ${describeLocation(expression)}`,
417
+ explanation === undefined
418
+ ? `TLC encountered a non-enumerable quantifier bound\n${text}.\n${describeRange(expression)}`
419
+ : `TLC encountered the non-enumerable quantifier bound\n${text}\n${describeRange(expression)}\n${explanation}`,
303
420
  );
304
421
  }
305
- return toFiniteSet(set).elements;
422
+ return typeof set === "object" && set.kind === "Interval"
423
+ ? getIntervalElements(set)
424
+ : toFiniteSet(set).elements;
306
425
  };
307
426
 
308
427
  export const forEachBinding = (
@@ -336,14 +455,66 @@ export const bindArgument = (bounds: Bound[], scope: Scope | undefined, argument
336
455
  );
337
456
  };
338
457
 
458
+ // TLC prints a function it can't enumerate as its parameters and the location of its body. It
459
+ // prints only the first name of a group such as `m, n \in S`.
460
+ interface LazyFunctionTemplate {
461
+ parameters: { index: number; name: string }[];
462
+ body: string;
463
+ }
464
+
465
+ const lazyFunctionTemplates = new WeakMap<Expression, LazyFunctionTemplate>();
466
+
467
+ const getLazyFunctionTemplate = (bounds: Bound[], body: Expression) => {
468
+ const cached = lazyFunctionTemplates.get(body);
469
+ if (cached) return cached;
470
+ const range = body.location.range;
471
+ if (!range) return undefined;
472
+ const template = {
473
+ parameters: bounds.flatMap((bound, index) =>
474
+ index > 0 && bounds[index - 1].set === bound.set
475
+ ? []
476
+ : [
477
+ {
478
+ index,
479
+ name:
480
+ bound.pattern.kind === "Name"
481
+ ? bound.pattern.name
482
+ : `<<${bound.pattern.names.join(", ")}>>`,
483
+ },
484
+ ],
485
+ ),
486
+ body: formatRange(range),
487
+ };
488
+ lazyFunctionTemplates.set(body, template);
489
+ return template;
490
+ };
491
+
492
+ const getLazyFunctionSource = (bounds: Bound[], body: Expression, domain: Value) => {
493
+ const template = getLazyFunctionTemplate(bounds, body);
494
+ if (!template) return undefined;
495
+ const domains =
496
+ bounds.length > 1 && typeof domain === "object" && domain.kind === "ProductSet"
497
+ ? domain.factors
498
+ : [domain];
499
+ return {
500
+ parameters: template.parameters.map((parameter) => ({
501
+ name: parameter.name,
502
+ domain: domains[parameter.index],
503
+ })),
504
+ body: template.body,
505
+ };
506
+ };
507
+
339
508
  const createLazyFunction = (
340
509
  domain: Value,
341
510
  compute: (argument: Value, self: LazyFunctionValue) => Value,
342
- ): LazyFunctionValue => {
511
+ source: LazyFunctionSource | undefined,
512
+ ) => {
343
513
  const memo = new Map<number, Value[]>();
344
514
  const self: LazyFunctionValue = {
345
515
  kind: "LazyFunction",
346
516
  domain,
517
+ source,
347
518
  apply: (argument) => {
348
519
  const hash = getValueHash(argument);
349
520
  const bucket = memo.get(hash);
@@ -362,18 +533,29 @@ const createLazyFunction = (
362
533
  return self;
363
534
  };
364
535
 
365
- const createRecursiveFunction = (definition: FunctionDefinition, context: Context) =>
366
- createLazyFunction(getBoundsDomain(definition.bounds, context), (argument, self) =>
367
- evaluate(
368
- definition.body,
369
- withScope(
370
- context,
371
- bindArgument(definition.bounds, bindValue(undefined, definition.name, self), argument),
536
+ const createRecursiveFunction = (definition: FunctionDefinition, context: Context) => {
537
+ const domain = getBoundsDomain(definition.bounds, context);
538
+ const functionValue = createLazyFunction(
539
+ domain,
540
+ (argument, self) =>
541
+ evaluate(
542
+ definition.body,
543
+ withScope(
544
+ context,
545
+ bindArgument(definition.bounds, bindValue(undefined, definition.name, self), argument),
546
+ ),
372
547
  ),
373
- ),
548
+ getLazyFunctionSource(definition.bounds, definition.body, domain),
374
549
  );
550
+ return definition.bounds.length === 1 ? markLambdaFunction(functionValue) : functionValue;
551
+ };
552
+
553
+ const evaluateDefinitionBody = (definition: Definition, context: Context) =>
554
+ definition.kind === "Function"
555
+ ? createRecursiveFunction(definition, context)
556
+ : evaluate(definition.body, context);
375
557
 
376
- const evaluateDefinitionValue = (definition: Definition, context: Context): Value => {
558
+ const evaluateDefinitionValue = (definition: Definition, context: Context) => {
377
559
  const isConstant = isConstantLevel(definition, context.environment);
378
560
  const cached = isConstant ? context.environment.definitionValues.get(definition) : undefined;
379
561
  if (cached !== undefined) return cached;
@@ -383,11 +565,11 @@ const evaluateDefinitionValue = (definition: Definition, context: Context): Valu
383
565
  );
384
566
  }
385
567
  const topLevel = withScope(context, undefined);
386
- const value =
387
- definition.kind === "Function"
388
- ? createRecursiveFunction(definition, topLevel)
389
- : evaluate(definition.body, topLevel);
390
- if (isConstant) context.environment.definitionValues.set(definition, value);
568
+ if (!isConstant) return evaluateDefinitionBody(definition, topLevel);
569
+ const value = context.environment.runtime.evaluateAtStartup(() =>
570
+ evaluateDefinitionBody(definition, topLevel),
571
+ );
572
+ context.environment.definitionValues.set(definition, value);
391
573
  return value;
392
574
  };
393
575
 
@@ -477,11 +659,7 @@ const evaluateOperatorArgument = (expression: Expression, context: Context): Ope
477
659
  return operator;
478
660
  };
479
661
 
480
- export const callOperatorWithValues = (
481
- operator: Operator,
482
- args: Value[],
483
- context: Context,
484
- ): Value => {
662
+ const callOperatorWithValues = (operator: Operator, args: Value[], context: Context): Value => {
485
663
  if (operator.kind === "Builtin") return operator.evaluate(args, context);
486
664
  if (operator.kind === "Operator") return operator.call(args);
487
665
  const scope = operator.params.reduce<Scope | undefined>(
@@ -499,6 +677,11 @@ const toOperatorValue = (operator: Operator, context: Context): OperatorValue =>
499
677
  name: operator.name,
500
678
  arity: getArity(operator),
501
679
  call: (args) => callOperatorWithValues(operator, args, context),
680
+ literalBody:
681
+ operator.kind === "Closure" &&
682
+ (operator.body.kind === "Number" || operator.body.kind === "String")
683
+ ? operator.body.value
684
+ : undefined,
502
685
  };
503
686
 
504
687
  const isOperatorArgument = (expression: Expression, context: Context) => {
@@ -551,7 +734,9 @@ const compileArgumentBinder = (argument: Expression, environment: Environment):
551
734
  if (global === undefined) {
552
735
  return (context) => {
553
736
  const binding = lookupScope(context.scope, argument.name);
554
- return binding?.kind === "Value" ? binding : createThunk(argument, context);
737
+ return binding?.kind === "Value" || binding?.kind === "Thunk"
738
+ ? binding
739
+ : createThunk(argument, context);
555
740
  };
556
741
  }
557
742
  }
@@ -590,7 +775,7 @@ export const bindOperatorArguments = (
590
775
  return scope;
591
776
  };
592
777
 
593
- const callOperator = (operator: Operator, args: Expression[], context: Context): Value => {
778
+ const callOperator = (operator: Operator, args: Expression[], context: Context) => {
594
779
  if (operator.kind === "Closure") {
595
780
  return evaluate(
596
781
  operator.body,
@@ -609,7 +794,11 @@ const callOperator = (operator: Operator, args: Expression[], context: Context):
609
794
  );
610
795
  };
611
796
 
612
- export const bindDefinitions = (definitions: Definition[], context: Context): Scope | undefined =>
797
+ export const bindDefinitions = (
798
+ definitions: Definition[],
799
+ context: Context,
800
+ reevaluated?: ReadonlySet<Definition>,
801
+ ): Scope | undefined =>
613
802
  definitions.reduce<Scope | undefined>((scope, definition) => {
614
803
  const node: Scope = {
615
804
  name: definition.name,
@@ -633,7 +822,7 @@ export const bindDefinitions = (definitions: Definition[], context: Context): Sc
633
822
  };
634
823
  return node;
635
824
  }
636
- if (definition.params.length > 0) {
825
+ if (definition.params.length > 0 || reevaluated?.has(definition)) {
637
826
  node.binding = {
638
827
  kind: "Operator",
639
828
  operator: {
@@ -684,10 +873,10 @@ const evaluateLocalIdentifier = (name: string, expression: Expression, context:
684
873
  throw new TlaEvaluationError(`Unknown identifier ${name} at ${describeLocation(expression)}`);
685
874
  };
686
875
 
687
- const isTlcInteger = (value: number) =>
688
- Number.isInteger(value) && value <= 2147483647 && value >= -2147483648;
876
+ export const isTlcInteger = (value: number) =>
877
+ Number.isInteger(value) && value <= MAXIMUM_INTEGER && value >= MINIMUM_INTEGER;
689
878
 
690
- export const IMPURE_BUILTINS = new Set([
879
+ const IMPURE_BUILTINS = new Set([
691
880
  "Print",
692
881
  "PrintT",
693
882
  "Assert",
@@ -700,6 +889,58 @@ export const IMPURE_BUILTINS = new Set([
700
889
  export const isImpureBuiltin = (name: string, environment: Environment) =>
701
890
  IMPURE_BUILTINS.has(name) || environment.builtins.get(name)?.isStateLevel === true;
702
891
 
892
+ const impureDefinitions = new WeakMap<Definition, boolean>();
893
+
894
+ const isEvaluatedOnce = (definition: Definition, environment: Environment) =>
895
+ definition.kind === "Operator" &&
896
+ definition.params.length === 0 &&
897
+ isConstantLevel(definition, environment);
898
+
899
+ const callsImpureBuiltin = (expression: Expression, environment: Environment): boolean => {
900
+ const name =
901
+ expression.kind === "Identifier"
902
+ ? expression.name
903
+ : expression.kind === "Apply" || expression.kind === "OperatorReference"
904
+ ? expression.operator
905
+ : undefined;
906
+ if (name !== undefined && !isLocallyResolved(expression)) {
907
+ const definition = getDefinition(name, environment);
908
+ if (!definition && isImpureBuiltin(name, environment)) return true;
909
+ if (
910
+ definition &&
911
+ !isEvaluatedOnce(definition, environment) &&
912
+ isImpureDefinition(definition, environment)
913
+ ) {
914
+ return true;
915
+ }
916
+ }
917
+ return getChildren(expression).some((child) => callsImpureBuiltin(child, environment));
918
+ };
919
+
920
+ // TLC evaluates a constant definition without parameters once, so only other definitions pass
921
+ // on the side effects and random choices of the builtins they call.
922
+ const isImpureDefinition = (definition: Definition, environment: Environment): boolean => {
923
+ const cached = impureDefinitions.get(definition);
924
+ if (cached !== undefined) return cached;
925
+ impureDefinitions.set(definition, false);
926
+ const isImpure =
927
+ callsImpureBuiltin(definition.body, environment) ||
928
+ (definition.kind === "Function" &&
929
+ definition.bounds.some((bound) => callsImpureBuiltin(bound.set, environment)));
930
+ impureDefinitions.set(definition, isImpure);
931
+ return isImpure;
932
+ };
933
+
934
+ // TLC's next-state generator does not bind LET definitions to values, so each reference to one
935
+ // that calls an impure builtin evaluates it again.
936
+ export const getReevaluatedDefinitions = (definitions: Definition[], environment: Environment) =>
937
+ new Set(definitions.filter((definition) => isImpureDefinition(definition, environment)));
938
+
939
+ export const isImpure = (environment: Environment) =>
940
+ [...environment.definitions.values()].some((definition) =>
941
+ isImpureDefinition(definition, environment),
942
+ );
943
+
703
944
  const closedConstants = new WeakMap<Expression, boolean>();
704
945
 
705
946
  export const isClosedConstant = (expression: Expression, environment: Environment): boolean => {
@@ -742,7 +983,7 @@ const isClosedIn = (
742
983
  expression: Expression,
743
984
  environment: Environment,
744
985
  bound: ReadonlySet<string>,
745
- ): boolean =>
986
+ ) =>
746
987
  bound.size === 0
747
988
  ? isClosedConstant(expression, environment)
748
989
  : computeIsClosedConstant(expression, environment, bound);
@@ -760,28 +1001,24 @@ const computeIsClosedConstant = (
760
1001
  if (expression.kind === "Identifier") {
761
1002
  const global = findGlobalName(expression.name, environment);
762
1003
  if (global?.kind === "Value") return true;
763
- return (
764
- global?.kind === "Definition" &&
765
- global.definition.kind === "Operator" &&
766
- global.definition.params.length === 0 &&
767
- isConstantLevel(global.definition, environment)
768
- );
1004
+ return global?.kind === "Definition" && isEvaluatedOnce(global.definition, environment);
769
1005
  }
770
1006
  if (expression.kind === "Apply") {
771
1007
  if (isImpureBuiltin(expression.operator, environment) || expression.operator === "~>") {
772
1008
  return false;
773
1009
  }
774
1010
  const operator = resolveGlobalOperator(expression.operator, environment);
775
- const isPure =
776
- operator?.kind === "Builtin" ||
777
- (operator?.kind === "Closure" &&
778
- (() => {
779
- const definition = environment.definitions.get(
780
- environment.substitutions.get(expression.operator) ?? expression.operator,
781
- );
782
- return definition !== undefined && isConstantLevel(definition, environment);
783
- })());
784
- if (!isPure || operator === undefined) return false;
1011
+ if (operator?.kind !== "Builtin" && operator?.kind !== "Closure") return false;
1012
+ if (operator.kind === "Closure") {
1013
+ const definition = getDefinition(expression.operator, environment);
1014
+ if (
1015
+ !definition ||
1016
+ !isConstantLevel(definition, environment) ||
1017
+ isImpureDefinition(definition, environment)
1018
+ ) {
1019
+ return false;
1020
+ }
1021
+ }
785
1022
  const params = operator.kind === "Closure" ? operator.params : [];
786
1023
  const hasOperatorArgument = params.some((param) => param.arity > 0);
787
1024
  if (hasOperatorArgument) return false;
@@ -855,7 +1092,7 @@ export const getEvaluator = (expression: Expression, environment: Environment):
855
1092
 
856
1093
  const entryEvaluators = new WeakMap<Expression, Evaluator>();
857
1094
 
858
- const getEntryEvaluator = (expression: Expression, environment: Environment): Evaluator => {
1095
+ const getEntryEvaluator = (expression: Expression, environment: Environment) => {
859
1096
  const cached = entryEvaluators.get(expression);
860
1097
  if (cached) return cached;
861
1098
  const evaluator =
@@ -1031,18 +1268,79 @@ export const compileArgumentScope = (
1031
1268
  };
1032
1269
  };
1033
1270
 
1271
+ interface MemoizedCall {
1272
+ args: Value[];
1273
+ value: Value;
1274
+ }
1275
+
1276
+ const MEMOIZED_CALL_LIMIT = 1 << 16;
1277
+
1278
+ const callMemos = new WeakMap<Closure, { calls: Map<number, MemoizedCall[]>; size: number }>();
1279
+
1280
+ const getCallMemo = (closure: Closure) => {
1281
+ const cached = callMemos.get(closure);
1282
+ if (cached) return cached;
1283
+ const memo = { calls: new Map<number, MemoizedCall[]>(), size: 0 };
1284
+ callMemos.set(closure, memo);
1285
+ return memo;
1286
+ };
1287
+
1288
+ const getArgumentValues = (scope: Scope | undefined, arity: number) => {
1289
+ const values: Value[] = [];
1290
+ for (let current = scope; values.length < arity; current = current?.parent) {
1291
+ if (current?.binding.kind !== "Value") return undefined;
1292
+ values.push(current.binding.value);
1293
+ }
1294
+ return values;
1295
+ };
1296
+
1297
+ const isSameArguments = (left: Value[], right: Value[]) =>
1298
+ left.every((value, index) => isSameValue(value, right[index]));
1299
+
1300
+ // A constant-level operator without side effects yields the same value for the same arguments,
1301
+ // so its calls with evaluated arguments share one result.
1302
+ const isMemoizable = (expression: Expression & { kind: "Apply" }, environment: Environment) => {
1303
+ const definition = getDefinition(expression.operator, environment);
1304
+ return (
1305
+ definition?.kind === "Operator" &&
1306
+ definition.params.length > 0 &&
1307
+ definition.params.every((param) => param.arity === 0) &&
1308
+ isConstantLevel(definition, environment) &&
1309
+ !isImpureDefinition(definition, environment)
1310
+ );
1311
+ };
1312
+
1034
1313
  const compileClosureCall = (
1035
1314
  closure: Closure,
1036
1315
  args: Expression[],
1037
1316
  environment: Environment,
1317
+ isMemoized: boolean,
1038
1318
  ): Evaluator => {
1039
1319
  const bindArguments = compileArgumentScope(closure, args, environment);
1040
1320
  const compiled = { body: undefined as Evaluator | undefined };
1041
- return (context) => {
1042
- const scope = bindArguments(context);
1321
+ const evaluateBody = (scope: Scope | undefined, context: Context) => {
1043
1322
  compiled.body ??= getEntryEvaluator(closure.body, environment);
1044
1323
  return compiled.body({ environment, scope, state: context.state, next: context.next });
1045
1324
  };
1325
+ if (!isMemoized) return (context) => evaluateBody(bindArguments(context), context);
1326
+ const memo = getCallMemo(closure);
1327
+ return (context) => {
1328
+ const scope = bindArguments(context);
1329
+ const values = getArgumentValues(scope, args.length);
1330
+ if (!values) return evaluateBody(scope, context);
1331
+ let hash = values.length;
1332
+ for (const value of values) hash = combineHash(hash, getValueHash(value));
1333
+ const bucket = memo.calls.get(hash);
1334
+ const known = bucket?.find((call) => isSameArguments(call.args, values));
1335
+ if (known) return known.value;
1336
+ const value = evaluateBody(scope, context);
1337
+ if (memo.size < MEMOIZED_CALL_LIMIT) {
1338
+ memo.size++;
1339
+ if (bucket) bucket.push({ args: values, value });
1340
+ else memo.calls.set(hash, [{ args: values, value }]);
1341
+ }
1342
+ return value;
1343
+ };
1046
1344
  };
1047
1345
 
1048
1346
  const compileApply = (
@@ -1065,7 +1363,29 @@ const compileApply = (
1065
1363
  const global = isLocallyResolved(expression)
1066
1364
  ? undefined
1067
1365
  : resolveGlobalOperator(expression.operator, environment);
1068
- if (global?.kind === "Closure") return compileClosureCall(global, args, environment);
1366
+ if (global?.kind === "Closure") {
1367
+ return compileClosureCall(global, args, environment, isMemoizable(expression, environment));
1368
+ }
1369
+ if (global?.kind === "Builtin" && global.name === "DOMAIN") {
1370
+ const operand = getEvaluator(args[0], environment);
1371
+ const domain = getDomainOperator(expression);
1372
+ return (context) => domain(operand(context));
1373
+ }
1374
+ // TLC sorts the value of a constant TLCEval argument only where no context binds a name.
1375
+ if (global?.kind === "Builtin" && global.name === "TLCEval" && args.length === 1) {
1376
+ const operand = getEvaluator(args[0], environment);
1377
+ if (contextualApplications.has(expression) || isStateDependent(args[0], environment)) {
1378
+ return operand;
1379
+ }
1380
+ return (context) =>
1381
+ context.scope === undefined ? global.evaluate([operand(context)]) : operand(context);
1382
+ }
1383
+ if (global?.kind === "Builtin" && global.name === "\\subseteq" && args.length === 2) {
1384
+ const left = getEvaluator(args[0], environment);
1385
+ const right = getEvaluator(args[1], environment);
1386
+ const subset = getSubsetOperator(expression, global);
1387
+ return (context) => subset(left(context), right(context));
1388
+ }
1069
1389
  if (global?.kind === "Builtin") return compileBuiltinCall(global, args, environment);
1070
1390
  return (context) => {
1071
1391
  const operator = resolveOperator(expression.operator, context);
@@ -1134,7 +1454,7 @@ const compileQuantifier = (
1134
1454
  };
1135
1455
  };
1136
1456
 
1137
- const lookupKey = (pattern: BoundPattern, scope: Scope | undefined): Value | undefined => {
1457
+ const lookupKey = (pattern: BoundPattern, scope: Scope | undefined) => {
1138
1458
  if (pattern.kind === "Tuple") {
1139
1459
  return createTuple(
1140
1460
  pattern.names.map((name) => {
@@ -1147,10 +1467,21 @@ const lookupKey = (pattern: BoundPattern, scope: Scope | undefined): Value | und
1147
1467
  return binding?.kind === "Value" ? binding.value : undefined;
1148
1468
  };
1149
1469
 
1470
+ // TLC keeps a function constructor lazy when it is the function of an application, or the base of
1471
+ // an EXCEPT that is, so it evaluates the body only at the arguments it is applied to.
1472
+ const keptLazyExpressions = new WeakSet<Expression>();
1473
+
1474
+ export const keepLazy = (expression: Expression) => {
1475
+ if (expression.kind === "FunctionConstruction" || expression.kind === "Except") {
1476
+ keptLazyExpressions.add(expression);
1477
+ }
1478
+ };
1479
+
1150
1480
  const compileFunctionConstruction = (
1151
1481
  expression: Expression & { kind: "FunctionConstruction" },
1152
1482
  environment: Environment,
1153
1483
  ): Evaluator => {
1484
+ const isKeptLazy = keptLazyExpressions.has(expression);
1154
1485
  const body = getEvaluator(expression.body, environment);
1155
1486
  const bounds = expression.bounds;
1156
1487
  const bound = bounds[0];
@@ -1158,17 +1489,22 @@ const compileFunctionConstruction = (
1158
1489
  const set = getEvaluator(bound.set, environment);
1159
1490
  return (context) => {
1160
1491
  const domain = isSingleName ? set(context) : getBoundsDomain(bounds, context);
1161
- if (expression.isRecursive || !isEnumerable(domain)) {
1162
- return createLazyFunction(domain, (argument) =>
1163
- body(withScope(context, bindArgument(bounds, context.scope, argument))),
1492
+ if (expression.isRecursive || isKeptLazy || !isEnumerable(domain)) {
1493
+ const functionValue = createLazyFunction(
1494
+ domain,
1495
+ (argument) => body(withScope(context, bindArgument(bounds, context.scope, argument))),
1496
+ getLazyFunctionSource(bounds, expression.body, domain),
1164
1497
  );
1498
+ return bounds.length === 1 ? markLambdaFunction(functionValue) : functionValue;
1165
1499
  }
1166
1500
  if (isSingleName && bound.pattern.kind === "Name") {
1167
1501
  const name = bound.pattern.name;
1168
- const keys = toFiniteSet(domain).elements;
1169
- return createSortedFunction(
1170
- keys,
1171
- keys.map((key) => body(withScope(context, bindValue(context.scope, name, key)))),
1502
+ const keys = getFunctionDomain(domain);
1503
+ return markLambdaFunction(
1504
+ createSortedFunction(
1505
+ keys,
1506
+ keys.map((key) => body(withScope(context, bindValue(context.scope, name, key)))),
1507
+ ),
1172
1508
  );
1173
1509
  }
1174
1510
  const pairs: [Value, Value][] = [];
@@ -1188,21 +1524,22 @@ const compileExcept = (
1188
1524
  expression: Expression & { kind: "Except" },
1189
1525
  environment: Environment,
1190
1526
  ): Evaluator => {
1191
- const fn = getEvaluator(expression.function, environment);
1527
+ const isKeptLazy = keptLazyExpressions.has(expression);
1528
+ if (isKeptLazy) keepLazy(expression.function);
1529
+ const evaluateFunction = getEvaluator(expression.function, environment);
1192
1530
  const updates = expression.updates.map((update) => ({
1193
- path: update.path.map((step) =>
1531
+ path: update.path.map((step): CompiledStep =>
1194
1532
  step.kind === "Field"
1195
- ? { kind: "Field" as const, name: step.name, args: [] as Evaluator[] }
1533
+ ? { kind: "Field", name: step.name, args: [] }
1196
1534
  : {
1197
- kind: "Index" as const,
1535
+ kind: "Index",
1198
1536
  name: "",
1199
1537
  args: step.args.map((arg) => getEvaluator(arg, environment)),
1200
1538
  },
1201
1539
  ),
1202
1540
  value: getEvaluator(update.value, environment),
1203
1541
  }));
1204
- type CompiledStep = (typeof updates)[number]["path"][number];
1205
- const getKey = (step: CompiledStep, context: Context): Value => {
1542
+ const getKey = (step: CompiledStep, context: Context) => {
1206
1543
  if (step.kind === "Field") return step.name;
1207
1544
  if (step.args.length === 1) return step.args[0](context);
1208
1545
  return createTuple(step.args.map((arg) => arg(context)));
@@ -1217,12 +1554,15 @@ const compileExcept = (
1217
1554
  if (depth === path.length) {
1218
1555
  return value(withScope(context, bindValue(context.scope, EXCEPT_AT, current)));
1219
1556
  }
1220
- return updateFunction(current, getKey(path[depth], context), (previous) =>
1221
- update(previous, path, depth + 1, context, value),
1557
+ return updateFunction(
1558
+ current,
1559
+ getKey(path[depth], context),
1560
+ (previous) => update(previous, path, depth + 1, context, value),
1561
+ isKeptLazy && depth === 0,
1222
1562
  );
1223
1563
  };
1224
1564
  return (context) => {
1225
- let current = fn(context);
1565
+ let current = evaluateFunction(context);
1226
1566
  for (const item of updates) current = update(current, item.path, 0, context, item.value);
1227
1567
  return current;
1228
1568
  };
@@ -1244,7 +1584,8 @@ const compileSetComprehension = (
1244
1584
  "a set filter predicate",
1245
1585
  );
1246
1586
  const isReducible =
1247
- typeof base === "object" && (base.kind === "Set" || base.kind === "Interval");
1587
+ typeof base === "object" &&
1588
+ (base.kind === "Set" || base.kind === "MixedSet" || base.kind === "Interval");
1248
1589
  if (!isReducible) {
1249
1590
  if (isSetValue(base)) return { kind: "FilteredSet", base, test };
1250
1591
  throw new TlaEvaluationError(
@@ -1262,7 +1603,7 @@ const compileSetComprehension = (
1262
1603
  elements.push(mapped(withScope(context, scope)));
1263
1604
  return false;
1264
1605
  });
1265
- return createSet(elements);
1606
+ return createEnumeratedSet(elements);
1266
1607
  };
1267
1608
  }
1268
1609
  const set = expression.set;
@@ -1284,7 +1625,7 @@ const compileSetComprehension = (
1284
1625
  });
1285
1626
  if (chosen.value === undefined) {
1286
1627
  throw new TlaEvaluationError(
1287
- `Attempted to compute CHOOSE x \\in S : P, but no element of S satisfies P at ${describeLocation(expression)}`,
1628
+ `Attempted to compute the value of an expression of form\nCHOOSE x \\in S: P, but no element of S satisfied P.\n${describeRange(expression)}`,
1288
1629
  );
1289
1630
  }
1290
1631
  return chosen.value;
@@ -1292,7 +1633,7 @@ const compileSetComprehension = (
1292
1633
  };
1293
1634
 
1294
1635
  const compileRecord = (recordFields: RecordField[], environment: Environment): Evaluator => {
1295
- const fields = [...recordFields].sort((left, right) => (left.name < right.name ? -1 : 1));
1636
+ const fields = [...recordFields].sort((left, right) => compareValues(left.name, right.name));
1296
1637
  const hasDuplicates = fields.some(
1297
1638
  (field, index) => index > 0 && fields[index - 1].name === field.name,
1298
1639
  );
@@ -1303,7 +1644,16 @@ const compileRecord = (recordFields: RecordField[], environment: Environment): E
1303
1644
  fields.map((field, index): [Value, Value] => [field.name, values[index](context)]),
1304
1645
  );
1305
1646
  }
1306
- const names: Value[] = fields.map((field) => field.name);
1647
+ const names = fields.map((field) => field.name);
1648
+ const writtenNames = recordFields.map((field) => field.name);
1649
+ if (writtenNames.some((name, index) => name !== names[index])) {
1650
+ return (context) =>
1651
+ createWrittenRecord(
1652
+ names,
1653
+ values.map((value) => value(context)),
1654
+ writtenNames,
1655
+ );
1656
+ }
1307
1657
  return (context) =>
1308
1658
  createSortedFunction(
1309
1659
  names,
@@ -1311,25 +1661,21 @@ const compileRecord = (recordFields: RecordField[], environment: Environment): E
1311
1661
  );
1312
1662
  };
1313
1663
 
1314
- const createUnchanged = (expression: Expression, location: Expression["location"]): Expression => ({
1315
- kind: "Unchanged",
1316
- expression,
1317
- location,
1318
- });
1319
-
1320
- const throwing =
1321
- (createError: () => Error): Evaluator =>
1322
- () => {
1323
- throw createError();
1324
- };
1325
-
1326
1664
  const compile = (expression: Expression, environment: Environment): Evaluator => {
1327
1665
  const child = (node: Expression) => getEvaluator(node, environment);
1328
1666
  switch (expression.kind) {
1329
1667
  case "Number": {
1330
1668
  const value = expression.value;
1331
- return isTlcInteger(value) ? () => value : () => checkInteger(value);
1669
+ return isTlcInteger(value)
1670
+ ? () => value
1671
+ : () => {
1672
+ throw createBigNumberError(expression);
1673
+ };
1332
1674
  }
1675
+ case "Decimal":
1676
+ return () => {
1677
+ throw createRealNumberError(expression);
1678
+ };
1333
1679
  case "String": {
1334
1680
  const value = expression.value;
1335
1681
  return () => value;
@@ -1340,17 +1686,18 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
1340
1686
  return compileApply(expression, environment);
1341
1687
  case "OperatorReference":
1342
1688
  case "Lambda":
1343
- return throwing(
1344
- () =>
1345
- new TlaEvaluationError(
1346
- `An operator cannot be used as a value at ${describeLocation(expression)}`,
1347
- ),
1348
- );
1689
+ return () => {
1690
+ throw new TlaEvaluationError(
1691
+ `An operator cannot be used as a value at ${describeLocation(expression)}`,
1692
+ );
1693
+ };
1349
1694
  case "And": {
1350
1695
  const operands = expression.operands.map(child);
1351
1696
  return (context) => {
1352
1697
  for (let index = 0; index < operands.length; index++) {
1353
- if (!expectBoolean(operands[index](context), "a conjunct")) return false;
1698
+ const value = operands[index](context);
1699
+ if (typeof value !== "boolean") throw getConjunctError(expression, index, value);
1700
+ if (!value) return false;
1354
1701
  }
1355
1702
  return true;
1356
1703
  };
@@ -1393,10 +1740,11 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
1393
1740
  const condition = child(expression.condition);
1394
1741
  const consequent = child(expression.consequent);
1395
1742
  const alternative = child(expression.alternative);
1396
- return (context) =>
1397
- expectBoolean(condition(context), "an IF condition")
1398
- ? consequent(context)
1399
- : alternative(context);
1743
+ return (context) => {
1744
+ const value = condition(context);
1745
+ if (typeof value !== "boolean") throw getConditionError(expression, value);
1746
+ return value ? consequent(context) : alternative(context);
1747
+ };
1400
1748
  }
1401
1749
  case "Case": {
1402
1750
  const arms = expression.arms.map((arm) => ({
@@ -1421,19 +1769,18 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
1421
1769
  case "Exists":
1422
1770
  return compileQuantifier(expression, environment);
1423
1771
  case "UnboundedQuantifier":
1424
- return throwing(
1425
- () =>
1426
- new TlaEvaluationError(
1427
- `TLC cannot evaluate an unbounded quantifier at ${describeLocation(expression)}`,
1428
- ),
1429
- );
1772
+ return () => {
1773
+ throw new TlaEvaluationError(
1774
+ `TLC cannot evaluate an unbounded quantifier at ${describeLocation(expression)}`,
1775
+ );
1776
+ };
1430
1777
  case "Choose":
1431
1778
  case "SetFilter":
1432
1779
  case "SetMap":
1433
1780
  return compileSetComprehension(expression, environment);
1434
1781
  case "SetEnumeration": {
1435
1782
  const elements = expression.elements.map(child);
1436
- return (context) => createSet(elements.map((element) => element(context)));
1783
+ return (context) => createEnumeratedSet(elements.map((element) => element(context)));
1437
1784
  }
1438
1785
  case "Tuple": {
1439
1786
  const elements = expression.elements.map(child);
@@ -1450,7 +1797,7 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
1450
1797
  return compileRecord(expression.fields, environment);
1451
1798
  case "RecordSet": {
1452
1799
  const fields = [...expression.fields].sort((left, right) =>
1453
- left.name < right.name ? -1 : 1,
1800
+ compareValues(left.name, right.name),
1454
1801
  );
1455
1802
  const names = fields.map((field) => field.name);
1456
1803
  const sets = fields.map((field) => child(field.value));
@@ -1469,18 +1816,21 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
1469
1816
  return binding.value;
1470
1817
  };
1471
1818
  case "FunctionApplication": {
1472
- const fn = child(expression.function);
1819
+ keepLazy(expression.function);
1820
+ const evaluateFunction = child(expression.function);
1473
1821
  const args = expression.args.map(child);
1474
1822
  if (args.length === 1) {
1475
1823
  const argument = args[0];
1476
- return (context) => applyFunction(fn(context), argument(context));
1824
+ return (context) => applyFunction(evaluateFunction(context), argument(context));
1477
1825
  }
1478
- return (context) => applyFunction(fn(context), createTuple(args.map((arg) => arg(context))));
1826
+ return (context) =>
1827
+ applyFunction(evaluateFunction(context), createTuple(args.map((arg) => arg(context))));
1479
1828
  }
1480
1829
  case "FieldAccess": {
1481
1830
  const record = child(expression.record);
1482
1831
  const field = expression.field;
1483
- return (context) => applyFunction(record(context), field);
1832
+ const describeMissing = getFieldError(expression);
1833
+ return (context) => applyFunction(record(context), field, describeMissing);
1484
1834
  }
1485
1835
  case "Product": {
1486
1836
  const factors = expression.factors.map(child);
@@ -1492,7 +1842,11 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
1492
1842
  case "BoxAction":
1493
1843
  case "AngleAction": {
1494
1844
  const action = child(expression.action);
1495
- const unchanged = child(createUnchanged(expression.subscript, expression.location));
1845
+ const unchanged = child({
1846
+ kind: "Unchanged",
1847
+ expression: expression.subscript,
1848
+ location: expression.location,
1849
+ });
1496
1850
  const isBox = expression.kind === "BoxAction";
1497
1851
  return (context) =>
1498
1852
  isBox
@@ -1506,21 +1860,19 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
1506
1860
  case "Label":
1507
1861
  return child(expression.body);
1508
1862
  case "Subexpression":
1509
- return throwing(
1510
- () =>
1511
- new TlaEvaluationError(
1512
- `The subexpression reference at ${describeLocation(expression)} does not name a subexpression`,
1513
- ),
1514
- );
1863
+ return () => {
1864
+ throw new TlaEvaluationError(
1865
+ `The subexpression reference at ${describeLocation(expression)} does not name a subexpression`,
1866
+ );
1867
+ };
1515
1868
  case "Always":
1516
1869
  case "Eventually":
1517
1870
  case "WeakFairness":
1518
1871
  case "StrongFairness":
1519
- return throwing(
1520
- () =>
1521
- new TlaEvaluationError(
1522
- `TLC cannot evaluate a temporal formula in a state at ${describeLocation(expression)}`,
1523
- ),
1524
- );
1872
+ return () => {
1873
+ throw new TlaEvaluationError(
1874
+ `TLC cannot evaluate a temporal formula in a state at ${describeLocation(expression)}`,
1875
+ );
1876
+ };
1525
1877
  }
1526
1878
  };