tla-ts 0.0.2 → 0.0.3-dev.3cf2ad0

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,889 @@
1
+ // A port of tlapm's obligation printer: E_fmt with Fmtutil's minimal parenthesization, the
2
+ // operator table in Optable, and the identifier renaming in Ctx.
3
+
4
+ import { TlaProofError } from "./errors";
5
+ import {
6
+ getHypName,
7
+ type Bound,
8
+ type Builtin,
9
+ type Defn,
10
+ type Expr,
11
+ type Hyp,
12
+ type Param,
13
+ type Selector,
14
+ type Sequent,
15
+ type Shape,
16
+ } from "./obligation-expression";
17
+ import { format, formatDelimited, renderDoc, text, type Doc } from "./obligation-format";
18
+
19
+ export type Fixity = "Nonfix" | "Prefix" | "Postfix" | "InfixLeft" | "InfixNon" | "InfixRight";
20
+
21
+ export interface Operator {
22
+ name: string;
23
+ low: number;
24
+ high: number;
25
+ fixity: Fixity;
26
+ }
27
+
28
+ const OPERATORS: [string, number, number, Fixity, string[]][] = [
29
+ ["=>", 1, 1, "InfixNon", []],
30
+ ["<=>", 2, 2, "InfixNon", ["\\equiv"]],
31
+ ["/\\", 3, 3, "InfixLeft", ["\\land"]],
32
+ ["\\/", 3, 3, "InfixLeft", ["\\lor"]],
33
+ ["~", 4, 4, "Prefix", ["\\neg", "\\lnot"]],
34
+ ["=", 5, 5, "InfixNon", []],
35
+ ["#", 5, 5, "InfixNon", ["/="]],
36
+ ["SUBSET", 8, 8, "Prefix", []],
37
+ ["UNION", 8, 8, "Prefix", []],
38
+ ["DOMAIN", 9, 9, "Prefix", []],
39
+ ["\\subseteq", 5, 5, "InfixNon", []],
40
+ ["\\in", 5, 5, "InfixNon", []],
41
+ ["\\notin", 5, 5, "InfixNon", []],
42
+ ["\\", 8, 8, "InfixNon", []],
43
+ ["\\cap", 8, 8, "InfixLeft", ["\\intersect"]],
44
+ ["\\cup", 8, 8, "InfixLeft", ["\\union"]],
45
+ ["\\X", 10, 13, "Prefix", ["\\times"]],
46
+ ["'", 15, 15, "Postfix", []],
47
+ ["~>", 2, 2, "InfixNon", ["\\leadsto"]],
48
+ ["ENABLED", 4, 15, "Prefix", []],
49
+ ["UNCHANGED", 4, 15, "Prefix", []],
50
+ ["\\cdot", 5, 14, "InfixLeft", []],
51
+ ["-+->", 2, 2, "InfixNon", []],
52
+ ["[]", 4, 15, "Prefix", []],
53
+ ["<>", 4, 15, "Prefix", []],
54
+ ["^", 14, 14, "InfixNon", []],
55
+ ["/", 13, 13, "InfixNon", []],
56
+ ["*", 13, 13, "InfixLeft", []],
57
+ ["-.", 12, 12, "Prefix", ["-"]],
58
+ ["-", 11, 11, "InfixLeft", []],
59
+ ["+", 10, 10, "InfixLeft", []],
60
+ ["^+", 15, 15, "Postfix", []],
61
+ ["^*", 15, 15, "Postfix", []],
62
+ ["^#", 15, 15, "Postfix", []],
63
+ ["<", 5, 5, "InfixNon", []],
64
+ ["=<", 5, 5, "InfixNon", ["<=", "\\leq"]],
65
+ [">", 5, 5, "InfixNon", []],
66
+ [">=", 5, 5, "InfixNon", ["\\geq"]],
67
+ ["...", 9, 9, "InfixNon", []],
68
+ ["..", 9, 9, "InfixNon", []],
69
+ ["|", 10, 11, "InfixLeft", []],
70
+ ["||", 10, 11, "InfixLeft", []],
71
+ ["&&", 13, 13, "InfixLeft", []],
72
+ ["&", 13, 13, "InfixLeft", []],
73
+ ["$$", 9, 13, "InfixLeft", []],
74
+ ["$", 9, 13, "InfixLeft", []],
75
+ ["??", 9, 13, "InfixLeft", []],
76
+ ["%%", 10, 11, "InfixLeft", []],
77
+ ["%", 10, 11, "InfixLeft", ["\\mod"]],
78
+ ["##", 9, 13, "InfixLeft", []],
79
+ ["++", 10, 10, "InfixLeft", []],
80
+ ["--", 11, 11, "InfixLeft", []],
81
+ ["**", 13, 13, "InfixLeft", []],
82
+ ["//", 13, 13, "InfixNon", []],
83
+ ["^^", 14, 14, "InfixNon", []],
84
+ ["@@", 6, 6, "InfixLeft", []],
85
+ ["!!", 9, 13, "InfixNon", []],
86
+ ["|-", 5, 5, "InfixNon", []],
87
+ ["|=", 5, 5, "InfixNon", []],
88
+ ["-|", 5, 5, "InfixNon", []],
89
+ ["=|", 5, 5, "InfixNon", []],
90
+ ["<:", 7, 7, "InfixNon", []],
91
+ [":>", 7, 7, "InfixNon", []],
92
+ [":=", 5, 5, "InfixNon", []],
93
+ ["::=", 5, 5, "InfixNon", []],
94
+ ["(+)", 10, 10, "InfixLeft", ["\\oplus"]],
95
+ ["(-)", 11, 11, "InfixLeft", ["\\ominus"]],
96
+ ["(.)", 13, 13, "InfixLeft", ["\\odot"]],
97
+ ["(/)", 13, 13, "InfixNon", ["\\oslash"]],
98
+ ["(\\X)", 13, 13, "InfixLeft", ["\\otimes"]],
99
+ ["\\uplus", 9, 13, "InfixLeft", []],
100
+ ["\\sqcap", 9, 13, "InfixLeft", []],
101
+ ["\\sqcup", 9, 13, "InfixLeft", []],
102
+ ["\\div", 13, 13, "InfixNon", []],
103
+ ["\\wr", 9, 14, "InfixNon", []],
104
+ ["\\star", 13, 13, "InfixLeft", []],
105
+ ["\\o", 13, 13, "InfixLeft", ["\\circ"]],
106
+ ["\\bigcirc", 13, 13, "InfixLeft", []],
107
+ ["\\bullet", 13, 13, "InfixLeft", []],
108
+ ["\\prec", 5, 5, "InfixNon", []],
109
+ ["\\succ", 5, 5, "InfixNon", []],
110
+ ["\\preceq", 5, 5, "InfixNon", []],
111
+ ["\\succeq", 5, 5, "InfixNon", []],
112
+ ["\\sim", 5, 5, "InfixNon", []],
113
+ ["\\simeq", 5, 5, "InfixNon", []],
114
+ ["\\ll", 5, 5, "InfixNon", []],
115
+ ["\\gg", 5, 5, "InfixNon", []],
116
+ ["\\asymp", 5, 5, "InfixNon", []],
117
+ ["\\subset", 5, 5, "InfixNon", []],
118
+ ["\\supset", 5, 5, "InfixNon", []],
119
+ ["\\supseteq", 5, 5, "InfixNon", []],
120
+ ["\\approx", 5, 5, "InfixNon", []],
121
+ ["\\cong", 5, 5, "InfixNon", []],
122
+ ["\\sqsubset", 5, 5, "InfixNon", []],
123
+ ["\\sqsubseteq", 5, 5, "InfixNon", []],
124
+ ["\\sqsupset", 5, 5, "InfixNon", []],
125
+ ["\\sqsupseteq", 5, 5, "InfixNon", []],
126
+ ["\\doteq", 5, 5, "InfixNon", []],
127
+ ["\\propto", 5, 5, "InfixNon", []],
128
+ ];
129
+
130
+ // OCaml's Hashtbl.find returns the most recent binding, so later aliases win over earlier names.
131
+ const OPERATOR_TABLE = new Map<string, Operator>();
132
+ for (const [name, low, high, fixity, aliases] of OPERATORS) {
133
+ const operator = { name, low, high, fixity };
134
+ for (const spelling of [name, ...aliases]) OPERATOR_TABLE.set(spelling, operator);
135
+ }
136
+
137
+ const getNonfix = (name: string): Operator => ({ name, low: -1, high: -1, fixity: "Nonfix" });
138
+
139
+ export const lookupOperator = (name: string) => OPERATOR_TABLE.get(name) ?? getNonfix(name);
140
+
141
+ const STANDARD_NAMES: Partial<Record<Builtin, string>> = {
142
+ Implies: "=>",
143
+ Equiv: "<=>",
144
+ Conj: "/\\",
145
+ Disj: "\\/",
146
+ Neg: "~",
147
+ Eq: "=",
148
+ Neq: "#",
149
+ SUBSET: "SUBSET",
150
+ UNION: "UNION",
151
+ DOMAIN: "DOMAIN",
152
+ Subseteq: "\\subseteq",
153
+ Mem: "\\in",
154
+ Notmem: "\\notin",
155
+ Setminus: "\\",
156
+ Cap: "\\cap",
157
+ Cup: "\\cup",
158
+ Prime: "'",
159
+ StrongPrime: "'",
160
+ Leadsto: "~>",
161
+ ENABLED: "ENABLED",
162
+ UNCHANGED: "UNCHANGED",
163
+ Cdot: "\\cdot",
164
+ Actplus: "-+->",
165
+ BoxTrue: "[]",
166
+ BoxFalse: "[]",
167
+ Diamond: "<>",
168
+ Plus: "+",
169
+ Minus: "-",
170
+ Times: "*",
171
+ Ratio: "/",
172
+ Quotient: "\\div",
173
+ Remainder: "%",
174
+ Exp: "^",
175
+ Lteq: "=<",
176
+ Lt: "<",
177
+ Gteq: ">=",
178
+ Gt: ">",
179
+ Range: "..",
180
+ Cat: "\\o",
181
+ OneArg: ":>",
182
+ Extend: "@@",
183
+ };
184
+
185
+ const getStandardForm = (builtin: Builtin): Operator => {
186
+ if (builtin === "Divides") return { name: "?|", low: 10, high: 11, fixity: "InfixNon" };
187
+ if (builtin === "Uminus") return { ...lookupOperator("-."), name: "-" };
188
+ const name = STANDARD_NAMES[builtin];
189
+ return name ? lookupOperator(name) : getNonfix(builtin);
190
+ };
191
+
192
+ interface Ident {
193
+ rep: string;
194
+ salt: number;
195
+ }
196
+
197
+ const getIdentName = (ident: Ident) =>
198
+ ident.salt === 0 ? ident.rep : `${ident.rep}_${ident.salt}`;
199
+
200
+ // Ctx as a mutable stack: printing binds and unbinds names in strict nesting order.
201
+ interface Context {
202
+ stack: Ident[];
203
+ idents: Map<string, Ident[]>;
204
+ defined: Map<string, number>;
205
+ }
206
+
207
+ const createContext = (): Context => ({ stack: [], idents: new Map(), defined: new Map() });
208
+
209
+ const pushName = (context: Context, rep: string) => {
210
+ const previous = context.idents.get(rep)?.at(-1);
211
+ let ident: Ident = previous ? { rep, salt: previous.salt + 1 } : { rep, salt: 0 };
212
+ while (context.defined.has(getIdentName(ident))) ident = { rep, salt: ident.salt + 1 };
213
+ context.stack.push(ident);
214
+ const idents = context.idents.get(rep);
215
+ if (idents) idents.push(ident);
216
+ else context.idents.set(rep, [ident]);
217
+ const key = getIdentName(ident);
218
+ context.defined.set(key, (context.defined.get(key) ?? 0) + 1);
219
+ return key;
220
+ };
221
+
222
+ const popNames = (context: Context, count: number) => {
223
+ for (let index = 0; index < count; index++) {
224
+ const ident = context.stack.pop();
225
+ if (!ident) return;
226
+ context.idents.get(ident.rep)?.pop();
227
+ const key = getIdentName(ident);
228
+ const remaining = (context.defined.get(key) ?? 1) - 1;
229
+ if (remaining === 0) context.defined.delete(key);
230
+ else context.defined.set(key, remaining);
231
+ }
232
+ };
233
+
234
+ const withNames = <Result>(
235
+ context: Context,
236
+ names: string[],
237
+ body: (idents: string[]) => Result,
238
+ ) => {
239
+ const idents = names.map((name) => pushName(context, name));
240
+ const result = body(idents);
241
+ popNames(context, names.length);
242
+ return result;
243
+ };
244
+
245
+ type Assoc = "Left" | "Non" | "Right";
246
+
247
+ type Minimal =
248
+ | { kind: "Atm"; doc: Doc }
249
+ | { kind: "Big"; doc: Doc }
250
+ | { kind: "Op"; name: string; doc: Doc; low: number; high: number; skeleton: Skeleton };
251
+
252
+ type Skeleton =
253
+ | { kind: "Infix"; assoc: Assoc; left: Minimal; right: Minimal }
254
+ | { kind: "Prefix"; operand: Minimal }
255
+ | { kind: "Postfix"; operand: Minimal };
256
+
257
+ const isBelow = (high: number, expression: Minimal) =>
258
+ expression.kind === "Atm" || (expression.kind === "Op" && high < expression.low);
259
+
260
+ const withParens = (doc: Doc): Doc => [format("("), doc, format(")")];
261
+
262
+ const printOperand = (operand: Minimal, isBare: boolean) =>
263
+ isBare ? printMinimal(operand) : withParens(printMinimal(operand));
264
+
265
+ const printMinimal = (expression: Minimal): Doc => {
266
+ if (expression.kind !== "Op") return expression.doc;
267
+ const skeleton = expression.skeleton;
268
+ const isUnder = (operand: Minimal) => isBelow(expression.high, operand);
269
+ switch (skeleton.kind) {
270
+ case "Prefix":
271
+ case "Postfix": {
272
+ const operand = skeleton.operand;
273
+ const operandDoc = printOperand(
274
+ operand,
275
+ isUnder(operand) || (operand.kind === "Op" && operand.skeleton.kind === skeleton.kind),
276
+ );
277
+ const docs =
278
+ skeleton.kind === "Prefix" ? [expression.doc, operandDoc] : [operandDoc, expression.doc];
279
+ return [format("@[<h>"), ...docs, format("@]")];
280
+ }
281
+ case "Infix": {
282
+ const left = skeleton.left;
283
+ const right = skeleton.right;
284
+ const isLeftBare =
285
+ isUnder(left) ||
286
+ (left.kind === "Op" && left.name === expression.name && skeleton.assoc === "Left");
287
+ const isRightBare =
288
+ isUnder(right) ||
289
+ (skeleton.assoc === "Right" &&
290
+ right.kind === "Op" &&
291
+ right.skeleton.kind === "Infix" &&
292
+ right.skeleton.assoc === "Right");
293
+ return [
294
+ format("@[<b0>"),
295
+ printOperand(left, isLeftBare),
296
+ expression.doc,
297
+ printOperand(right, isRightBare),
298
+ format("@]"),
299
+ ];
300
+ }
301
+ }
302
+ };
303
+
304
+ const atom = (doc: Doc): Minimal => ({ kind: "Atm", doc });
305
+
306
+ const big = (doc: Doc): Minimal => ({ kind: "Big", doc });
307
+
308
+ const isWith = (expression: Expr): boolean => {
309
+ if (expression.kind === "Lambda") return isWith(expression.body);
310
+ if (expression.kind === "Sequent") return isWith(expression.sequent.active);
311
+ return expression.kind === "With";
312
+ };
313
+
314
+ const isElided = (hyp: Hyp) => {
315
+ if (hyp.kind === "Defn") {
316
+ const definition = hyp.definition;
317
+ const hasWith = definition.kind === "Operator" && isWith(definition.body);
318
+ return hyp.visibility === "Hidden" || hasWith || hyp.whereDefined !== "User";
319
+ }
320
+ if (hyp.kind === "Fact") return hyp.visibility === "Hidden" || isWith(hyp.expression);
321
+ return false;
322
+ };
323
+
324
+ const printShape = (shape: Shape): Doc =>
325
+ shape === 0
326
+ ? []
327
+ : format(
328
+ "@[<hov1>(%a)@]",
329
+ formatDelimited(
330
+ Array.from({ length: shape }, () => "_"),
331
+ text,
332
+ ),
333
+ );
334
+
335
+ const printParam = (param: Param): Doc => format("%s%a", param.name, printShape(param.shape));
336
+
337
+ const LEVEL_NAMES = {
338
+ Constant: "CONSTANT",
339
+ State: "STATE",
340
+ Action: "ACTION",
341
+ Temporal: "TEMPORAL",
342
+ Unknown: "UNKNOWN",
343
+ };
344
+
345
+ // hyps names the indices beyond the printer's own context, as the hypothesis deque does in E_fmt.
346
+ const createPrinter = (hyps: Hyp[] = []) => {
347
+ const context = createContext();
348
+ const printExpr = (expression: Expr): Doc => printMinimal(formatExpr(expression));
349
+
350
+ const printLambda = (params: Param[], body: Expr) =>
351
+ withNames(
352
+ context,
353
+ params.map((param) => param.name),
354
+ () =>
355
+ format(
356
+ "@[<b2>LAMBDA @[<hv0>%a@] :@ %a@]",
357
+ formatDelimited(params, printParam),
358
+ printExpr(body),
359
+ ),
360
+ );
361
+
362
+ // fmt_bounds: names bind left to right, domains print in the enclosing context, and dittos
363
+ // share the previous domain.
364
+ const formatBounds = <Result>(bounds: Bound[], body: (boundsDoc: Doc) => Result): Result => {
365
+ const domains = bounds.map((bound) =>
366
+ bound.domain.kind === "Domain" ? printExpr(bound.domain.expression) : undefined,
367
+ );
368
+ return withNames(
369
+ context,
370
+ bounds.map((bound) => bound.name),
371
+ (names) => {
372
+ const chunks: { names: string[]; index: number }[] = [];
373
+ bounds.forEach((bound, index) => {
374
+ if (index > 0 && bound.domain.kind === "Ditto") chunks.at(-1)?.names.push(names[index]);
375
+ else chunks.push({ names: [names[index]], index });
376
+ });
377
+ const boundsDoc = formatDelimited(chunks, (chunk) => {
378
+ const namesDoc = formatDelimited(chunk.names, text);
379
+ const domain = bounds[chunk.index].domain;
380
+ if (domain.kind === "NoDomain") return namesDoc;
381
+ if (domain.kind === "Ditto") return format("@[<hov0>%a@ \\in ??????@]", namesDoc);
382
+ return format("@[<hov0>%a@ \\in %a@]", namesDoc, domains[chunk.index] ?? []);
383
+ });
384
+ return body(boundsDoc);
385
+ },
386
+ );
387
+ };
388
+
389
+ const printSelector = (selector: Selector): Doc => {
390
+ switch (selector.kind) {
391
+ case "Down":
392
+ return format(":");
393
+ case "Left":
394
+ return format("<<");
395
+ case "Right":
396
+ return format(">>");
397
+ case "At":
398
+ return text("@");
399
+ case "Number":
400
+ return text(String(selector.value));
401
+ case "Instance":
402
+ return format("@[<b1>(%a)@]", formatDelimited(selector.args, printExpr));
403
+ case "Label":
404
+ return selector.args.length === 0
405
+ ? text(selector.label)
406
+ : format(
407
+ "@[<b2>%s@,@[<b1>(%a)@]@]",
408
+ selector.label,
409
+ formatDelimited(selector.args, printExpr),
410
+ );
411
+ }
412
+ };
413
+
414
+ const printSelectors = (selectors: Selector[]): Doc =>
415
+ selectors.length === 0
416
+ ? []
417
+ : [format("!"), formatDelimited(selectors, printSelector, format("!"))];
418
+
419
+ const getIndexName = (index: number) => {
420
+ if (index < 0) return `#$$${Math.abs(index)}`;
421
+ if (index === 0) return "#$0";
422
+ if (index <= context.stack.length) {
423
+ return getIdentName(context.stack[context.stack.length - index]);
424
+ }
425
+ const hyp = hyps[hyps.length - (index - context.stack.length)];
426
+ if (hyp) return getHypName(hyp) ?? "??? FACT ???";
427
+ return `#$${index}`;
428
+ };
429
+
430
+ const formatApply = (operator: Expr, args: Expr[]): Minimal => {
431
+ if (operator.kind === "Lambda") {
432
+ return {
433
+ kind: "Op",
434
+ name: " ",
435
+ doc: format("@ "),
436
+ low: 20,
437
+ high: 20,
438
+ skeleton: {
439
+ kind: "Infix",
440
+ assoc: "Left",
441
+ left: big(printLambda(operator.params, operator.body)),
442
+ right: atom(format("(%a)", formatDelimited(args, printExpr))),
443
+ },
444
+ };
445
+ }
446
+ if (operator.kind === "Bang") {
447
+ return args.length === 0
448
+ ? formatExpr(operator)
449
+ : atom(
450
+ format("@[<b2>%a@[<b1>(%a)@]@]", printExpr(operator), formatDelimited(args, printExpr)),
451
+ );
452
+ }
453
+ if (operator.kind === "Internal" && args.length === 1) {
454
+ if (operator.builtin === "BoxFalse" || operator.builtin === "Unprimable") {
455
+ return formatExpr(args[0]);
456
+ }
457
+ if (operator.builtin === "Irregular") {
458
+ return atom(format("@[<h>@<1>%s%a@<1>%s@]", "<[", printExpr(args[0]), "]>"));
459
+ }
460
+ }
461
+ const top = (() => {
462
+ switch (operator.kind) {
463
+ case "Ix":
464
+ return lookupOperator(getIndexName(operator.index));
465
+ case "Opaque": {
466
+ const found = lookupOperator(operator.name);
467
+ return { ...found, name: `?${found.name}` };
468
+ }
469
+ case "Internal":
470
+ return getStandardForm(operator.builtin);
471
+ default:
472
+ throw new TlaProofError("Cannot print this operator");
473
+ }
474
+ })();
475
+ const getOp = (doc: Doc, skeleton: Skeleton): Minimal => ({
476
+ kind: "Op",
477
+ name: top.name,
478
+ doc,
479
+ low: top.low,
480
+ high: top.high,
481
+ skeleton,
482
+ });
483
+ if (args.length === 0) return atom(text(top.name));
484
+ if (top.fixity === "Nonfix") {
485
+ return atom(format("@[<b2>%s@[<b1>(%a)@]@]", top.name, formatDelimited(args, printExpr)));
486
+ }
487
+ if (top.fixity === "Prefix" && args.length === 1) {
488
+ return getOp(/[A-Za-z]$/.test(top.name) ? format("%s@ ", top.name) : text(top.name), {
489
+ kind: "Prefix",
490
+ operand: formatExpr(args[0]),
491
+ });
492
+ }
493
+ if (top.fixity === "Postfix" && args.length === 1) {
494
+ return getOp(text(top.name), { kind: "Postfix", operand: formatExpr(args[0]) });
495
+ }
496
+ if (top.fixity.startsWith("Infix") && args.length === 2) {
497
+ const assoc: Assoc =
498
+ top.fixity === "InfixLeft" ? "Left" : top.fixity === "InfixRight" ? "Right" : "Non";
499
+ return getOp(top.name === ".." ? text(top.name) : format("@ %s ", top.name), {
500
+ kind: "Infix",
501
+ assoc,
502
+ left: formatExpr(args[0]),
503
+ right: formatExpr(args[1]),
504
+ });
505
+ }
506
+ throw new TlaProofError(`Arity mismatch for operator ${top.name}`);
507
+ };
508
+
509
+ const formatExpr = (expression: Expr): Minimal => {
510
+ switch (expression.kind) {
511
+ case "Ix":
512
+ case "Opaque":
513
+ case "Internal":
514
+ return formatApply(expression, []);
515
+ case "Lambda":
516
+ return big(printLambda(expression.params, expression.body));
517
+ case "Bang":
518
+ return atom([printExpr(expression.expression), printSelectors(expression.selectors)]);
519
+ case "Apply":
520
+ return formatApply(expression.operator, expression.args);
521
+ case "Sequent":
522
+ return big(printSequent(expression.sequent));
523
+ case "With":
524
+ return formatExpr(expression.expression);
525
+ case "If":
526
+ return big(
527
+ format(
528
+ "@[<hv2>@[<b2>IF@ %a@]@ @[<b2>THEN %a@]@ @[<b2>ELSE %a@]@]",
529
+ printExpr(expression.condition),
530
+ printExpr(expression.consequent),
531
+ printExpr(expression.alternative),
532
+ ),
533
+ );
534
+ case "List": {
535
+ if (expression.bullet === "Refs" && expression.items.length === 0)
536
+ return atom(format("TRUE"));
537
+ if (expression.bullet === "Refs" && expression.items.length === 1) {
538
+ return formatExpr(expression.items[0]);
539
+ }
540
+ const bullet = expression.bullet === "Or" ? "\\/" : "/\\";
541
+ return big(
542
+ format(
543
+ "@[<v0>%a@]",
544
+ formatDelimited(
545
+ expression.items,
546
+ (item) => format("@[<h>%s %a@]", bullet, printExpr(item)),
547
+ format("@,"),
548
+ ),
549
+ ),
550
+ );
551
+ }
552
+ case "Let": {
553
+ const definitionsDoc = formatDelimited(
554
+ expression.definitions,
555
+ printDefinition,
556
+ format("@,"),
557
+ );
558
+ const bodyDoc = printExpr(expression.body);
559
+ popNames(context, expression.definitions.length);
560
+ return big([format("@[<hv0>LET @[<v0>"), definitionsDoc, format("@]@\nIN %a@]", bodyDoc)]);
561
+ }
562
+ case "Quant":
563
+ return formatBounds(expression.bounds, (boundsDoc) =>
564
+ big(
565
+ format(
566
+ "@[<b3>%s @[<b0>%a@] :@ %a@]",
567
+ expression.quantifier === "Forall" ? "\\A" : "\\E",
568
+ boundsDoc,
569
+ printExpr(expression.body),
570
+ ),
571
+ ),
572
+ );
573
+ case "Tquant":
574
+ return withNames(context, expression.names, (names) =>
575
+ big(
576
+ format(
577
+ "@[<b4>%s @[<b0>%a@] : %a@]",
578
+ expression.quantifier === "Forall" ? "\\AA" : "\\EE",
579
+ formatDelimited(names, text),
580
+ printExpr(expression.body),
581
+ ),
582
+ ),
583
+ );
584
+ case "Choose": {
585
+ const domainDoc = expression.domain && printExpr(expression.domain);
586
+ return withNames(context, [expression.name], ([name]) =>
587
+ big(
588
+ domainDoc
589
+ ? format(
590
+ "@[<b2>CHOOSE @[<b2>%s \\in@ %a@] :@ %a@]",
591
+ name,
592
+ domainDoc,
593
+ printExpr(expression.body),
594
+ )
595
+ : format("@[<b2>CHOOSE %s :@ %a@]", name, printExpr(expression.body)),
596
+ ),
597
+ );
598
+ }
599
+ case "SetSt": {
600
+ const domainDoc = printExpr(expression.domain);
601
+ return withNames(context, [expression.name], ([name]) =>
602
+ atom(
603
+ format(
604
+ "@[<b3>{@[<b2>%s \\in@ %a@] :@ %a}@]",
605
+ name,
606
+ domainDoc,
607
+ printExpr(expression.body),
608
+ ),
609
+ ),
610
+ );
611
+ }
612
+ case "SetOf":
613
+ return formatBounds(expression.bounds, (boundsDoc) =>
614
+ atom(format("@[<b3>{%a :@ %a}@]", printExpr(expression.element), boundsDoc)),
615
+ );
616
+ case "SetEnum":
617
+ return atom(format("@[<h>{@[<b0>%a@]}@]", formatDelimited(expression.elements, printExpr)));
618
+ case "Arrow":
619
+ return atom(
620
+ format("@[<b3>[%a ->@ %a]@]", printExpr(expression.domain), printExpr(expression.range)),
621
+ );
622
+ case "Fcn":
623
+ return formatBounds(expression.bounds, (boundsDoc) =>
624
+ atom(format("@[<b3>[%a |->@ %a]@]", boundsDoc, printExpr(expression.body))),
625
+ );
626
+ case "FcnApp": {
627
+ const target = formatExpr(expression.function);
628
+ const isAtom = target.kind === "Atm";
629
+ return atom(
630
+ format(
631
+ "@[<h>%s%a%s@[<b1>[%a]@]@]",
632
+ isAtom ? "" : "(",
633
+ printMinimal(target),
634
+ isAtom ? "" : ")",
635
+ formatDelimited(expression.args, printExpr),
636
+ ),
637
+ );
638
+ }
639
+ case "Product": {
640
+ if (expression.factors.length === 1) return formatExpr(expression.factors[0]);
641
+ if (expression.factors.length === 0)
642
+ throw new TlaProofError("Expr.Fmt.fmt_expr: PRODUCT []");
643
+ return {
644
+ kind: "Op",
645
+ name: "\\X",
646
+ doc: format("@ \\X "),
647
+ low: 10,
648
+ high: 13,
649
+ skeleton: {
650
+ kind: "Infix",
651
+ assoc: "Right",
652
+ left: formatExpr(expression.factors[0]),
653
+ right: formatExpr({ ...expression, factors: expression.factors.slice(1) }),
654
+ },
655
+ };
656
+ }
657
+ case "Tuple":
658
+ return atom(format("@[<b2><<%a>>@]", formatDelimited(expression.elements, printExpr)));
659
+ case "Rect":
660
+ return atom(
661
+ format(
662
+ "@[<b1>[%a]@]",
663
+ formatDelimited(expression.fields, (field) =>
664
+ format("@[<h>%s : %a@]", field.name, printExpr(field.value)),
665
+ ),
666
+ ),
667
+ );
668
+ case "Record":
669
+ return atom(
670
+ format(
671
+ "@[<b1>[%a]@]",
672
+ formatDelimited(expression.fields, (field) =>
673
+ format("@[<h>%s |-> %a@]", field.name, printExpr(field.value)),
674
+ ),
675
+ ),
676
+ );
677
+ case "Except":
678
+ return atom(
679
+ format(
680
+ "@[<b3>[%a EXCEPT@ @[<v0>%a@]]@]",
681
+ printExpr(expression.function),
682
+ formatDelimited(expression.specs, (spec) =>
683
+ format(
684
+ "!@[<h>%a = %a@]",
685
+ spec.trail.map((point) =>
686
+ point.kind === "Dot"
687
+ ? format(".%s", point.field)
688
+ : format("[%a]", printExpr(point.expression)),
689
+ ),
690
+ printExpr(spec.value),
691
+ ),
692
+ ),
693
+ ),
694
+ );
695
+ case "Dot":
696
+ return {
697
+ kind: "Op",
698
+ name: ".",
699
+ doc: format("."),
700
+ low: 16,
701
+ high: 16,
702
+ skeleton: {
703
+ kind: "Infix",
704
+ assoc: "Left",
705
+ left: formatExpr(expression.expression),
706
+ right: atom(text(expression.field)),
707
+ },
708
+ };
709
+ case "Sub":
710
+ case "Tsub": {
711
+ const isBox = expression.modal === "Box";
712
+ const open = expression.kind === "Sub" ? (isBox ? "[" : "<<") : isBox ? "[][" : "<><<";
713
+ return atom(
714
+ format(
715
+ "@[<h>%s@[<b2>%a@]%s%a@]",
716
+ open,
717
+ printExpr(expression.action),
718
+ isBox ? "]_" : ">>_",
719
+ printExpr(expression.subscript),
720
+ ),
721
+ );
722
+ }
723
+ case "Fair":
724
+ return big(
725
+ format(
726
+ "@[<h>%s_%a (%a)@]",
727
+ expression.fairness === "Weak" ? "WF" : "SF",
728
+ withParens(printExpr(expression.subscript)),
729
+ printExpr(expression.action),
730
+ ),
731
+ );
732
+ case "Case":
733
+ return big(
734
+ format(
735
+ "@[<v2>CASE %a%a@]",
736
+ formatDelimited(
737
+ expression.arms,
738
+ (arm) => format("@[<b2>%a ->@ %a@]", printExpr(arm.condition), printExpr(arm.body)),
739
+ format("@,[] "),
740
+ ),
741
+ expression.other
742
+ ? format("@,[] @[<b2>OTHER ->@ %a@]", printExpr(expression.other))
743
+ : [],
744
+ ),
745
+ );
746
+ case "String":
747
+ return atom(format('"%s"', expression.value));
748
+ case "Num":
749
+ return atom(
750
+ text(
751
+ expression.fraction === ""
752
+ ? expression.integer
753
+ : `${expression.integer}.${expression.fraction}`,
754
+ ),
755
+ );
756
+ case "At":
757
+ return atom(text("@"));
758
+ case "Parens": {
759
+ const form = expression.form;
760
+ if (form.kind === "Syntax") return formatExpr(expression.expression);
761
+ const names =
762
+ form.kind === "Nlabel"
763
+ ? form.names
764
+ : form.indices.map((entry) =>
765
+ entry.index >= 1 && entry.index <= context.stack.length
766
+ ? getIndexName(entry.index)
767
+ : `¶${entry.index}`,
768
+ );
769
+ const inner = formatExpr(expression.expression);
770
+ if (names.length === 0) {
771
+ const pattern = inner.kind === "Op" ? "%s::(%a)" : "%s::%a";
772
+ return atom(format(pattern, form.label, printMinimal(inner)));
773
+ }
774
+ const namesDoc = formatDelimited(names, text);
775
+ if (inner.kind === "Atm")
776
+ return atom(format("%s(%a)::%a", form.label, namesDoc, printMinimal(inner)));
777
+ if (inner.kind === "Big") {
778
+ return atom(format("%s(%a)::(%a)", form.label, namesDoc, printMinimal(inner)));
779
+ }
780
+ return atom(format("(%s(%a):: %a)", form.label, namesDoc, printMinimal(inner)));
781
+ }
782
+ }
783
+ };
784
+
785
+ // pp_print_defn: returns the definition's document and binds its name for what follows.
786
+ const printDefinition = (definition: Defn): Doc => {
787
+ switch (definition.kind) {
788
+ case "Operator":
789
+ case "Bpragma": {
790
+ const body = definition.body;
791
+ if (body.kind === "Lambda") {
792
+ const bodyDoc = withNames(
793
+ context,
794
+ body.params.map((param) => param.name),
795
+ () => printExpr(body.body),
796
+ );
797
+ const name = pushName(context, definition.name);
798
+ return format(
799
+ "@[<b2>%s@[<b1>(%a)@] ==@ %a@]",
800
+ name,
801
+ formatDelimited(body.params, printParam),
802
+ bodyDoc,
803
+ );
804
+ }
805
+ const bodyDoc = printExpr(body);
806
+ return format("@[<b2>%s ==@ %a@]", pushName(context, definition.name), bodyDoc);
807
+ }
808
+ case "Instance": {
809
+ const instance = definition.instance;
810
+ const substitutionsDoc = withNames(context, instance.args, (args) => {
811
+ const doc = format(
812
+ "@[<b2>INSTANCE %s%a@]",
813
+ instance.module,
814
+ instance.substitutions.length === 0
815
+ ? []
816
+ : format(
817
+ "@ WITH@ %a",
818
+ formatDelimited(instance.substitutions, (entry) =>
819
+ format("@[<b2>%s <- %a@]", entry.name, printExpr(entry.expression)),
820
+ ),
821
+ ),
822
+ );
823
+ return { doc, args };
824
+ });
825
+ const name = pushName(context, definition.name);
826
+ const head =
827
+ instance.args.length === 0
828
+ ? text(name)
829
+ : format("%s (@[<b0>%a@])", name, formatDelimited(substitutionsDoc.args, text));
830
+ return format("@[<b2>%a == %a@]", head, substitutionsDoc.doc);
831
+ }
832
+ case "Recursive": {
833
+ const name = pushName(context, definition.name);
834
+ return format("@[<b2>NEW CONSTANT %s%a@]", name, printShape(definition.shape));
835
+ }
836
+ }
837
+ };
838
+
839
+ // pp_print_hyp: prints the hypothesis in the context before it, then binds it.
840
+ const printHyp = (hyp: Hyp): Doc => {
841
+ switch (hyp.kind) {
842
+ case "Fresh": {
843
+ const domainDoc =
844
+ hyp.domain && hyp.domainVisibility === "Visible" ? printExpr(hyp.domain) : undefined;
845
+ const name = pushName(context, hyp.name);
846
+ return [
847
+ format("NEW %s %s%a", LEVEL_NAMES[hyp.level], name, printShape(hyp.shape)),
848
+ domainDoc ? format(" \\in %a", domainDoc) : [],
849
+ ];
850
+ }
851
+ case "Flex":
852
+ return format("NEW VARIABLE %s", pushName(context, hyp.name));
853
+ case "Defn":
854
+ return printDefinition(hyp.definition);
855
+ case "Fact": {
856
+ const doc = format("@[<h>%a @]", printExpr(hyp.expression));
857
+ pushName(context, "");
858
+ return doc;
859
+ }
860
+ }
861
+ };
862
+
863
+ const printSequent = (sequent: Sequent): Doc => {
864
+ if (sequent.context.length === 0) return printExpr(sequent.active);
865
+ const hypDocs: Doc[] = [];
866
+ for (const hyp of sequent.context) {
867
+ if (isElided(hyp)) pushName(context, getHypName(hyp) ?? "");
868
+ else hypDocs.push(printHyp(hyp));
869
+ }
870
+ const activeDoc = printExpr(sequent.active);
871
+ popNames(context, sequent.context.length);
872
+ if (hypDocs.length === 0) return activeDoc;
873
+ return [
874
+ format("@[<v0>ASSUME @[<v0>"),
875
+ formatDelimited(hypDocs, (doc) => doc),
876
+ format("@]@\nPROVE %a@]", activeDoc),
877
+ ];
878
+ };
879
+
880
+ return { printSequent, printExpr };
881
+ };
882
+
883
+ export const printExpression = (hyps: Hyp[], expression: Expr) =>
884
+ createPrinter(hyps).printExpr(expression);
885
+
886
+ export const formatObligation = (sequent: Sequent) => {
887
+ const printer = createPrinter();
888
+ return renderDoc(format("@[<b0>%a@]", printer.printSequent(sequent))).trimEnd();
889
+ };