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
@@ -0,0 +1,184 @@
1
+ import type { Declaration, Definition, Expression, SourceLocation } from "./ast";
2
+ import { TlaSyntaxError } from "./errors";
3
+ import {
4
+ createLevelAnalyzer,
5
+ getArgumentKey,
6
+ MAX_LEVEL,
7
+ parseArgumentParamKey,
8
+ putArgConstraint,
9
+ putConstraint,
10
+ type OperatorLevels,
11
+ type Scope,
12
+ } from "./levels";
13
+
14
+ export interface InstanceCheck {
15
+ module: string;
16
+ instancer: string;
17
+ lets: Definition[];
18
+ substitutions: Map<string, Expression>;
19
+ location: SourceLocation;
20
+ }
21
+
22
+ interface ModuleContext {
23
+ constants: Declaration[];
24
+ variables: string[];
25
+ definitions: Definition[];
26
+ assumptions: Expression[];
27
+ getDefinition: (name: string) => Definition | undefined;
28
+ }
29
+
30
+ interface ModuleLevels {
31
+ isConstant: boolean;
32
+ constraints: Map<string, number>;
33
+ argConstraints: Map<string, number>;
34
+ argParams: Set<string>;
35
+ }
36
+
37
+ interface Substituted {
38
+ name: string;
39
+ maximumLevel: number;
40
+ arity: number;
41
+ level: number;
42
+ operator: OperatorLevels | undefined;
43
+ isLeibniz: boolean;
44
+ }
45
+
46
+ const getMaxLevel = (operator: OperatorLevels, index: number) =>
47
+ operator.maxLevels[index] ?? MAX_LEVEL;
48
+
49
+ export const checkInstances = (
50
+ checks: InstanceCheck[],
51
+ getModule: (name: string) => ModuleContext,
52
+ ) => {
53
+ const analyzers = new Map<string, ReturnType<typeof createLevelAnalyzer>>();
54
+ const getAnalyzer = (name: string) => {
55
+ const cached = analyzers.get(name);
56
+ if (cached) return cached;
57
+ const module = getModule(name);
58
+ const analyzer = createLevelAnalyzer({
59
+ constants: new Set(module.constants.map((constant) => constant.name)),
60
+ variables: new Set(module.variables),
61
+ getDefinition: module.getDefinition,
62
+ });
63
+ analyzers.set(name, analyzer);
64
+ return analyzer;
65
+ };
66
+
67
+ const moduleLevels = new Map<string, ModuleLevels>();
68
+ const getModuleLevels = (name: string) => {
69
+ const cached = moduleLevels.get(name);
70
+ if (cached) return cached;
71
+ const module = getModule(name);
72
+ const analyzer = getAnalyzer(name);
73
+ const definitions = module.definitions.map((definition) =>
74
+ analyzer.analyzeDefinition(definition, new Map()),
75
+ );
76
+ const levels: ModuleLevels = {
77
+ isConstant:
78
+ module.variables.length === 0 && definitions.every((definition) => definition.level === 0),
79
+ constraints: new Map(),
80
+ argConstraints: new Map(),
81
+ argParams: new Set(),
82
+ };
83
+ if (!levels.isConstant) {
84
+ module.constants.forEach((constant) => putConstraint(levels.constraints, constant.name, 0));
85
+ }
86
+ const addLevels = (
87
+ data: Omit<ModuleLevels, "isConstant">,
88
+ isKept: (operator: string, param: string) => boolean,
89
+ ) => {
90
+ data.constraints.forEach((value, key) => putConstraint(levels.constraints, key, value));
91
+ data.argConstraints.forEach((value, key) =>
92
+ putArgConstraint(levels.argConstraints, key, value),
93
+ );
94
+ data.argParams.forEach((key) => {
95
+ const parameter = parseArgumentParamKey(key);
96
+ if (isKept(parameter.operator, parameter.param)) levels.argParams.add(key);
97
+ });
98
+ };
99
+ definitions.forEach((definition) =>
100
+ addLevels(definition, (operator, param) =>
101
+ definition.formals.every((formal) => formal.name !== operator && formal.name !== param),
102
+ ),
103
+ );
104
+ module.assumptions.forEach((assumption) =>
105
+ addLevels(analyzer.analyze(assumption, new Map()), () => true),
106
+ );
107
+ moduleLevels.set(name, levels);
108
+ return levels;
109
+ };
110
+
111
+ const messages = new Set<string>();
112
+ checks.forEach((check) => {
113
+ const location = `at line ${check.location.line}, column ${check.location.column}`;
114
+ const addMessage = (message: string) => messages.add(`${message} ${location}`);
115
+ const instancee = getModule(check.module);
116
+ const levels = getModuleLevels(check.module);
117
+ const analyzer = getAnalyzer(check.instancer);
118
+ const scope: Scope = new Map();
119
+ check.lets.forEach((definition) =>
120
+ scope.set(definition.name, { kind: "Let", definition, scope }),
121
+ );
122
+ const declarations = [
123
+ ...instancee.constants.map((constant) => ({ ...constant, maximumLevel: 0 })),
124
+ ...instancee.variables.map((variable) => ({ name: variable, arity: 0, maximumLevel: 1 })),
125
+ ];
126
+ const substituted = declarations.flatMap((declaration): Substituted[] => {
127
+ const expression = check.substitutions.get(declaration.name);
128
+ if (!expression) return [];
129
+ if (declaration.arity === 0) {
130
+ const data = analyzer.analyze(expression, scope);
131
+ return [{ ...declaration, level: data.level, operator: undefined, isLeibniz: true }];
132
+ }
133
+ const operand = analyzer.toOperand(expression, scope);
134
+ return [
135
+ {
136
+ ...declaration,
137
+ level: operand.data.level,
138
+ operator: operand.operator,
139
+ isLeibniz: operand.operator === undefined || operand.operator.nonLeibniz.size === 0,
140
+ },
141
+ ];
142
+ });
143
+ const getLevelMessage = (name: string, level: number) =>
144
+ `Level error in instantiating module '${check.module}': the level of the expression or operator substituted for '${name}' must be at most ${level}`;
145
+ substituted.forEach((item) => {
146
+ if (!levels.isConstant && item.level > item.maximumLevel) {
147
+ addMessage(getLevelMessage(item.name, item.maximumLevel));
148
+ }
149
+ if (!item.isLeibniz) {
150
+ addMessage(
151
+ `Error in instantiating module '${check.module}': a non-Leibniz operator substituted for '${item.name}'`,
152
+ );
153
+ }
154
+ });
155
+ substituted.forEach((item) => {
156
+ const maximum = levels.constraints.get(item.name);
157
+ if (maximum !== undefined && item.level > maximum)
158
+ addMessage(getLevelMessage(item.name, maximum));
159
+ const operator = item.operator;
160
+ if (!operator) return;
161
+ Array.from({ length: item.arity }, (_, index) => {
162
+ const minimum = levels.argConstraints.get(getArgumentKey(item.name, index));
163
+ if (minimum !== undefined && getMaxLevel(operator, index) < minimum) {
164
+ addMessage(
165
+ `Level error in instantiating module '${check.module}': the level of the argument ${index + 1} of the operator ${operator.name} must be at least ${minimum}`,
166
+ );
167
+ }
168
+ });
169
+ });
170
+ levels.argParams.forEach((key) => {
171
+ const parameter = parseArgumentParamKey(key);
172
+ const operator = substituted.find((item) => item.name === parameter.operator)?.operator;
173
+ const argument = substituted.find((item) => item.name === parameter.param);
174
+ if (!operator || !argument) return;
175
+ const maximum = getMaxLevel(operator, parameter.index);
176
+ if (argument.level > maximum) {
177
+ addMessage(
178
+ `Level error when instantiating module '${check.module}': the level of the argument ${parameter.index} of the operator ${parameter.operator}' must be at most ${maximum}`,
179
+ );
180
+ }
181
+ });
182
+ });
183
+ if (messages.size > 0) throw new TlaSyntaxError([...messages].join("\n"));
184
+ };
package/src/successors.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  import type { Bound, Definition, Expression } from "./ast";
2
+ import { getReadVariables } from "./dependencies";
2
3
  import { TlaEvaluationError } from "./errors";
4
+ import { EXIT_STATUS, setExitCode } from "./exit-codes";
3
5
  import {
6
+ describeLocation,
4
7
  bindArgument,
5
8
  bindDefinitions,
6
9
  bindOperatorArguments,
@@ -10,7 +13,10 @@ import {
10
13
  expectBoolean,
11
14
  forEachBinding,
12
15
  getBoundsDomain,
16
+ getChildren,
13
17
  getDefinition,
18
+ getEnumeratedElements,
19
+ getReevaluatedDefinitions,
14
20
  lookupScope,
15
21
  resolveApplyOperator,
16
22
  resolveGlobalOperator,
@@ -20,12 +26,13 @@ import {
20
26
  type Scope,
21
27
  type State,
22
28
  } from "./evaluate";
29
+ import { mapChildren } from "./instances";
23
30
  import {
24
31
  createTuple,
25
32
  formatValue,
33
+ getStreamedElements,
26
34
  isElementOf,
27
35
  isEnumerable,
28
- toFiniteSet,
29
36
  valuesEqual,
30
37
  type Value,
31
38
  } from "./values";
@@ -39,6 +46,7 @@ interface Run {
39
46
  environment: Environment;
40
47
  isInit: boolean;
41
48
  current: Value[] | undefined;
49
+ isStopped: boolean;
42
50
  }
43
51
 
44
52
  type Continuation = (partial: State, action: string | undefined) => void;
@@ -62,10 +70,7 @@ type Conjunct = { expression: Expression } & (
62
70
  const initRunners = new WeakMap<Expression, Runner>();
63
71
  const nextRunners = new WeakMap<Expression, Runner>();
64
72
 
65
- const describeLocation = (expression: Expression) =>
66
- `line ${expression.location.line}, column ${expression.location.column}`;
67
-
68
- const getContext = (run: Run, scope: Scope | undefined, partial: State): Context =>
73
+ const getContext = (run: Run, scope: Scope | undefined, partial: State) =>
69
74
  run.isInit
70
75
  ? { environment: run.environment, scope, state: partial, next: undefined }
71
76
  : { environment: run.environment, scope, state: run.current, next: partial };
@@ -79,7 +84,12 @@ const assign = (partial: State, index: number, value: Value) => {
79
84
  const holds = (expression: Expression, context: Context) => {
80
85
  const value = evaluate(expression, context);
81
86
  if (typeof value === "boolean") return value;
82
- return expectBoolean(value, `the action at ${describeLocation(expression)}`);
87
+ try {
88
+ return expectBoolean(value, `the action at ${describeLocation(expression)}`);
89
+ } catch (error) {
90
+ // TLC fails with an unexpected exception when the next-state relation isn't a boolean.
91
+ throw context.next === undefined ? error : setExitCode(error, EXIT_STATUS.error);
92
+ }
83
93
  };
84
94
 
85
95
  const isAssignmentOperator = (operator: string) =>
@@ -188,16 +198,12 @@ const ACTION_KINDS = new Set([
188
198
  "FunctionApplication",
189
199
  ]);
190
200
 
191
- const isGuard = (expression: Expression, environment: Environment, isInit: boolean): boolean => {
201
+ const isGuard = (expression: Expression, environment: Environment, isInit: boolean) => {
192
202
  if (ACTION_KINDS.has(expression.kind)) return false;
193
203
  if (expression.kind === "Identifier") {
194
- if (
195
- environment.variableIndex.has(expression.name) ||
196
- environment.constants.has(expression.name)
197
- ) {
198
- return true;
199
- }
200
- return false;
204
+ return (
205
+ environment.variableIndex.has(expression.name) || environment.constants.has(expression.name)
206
+ );
201
207
  }
202
208
  if (expression.kind !== "Apply") return true;
203
209
  if (expression.operator === "=>") return false;
@@ -212,6 +218,56 @@ const isGuard = (expression: Expression, environment: Environment, isInit: boole
212
218
  return definition === undefined && environment.builtins.has(expression.operator);
213
219
  };
214
220
 
221
+ const NO_NAMES: ReadonlySet<string> = new Set();
222
+
223
+ // The local names an expression reads from enclosing scopes, or undefined when it applies a local
224
+ // operator, whose body may assign variables.
225
+ const getReadLocals = (
226
+ expression: Expression,
227
+ bound: ReadonlySet<string>,
228
+ ): string[] | undefined => {
229
+ if (isLocallyResolved(expression)) {
230
+ if (expression.kind !== "Identifier") return undefined;
231
+ return bound.has(expression.name) ? [] : [expression.name];
232
+ }
233
+ const names: string[] = [];
234
+ const result = { isReadOnly: true };
235
+ mapChildren(expression, (child, childNames) => {
236
+ const childLocals = result.isReadOnly
237
+ ? getReadLocals(child, childNames.length === 0 ? bound : new Set([...bound, ...childNames]))
238
+ : [];
239
+ if (childLocals) names.push(...childLocals);
240
+ else result.isReadOnly = false;
241
+ return child;
242
+ });
243
+ return result.isReadOnly ? names : undefined;
244
+ };
245
+
246
+ // TLC generates a successor for every disjunct and witness that holds, so an expression only
247
+ // checks like a guard when it has neither.
248
+ const hasBranches = (
249
+ expression: Expression,
250
+ environment: Environment,
251
+ visited: Set<Definition>,
252
+ ): boolean => {
253
+ if (expression.kind === "Or" || expression.kind === "Exists") return true;
254
+ const name =
255
+ expression.kind === "Identifier"
256
+ ? expression.name
257
+ : expression.kind === "Apply"
258
+ ? expression.operator
259
+ : undefined;
260
+ const definition =
261
+ name === undefined || isLocallyResolved(expression)
262
+ ? undefined
263
+ : getDefinition(name, environment);
264
+ if (definition && !visited.has(definition)) {
265
+ visited.add(definition);
266
+ if (hasBranches(definition.body, environment, visited)) return true;
267
+ }
268
+ return getChildren(expression).some((child) => hasBranches(child, environment, visited));
269
+ };
270
+
215
271
  const toConjunct = (
216
272
  expression: Expression,
217
273
  environment: Environment,
@@ -332,8 +388,8 @@ const compileMembership = (
332
388
  `In computing next states, the right side of \\in is not enumerable: ${formatValue(value)} at ${describeLocation(expression)}`,
333
389
  );
334
390
  }
335
- const elements = toFiniteSet(value).elements;
336
- for (let position = 0; position < elements.length; position++) {
391
+ const elements = getStreamedElements(value);
392
+ for (let position = 0; position < elements.length && !run.isStopped; position++) {
337
393
  onComplete(assign(partial, index, elements[position]), action);
338
394
  }
339
395
  };
@@ -542,6 +598,104 @@ const compileIdentifier = (
542
598
  };
543
599
  };
544
600
 
601
+ // A PlusCal action starts with a guard such as pc[self] = "L1", and a process is a disjunction of
602
+ // its actions. The labels an expression requires of one variable at one bound name let a
603
+ // disjunction skip the operands whose guard is false without evaluating them.
604
+ interface LabelGuard {
605
+ variable: number;
606
+ argument: string;
607
+ labels: Set<string>;
608
+ }
609
+
610
+ const getLabelGuard = (
611
+ expression: Expression,
612
+ environment: Environment,
613
+ visited: Set<Expression>,
614
+ ): LabelGuard | undefined => {
615
+ if (visited.has(expression)) return undefined;
616
+ visited.add(expression);
617
+ if (expression.kind === "And") {
618
+ const first = expression.operands[0];
619
+ if (first.kind !== "Apply" || first.operator !== "=" || isLocallyResolved(first)) {
620
+ return undefined;
621
+ }
622
+ const target = first.args[0];
623
+ const label = first.args[1];
624
+ if (target.kind !== "FunctionApplication" || label.kind !== "String") return undefined;
625
+ const argument = target.args.length === 1 ? target.args[0] : undefined;
626
+ if (argument?.kind !== "Identifier" || !isLocallyResolved(argument)) return undefined;
627
+ const variable = getStaticVariableIndex(target.function, environment, false);
628
+ if (variable === undefined) return undefined;
629
+ return { variable, argument: argument.name, labels: new Set([label.value]) };
630
+ }
631
+ if (expression.kind === "Or") {
632
+ const guards = expression.operands.map((operand) =>
633
+ getLabelGuard(operand, environment, visited),
634
+ );
635
+ const first = guards[0];
636
+ const isShared = guards.every(
637
+ (guard) =>
638
+ guard !== undefined &&
639
+ guard.variable === first?.variable &&
640
+ guard.argument === first.argument,
641
+ );
642
+ if (!first || !isShared) return undefined;
643
+ const labels = new Set(guards.flatMap((guard) => [...(guard?.labels ?? [])]));
644
+ return { variable: first.variable, argument: first.argument, labels };
645
+ }
646
+ if (expression.kind !== "Apply" || isLocallyResolved(expression)) return undefined;
647
+ const closure = resolveGlobalOperator(expression.operator, environment);
648
+ if (closure?.kind !== "Closure" || closure.scope !== undefined) return undefined;
649
+ const guard = getLabelGuard(closure.body, environment, visited);
650
+ const position = closure.params.findIndex(
651
+ (param) => param.arity === 0 && param.name === guard?.argument,
652
+ );
653
+ const argument = expression.args[position];
654
+ if (!guard || argument?.kind !== "Identifier" || !isLocallyResolved(argument)) {
655
+ return undefined;
656
+ }
657
+ return { variable: guard.variable, argument: argument.name, labels: guard.labels };
658
+ };
659
+
660
+ const getLabel = (run: Run, scope: Scope | undefined, guard: LabelGuard) => {
661
+ const binding = lookupScope(scope, guard.argument);
662
+ const functionValue = run.current?.[guard.variable];
663
+ if (
664
+ binding?.kind !== "Value" ||
665
+ typeof functionValue !== "object" ||
666
+ functionValue.kind !== "Function"
667
+ ) {
668
+ return undefined;
669
+ }
670
+ const position = functionValue.domain.indexOf(binding.value);
671
+ const label = position === -1 ? undefined : functionValue.range[position];
672
+ return typeof label === "string" ? label : undefined;
673
+ };
674
+
675
+ const compileDisjunction = (
676
+ operands: Expression[],
677
+ environment: Environment,
678
+ isInit: boolean,
679
+ ): Runner => {
680
+ const runners = operands.map((operand) => getRunner(operand, environment, isInit));
681
+ const guards = isInit
682
+ ? []
683
+ : operands.map((operand) => getLabelGuard(operand, environment, new Set()));
684
+ const dispatch = guards.find((guard) => guard !== undefined);
685
+ const labels = guards.map((guard) =>
686
+ guard?.variable === dispatch?.variable && guard?.argument === dispatch?.argument
687
+ ? guard?.labels
688
+ : undefined,
689
+ );
690
+ return (run, scope, partial, action, isFixed, onComplete) => {
691
+ const label = dispatch && getLabel(run, scope, dispatch);
692
+ for (let index = 0; index < runners.length && !run.isStopped; index++) {
693
+ if (label !== undefined && labels[index]?.has(label) === false) continue;
694
+ runners[index](run, scope, partial, action, isFixed, onComplete);
695
+ }
696
+ };
697
+ };
698
+
545
699
  const compileAction = (
546
700
  expression: Expression,
547
701
  environment: Environment,
@@ -551,26 +705,19 @@ const compileAction = (
551
705
  switch (expression.kind) {
552
706
  case "And":
553
707
  return compileConjunction(expression.operands, environment, isInit);
554
- case "Or": {
555
- const operands = expression.operands.map(child);
556
- return (run, scope, partial, action, isFixed, onComplete) => {
557
- for (const operand of operands) operand(run, scope, partial, action, isFixed, onComplete);
558
- };
559
- }
708
+ case "Or":
709
+ return compileDisjunction(expression.operands, environment, isInit);
560
710
  case "Exists": {
561
711
  const body = child(expression.body);
562
712
  const bound = expression.bounds[0];
563
713
  if (expression.bounds.length === 1 && bound.pattern.kind === "Name") {
564
714
  const name = bound.pattern.name;
565
715
  return (run, scope, partial, action, isFixed, onComplete) => {
566
- const set = evaluate(bound.set, getContext(run, scope, partial));
567
- if (!isEnumerable(set)) {
568
- throw new TlaEvaluationError(
569
- `TLC encountered a non-enumerable quantifier bound ${formatValue(set)} at ${describeLocation(bound.set)}`,
570
- );
571
- }
572
- const elements = toFiniteSet(set).elements;
573
- for (let index = 0; index < elements.length; index++) {
716
+ const elements = getEnumeratedElements(
717
+ evaluate(bound.set, getContext(run, scope, partial)),
718
+ bound.set,
719
+ );
720
+ for (let index = 0; index < elements.length && !run.isStopped; index++) {
574
721
  body(
575
722
  run,
576
723
  bindValue(scope, name, elements[index]),
@@ -585,12 +732,18 @@ const compileAction = (
585
732
  return (run, scope, partial, action, isFixed, onComplete) => {
586
733
  forEachBinding(expression.bounds, getContext(run, scope, partial), (boundScope) => {
587
734
  body(run, boundScope, partial, action, isFixed, onComplete);
588
- return false;
735
+ return run.isStopped;
589
736
  });
590
737
  };
591
738
  }
592
739
  case "Forall": {
593
740
  const body = child(expression.body);
741
+ const readLocals =
742
+ !isInit &&
743
+ getReadVariables(expression, environment) !== undefined &&
744
+ !hasBranches(expression.body, environment, new Set())
745
+ ? getReadLocals(expression, NO_NAMES)
746
+ : undefined;
594
747
  return (run, scope, partial, action, _isFixed, onComplete) => {
595
748
  const boundScopes: (Scope | undefined)[] = [];
596
749
  forEachBinding(expression.bounds, getContext(run, scope, partial), (boundScope) => {
@@ -598,6 +751,16 @@ const compileAction = (
598
751
  return false;
599
752
  });
600
753
  const scopes = [...boundScopes.slice(0, 1), ...boundScopes.slice(1).reverse()];
754
+ const isBoundToValues = readLocals?.every(
755
+ (name) => lookupScope(scope, name)?.kind === "Value",
756
+ );
757
+ if (isBoundToValues) {
758
+ const isSatisfied = scopes.every((boundScope) =>
759
+ holds(expression.body, getContext(run, boundScope, partial)),
760
+ );
761
+ if (isSatisfied) onComplete(partial, action);
762
+ return;
763
+ }
601
764
  const visit = (index: number, nextPartial: State, nextAction: string | undefined): void => {
602
765
  if (index === scopes.length) {
603
766
  onComplete(nextPartial, nextAction);
@@ -647,10 +810,11 @@ const compileAction = (
647
810
  }
648
811
  case "Let": {
649
812
  const body = child(expression.body);
813
+ const reevaluated = getReevaluatedDefinitions(expression.definitions, environment);
650
814
  return (run, scope, partial, action, isFixed, onComplete) =>
651
815
  body(
652
816
  run,
653
- bindDefinitions(expression.definitions, getContext(run, scope, partial)),
817
+ bindDefinitions(expression.definitions, getContext(run, scope, partial), reevaluated),
654
818
  partial,
655
819
  action,
656
820
  isFixed,
@@ -751,7 +915,7 @@ export const isEnabled = (expression: Expression, context: Context): boolean =>
751
915
  const environment = context.environment;
752
916
  const found = { value: false };
753
917
  getRunner(expression, environment, false)(
754
- { environment, isInit: false, current: state },
918
+ { environment, isInit: false, current: state, isStopped: false },
755
919
  context.scope,
756
920
  getEmptyState(environment),
757
921
  undefined,
@@ -763,48 +927,99 @@ export const isEnabled = (expression: Expression, context: Context): boolean =>
763
927
  return found.value;
764
928
  };
765
929
 
766
- export const getInitialStates = (expression: Expression, environment: Environment): Value[][] => {
767
- const states: Value[][] = [];
768
- const run: Run = { environment, isInit: true, current: undefined };
930
+ export const forEachInitialState = (
931
+ expression: Expression,
932
+ environment: Environment,
933
+ onState: (state: Value[]) => boolean,
934
+ scope?: Scope,
935
+ ) => {
936
+ const run: Run = { environment, isInit: true, current: undefined, isStopped: false };
769
937
  getRunner(expression, environment, true)(
770
938
  run,
771
- undefined,
939
+ scope,
772
940
  getEmptyState(environment),
773
941
  undefined,
774
942
  false,
775
943
  (partial) => {
944
+ if (run.isStopped) return;
776
945
  if (!isComplete(partial)) {
777
- throw new TlaEvaluationError(
778
- `The initial predicate does not assign ${getMissingVariables(partial, environment).join(", ")}`,
946
+ // TLC reports an initial state that isn't completely specified as an illegal state, not as
947
+ // an evaluation failure.
948
+ throw setExitCode(
949
+ new TlaEvaluationError(
950
+ `The initial predicate does not assign ${getMissingVariables(partial, environment).join(", ")}`,
951
+ ),
952
+ EXIT_STATUS.error,
779
953
  );
780
954
  }
781
- states.push(partial);
955
+ run.isStopped = onState(partial);
956
+ },
957
+ );
958
+ };
959
+
960
+ export const getInitialStates = (
961
+ expression: Expression,
962
+ environment: Environment,
963
+ scope?: Scope,
964
+ ): Value[][] => {
965
+ const states: Value[][] = [];
966
+ forEachInitialState(
967
+ expression,
968
+ environment,
969
+ (state) => {
970
+ states.push(state);
971
+ return false;
782
972
  },
973
+ scope,
783
974
  );
784
975
  return states;
785
976
  };
786
977
 
787
- export const getSuccessors = (
978
+ export const forEachSuccessor = (
788
979
  expression: Expression,
789
980
  state: Value[],
790
981
  environment: Environment,
791
- ): Successor[] => {
792
- const successors: Successor[] = [];
793
- const run: Run = { environment, isInit: false, current: state };
982
+ onSuccessor: (successor: Successor) => boolean,
983
+ scope?: Scope,
984
+ ) => {
985
+ const run: Run = { environment, isInit: false, current: state, isStopped: false };
986
+ const counter = { successors: 0 };
794
987
  getRunner(expression, environment, false)(
795
988
  run,
796
- undefined,
989
+ scope,
797
990
  getEmptyState(environment),
798
991
  undefined,
799
992
  false,
800
993
  (partial, action) => {
994
+ if (run.isStopped) return;
801
995
  if (!isComplete(partial)) {
802
996
  throw new TlaEvaluationError(
803
997
  `Successor state is not completely specified by action ${action ?? "Next"}. The following variables are not assigned: ${getMissingVariables(partial, environment).join(", ")}`,
804
998
  );
805
999
  }
806
- successors.push({ state: partial, action });
1000
+ counter.successors++;
1001
+ run.isStopped = onSuccessor({ state: partial, action });
1002
+ },
1003
+ );
1004
+ return counter.successors;
1005
+ };
1006
+
1007
+ export const getSuccessors = (
1008
+ expression: Expression,
1009
+ state: Value[],
1010
+ environment: Environment,
1011
+ scope?: Scope,
1012
+ ): Successor[] => {
1013
+ const successors: Successor[] = [];
1014
+ forEachSuccessor(
1015
+ expression,
1016
+ state,
1017
+ environment,
1018
+ (successor) => {
1019
+ successors.push(successor);
1020
+ return false;
807
1021
  },
1022
+ scope,
808
1023
  );
809
1024
  return successors;
810
1025
  };