tla-ts 0.0.2 → 0.0.3-dev.763cc71

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.
@@ -0,0 +1,1060 @@
1
+ // tlapm's expression IR (E_t) with de Bruijn indices, and its substitutions (E_subst) and
2
+ // alpha-equivalence (E_eq).
3
+
4
+ import { TlaProofError } from "./errors";
5
+
6
+ export interface Locus {
7
+ file: string;
8
+ startLine: number;
9
+ startColumn: number;
10
+ endLine: number;
11
+ endColumn: number;
12
+ }
13
+
14
+ export interface ObligationWarning {
15
+ text: string;
16
+ channel: "toolbox" | "toolboxOnly" | "stderr" | "stdout";
17
+ }
18
+
19
+ export const getLocusPosition = (locus: Locus) => {
20
+ const end = locus.endColumn - 1;
21
+ if (locus.startLine !== locus.endLine)
22
+ return `line ${locus.startLine}, character ${locus.startColumn} to line ${locus.endLine}, character ${end}`;
23
+ return locus.startColumn >= end
24
+ ? `line ${locus.startLine}, character ${locus.startColumn}`
25
+ : `line ${locus.startLine}, characters ${locus.startColumn}-${end}`;
26
+ };
27
+
28
+ export const getLocusText = (locus: Locus) =>
29
+ `File ${JSON.stringify(locus.file)}, ${getLocusPosition(locus)}`;
30
+
31
+ export const getAtPrefix = (locus: Locus | undefined) =>
32
+ `${locus ? getLocusText(locus) : "<unknown location>"}:\n`;
33
+
34
+ export const getErrorNote = (locus: Locus | undefined, message: string): ObligationWarning => ({
35
+ text: `${getAtPrefix(locus)}Error: ${message}`,
36
+ channel: "stderr",
37
+ });
38
+
39
+ export const getLocusPrefix = (locus: Locus | undefined) =>
40
+ locus ? `${getLocusText(locus)} :\n` : "";
41
+
42
+ export type Builtin =
43
+ | "TRUE"
44
+ | "FALSE"
45
+ | "Implies"
46
+ | "Equiv"
47
+ | "Conj"
48
+ | "Disj"
49
+ | "Neg"
50
+ | "Eq"
51
+ | "Neq"
52
+ | "STRING"
53
+ | "BOOLEAN"
54
+ | "SUBSET"
55
+ | "UNION"
56
+ | "DOMAIN"
57
+ | "Subseteq"
58
+ | "Mem"
59
+ | "Notmem"
60
+ | "Setminus"
61
+ | "Cap"
62
+ | "Cup"
63
+ | "Prime"
64
+ | "StrongPrime"
65
+ | "Leadsto"
66
+ | "ENABLED"
67
+ | "UNCHANGED"
68
+ | "Cdot"
69
+ | "Actplus"
70
+ | "BoxTrue"
71
+ | "BoxFalse"
72
+ | "Diamond"
73
+ | "Nat"
74
+ | "Int"
75
+ | "Real"
76
+ | "Plus"
77
+ | "Minus"
78
+ | "Uminus"
79
+ | "Times"
80
+ | "Ratio"
81
+ | "Quotient"
82
+ | "Remainder"
83
+ | "Exp"
84
+ | "Infinity"
85
+ | "Lteq"
86
+ | "Lt"
87
+ | "Gteq"
88
+ | "Gt"
89
+ | "Divides"
90
+ | "Range"
91
+ | "Seq"
92
+ | "Len"
93
+ | "BSeq"
94
+ | "Cat"
95
+ | "Append"
96
+ | "Head"
97
+ | "Tail"
98
+ | "SubSeq"
99
+ | "SelectSeq"
100
+ | "OneArg"
101
+ | "Extend"
102
+ | "Print"
103
+ | "PrintT"
104
+ | "Assert"
105
+ | "JavaTime"
106
+ | "TLCGet"
107
+ | "TLCSet"
108
+ | "Permutations"
109
+ | "SortSeq"
110
+ | "RandomElement"
111
+ | "Any"
112
+ | "ToString"
113
+ | "Unprimable"
114
+ | "Irregular";
115
+
116
+ export type Shape = number;
117
+
118
+ export type Level = "Constant" | "State" | "Action" | "Temporal" | "Unknown";
119
+
120
+ export type BoundDomain =
121
+ | { kind: "NoDomain" }
122
+ | { kind: "Domain"; expression: Expr }
123
+ | { kind: "Ditto" };
124
+
125
+ export interface Bound {
126
+ name: string;
127
+ level: Level;
128
+ domain: BoundDomain;
129
+ }
130
+
131
+ export type Selector =
132
+ | { kind: "Down" }
133
+ | { kind: "Number"; value: number }
134
+ | { kind: "Left" }
135
+ | { kind: "Right" }
136
+ | { kind: "At" }
137
+ | { kind: "Instance"; args: Expr[] }
138
+ | { kind: "Label"; label: string; args: Expr[] };
139
+
140
+ export type ExceptPoint = { kind: "Dot"; field: string } | { kind: "Apply"; expression: Expr };
141
+
142
+ interface ExceptSpec {
143
+ trail: ExceptPoint[];
144
+ value: Expr;
145
+ }
146
+
147
+ export type ParenForm =
148
+ | { kind: "Syntax" }
149
+ | { kind: "Nlabel"; label: string; names: string[] }
150
+ | { kind: "Xlabel"; label: string; indices: { name: string; index: number }[] };
151
+
152
+ export interface Param {
153
+ name: string;
154
+ shape: Shape;
155
+ location?: Locus;
156
+ }
157
+
158
+ // isAtomic is Expr.Deref.atomic: a resolved subexpression reference, which has no subexpressions.
159
+ interface ExprProps {
160
+ location?: Locus;
161
+ isAtomic?: boolean;
162
+ }
163
+
164
+ export type Expr = ExprCore & ExprProps;
165
+
166
+ export type ExprCore =
167
+ | { kind: "Ix"; index: number }
168
+ | { kind: "Opaque"; name: string }
169
+ | { kind: "Internal"; builtin: Builtin }
170
+ | { kind: "Lambda"; params: Param[]; body: Expr }
171
+ | { kind: "Sequent"; sequent: Sequent }
172
+ | { kind: "Bang"; expression: Expr; selectors: Selector[] }
173
+ | { kind: "Apply"; operator: Expr; args: Expr[] }
174
+ | { kind: "With"; expression: Expr; method: string }
175
+ | { kind: "If"; condition: Expr; consequent: Expr; alternative: Expr }
176
+ | { kind: "List"; bullet: "And" | "Or" | "Refs"; items: Expr[] }
177
+ | { kind: "Let"; definitions: Defn[]; body: Expr }
178
+ | { kind: "Quant"; quantifier: "Forall" | "Exists"; bounds: Bound[]; body: Expr }
179
+ | { kind: "Tquant"; quantifier: "Forall" | "Exists"; names: string[]; body: Expr }
180
+ | { kind: "Choose"; name: string; domain: Expr | undefined; body: Expr }
181
+ | { kind: "SetSt"; name: string; domain: Expr; body: Expr }
182
+ | { kind: "SetOf"; element: Expr; bounds: Bound[] }
183
+ | { kind: "SetEnum"; elements: Expr[] }
184
+ | { kind: "Product"; factors: Expr[] }
185
+ | { kind: "Tuple"; elements: Expr[] }
186
+ | { kind: "Fcn"; bounds: Bound[]; body: Expr }
187
+ | { kind: "FcnApp"; function: Expr; args: Expr[] }
188
+ | { kind: "Arrow"; domain: Expr; range: Expr }
189
+ | { kind: "Rect"; fields: Field[] }
190
+ | { kind: "Record"; fields: Field[] }
191
+ | { kind: "Except"; function: Expr; specs: ExceptSpec[] }
192
+ | { kind: "Dot"; expression: Expr; field: string }
193
+ | { kind: "Sub"; modal: "Box" | "Dia"; action: Expr; subscript: Expr }
194
+ | { kind: "Tsub"; modal: "Box" | "Dia"; action: Expr; subscript: Expr }
195
+ | { kind: "Fair"; fairness: "Weak" | "Strong"; subscript: Expr; action: Expr }
196
+ | { kind: "Case"; arms: { condition: Expr; body: Expr }[]; other: Expr | undefined }
197
+ | { kind: "String"; value: string }
198
+ | { kind: "Num"; integer: string; fraction: string }
199
+ | { kind: "At"; isExcept: boolean }
200
+ | { kind: "Parens"; expression: Expr; form: ParenForm };
201
+
202
+ interface Field {
203
+ name: string;
204
+ value: Expr;
205
+ }
206
+
207
+ export interface InstanceDefinition {
208
+ args: string[];
209
+ module: string;
210
+ substitutions: { name: string; expression: Expr }[];
211
+ location?: Locus;
212
+ }
213
+
214
+ export type Defn = DefnCore & { location?: Locus };
215
+
216
+ type DefnCore =
217
+ | { kind: "Recursive"; name: string; shape: Shape }
218
+ | { kind: "Operator"; name: string; body: Expr }
219
+ | { kind: "Instance"; name: string; instance: InstanceDefinition }
220
+ | { kind: "Bpragma"; name: string; body: Expr };
221
+
222
+ export type WhereDefined = "Builtin" | "Proof" | "User";
223
+
224
+ export type Visibility = "Visible" | "Hidden";
225
+
226
+ export type Hyp = HypCore & { location?: Locus };
227
+
228
+ type HypCore =
229
+ | {
230
+ kind: "Fresh";
231
+ name: string;
232
+ shape: Shape;
233
+ level: Level;
234
+ domain: Expr | undefined;
235
+ domainVisibility: Visibility;
236
+ }
237
+ | { kind: "Flex"; name: string }
238
+ | {
239
+ kind: "Defn";
240
+ definition: Defn;
241
+ whereDefined: WhereDefined;
242
+ visibility: Visibility;
243
+ isLocal: boolean;
244
+ }
245
+ | { kind: "Fact"; expression: Expr; visibility: Visibility };
246
+
247
+ export interface Sequent {
248
+ context: Hyp[];
249
+ active: Expr;
250
+ }
251
+
252
+ export type Substitution =
253
+ | { kind: "Shift"; amount: number }
254
+ | { kind: "Cons"; expression: Expr; rest: Substitution }
255
+ | { kind: "Compose"; outer: Substitution; inner: Substitution }
256
+ | { kind: "Bump"; amount: number; rest: Substitution };
257
+
258
+ export const shift = (amount: number): Substitution => ({ kind: "Shift", amount });
259
+
260
+ export const consSubstitution = (expression: Expr, rest: Substitution): Substitution => ({
261
+ kind: "Cons",
262
+ expression,
263
+ rest,
264
+ });
265
+
266
+ // Binds the expressions in order, so the last one becomes index 1.
267
+ export const consAll = (expressions: Expr[], rest: Substitution) =>
268
+ expressions.reduce((inner, expression) => consSubstitution(expression, inner), rest);
269
+
270
+ export const bumpBy = (amount: number, substitution: Substitution): Substitution =>
271
+ substitution.kind === "Bump"
272
+ ? { kind: "Bump", amount: substitution.amount + amount, rest: substitution.rest }
273
+ : { kind: "Bump", amount, rest: substitution };
274
+
275
+ // tlapm's core @@ from, which takes the properties of from.
276
+ export const at = (core: ExprCore, from: ExprProps): Expr => {
277
+ const located = from.location ? { ...core, location: from.location } : core;
278
+ return from.isAtomic ? { ...located, isAtomic: true } : located;
279
+ };
280
+
281
+ export const getHypName = (hyp: Hyp) => {
282
+ switch (hyp.kind) {
283
+ case "Fresh":
284
+ case "Flex":
285
+ return hyp.name;
286
+ case "Defn":
287
+ return hyp.definition.name;
288
+ case "Fact":
289
+ return undefined;
290
+ }
291
+ };
292
+
293
+ export const applyBuiltin = (builtin: Builtin, args: Expr[], from: ExprProps): Expr =>
294
+ at({ kind: "Apply", operator: at({ kind: "Internal", builtin }, from), args }, from);
295
+
296
+ export const exprifySequent = (sequent: Sequent): ExprCore =>
297
+ sequent.context.length === 0 ? sequent.active : { kind: "Sequent", sequent };
298
+
299
+ // A persistent context, newest hypothesis first, so binders extend it without copying.
300
+ export interface Scope {
301
+ hyp: Hyp;
302
+ rest: Scope | undefined;
303
+ size: number;
304
+ }
305
+
306
+ export const adjoin = (scope: Scope | undefined, hyp: Hyp): Scope => ({
307
+ hyp,
308
+ rest: scope,
309
+ size: (scope?.size ?? 0) + 1,
310
+ });
311
+
312
+ export const adjoinAll = (scope: Scope | undefined, hyps: Hyp[]) =>
313
+ hyps.reduce<Scope | undefined>(adjoin, scope);
314
+
315
+ export const getScopeHyps = (scope: Scope | undefined): Hyp[] => {
316
+ const hyps: Hyp[] = [];
317
+ for (let current = scope; current; current = current.rest) hyps.push(current.hyp);
318
+ return hyps.reverse();
319
+ };
320
+
321
+ export const findNamed = (scope: Scope | undefined, name: string) => {
322
+ let depth = 0;
323
+ for (let current = scope; current; current = current.rest) {
324
+ if (getHypName(current.hyp) === name) return { depth, hyp: current.hyp };
325
+ depth++;
326
+ }
327
+ return undefined;
328
+ };
329
+
330
+ export const getScopeAt = (scope: Scope | undefined, depth: number) => {
331
+ let current = scope;
332
+ for (let index = 0; index < depth && current; index++) current = current.rest;
333
+ return current;
334
+ };
335
+
336
+ // Maps the items in order, threading a state from each item to the next.
337
+ export const mapAccumulate = <State, Item, Result>(
338
+ state: State,
339
+ items: Item[],
340
+ step: (state: State, item: Item) => [State, Result],
341
+ ): [State, Result[]] => {
342
+ const results: Result[] = [];
343
+ const final = items.reduce((current, item) => {
344
+ const [next, result] = step(current, item);
345
+ results.push(result);
346
+ return next;
347
+ }, state);
348
+ return [final, results];
349
+ };
350
+
351
+ export const getFresh = (name: string, level: Level, shape = 0): Hyp => ({
352
+ kind: "Fresh",
353
+ name,
354
+ shape,
355
+ level,
356
+ domain: undefined,
357
+ domainVisibility: "Visible",
358
+ });
359
+
360
+ export const getBoundedFresh = (name: string, domain: Expr): Hyp => ({
361
+ kind: "Fresh",
362
+ name,
363
+ shape: 0,
364
+ level: "Constant",
365
+ domain,
366
+ domainVisibility: "Visible",
367
+ });
368
+
369
+ const applyIndex = (substitution: Substitution, index: number, from: Expr): Expr => {
370
+ switch (substitution.kind) {
371
+ case "Shift":
372
+ return at({ kind: "Ix", index: substitution.amount + index }, from);
373
+ case "Cons":
374
+ return index === 1
375
+ ? at(stripProps(substitution.expression), from)
376
+ : applyIndex(substitution.rest, index - 1, from);
377
+ case "Compose":
378
+ return substituteExpr(substitution.outer, applyIndex(substitution.inner, index, from));
379
+ case "Bump":
380
+ return index <= substitution.amount
381
+ ? at({ kind: "Ix", index }, from)
382
+ : substituteExpr(
383
+ shift(substitution.amount),
384
+ applyIndex(substitution.rest, index - substitution.amount, from),
385
+ );
386
+ }
387
+ };
388
+
389
+ export const substituteExprs = (substitution: Substitution, expressions: Expr[]) =>
390
+ expressions.map((expression) => substituteExpr(substitution, expression));
391
+
392
+ // Beta-reduces a lambda applied to arguments, as E_subst.normalize does.
393
+ export const normalizeApply = (operator: Expr, args: Expr[]): ExprCore => {
394
+ if (operator.kind === "Lambda") {
395
+ if (operator.params.length !== args.length) throw new TlaProofError("Arity mismatch");
396
+ return stripProps(substituteExpr(consAll(args, shift(0)), operator.body));
397
+ }
398
+ if (operator.kind === "Apply") {
399
+ return { kind: "Apply", operator: operator.operator, args: [...operator.args, ...args] };
400
+ }
401
+ return args.length === 0 ? stripProps(operator) : { kind: "Apply", operator, args };
402
+ };
403
+
404
+ // The node without its properties, as tlapm's e.core.
405
+ export const stripProps = (expression: Expr): ExprCore => {
406
+ if (!expression.location && !expression.isAtomic) return expression;
407
+ const { location: _location, isAtomic: _isAtomic, ...core } = expression;
408
+ return core;
409
+ };
410
+
411
+ export const substituteBounds = (
412
+ substitution: Substitution,
413
+ bounds: Bound[],
414
+ ): [Substitution, Bound[]] => [
415
+ bumpBy(bounds.length, substitution),
416
+ mapBoundDomains(bounds, (domain) => substituteExpr(substitution, domain)),
417
+ ];
418
+
419
+ export const substituteDefn = (substitution: Substitution, definition: Defn): Defn => {
420
+ switch (definition.kind) {
421
+ case "Recursive":
422
+ case "Operator":
423
+ case "Bpragma":
424
+ return mapDefnBody(definition, (body) => substituteExpr(substitution, body));
425
+ case "Instance": {
426
+ const inner = bumpBy(definition.instance.args.length, substitution);
427
+ return {
428
+ ...definition,
429
+ instance: {
430
+ ...definition.instance,
431
+ substitutions: definition.instance.substitutions.map((entry) => ({
432
+ name: entry.name,
433
+ expression: substituteExpr(inner, entry.expression),
434
+ })),
435
+ },
436
+ };
437
+ }
438
+ }
439
+ };
440
+
441
+ export const substituteHyp = (substitution: Substitution, hyp: Hyp): Hyp => {
442
+ switch (hyp.kind) {
443
+ case "Fresh":
444
+ return hyp.domain ? { ...hyp, domain: substituteExpr(substitution, hyp.domain) } : hyp;
445
+ case "Flex":
446
+ return hyp;
447
+ case "Defn":
448
+ return { ...hyp, definition: substituteDefn(substitution, hyp.definition) };
449
+ case "Fact":
450
+ return { ...hyp, expression: substituteExpr(substitution, hyp.expression) };
451
+ }
452
+ };
453
+
454
+ export const substituteSequent = (substitution: Substitution, sequent: Sequent): Sequent => {
455
+ const [inner, context] = mapAccumulate(substitution, sequent.context, (current, hyp) => [
456
+ bumpBy(1, current),
457
+ substituteHyp(current, hyp),
458
+ ]);
459
+ return { context, active: substituteExpr(inner, sequent.active) };
460
+ };
461
+
462
+ export const substituteExpr = (substitution: Substitution, expression: Expr): Expr => {
463
+ const recurse = (inner: Expr) => substituteExpr(substitution, inner);
464
+ switch (expression.kind) {
465
+ case "Internal":
466
+ case "Opaque":
467
+ case "String":
468
+ case "Num":
469
+ case "At":
470
+ return expression;
471
+ case "Ix":
472
+ return applyIndex(substitution, expression.index, expression);
473
+ case "Apply":
474
+ return at(
475
+ normalizeApply(
476
+ recurse(expression.operator),
477
+ substituteExprs(substitution, expression.args),
478
+ ),
479
+ expression,
480
+ );
481
+ case "Bang":
482
+ return {
483
+ ...expression,
484
+ expression: recurse(expression.expression),
485
+ selectors: expression.selectors.map((selector) => mapSelectorArgs(selector, recurse)),
486
+ };
487
+ case "Lambda":
488
+ return {
489
+ ...expression,
490
+ body: substituteExpr(bumpBy(expression.params.length, substitution), expression.body),
491
+ };
492
+ case "Let": {
493
+ const [inner, definitions] = mapAccumulate(
494
+ substitution,
495
+ expression.definitions,
496
+ (current, definition) => [bumpBy(1, current), substituteDefn(current, definition)],
497
+ );
498
+ return { ...expression, definitions, body: substituteExpr(inner, expression.body) };
499
+ }
500
+ case "Sequent":
501
+ return { ...expression, sequent: substituteSequent(substitution, expression.sequent) };
502
+ case "With":
503
+ return { ...expression, expression: recurse(expression.expression) };
504
+ case "If":
505
+ return {
506
+ ...expression,
507
+ condition: recurse(expression.condition),
508
+ consequent: recurse(expression.consequent),
509
+ alternative: recurse(expression.alternative),
510
+ };
511
+ case "List":
512
+ return { ...expression, items: substituteExprs(substitution, expression.items) };
513
+ case "Quant": {
514
+ const [inner, bounds] = substituteBounds(substitution, expression.bounds);
515
+ return { ...expression, bounds, body: substituteExpr(inner, expression.body) };
516
+ }
517
+ case "Tquant":
518
+ return {
519
+ ...expression,
520
+ body: substituteExpr(bumpBy(expression.names.length, substitution), expression.body),
521
+ };
522
+ case "Choose":
523
+ return {
524
+ ...expression,
525
+ domain: expression.domain && recurse(expression.domain),
526
+ body: substituteExpr(bumpBy(1, substitution), expression.body),
527
+ };
528
+ case "SetSt":
529
+ return {
530
+ ...expression,
531
+ domain: recurse(expression.domain),
532
+ body: substituteExpr(bumpBy(1, substitution), expression.body),
533
+ };
534
+ case "SetOf": {
535
+ const [inner, bounds] = substituteBounds(substitution, expression.bounds);
536
+ return { ...expression, bounds, element: substituteExpr(inner, expression.element) };
537
+ }
538
+ case "SetEnum":
539
+ return { ...expression, elements: substituteExprs(substitution, expression.elements) };
540
+ case "Arrow":
541
+ return {
542
+ ...expression,
543
+ domain: recurse(expression.domain),
544
+ range: recurse(expression.range),
545
+ };
546
+ case "Fcn": {
547
+ const [inner, bounds] = substituteBounds(substitution, expression.bounds);
548
+ return { ...expression, bounds, body: substituteExpr(inner, expression.body) };
549
+ }
550
+ case "FcnApp":
551
+ return {
552
+ ...expression,
553
+ function: recurse(expression.function),
554
+ args: substituteExprs(substitution, expression.args),
555
+ };
556
+ case "Product":
557
+ return { ...expression, factors: substituteExprs(substitution, expression.factors) };
558
+ case "Tuple":
559
+ return { ...expression, elements: substituteExprs(substitution, expression.elements) };
560
+ case "Rect":
561
+ case "Record":
562
+ return {
563
+ ...expression,
564
+ fields: expression.fields.map((field) => ({
565
+ name: field.name,
566
+ value: recurse(field.value),
567
+ })),
568
+ };
569
+ case "Except":
570
+ return {
571
+ ...expression,
572
+ function: recurse(expression.function),
573
+ specs: mapExceptSpecs(expression.specs, recurse),
574
+ };
575
+ case "Dot":
576
+ return { ...expression, expression: recurse(expression.expression) };
577
+ case "Sub":
578
+ case "Tsub":
579
+ return {
580
+ ...expression,
581
+ action: recurse(expression.action),
582
+ subscript: recurse(expression.subscript),
583
+ };
584
+ case "Fair":
585
+ return {
586
+ ...expression,
587
+ subscript: recurse(expression.subscript),
588
+ action: recurse(expression.action),
589
+ };
590
+ case "Case":
591
+ return {
592
+ ...expression,
593
+ arms: expression.arms.map((arm) => ({
594
+ condition: recurse(arm.condition),
595
+ body: recurse(arm.body),
596
+ })),
597
+ other: expression.other && recurse(expression.other),
598
+ };
599
+ case "Parens":
600
+ return { ...expression, expression: recurse(expression.expression) };
601
+ }
602
+ };
603
+
604
+ export const shiftExpr = (amount: number, expression: Expr) =>
605
+ amount === 0 ? expression : substituteExpr(shift(amount), expression);
606
+
607
+ const isEqualList = <Item>(
608
+ left: Item[],
609
+ right: Item[],
610
+ isEqual: (left: Item, right: Item) => boolean,
611
+ ) => left.length === right.length && left.every((item, index) => isEqual(item, right[index]));
612
+
613
+ const isEqualFields = (left: Field[], right: Field[]) => {
614
+ const leftMap = new Map(left.map((field) => [field.name, field.value]));
615
+ const rightMap = new Map(right.map((field) => [field.name, field.value]));
616
+ if (leftMap.size !== rightMap.size) return false;
617
+ for (const [name, value] of leftMap) {
618
+ const other = rightMap.get(name);
619
+ if (!other || !isEqualExpr(value, other)) return false;
620
+ }
621
+ return true;
622
+ };
623
+
624
+ const isEqualBound = (left: Bound, right: Bound) =>
625
+ left.level === right.level &&
626
+ left.domain.kind === right.domain.kind &&
627
+ (left.domain.kind !== "Domain" ||
628
+ (right.domain.kind === "Domain" &&
629
+ isEqualExpr(left.domain.expression, right.domain.expression)));
630
+
631
+ const isEqualSelector = (left: Selector, right: Selector) => {
632
+ if (left.kind !== right.kind) return false;
633
+ switch (left.kind) {
634
+ case "Number":
635
+ return right.kind === "Number" && left.value === right.value;
636
+ case "Instance":
637
+ return right.kind === "Instance" && isEqualList(left.args, right.args, isEqualExpr);
638
+ case "Label":
639
+ return (
640
+ right.kind === "Label" &&
641
+ left.label === right.label &&
642
+ isEqualList(left.args, right.args, isEqualExpr)
643
+ );
644
+ default:
645
+ return true;
646
+ }
647
+ };
648
+
649
+ const isEqualDefn = (left: Defn, right: Defn): boolean => {
650
+ if (left.kind === "Operator" && right.kind === "Operator")
651
+ return isEqualExpr(left.body, right.body);
652
+ if (left.kind === "Bpragma" && right.kind === "Bpragma")
653
+ return isEqualExpr(left.body, right.body);
654
+ if (left.kind === "Instance" && right.kind === "Instance") {
655
+ const leftSubstitutions = left.instance.substitutions.map((entry): Field => ({
656
+ name: entry.name,
657
+ value: entry.expression,
658
+ }));
659
+ const rightSubstitutions = right.instance.substitutions.map((entry): Field => ({
660
+ name: entry.name,
661
+ value: entry.expression,
662
+ }));
663
+ return (
664
+ isEqualList(
665
+ left.instance.args,
666
+ right.instance.args,
667
+ (leftArg, rightArg) => leftArg === rightArg,
668
+ ) &&
669
+ left.instance.module === right.instance.module &&
670
+ isEqualFields(leftSubstitutions, rightSubstitutions)
671
+ );
672
+ }
673
+ return false;
674
+ };
675
+
676
+ export const isEqualHyp = (left: Hyp, right: Hyp): boolean => {
677
+ if (left.kind === "Fresh" && right.kind === "Fresh") {
678
+ if (left.level !== right.level || left.shape !== right.shape) return false;
679
+ if (!left.domain || !right.domain) return !left.domain && !right.domain;
680
+ return isEqualExpr(left.domain, right.domain);
681
+ }
682
+ if (left.kind === "Flex" && right.kind === "Flex") return true;
683
+ if (left.kind === "Defn" && right.kind === "Defn") {
684
+ return isEqualDefn(left.definition, right.definition) && left.isLocal === right.isLocal;
685
+ }
686
+ if (left.kind === "Fact" && right.kind === "Fact") {
687
+ return isEqualExpr(left.expression, right.expression);
688
+ }
689
+ return false;
690
+ };
691
+
692
+ const isEqualSequent = (left: Sequent, right: Sequent) =>
693
+ isEqualList(left.context, right.context, isEqualHyp) && isEqualExpr(left.active, right.active);
694
+
695
+ // Alpha-equivalence, including E_eq's quirk of comparing only the first CASE arm.
696
+ export const isEqualExpr = (left: Expr, right: Expr): boolean => {
697
+ if (left.kind === "Parens") return isEqualExpr(left.expression, right);
698
+ if (right.kind === "Parens") return isEqualExpr(left, right.expression);
699
+ switch (left.kind) {
700
+ case "Ix":
701
+ return right.kind === "Ix" && left.index === right.index;
702
+ case "Opaque":
703
+ return right.kind === "Opaque" && left.name === right.name;
704
+ case "Internal":
705
+ return right.kind === "Internal" && left.builtin === right.builtin;
706
+ case "Lambda":
707
+ return (
708
+ right.kind === "Lambda" &&
709
+ left.params.length === right.params.length &&
710
+ isEqualExpr(left.body, right.body)
711
+ );
712
+ case "Apply":
713
+ return (
714
+ right.kind === "Apply" &&
715
+ isEqualExpr(left.operator, right.operator) &&
716
+ isEqualList(left.args, right.args, isEqualExpr)
717
+ );
718
+ case "Sequent":
719
+ return right.kind === "Sequent" && isEqualSequent(left.sequent, right.sequent);
720
+ case "Bang":
721
+ return (
722
+ right.kind === "Bang" &&
723
+ isEqualExpr(left.expression, right.expression) &&
724
+ isEqualList(left.selectors, right.selectors, isEqualSelector)
725
+ );
726
+ case "With":
727
+ return right.kind === "With" && isEqualExpr(left.expression, right.expression);
728
+ case "Let":
729
+ return (
730
+ right.kind === "Let" &&
731
+ isEqualList(left.definitions, right.definitions, isEqualDefn) &&
732
+ isEqualExpr(left.body, right.body)
733
+ );
734
+ case "If":
735
+ return (
736
+ right.kind === "If" &&
737
+ isEqualExpr(left.condition, right.condition) &&
738
+ isEqualExpr(left.consequent, right.consequent) &&
739
+ isEqualExpr(left.alternative, right.alternative)
740
+ );
741
+ case "List":
742
+ return (
743
+ right.kind === "List" &&
744
+ left.bullet === right.bullet &&
745
+ isEqualList(left.items, right.items, isEqualExpr)
746
+ );
747
+ case "Quant":
748
+ return (
749
+ right.kind === "Quant" &&
750
+ left.quantifier === right.quantifier &&
751
+ isEqualList(left.bounds, right.bounds, isEqualBound) &&
752
+ isEqualExpr(left.body, right.body)
753
+ );
754
+ case "Tquant":
755
+ return (
756
+ right.kind === "Tquant" &&
757
+ left.quantifier === right.quantifier &&
758
+ isEqualList(left.names, right.names, (leftName, rightName) => leftName === rightName) &&
759
+ isEqualExpr(left.body, right.body)
760
+ );
761
+ case "Choose":
762
+ if (right.kind !== "Choose") return false;
763
+ if (left.domain && right.domain) {
764
+ return isEqualExpr(left.domain, right.domain) && isEqualExpr(left.body, right.body);
765
+ }
766
+ return !left.domain && !right.domain && isEqualExpr(left.body, right.body);
767
+ case "SetSt":
768
+ return (
769
+ right.kind === "SetSt" &&
770
+ isEqualExpr(left.domain, right.domain) &&
771
+ isEqualExpr(left.body, right.body)
772
+ );
773
+ case "SetOf":
774
+ return (
775
+ right.kind === "SetOf" &&
776
+ isEqualExpr(left.element, right.element) &&
777
+ isEqualList(left.bounds, right.bounds, isEqualBound)
778
+ );
779
+ case "SetEnum":
780
+ return right.kind === "SetEnum" && isEqualList(left.elements, right.elements, isEqualExpr);
781
+ case "Arrow":
782
+ return (
783
+ right.kind === "Arrow" &&
784
+ isEqualExpr(left.domain, right.domain) &&
785
+ isEqualExpr(left.range, right.range)
786
+ );
787
+ case "Fcn":
788
+ return (
789
+ right.kind === "Fcn" &&
790
+ isEqualList(left.bounds, right.bounds, isEqualBound) &&
791
+ isEqualExpr(left.body, right.body)
792
+ );
793
+ case "FcnApp":
794
+ return (
795
+ right.kind === "FcnApp" &&
796
+ isEqualList([left.function, ...left.args], [right.function, ...right.args], isEqualExpr)
797
+ );
798
+ case "Tuple":
799
+ return right.kind === "Tuple" && isEqualList(left.elements, right.elements, isEqualExpr);
800
+ case "Product":
801
+ return right.kind === "Product" && isEqualList(left.factors, right.factors, isEqualExpr);
802
+ case "Rect":
803
+ return right.kind === "Rect" && isEqualFields(left.fields, right.fields);
804
+ case "Record":
805
+ return right.kind === "Record" && isEqualFields(left.fields, right.fields);
806
+ case "Except":
807
+ return (
808
+ right.kind === "Except" &&
809
+ isEqualExpr(left.function, right.function) &&
810
+ isEqualList(
811
+ left.specs,
812
+ right.specs,
813
+ (leftSpec, rightSpec) =>
814
+ isEqualList(leftSpec.trail, rightSpec.trail, (leftPoint, rightPoint) =>
815
+ leftPoint.kind === "Dot"
816
+ ? rightPoint.kind === "Dot" && leftPoint.field === rightPoint.field
817
+ : rightPoint.kind === "Apply" &&
818
+ isEqualExpr(leftPoint.expression, rightPoint.expression),
819
+ ) && isEqualExpr(leftSpec.value, rightSpec.value),
820
+ )
821
+ );
822
+ case "Dot":
823
+ return (
824
+ right.kind === "Dot" &&
825
+ isEqualExpr(left.expression, right.expression) &&
826
+ left.field === right.field
827
+ );
828
+ case "Sub":
829
+ case "Tsub":
830
+ return (
831
+ right.kind === left.kind &&
832
+ left.modal === right.modal &&
833
+ isEqualExpr(left.action, right.action) &&
834
+ isEqualExpr(left.subscript, right.subscript)
835
+ );
836
+ case "Fair":
837
+ return (
838
+ right.kind === "Fair" &&
839
+ left.fairness === right.fairness &&
840
+ isEqualExpr(left.subscript, right.subscript) &&
841
+ isEqualExpr(left.action, right.action)
842
+ );
843
+ case "Case": {
844
+ if (right.kind !== "Case") return false;
845
+ const isArmsEqual =
846
+ left.arms.length === 0 || right.arms.length === 0
847
+ ? left.arms.length === right.arms.length
848
+ : isEqualExpr(left.arms[0].condition, right.arms[0].condition) &&
849
+ isEqualExpr(left.arms[0].body, right.arms[0].body);
850
+ if (!isArmsEqual) return false;
851
+ if (left.other && right.other) return isEqualExpr(left.other, right.other);
852
+ return !left.other && !right.other;
853
+ }
854
+ case "String":
855
+ return right.kind === "String" && left.value === right.value;
856
+ case "Num":
857
+ return (
858
+ right.kind === "Num" && left.integer === right.integer && left.fraction === right.fraction
859
+ );
860
+ case "At":
861
+ return right.kind === "At" && left.isExcept === right.isExcept;
862
+ }
863
+ };
864
+
865
+ export const mapBoundDomains = (bounds: Bound[], map: (domain: Expr) => Expr): Bound[] =>
866
+ bounds.map((bound) =>
867
+ bound.domain.kind === "Domain"
868
+ ? { ...bound, domain: { kind: "Domain", expression: map(bound.domain.expression) } }
869
+ : bound,
870
+ );
871
+
872
+ export const mapSelectorArgs = (selector: Selector, map: (arg: Expr) => Expr): Selector =>
873
+ selector.kind === "Instance" || selector.kind === "Label"
874
+ ? { ...selector, args: selector.args.map(map) }
875
+ : selector;
876
+
877
+ export const mapExceptSpecs = (specs: ExceptSpec[], map: (inner: Expr) => Expr): ExceptSpec[] =>
878
+ specs.map((spec) => {
879
+ const value = map(spec.value);
880
+ const trail = spec.trail.map((point): ExceptPoint =>
881
+ point.kind === "Apply" ? { kind: "Apply", expression: map(point.expression) } : point,
882
+ );
883
+ return { trail, value };
884
+ });
885
+
886
+ export const mapDefnBody = (definition: Defn, map: (body: Expr) => Expr): Defn =>
887
+ definition.kind === "Operator" || definition.kind === "Bpragma"
888
+ ? { ...definition, body: map(definition.body) }
889
+ : definition;
890
+
891
+ const mapHyp = (hyp: Hyp, map: (inner: Expr) => Expr, hyps: "visible" | "all"): Hyp => {
892
+ if (hyp.kind === "Fresh") return hyp.domain ? { ...hyp, domain: map(hyp.domain) } : hyp;
893
+ if (hyp.kind === "Flex" || (hyps === "visible" && hyp.visibility === "Hidden")) return hyp;
894
+ if (hyp.kind === "Defn") return { ...hyp, definition: mapDefnBody(hyp.definition, map) };
895
+ return { ...hyp, expression: map(hyp.expression) };
896
+ };
897
+
898
+ // Visit.map, threading a state through each binder: a scope or a substitution. Visit.map_visible_hyp
899
+ // binds hidden hypotheses without visiting them, and Visit.map visits them too. Children map in
900
+ // OCaml evaluation order, last constructor argument first, which decides the error tlapm reports.
901
+ export const mapScoped = <State>(
902
+ state: State,
903
+ expression: Expr,
904
+ map: (state: State, inner: Expr) => Expr,
905
+ bind: (state: State, hyp: Hyp) => State,
906
+ hyps: "visible" | "all" = "visible",
907
+ ): Expr => {
908
+ const recurse = (inner: Expr) => map(state, inner);
909
+ const bindBounds = (bounds: Bound[]) =>
910
+ bounds.reduce((current, bound) => bind(current, getFresh(bound.name, bound.level)), state);
911
+ switch (expression.kind) {
912
+ case "Ix":
913
+ case "Internal":
914
+ case "Opaque":
915
+ case "String":
916
+ case "Num":
917
+ case "At":
918
+ return expression;
919
+ case "Lambda": {
920
+ const inner = expression.params.reduce((current, param) => {
921
+ const fresh = getFresh(param.name, "Unknown", param.shape);
922
+ return bind(current, param.location ? { ...fresh, location: param.location } : fresh);
923
+ }, state);
924
+ return { ...expression, body: map(inner, expression.body) };
925
+ }
926
+ case "Sequent": {
927
+ const [inner, context] = mapAccumulate(state, expression.sequent.context, (current, hyp) => {
928
+ const mapped = mapHyp(hyp, (child) => map(current, child), hyps);
929
+ return [bind(current, mapped), mapped];
930
+ });
931
+ return { ...expression, sequent: { context, active: map(inner, expression.sequent.active) } };
932
+ }
933
+ case "Let": {
934
+ const [inner, definitions] = mapAccumulate(
935
+ state,
936
+ expression.definitions,
937
+ (current, definition) => {
938
+ const mapped = mapDefnBody(definition, (body) => map(current, body));
939
+ const hyp: Hyp = {
940
+ kind: "Defn",
941
+ definition: mapped,
942
+ whereDefined: "User",
943
+ visibility: "Visible",
944
+ isLocal: true,
945
+ };
946
+ return [bind(current, hyp), mapped];
947
+ },
948
+ );
949
+ return { ...expression, definitions, body: map(inner, expression.body) };
950
+ }
951
+ case "Quant":
952
+ case "Fcn": {
953
+ const bounds = mapBoundDomains(expression.bounds, recurse);
954
+ return { ...expression, bounds, body: map(bindBounds(bounds), expression.body) };
955
+ }
956
+ case "SetOf": {
957
+ const bounds = mapBoundDomains(expression.bounds, recurse);
958
+ return { ...expression, bounds, element: map(bindBounds(bounds), expression.element) };
959
+ }
960
+ case "Tquant": {
961
+ const inner = expression.names.reduce(
962
+ (current, name) => bind(current, { kind: "Flex", name }),
963
+ state,
964
+ );
965
+ return { ...expression, body: map(inner, expression.body) };
966
+ }
967
+ case "Choose": {
968
+ const domain = expression.domain && recurse(expression.domain);
969
+ const hyp = domain
970
+ ? getBoundedFresh(expression.name, domain)
971
+ : getFresh(expression.name, "Constant");
972
+ return { ...expression, domain, body: map(bind(state, hyp), expression.body) };
973
+ }
974
+ case "SetSt": {
975
+ const domain = recurse(expression.domain);
976
+ const hyp = getBoundedFresh(expression.name, domain);
977
+ return { ...expression, domain, body: map(bind(state, hyp), expression.body) };
978
+ }
979
+ case "Bang": {
980
+ const selectors = expression.selectors.map((selector) => mapSelectorArgs(selector, recurse));
981
+ return { ...expression, expression: recurse(expression.expression), selectors };
982
+ }
983
+ case "Apply": {
984
+ const args = expression.args.map(recurse);
985
+ return { ...expression, operator: recurse(expression.operator), args };
986
+ }
987
+ case "With":
988
+ case "Dot":
989
+ case "Parens":
990
+ return { ...expression, expression: recurse(expression.expression) };
991
+ case "If": {
992
+ const alternative = recurse(expression.alternative);
993
+ const consequent = recurse(expression.consequent);
994
+ return { ...expression, condition: recurse(expression.condition), consequent, alternative };
995
+ }
996
+ case "List":
997
+ return { ...expression, items: expression.items.map(recurse) };
998
+ case "SetEnum":
999
+ case "Tuple":
1000
+ return { ...expression, elements: expression.elements.map(recurse) };
1001
+ case "Product":
1002
+ return { ...expression, factors: expression.factors.map(recurse) };
1003
+ case "FcnApp": {
1004
+ const args = expression.args.map(recurse);
1005
+ return { ...expression, function: recurse(expression.function), args };
1006
+ }
1007
+ case "Arrow": {
1008
+ const range = recurse(expression.range);
1009
+ return { ...expression, domain: recurse(expression.domain), range };
1010
+ }
1011
+ case "Rect":
1012
+ case "Record":
1013
+ return {
1014
+ ...expression,
1015
+ fields: expression.fields.map((field) => ({
1016
+ name: field.name,
1017
+ value: recurse(field.value),
1018
+ })),
1019
+ };
1020
+ case "Except": {
1021
+ const specs = mapExceptSpecs(expression.specs, recurse);
1022
+ return { ...expression, function: recurse(expression.function), specs };
1023
+ }
1024
+ case "Sub":
1025
+ case "Tsub":
1026
+ case "Fair":
1027
+ return {
1028
+ ...expression,
1029
+ subscript: recurse(expression.subscript),
1030
+ action: recurse(expression.action),
1031
+ };
1032
+ case "Case": {
1033
+ const other = expression.other && recurse(expression.other);
1034
+ const arms = expression.arms.map((arm) => {
1035
+ const body = recurse(arm.body);
1036
+ return { condition: recurse(arm.condition), body };
1037
+ });
1038
+ return { ...expression, arms, other };
1039
+ }
1040
+ }
1041
+ };
1042
+
1043
+ // Visit.map without a state: every child, the hidden hypotheses of an ASSUME included.
1044
+ export const mapChildren = (expression: Expr, map: (inner: Expr) => Expr): Expr =>
1045
+ mapScoped(
1046
+ undefined,
1047
+ expression,
1048
+ (_state, inner) => map(inner),
1049
+ () => undefined,
1050
+ "all",
1051
+ );
1052
+
1053
+ export const getChildren = (expression: Expr) => {
1054
+ const children: Expr[] = [];
1055
+ mapChildren(expression, (inner) => {
1056
+ children.push(inner);
1057
+ return inner;
1058
+ });
1059
+ return children;
1060
+ };