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.
- package/README.md +81 -42
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +215 -30
- package/dist/index.d.ts +215 -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 +20 -1
- package/dist/run-cli.js +20 -1
- package/dist/transfer.cjs +388 -39
- package/dist/transfer.js +388 -39
- package/package.json +6 -2
- package/src/ast.ts +122 -5
- package/src/bags.ts +161 -71
- package/src/builtins.ts +203 -23
- package/src/bundled-modules.ts +63 -32
- package/src/check.ts +945 -238
- package/src/cli-options.ts +58 -0
- package/src/codegen.ts +52 -18
- package/src/config.ts +39 -11
- package/src/environment.ts +485 -140
- package/src/errors.ts +59 -1
- package/src/evaluate.ts +489 -137
- 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 +103 -28
- package/src/liveness.ts +47 -91
- package/src/obligation-action.ts +384 -0
- package/src/obligation-anon.ts +575 -0
- package/src/obligation-deref.ts +637 -0
- package/src/obligation-expression.ts +1060 -0
- package/src/obligation-format.ts +406 -0
- package/src/obligation-generate.ts +1074 -0
- package/src/obligation-levels.ts +329 -0
- package/src/obligation-module.ts +1042 -0
- package/src/obligation-printer.ts +889 -0
- package/src/obligation-proof.ts +76 -0
- package/src/obligation-source.ts +1084 -0
- package/src/obligation-tuples.ts +184 -0
- package/src/obligations.ts +316 -0
- package/src/parallel-worker.ts +57 -28
- package/src/parallel.ts +116 -62
- package/src/parser.ts +958 -205
- package/src/pretty-print.ts +259 -0
- package/src/randomization.ts +24 -113
- package/src/resolve.ts +472 -41
- package/src/run-cli.ts +427 -61
- 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 +26 -5
- 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
|
-
|
|
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) => {
|
|
@@ -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"
|
|
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)
|
|
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 = (
|
|
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 <=
|
|
876
|
+
export const isTlcInteger = (value: number) =>
|
|
877
|
+
Number.isInteger(value) && value <= MAXIMUM_INTEGER && value >= MINIMUM_INTEGER;
|
|
689
878
|
|
|
690
|
-
|
|
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
|
-
)
|
|
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
|
-
|
|
776
|
-
|
|
777
|
-
(operator
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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)
|
|
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
|
-
|
|
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")
|
|
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)
|
|
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
|
-
|
|
1163
|
-
|
|
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 =
|
|
1169
|
-
return
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
|
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"
|
|
1533
|
+
? { kind: "Field", name: step.name, args: [] }
|
|
1196
1534
|
: {
|
|
1197
|
-
kind: "Index"
|
|
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
|
-
|
|
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(
|
|
1221
|
-
|
|
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 =
|
|
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" &&
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
1344
|
-
(
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
|
1425
|
-
(
|
|
1426
|
-
|
|
1427
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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(
|
|
1824
|
+
return (context) => applyFunction(evaluateFunction(context), argument(context));
|
|
1477
1825
|
}
|
|
1478
|
-
return (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
|
-
|
|
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(
|
|
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
|
|
1510
|
-
(
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
|
1520
|
-
(
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
};
|