tla-ts 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -41
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +166 -30
- package/dist/index.d.ts +166 -30
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +2 -2
- package/dist/parallel-worker.js +2 -2
- package/dist/parallel.cjs +1 -1
- package/dist/parallel.js +2 -2
- package/dist/run-cli.cjs +2 -1
- package/dist/run-cli.js +2 -1
- package/dist/transfer.cjs +388 -39
- package/dist/transfer.js +388 -39
- package/package.json +4 -2
- package/src/ast.ts +82 -5
- package/src/bags.ts +161 -71
- package/src/builtins.ts +203 -23
- package/src/bundled-modules.ts +63 -32
- package/src/check.ts +904 -230
- package/src/codegen.ts +52 -18
- package/src/config.ts +39 -11
- package/src/environment.ts +485 -140
- package/src/errors.ts +22 -0
- package/src/evaluate.ts +486 -136
- package/src/exit-codes.ts +75 -0
- package/src/fingerprints.ts +2 -2
- package/src/index.ts +9 -4
- package/src/instances.ts +221 -66
- package/src/java-overrides.ts +241 -0
- package/src/json.ts +153 -72
- package/src/levels.ts +850 -0
- package/src/lexer.ts +97 -27
- package/src/liveness.ts +47 -91
- package/src/parallel-worker.ts +57 -28
- package/src/parallel.ts +116 -62
- package/src/parser.ts +703 -183
- package/src/pretty-print.ts +259 -0
- package/src/randomization.ts +24 -113
- package/src/resolve.ts +472 -41
- package/src/run-cli.ts +142 -44
- package/src/runtime.ts +63 -28
- package/src/spec-description.ts +509 -0
- package/src/standard-levels.ts +105 -0
- package/src/standard-modules.ts +223 -92
- package/src/subexpressions.ts +21 -35
- package/src/substitution-levels.ts +184 -0
- package/src/successors.ts +261 -46
- package/src/symmetry.ts +65 -21
- package/src/tableau.ts +43 -37
- package/src/tlc-ext.ts +120 -33
- package/src/tlc-module.ts +71 -47
- package/src/tlc-modules.ts +1022 -0
- package/src/tlc-order.ts +497 -0
- package/src/tlc-random.ts +411 -0
- package/src/tlc-summary.ts +109 -0
- package/src/transfer.ts +4 -4
- package/src/values.ts +1186 -164
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
|
-
|
|
23
|
+
combineHash,
|
|
24
|
+
compareValues,
|
|
19
25
|
createFunction,
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
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 (!
|
|
413
|
+
if (!isSetValue(set) || isOverriddenSet(set)) {
|
|
414
|
+
const text = formatValue(set);
|
|
415
|
+
const explanation = getBoundExplanation(set, text);
|
|
301
416
|
throw new TlaEvaluationError(
|
|
302
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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)
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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) => {
|
|
@@ -590,7 +773,7 @@ export const bindOperatorArguments = (
|
|
|
590
773
|
return scope;
|
|
591
774
|
};
|
|
592
775
|
|
|
593
|
-
const callOperator = (operator: Operator, args: Expression[], context: Context)
|
|
776
|
+
const callOperator = (operator: Operator, args: Expression[], context: Context) => {
|
|
594
777
|
if (operator.kind === "Closure") {
|
|
595
778
|
return evaluate(
|
|
596
779
|
operator.body,
|
|
@@ -609,7 +792,11 @@ const callOperator = (operator: Operator, args: Expression[], context: Context):
|
|
|
609
792
|
);
|
|
610
793
|
};
|
|
611
794
|
|
|
612
|
-
export const bindDefinitions = (
|
|
795
|
+
export const bindDefinitions = (
|
|
796
|
+
definitions: Definition[],
|
|
797
|
+
context: Context,
|
|
798
|
+
reevaluated?: ReadonlySet<Definition>,
|
|
799
|
+
): Scope | undefined =>
|
|
613
800
|
definitions.reduce<Scope | undefined>((scope, definition) => {
|
|
614
801
|
const node: Scope = {
|
|
615
802
|
name: definition.name,
|
|
@@ -633,7 +820,7 @@ export const bindDefinitions = (definitions: Definition[], context: Context): Sc
|
|
|
633
820
|
};
|
|
634
821
|
return node;
|
|
635
822
|
}
|
|
636
|
-
if (definition.params.length > 0) {
|
|
823
|
+
if (definition.params.length > 0 || reevaluated?.has(definition)) {
|
|
637
824
|
node.binding = {
|
|
638
825
|
kind: "Operator",
|
|
639
826
|
operator: {
|
|
@@ -684,10 +871,10 @@ const evaluateLocalIdentifier = (name: string, expression: Expression, context:
|
|
|
684
871
|
throw new TlaEvaluationError(`Unknown identifier ${name} at ${describeLocation(expression)}`);
|
|
685
872
|
};
|
|
686
873
|
|
|
687
|
-
const isTlcInteger = (value: number) =>
|
|
688
|
-
Number.isInteger(value) && value <=
|
|
874
|
+
export const isTlcInteger = (value: number) =>
|
|
875
|
+
Number.isInteger(value) && value <= MAXIMUM_INTEGER && value >= MINIMUM_INTEGER;
|
|
689
876
|
|
|
690
|
-
|
|
877
|
+
const IMPURE_BUILTINS = new Set([
|
|
691
878
|
"Print",
|
|
692
879
|
"PrintT",
|
|
693
880
|
"Assert",
|
|
@@ -700,6 +887,58 @@ export const IMPURE_BUILTINS = new Set([
|
|
|
700
887
|
export const isImpureBuiltin = (name: string, environment: Environment) =>
|
|
701
888
|
IMPURE_BUILTINS.has(name) || environment.builtins.get(name)?.isStateLevel === true;
|
|
702
889
|
|
|
890
|
+
const impureDefinitions = new WeakMap<Definition, boolean>();
|
|
891
|
+
|
|
892
|
+
const isEvaluatedOnce = (definition: Definition, environment: Environment) =>
|
|
893
|
+
definition.kind === "Operator" &&
|
|
894
|
+
definition.params.length === 0 &&
|
|
895
|
+
isConstantLevel(definition, environment);
|
|
896
|
+
|
|
897
|
+
const callsImpureBuiltin = (expression: Expression, environment: Environment): boolean => {
|
|
898
|
+
const name =
|
|
899
|
+
expression.kind === "Identifier"
|
|
900
|
+
? expression.name
|
|
901
|
+
: expression.kind === "Apply" || expression.kind === "OperatorReference"
|
|
902
|
+
? expression.operator
|
|
903
|
+
: undefined;
|
|
904
|
+
if (name !== undefined && !isLocallyResolved(expression)) {
|
|
905
|
+
const definition = getDefinition(name, environment);
|
|
906
|
+
if (!definition && isImpureBuiltin(name, environment)) return true;
|
|
907
|
+
if (
|
|
908
|
+
definition &&
|
|
909
|
+
!isEvaluatedOnce(definition, environment) &&
|
|
910
|
+
isImpureDefinition(definition, environment)
|
|
911
|
+
) {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return getChildren(expression).some((child) => callsImpureBuiltin(child, environment));
|
|
916
|
+
};
|
|
917
|
+
|
|
918
|
+
// TLC evaluates a constant definition without parameters once, so only other definitions pass
|
|
919
|
+
// on the side effects and random choices of the builtins they call.
|
|
920
|
+
const isImpureDefinition = (definition: Definition, environment: Environment): boolean => {
|
|
921
|
+
const cached = impureDefinitions.get(definition);
|
|
922
|
+
if (cached !== undefined) return cached;
|
|
923
|
+
impureDefinitions.set(definition, false);
|
|
924
|
+
const isImpure =
|
|
925
|
+
callsImpureBuiltin(definition.body, environment) ||
|
|
926
|
+
(definition.kind === "Function" &&
|
|
927
|
+
definition.bounds.some((bound) => callsImpureBuiltin(bound.set, environment)));
|
|
928
|
+
impureDefinitions.set(definition, isImpure);
|
|
929
|
+
return isImpure;
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
// TLC's next-state generator does not bind LET definitions to values, so each reference to one
|
|
933
|
+
// that calls an impure builtin evaluates it again.
|
|
934
|
+
export const getReevaluatedDefinitions = (definitions: Definition[], environment: Environment) =>
|
|
935
|
+
new Set(definitions.filter((definition) => isImpureDefinition(definition, environment)));
|
|
936
|
+
|
|
937
|
+
export const isImpure = (environment: Environment) =>
|
|
938
|
+
[...environment.definitions.values()].some((definition) =>
|
|
939
|
+
isImpureDefinition(definition, environment),
|
|
940
|
+
);
|
|
941
|
+
|
|
703
942
|
const closedConstants = new WeakMap<Expression, boolean>();
|
|
704
943
|
|
|
705
944
|
export const isClosedConstant = (expression: Expression, environment: Environment): boolean => {
|
|
@@ -742,7 +981,7 @@ const isClosedIn = (
|
|
|
742
981
|
expression: Expression,
|
|
743
982
|
environment: Environment,
|
|
744
983
|
bound: ReadonlySet<string>,
|
|
745
|
-
)
|
|
984
|
+
) =>
|
|
746
985
|
bound.size === 0
|
|
747
986
|
? isClosedConstant(expression, environment)
|
|
748
987
|
: computeIsClosedConstant(expression, environment, bound);
|
|
@@ -760,28 +999,24 @@ const computeIsClosedConstant = (
|
|
|
760
999
|
if (expression.kind === "Identifier") {
|
|
761
1000
|
const global = findGlobalName(expression.name, environment);
|
|
762
1001
|
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
|
-
);
|
|
1002
|
+
return global?.kind === "Definition" && isEvaluatedOnce(global.definition, environment);
|
|
769
1003
|
}
|
|
770
1004
|
if (expression.kind === "Apply") {
|
|
771
1005
|
if (isImpureBuiltin(expression.operator, environment) || expression.operator === "~>") {
|
|
772
1006
|
return false;
|
|
773
1007
|
}
|
|
774
1008
|
const operator = resolveGlobalOperator(expression.operator, environment);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
(operator
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1009
|
+
if (operator?.kind !== "Builtin" && operator?.kind !== "Closure") return false;
|
|
1010
|
+
if (operator.kind === "Closure") {
|
|
1011
|
+
const definition = getDefinition(expression.operator, environment);
|
|
1012
|
+
if (
|
|
1013
|
+
!definition ||
|
|
1014
|
+
!isConstantLevel(definition, environment) ||
|
|
1015
|
+
isImpureDefinition(definition, environment)
|
|
1016
|
+
) {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
785
1020
|
const params = operator.kind === "Closure" ? operator.params : [];
|
|
786
1021
|
const hasOperatorArgument = params.some((param) => param.arity > 0);
|
|
787
1022
|
if (hasOperatorArgument) return false;
|
|
@@ -855,7 +1090,7 @@ export const getEvaluator = (expression: Expression, environment: Environment):
|
|
|
855
1090
|
|
|
856
1091
|
const entryEvaluators = new WeakMap<Expression, Evaluator>();
|
|
857
1092
|
|
|
858
|
-
const getEntryEvaluator = (expression: Expression, environment: Environment)
|
|
1093
|
+
const getEntryEvaluator = (expression: Expression, environment: Environment) => {
|
|
859
1094
|
const cached = entryEvaluators.get(expression);
|
|
860
1095
|
if (cached) return cached;
|
|
861
1096
|
const evaluator =
|
|
@@ -1031,18 +1266,79 @@ export const compileArgumentScope = (
|
|
|
1031
1266
|
};
|
|
1032
1267
|
};
|
|
1033
1268
|
|
|
1269
|
+
interface MemoizedCall {
|
|
1270
|
+
args: Value[];
|
|
1271
|
+
value: Value;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const MEMOIZED_CALL_LIMIT = 1 << 16;
|
|
1275
|
+
|
|
1276
|
+
const callMemos = new WeakMap<Closure, { calls: Map<number, MemoizedCall[]>; size: number }>();
|
|
1277
|
+
|
|
1278
|
+
const getCallMemo = (closure: Closure) => {
|
|
1279
|
+
const cached = callMemos.get(closure);
|
|
1280
|
+
if (cached) return cached;
|
|
1281
|
+
const memo = { calls: new Map<number, MemoizedCall[]>(), size: 0 };
|
|
1282
|
+
callMemos.set(closure, memo);
|
|
1283
|
+
return memo;
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
const getArgumentValues = (scope: Scope | undefined, arity: number) => {
|
|
1287
|
+
const values: Value[] = [];
|
|
1288
|
+
for (let current = scope; values.length < arity; current = current?.parent) {
|
|
1289
|
+
if (current?.binding.kind !== "Value") return undefined;
|
|
1290
|
+
values.push(current.binding.value);
|
|
1291
|
+
}
|
|
1292
|
+
return values;
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
const isSameArguments = (left: Value[], right: Value[]) =>
|
|
1296
|
+
left.every((value, index) => isSameValue(value, right[index]));
|
|
1297
|
+
|
|
1298
|
+
// A constant-level operator without side effects yields the same value for the same arguments,
|
|
1299
|
+
// so its calls with evaluated arguments share one result.
|
|
1300
|
+
const isMemoizable = (expression: Expression & { kind: "Apply" }, environment: Environment) => {
|
|
1301
|
+
const definition = getDefinition(expression.operator, environment);
|
|
1302
|
+
return (
|
|
1303
|
+
definition?.kind === "Operator" &&
|
|
1304
|
+
definition.params.length > 0 &&
|
|
1305
|
+
definition.params.every((param) => param.arity === 0) &&
|
|
1306
|
+
isConstantLevel(definition, environment) &&
|
|
1307
|
+
!isImpureDefinition(definition, environment)
|
|
1308
|
+
);
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1034
1311
|
const compileClosureCall = (
|
|
1035
1312
|
closure: Closure,
|
|
1036
1313
|
args: Expression[],
|
|
1037
1314
|
environment: Environment,
|
|
1315
|
+
isMemoized: boolean,
|
|
1038
1316
|
): Evaluator => {
|
|
1039
1317
|
const bindArguments = compileArgumentScope(closure, args, environment);
|
|
1040
1318
|
const compiled = { body: undefined as Evaluator | undefined };
|
|
1041
|
-
|
|
1042
|
-
const scope = bindArguments(context);
|
|
1319
|
+
const evaluateBody = (scope: Scope | undefined, context: Context) => {
|
|
1043
1320
|
compiled.body ??= getEntryEvaluator(closure.body, environment);
|
|
1044
1321
|
return compiled.body({ environment, scope, state: context.state, next: context.next });
|
|
1045
1322
|
};
|
|
1323
|
+
if (!isMemoized) return (context) => evaluateBody(bindArguments(context), context);
|
|
1324
|
+
const memo = getCallMemo(closure);
|
|
1325
|
+
return (context) => {
|
|
1326
|
+
const scope = bindArguments(context);
|
|
1327
|
+
const values = getArgumentValues(scope, args.length);
|
|
1328
|
+
if (!values) return evaluateBody(scope, context);
|
|
1329
|
+
let hash = values.length;
|
|
1330
|
+
for (const value of values) hash = combineHash(hash, getValueHash(value));
|
|
1331
|
+
const bucket = memo.calls.get(hash);
|
|
1332
|
+
const known = bucket?.find((call) => isSameArguments(call.args, values));
|
|
1333
|
+
if (known) return known.value;
|
|
1334
|
+
const value = evaluateBody(scope, context);
|
|
1335
|
+
if (memo.size < MEMOIZED_CALL_LIMIT) {
|
|
1336
|
+
memo.size++;
|
|
1337
|
+
if (bucket) bucket.push({ args: values, value });
|
|
1338
|
+
else memo.calls.set(hash, [{ args: values, value }]);
|
|
1339
|
+
}
|
|
1340
|
+
return value;
|
|
1341
|
+
};
|
|
1046
1342
|
};
|
|
1047
1343
|
|
|
1048
1344
|
const compileApply = (
|
|
@@ -1065,7 +1361,29 @@ const compileApply = (
|
|
|
1065
1361
|
const global = isLocallyResolved(expression)
|
|
1066
1362
|
? undefined
|
|
1067
1363
|
: resolveGlobalOperator(expression.operator, environment);
|
|
1068
|
-
if (global?.kind === "Closure")
|
|
1364
|
+
if (global?.kind === "Closure") {
|
|
1365
|
+
return compileClosureCall(global, args, environment, isMemoizable(expression, environment));
|
|
1366
|
+
}
|
|
1367
|
+
if (global?.kind === "Builtin" && global.name === "DOMAIN") {
|
|
1368
|
+
const operand = getEvaluator(args[0], environment);
|
|
1369
|
+
const domain = getDomainOperator(expression);
|
|
1370
|
+
return (context) => domain(operand(context));
|
|
1371
|
+
}
|
|
1372
|
+
// TLC sorts the value of a constant TLCEval argument only where no context binds a name.
|
|
1373
|
+
if (global?.kind === "Builtin" && global.name === "TLCEval" && args.length === 1) {
|
|
1374
|
+
const operand = getEvaluator(args[0], environment);
|
|
1375
|
+
if (contextualApplications.has(expression) || isStateDependent(args[0], environment)) {
|
|
1376
|
+
return operand;
|
|
1377
|
+
}
|
|
1378
|
+
return (context) =>
|
|
1379
|
+
context.scope === undefined ? global.evaluate([operand(context)]) : operand(context);
|
|
1380
|
+
}
|
|
1381
|
+
if (global?.kind === "Builtin" && global.name === "\\subseteq" && args.length === 2) {
|
|
1382
|
+
const left = getEvaluator(args[0], environment);
|
|
1383
|
+
const right = getEvaluator(args[1], environment);
|
|
1384
|
+
const subset = getSubsetOperator(expression, global);
|
|
1385
|
+
return (context) => subset(left(context), right(context));
|
|
1386
|
+
}
|
|
1069
1387
|
if (global?.kind === "Builtin") return compileBuiltinCall(global, args, environment);
|
|
1070
1388
|
return (context) => {
|
|
1071
1389
|
const operator = resolveOperator(expression.operator, context);
|
|
@@ -1134,7 +1452,7 @@ const compileQuantifier = (
|
|
|
1134
1452
|
};
|
|
1135
1453
|
};
|
|
1136
1454
|
|
|
1137
|
-
const lookupKey = (pattern: BoundPattern, scope: Scope | undefined)
|
|
1455
|
+
const lookupKey = (pattern: BoundPattern, scope: Scope | undefined) => {
|
|
1138
1456
|
if (pattern.kind === "Tuple") {
|
|
1139
1457
|
return createTuple(
|
|
1140
1458
|
pattern.names.map((name) => {
|
|
@@ -1147,10 +1465,21 @@ const lookupKey = (pattern: BoundPattern, scope: Scope | undefined): Value | und
|
|
|
1147
1465
|
return binding?.kind === "Value" ? binding.value : undefined;
|
|
1148
1466
|
};
|
|
1149
1467
|
|
|
1468
|
+
// TLC keeps a function constructor lazy when it is the function of an application, or the base of
|
|
1469
|
+
// an EXCEPT that is, so it evaluates the body only at the arguments it is applied to.
|
|
1470
|
+
const keptLazyExpressions = new WeakSet<Expression>();
|
|
1471
|
+
|
|
1472
|
+
export const keepLazy = (expression: Expression) => {
|
|
1473
|
+
if (expression.kind === "FunctionConstruction" || expression.kind === "Except") {
|
|
1474
|
+
keptLazyExpressions.add(expression);
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1150
1478
|
const compileFunctionConstruction = (
|
|
1151
1479
|
expression: Expression & { kind: "FunctionConstruction" },
|
|
1152
1480
|
environment: Environment,
|
|
1153
1481
|
): Evaluator => {
|
|
1482
|
+
const isKeptLazy = keptLazyExpressions.has(expression);
|
|
1154
1483
|
const body = getEvaluator(expression.body, environment);
|
|
1155
1484
|
const bounds = expression.bounds;
|
|
1156
1485
|
const bound = bounds[0];
|
|
@@ -1158,17 +1487,22 @@ const compileFunctionConstruction = (
|
|
|
1158
1487
|
const set = getEvaluator(bound.set, environment);
|
|
1159
1488
|
return (context) => {
|
|
1160
1489
|
const domain = isSingleName ? set(context) : getBoundsDomain(bounds, context);
|
|
1161
|
-
if (expression.isRecursive || !isEnumerable(domain)) {
|
|
1162
|
-
|
|
1163
|
-
|
|
1490
|
+
if (expression.isRecursive || isKeptLazy || !isEnumerable(domain)) {
|
|
1491
|
+
const functionValue = createLazyFunction(
|
|
1492
|
+
domain,
|
|
1493
|
+
(argument) => body(withScope(context, bindArgument(bounds, context.scope, argument))),
|
|
1494
|
+
getLazyFunctionSource(bounds, expression.body, domain),
|
|
1164
1495
|
);
|
|
1496
|
+
return bounds.length === 1 ? markLambdaFunction(functionValue) : functionValue;
|
|
1165
1497
|
}
|
|
1166
1498
|
if (isSingleName && bound.pattern.kind === "Name") {
|
|
1167
1499
|
const name = bound.pattern.name;
|
|
1168
|
-
const keys =
|
|
1169
|
-
return
|
|
1170
|
-
|
|
1171
|
-
|
|
1500
|
+
const keys = getFunctionDomain(domain);
|
|
1501
|
+
return markLambdaFunction(
|
|
1502
|
+
createSortedFunction(
|
|
1503
|
+
keys,
|
|
1504
|
+
keys.map((key) => body(withScope(context, bindValue(context.scope, name, key)))),
|
|
1505
|
+
),
|
|
1172
1506
|
);
|
|
1173
1507
|
}
|
|
1174
1508
|
const pairs: [Value, Value][] = [];
|
|
@@ -1188,21 +1522,22 @@ const compileExcept = (
|
|
|
1188
1522
|
expression: Expression & { kind: "Except" },
|
|
1189
1523
|
environment: Environment,
|
|
1190
1524
|
): Evaluator => {
|
|
1191
|
-
const
|
|
1525
|
+
const isKeptLazy = keptLazyExpressions.has(expression);
|
|
1526
|
+
if (isKeptLazy) keepLazy(expression.function);
|
|
1527
|
+
const evaluateFunction = getEvaluator(expression.function, environment);
|
|
1192
1528
|
const updates = expression.updates.map((update) => ({
|
|
1193
|
-
path: update.path.map((step) =>
|
|
1529
|
+
path: update.path.map((step): CompiledStep =>
|
|
1194
1530
|
step.kind === "Field"
|
|
1195
|
-
? { kind: "Field"
|
|
1531
|
+
? { kind: "Field", name: step.name, args: [] }
|
|
1196
1532
|
: {
|
|
1197
|
-
kind: "Index"
|
|
1533
|
+
kind: "Index",
|
|
1198
1534
|
name: "",
|
|
1199
1535
|
args: step.args.map((arg) => getEvaluator(arg, environment)),
|
|
1200
1536
|
},
|
|
1201
1537
|
),
|
|
1202
1538
|
value: getEvaluator(update.value, environment),
|
|
1203
1539
|
}));
|
|
1204
|
-
|
|
1205
|
-
const getKey = (step: CompiledStep, context: Context): Value => {
|
|
1540
|
+
const getKey = (step: CompiledStep, context: Context) => {
|
|
1206
1541
|
if (step.kind === "Field") return step.name;
|
|
1207
1542
|
if (step.args.length === 1) return step.args[0](context);
|
|
1208
1543
|
return createTuple(step.args.map((arg) => arg(context)));
|
|
@@ -1217,12 +1552,15 @@ const compileExcept = (
|
|
|
1217
1552
|
if (depth === path.length) {
|
|
1218
1553
|
return value(withScope(context, bindValue(context.scope, EXCEPT_AT, current)));
|
|
1219
1554
|
}
|
|
1220
|
-
return updateFunction(
|
|
1221
|
-
|
|
1555
|
+
return updateFunction(
|
|
1556
|
+
current,
|
|
1557
|
+
getKey(path[depth], context),
|
|
1558
|
+
(previous) => update(previous, path, depth + 1, context, value),
|
|
1559
|
+
isKeptLazy && depth === 0,
|
|
1222
1560
|
);
|
|
1223
1561
|
};
|
|
1224
1562
|
return (context) => {
|
|
1225
|
-
let current =
|
|
1563
|
+
let current = evaluateFunction(context);
|
|
1226
1564
|
for (const item of updates) current = update(current, item.path, 0, context, item.value);
|
|
1227
1565
|
return current;
|
|
1228
1566
|
};
|
|
@@ -1244,7 +1582,8 @@ const compileSetComprehension = (
|
|
|
1244
1582
|
"a set filter predicate",
|
|
1245
1583
|
);
|
|
1246
1584
|
const isReducible =
|
|
1247
|
-
typeof base === "object" &&
|
|
1585
|
+
typeof base === "object" &&
|
|
1586
|
+
(base.kind === "Set" || base.kind === "MixedSet" || base.kind === "Interval");
|
|
1248
1587
|
if (!isReducible) {
|
|
1249
1588
|
if (isSetValue(base)) return { kind: "FilteredSet", base, test };
|
|
1250
1589
|
throw new TlaEvaluationError(
|
|
@@ -1262,7 +1601,7 @@ const compileSetComprehension = (
|
|
|
1262
1601
|
elements.push(mapped(withScope(context, scope)));
|
|
1263
1602
|
return false;
|
|
1264
1603
|
});
|
|
1265
|
-
return
|
|
1604
|
+
return createEnumeratedSet(elements);
|
|
1266
1605
|
};
|
|
1267
1606
|
}
|
|
1268
1607
|
const set = expression.set;
|
|
@@ -1284,7 +1623,7 @@ const compileSetComprehension = (
|
|
|
1284
1623
|
});
|
|
1285
1624
|
if (chosen.value === undefined) {
|
|
1286
1625
|
throw new TlaEvaluationError(
|
|
1287
|
-
`Attempted to compute
|
|
1626
|
+
`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
1627
|
);
|
|
1289
1628
|
}
|
|
1290
1629
|
return chosen.value;
|
|
@@ -1292,7 +1631,7 @@ const compileSetComprehension = (
|
|
|
1292
1631
|
};
|
|
1293
1632
|
|
|
1294
1633
|
const compileRecord = (recordFields: RecordField[], environment: Environment): Evaluator => {
|
|
1295
|
-
const fields = [...recordFields].sort((left, right) => (left.name
|
|
1634
|
+
const fields = [...recordFields].sort((left, right) => compareValues(left.name, right.name));
|
|
1296
1635
|
const hasDuplicates = fields.some(
|
|
1297
1636
|
(field, index) => index > 0 && fields[index - 1].name === field.name,
|
|
1298
1637
|
);
|
|
@@ -1303,7 +1642,16 @@ const compileRecord = (recordFields: RecordField[], environment: Environment): E
|
|
|
1303
1642
|
fields.map((field, index): [Value, Value] => [field.name, values[index](context)]),
|
|
1304
1643
|
);
|
|
1305
1644
|
}
|
|
1306
|
-
const names
|
|
1645
|
+
const names = fields.map((field) => field.name);
|
|
1646
|
+
const writtenNames = recordFields.map((field) => field.name);
|
|
1647
|
+
if (writtenNames.some((name, index) => name !== names[index])) {
|
|
1648
|
+
return (context) =>
|
|
1649
|
+
createWrittenRecord(
|
|
1650
|
+
names,
|
|
1651
|
+
values.map((value) => value(context)),
|
|
1652
|
+
writtenNames,
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1307
1655
|
return (context) =>
|
|
1308
1656
|
createSortedFunction(
|
|
1309
1657
|
names,
|
|
@@ -1311,25 +1659,21 @@ const compileRecord = (recordFields: RecordField[], environment: Environment): E
|
|
|
1311
1659
|
);
|
|
1312
1660
|
};
|
|
1313
1661
|
|
|
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
1662
|
const compile = (expression: Expression, environment: Environment): Evaluator => {
|
|
1327
1663
|
const child = (node: Expression) => getEvaluator(node, environment);
|
|
1328
1664
|
switch (expression.kind) {
|
|
1329
1665
|
case "Number": {
|
|
1330
1666
|
const value = expression.value;
|
|
1331
|
-
return isTlcInteger(value)
|
|
1667
|
+
return isTlcInteger(value)
|
|
1668
|
+
? () => value
|
|
1669
|
+
: () => {
|
|
1670
|
+
throw createBigNumberError(expression);
|
|
1671
|
+
};
|
|
1332
1672
|
}
|
|
1673
|
+
case "Decimal":
|
|
1674
|
+
return () => {
|
|
1675
|
+
throw createRealNumberError(expression);
|
|
1676
|
+
};
|
|
1333
1677
|
case "String": {
|
|
1334
1678
|
const value = expression.value;
|
|
1335
1679
|
return () => value;
|
|
@@ -1340,17 +1684,18 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
|
|
|
1340
1684
|
return compileApply(expression, environment);
|
|
1341
1685
|
case "OperatorReference":
|
|
1342
1686
|
case "Lambda":
|
|
1343
|
-
return
|
|
1344
|
-
(
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
);
|
|
1687
|
+
return () => {
|
|
1688
|
+
throw new TlaEvaluationError(
|
|
1689
|
+
`An operator cannot be used as a value at ${describeLocation(expression)}`,
|
|
1690
|
+
);
|
|
1691
|
+
};
|
|
1349
1692
|
case "And": {
|
|
1350
1693
|
const operands = expression.operands.map(child);
|
|
1351
1694
|
return (context) => {
|
|
1352
1695
|
for (let index = 0; index < operands.length; index++) {
|
|
1353
|
-
|
|
1696
|
+
const value = operands[index](context);
|
|
1697
|
+
if (typeof value !== "boolean") throw getConjunctError(expression, index, value);
|
|
1698
|
+
if (!value) return false;
|
|
1354
1699
|
}
|
|
1355
1700
|
return true;
|
|
1356
1701
|
};
|
|
@@ -1393,10 +1738,11 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
|
|
|
1393
1738
|
const condition = child(expression.condition);
|
|
1394
1739
|
const consequent = child(expression.consequent);
|
|
1395
1740
|
const alternative = child(expression.alternative);
|
|
1396
|
-
return (context) =>
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1741
|
+
return (context) => {
|
|
1742
|
+
const value = condition(context);
|
|
1743
|
+
if (typeof value !== "boolean") throw getConditionError(expression, value);
|
|
1744
|
+
return value ? consequent(context) : alternative(context);
|
|
1745
|
+
};
|
|
1400
1746
|
}
|
|
1401
1747
|
case "Case": {
|
|
1402
1748
|
const arms = expression.arms.map((arm) => ({
|
|
@@ -1421,19 +1767,18 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
|
|
|
1421
1767
|
case "Exists":
|
|
1422
1768
|
return compileQuantifier(expression, environment);
|
|
1423
1769
|
case "UnboundedQuantifier":
|
|
1424
|
-
return
|
|
1425
|
-
(
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
);
|
|
1770
|
+
return () => {
|
|
1771
|
+
throw new TlaEvaluationError(
|
|
1772
|
+
`TLC cannot evaluate an unbounded quantifier at ${describeLocation(expression)}`,
|
|
1773
|
+
);
|
|
1774
|
+
};
|
|
1430
1775
|
case "Choose":
|
|
1431
1776
|
case "SetFilter":
|
|
1432
1777
|
case "SetMap":
|
|
1433
1778
|
return compileSetComprehension(expression, environment);
|
|
1434
1779
|
case "SetEnumeration": {
|
|
1435
1780
|
const elements = expression.elements.map(child);
|
|
1436
|
-
return (context) =>
|
|
1781
|
+
return (context) => createEnumeratedSet(elements.map((element) => element(context)));
|
|
1437
1782
|
}
|
|
1438
1783
|
case "Tuple": {
|
|
1439
1784
|
const elements = expression.elements.map(child);
|
|
@@ -1450,7 +1795,7 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
|
|
|
1450
1795
|
return compileRecord(expression.fields, environment);
|
|
1451
1796
|
case "RecordSet": {
|
|
1452
1797
|
const fields = [...expression.fields].sort((left, right) =>
|
|
1453
|
-
left.name
|
|
1798
|
+
compareValues(left.name, right.name),
|
|
1454
1799
|
);
|
|
1455
1800
|
const names = fields.map((field) => field.name);
|
|
1456
1801
|
const sets = fields.map((field) => child(field.value));
|
|
@@ -1469,18 +1814,21 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
|
|
|
1469
1814
|
return binding.value;
|
|
1470
1815
|
};
|
|
1471
1816
|
case "FunctionApplication": {
|
|
1472
|
-
|
|
1817
|
+
keepLazy(expression.function);
|
|
1818
|
+
const evaluateFunction = child(expression.function);
|
|
1473
1819
|
const args = expression.args.map(child);
|
|
1474
1820
|
if (args.length === 1) {
|
|
1475
1821
|
const argument = args[0];
|
|
1476
|
-
return (context) => applyFunction(
|
|
1822
|
+
return (context) => applyFunction(evaluateFunction(context), argument(context));
|
|
1477
1823
|
}
|
|
1478
|
-
return (context) =>
|
|
1824
|
+
return (context) =>
|
|
1825
|
+
applyFunction(evaluateFunction(context), createTuple(args.map((arg) => arg(context))));
|
|
1479
1826
|
}
|
|
1480
1827
|
case "FieldAccess": {
|
|
1481
1828
|
const record = child(expression.record);
|
|
1482
1829
|
const field = expression.field;
|
|
1483
|
-
|
|
1830
|
+
const describeMissing = getFieldError(expression);
|
|
1831
|
+
return (context) => applyFunction(record(context), field, describeMissing);
|
|
1484
1832
|
}
|
|
1485
1833
|
case "Product": {
|
|
1486
1834
|
const factors = expression.factors.map(child);
|
|
@@ -1492,7 +1840,11 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
|
|
|
1492
1840
|
case "BoxAction":
|
|
1493
1841
|
case "AngleAction": {
|
|
1494
1842
|
const action = child(expression.action);
|
|
1495
|
-
const unchanged = child(
|
|
1843
|
+
const unchanged = child({
|
|
1844
|
+
kind: "Unchanged",
|
|
1845
|
+
expression: expression.subscript,
|
|
1846
|
+
location: expression.location,
|
|
1847
|
+
});
|
|
1496
1848
|
const isBox = expression.kind === "BoxAction";
|
|
1497
1849
|
return (context) =>
|
|
1498
1850
|
isBox
|
|
@@ -1506,21 +1858,19 @@ const compile = (expression: Expression, environment: Environment): Evaluator =>
|
|
|
1506
1858
|
case "Label":
|
|
1507
1859
|
return child(expression.body);
|
|
1508
1860
|
case "Subexpression":
|
|
1509
|
-
return
|
|
1510
|
-
(
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
);
|
|
1861
|
+
return () => {
|
|
1862
|
+
throw new TlaEvaluationError(
|
|
1863
|
+
`The subexpression reference at ${describeLocation(expression)} does not name a subexpression`,
|
|
1864
|
+
);
|
|
1865
|
+
};
|
|
1515
1866
|
case "Always":
|
|
1516
1867
|
case "Eventually":
|
|
1517
1868
|
case "WeakFairness":
|
|
1518
1869
|
case "StrongFairness":
|
|
1519
|
-
return
|
|
1520
|
-
(
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
);
|
|
1870
|
+
return () => {
|
|
1871
|
+
throw new TlaEvaluationError(
|
|
1872
|
+
`TLC cannot evaluate a temporal formula in a state at ${describeLocation(expression)}`,
|
|
1873
|
+
);
|
|
1874
|
+
};
|
|
1525
1875
|
}
|
|
1526
1876
|
};
|