tla-ts 0.0.2 → 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.
@@ -0,0 +1,1084 @@
1
+ // Converts the tla-ts AST into tlapm's parse trees: expressions name operators opaquely until
2
+ // anonymization resolves them, and proof steps get tlapm's levels and anonymous step numbers.
3
+
4
+ import type {
5
+ AssumeProve,
6
+ Definition,
7
+ Expression,
8
+ Fact,
9
+ Instance,
10
+ Module,
11
+ NewSymbol,
12
+ ProofStep,
13
+ QuantifierExpression,
14
+ SourceLocation,
15
+ SourceRange,
16
+ UseOrHide,
17
+ } from "./ast";
18
+ import { TlaProofError, TlaUnsupportedError } from "./errors";
19
+ import {
20
+ at,
21
+ type Bound,
22
+ type Builtin,
23
+ type Defn,
24
+ type Expr,
25
+ type Hyp,
26
+ type InstanceDefinition,
27
+ type Locus,
28
+ type Selector,
29
+ type Sequent,
30
+ } from "./obligation-expression";
31
+ import type { ModuleUnit, Mule } from "./obligation-module";
32
+ import {
33
+ expandChoose,
34
+ expandFcn,
35
+ expandQuant,
36
+ expandSetOf,
37
+ expandSetSt,
38
+ getUnbounded,
39
+ type TuplyBound,
40
+ } from "./obligation-tuples";
41
+ import { lookupOperator, type Fixity, type Operator } from "./obligation-printer";
42
+ import {
43
+ getUnique,
44
+ type Counter,
45
+ type Proof,
46
+ type Step,
47
+ type StepNumber,
48
+ type UseDef,
49
+ type Usable,
50
+ } from "./obligation-proof";
51
+
52
+ const BUILTINS: Record<string, Builtin> = {
53
+ "=>": "Implies",
54
+ "<=>": "Equiv",
55
+ "/\\": "Conj",
56
+ "\\/": "Disj",
57
+ "~": "Neg",
58
+ "=": "Eq",
59
+ "#": "Neq",
60
+ SUBSET: "SUBSET",
61
+ UNION: "UNION",
62
+ DOMAIN: "DOMAIN",
63
+ "\\subseteq": "Subseteq",
64
+ "\\in": "Mem",
65
+ "\\notin": "Notmem",
66
+ "\\": "Setminus",
67
+ "\\cap": "Cap",
68
+ "\\cup": "Cup",
69
+ "'": "Prime",
70
+ "~>": "Leadsto",
71
+ ENABLED: "ENABLED",
72
+ UNCHANGED: "UNCHANGED",
73
+ "\\cdot": "Cdot",
74
+ "-+->": "Actplus",
75
+ "[]": "BoxTrue",
76
+ "<>": "Diamond",
77
+ TRUE: "TRUE",
78
+ FALSE: "FALSE",
79
+ BOOLEAN: "BOOLEAN",
80
+ STRING: "STRING",
81
+ };
82
+
83
+ // Optable maps each spelling to its canonical name, and a prefix minus is the operator -.
84
+ const getCanonicalName = (name: string, isPrefix = false) =>
85
+ isPrefix && name === "-" ? "-." : lookupOperator(name).name;
86
+
87
+ const STEP_PATTERN = /^<(\d+|\*|\+)>([A-Za-z0-9_]*)\.*$/;
88
+
89
+ const mergeLoci = (left: Locus | undefined, right: Locus | undefined): Locus | undefined => {
90
+ if (!left || !right) return left ?? right;
91
+ const isLeftFirst =
92
+ left.startLine < right.startLine ||
93
+ (left.startLine === right.startLine && left.startColumn <= right.startColumn);
94
+ const isLeftLast =
95
+ left.endLine > right.endLine ||
96
+ (left.endLine === right.endLine && left.endColumn >= right.endColumn);
97
+ return {
98
+ file: left.file,
99
+ startLine: isLeftFirst ? left.startLine : right.startLine,
100
+ startColumn: isLeftFirst ? left.startColumn : right.startColumn,
101
+ endLine: isLeftLast ? left.endLine : right.endLine,
102
+ endColumn: isLeftLast ? left.endColumn : right.endColumn,
103
+ };
104
+ };
105
+
106
+ type Grouping =
107
+ | { kind: "Attached"; expression: Expression }
108
+ | { kind: "Reduced"; isFinal: boolean };
109
+
110
+ const PREFIX_KEYWORDS: Partial<Record<Expression["kind"], string>> = {
111
+ Always: "[]",
112
+ Eventually: "<>",
113
+ Enabled: "ENABLED",
114
+ Unchanged: "UNCHANGED",
115
+ };
116
+
117
+ const isSamePosition = (left: SourceLocation | undefined, right: SourceLocation | undefined) =>
118
+ left !== undefined &&
119
+ right !== undefined &&
120
+ left.line === right.line &&
121
+ left.column === right.column;
122
+
123
+ const getStart = (expression: Expression) => expression.location.range?.start;
124
+
125
+ const getEnd = (expression: Expression) => expression.location.range?.end;
126
+
127
+ const withEnd = (expression: Expression, end: SourceLocation | undefined): Expression => {
128
+ const range = expression.location.range;
129
+ return range && end
130
+ ? { ...expression, location: { ...expression.location, range: { ...range, end } } }
131
+ : expression;
132
+ };
133
+
134
+ // The operator an expression leaves open on tlapm's parse stack, with the operand after it.
135
+ const getOpenOperator = (expression: Expression) => {
136
+ if (expression.kind === "Apply") {
137
+ const operator = lookupOperator(expression.operator);
138
+ const isInfix = expression.args.length === 2 && operator.fixity.startsWith("Infix");
139
+ const isPrefix = expression.args.length === 1 && operator.fixity === "Prefix";
140
+ if (!isInfix && !isPrefix) return undefined;
141
+ const last = expression.args.length - 1;
142
+ return {
143
+ operator,
144
+ isInfix,
145
+ operand: expression.args[last],
146
+ replace: (operand: Expression): Expression => ({
147
+ ...expression,
148
+ args: [...expression.args.slice(0, last), operand],
149
+ }),
150
+ };
151
+ }
152
+ const keyword = PREFIX_KEYWORDS[expression.kind];
153
+ if (keyword === undefined || !("expression" in expression)) return undefined;
154
+ return {
155
+ operator: lookupOperator(keyword),
156
+ isInfix: false,
157
+ operand: expression.expression,
158
+ replace: (operand: Expression): Expression => ({ ...expression, expression: operand }),
159
+ };
160
+ };
161
+
162
+ const isBelow = (left: Operator, right: Operator) => left.high < right.low;
163
+
164
+ // Pco.resolve: tlapm's parser lets an operator take in every later infix operator that is not
165
+ // entirely below it, so SUBSET S \ T is SUBSET (S \ T) to tlapm though SANY reads (SUBSET S) \ T.
166
+ // This regroups an infix application the way tlapm does.
167
+ const regroup = (expression: Extract<Expression, { kind: "Apply" }>): Expression => {
168
+ const operator = lookupOperator(expression.operator);
169
+ const [left, right] = expression.args;
170
+ const end = getEnd(expression);
171
+ const getInfix = (operand: Expression): Expression => {
172
+ const range = expression.location.range;
173
+ const start = getStart(operand);
174
+ return {
175
+ ...expression,
176
+ args: [operand, right],
177
+ location:
178
+ range && start
179
+ ? { ...expression.location, range: { ...range, start } }
180
+ : expression.location,
181
+ };
182
+ };
183
+ const attach = (node: Expression, isOpen: boolean): Grouping => {
184
+ const open = isOpen ? getOpenOperator(node) : undefined;
185
+ if (!open) return { kind: "Reduced", isFinal: false };
186
+ const inner = attach(open.operand, isSamePosition(getEnd(open.operand), getEnd(node)));
187
+ if (inner.kind === "Attached")
188
+ return { kind: "Attached", expression: withEnd(open.replace(inner.expression), end) };
189
+ const isReduced =
190
+ !inner.isFinal &&
191
+ (isBelow(operator, open.operator) ||
192
+ (open.isInfix &&
193
+ operator.fixity === "InfixLeft" &&
194
+ open.operator.fixity === "InfixLeft" &&
195
+ !isBelow(open.operator, operator)));
196
+ if (isReduced) return { kind: "Reduced", isFinal: !isBelow(operator, open.operator) };
197
+ return { kind: "Attached", expression: withEnd(open.replace(getInfix(open.operand)), end) };
198
+ };
199
+ const grouping = attach(left, isSamePosition(getStart(left), getStart(expression)));
200
+ return grouping.kind === "Attached" ? grouping.expression : expression;
201
+ };
202
+
203
+ const TUPLY_FAILURE = "use instead the class`E_visit.iter_concrete`";
204
+
205
+ export const createSource = (file: string, text: string, counter: Counter, isExpanded: boolean) => {
206
+ const lines = text.split("\n");
207
+
208
+ const getRangeLocus = (range: SourceRange): Locus => ({
209
+ file,
210
+ startLine: range.start.line,
211
+ startColumn: range.start.column,
212
+ endLine: range.end.line,
213
+ endColumn: range.end.column + 1,
214
+ });
215
+
216
+ const withPragma = (locus: Locus) => {
217
+ const position = { line: locus.endLine - 1, column: locus.endColumn - 1 };
218
+ const isBlank = (character: string | undefined) => character === " " || character === "\r";
219
+ while (position.line < lines.length) {
220
+ const line = lines[position.line];
221
+ while (isBlank(line[position.column])) position.column++;
222
+ if (position.column < line.length) break;
223
+ position.line++;
224
+ position.column = 0;
225
+ }
226
+ if (!(lines[position.line] ?? "").startsWith("(*{", position.column)) return locus;
227
+ for (let index = position.line; index < lines.length; index++) {
228
+ const close = lines[index].indexOf("}*)", index === position.line ? position.column : 0);
229
+ if (close !== -1) return { ...locus, endLine: index + 1, endColumn: close + 4 };
230
+ }
231
+ return locus;
232
+ };
233
+
234
+ const getNameLocus = (name: string, location: SourceLocation): Locus => ({
235
+ file,
236
+ startLine: location.line,
237
+ startColumn: location.column,
238
+ endLine: location.line,
239
+ endColumn: location.column + name.length,
240
+ });
241
+
242
+ const getLocus = (location: SourceLocation): Locus =>
243
+ location.range
244
+ ? getRangeLocus(location.range)
245
+ : {
246
+ file,
247
+ startLine: location.line,
248
+ startColumn: location.column,
249
+ endLine: location.line,
250
+ endColumn: location.column + 1,
251
+ };
252
+
253
+ const getOperator = (name: string, location: Locus | undefined): Expr => {
254
+ const canonical = getCanonicalName(name);
255
+ const builtin = BUILTINS[canonical];
256
+ return at(builtin ? { kind: "Internal", builtin } : { kind: "Opaque", name: canonical }, {
257
+ location,
258
+ });
259
+ };
260
+
261
+ // A name with ! selects through instances, which anonymization resolves by joining the names.
262
+ const getName = (
263
+ name: string,
264
+ args: Expr[],
265
+ location: Locus,
266
+ argumentCounts?: number[],
267
+ ): Expr => {
268
+ if (name.startsWith("<")) {
269
+ const operator = at({ kind: "Opaque", name }, { location });
270
+ return at({ kind: "Bang", expression: operator, selectors: [] }, { location });
271
+ }
272
+ const builtin = BUILTINS[name];
273
+ if (builtin && args.length === 0) return at({ kind: "Internal", builtin }, { location });
274
+ const names = name.split("!");
275
+ if (names.length === 1) {
276
+ const operator = at({ kind: "Opaque", name }, { location });
277
+ return args.length === 0 ? operator : at({ kind: "Apply", operator, args }, { location });
278
+ }
279
+ const counts =
280
+ argumentCounts ?? names.map((_name, index) => (index === names.length - 1 ? args.length : 0));
281
+ const offsets = counts.map((_count, index) =>
282
+ counts.slice(0, index).reduce((total, count) => total + count, 0),
283
+ );
284
+ const getArgs = (index: number) => args.slice(offsets[index], offsets[index] + counts[index]);
285
+ const head = at({ kind: "Opaque", name: names[0] }, { location });
286
+ return at(
287
+ {
288
+ kind: "Bang",
289
+ expression:
290
+ counts[0] === 0
291
+ ? head
292
+ : at({ kind: "Apply", operator: head, args: getArgs(0) }, { location }),
293
+ selectors: names.slice(1).map((label, index) => ({
294
+ kind: "Label",
295
+ label: getCanonicalName(label),
296
+ args: getArgs(index + 1),
297
+ })),
298
+ },
299
+ { location },
300
+ );
301
+ };
302
+
303
+ const convertBounds = (bounds: QuantifierExpression["bounds"], isExcept: boolean): Bound[] =>
304
+ bounds.map((bound, index) => {
305
+ if (bound.pattern.kind === "Tuple")
306
+ throw new TlaProofError("Unexpected tuple declaration", { failure: TUPLY_FAILURE });
307
+ const isDitto = index > 0 && bounds[index - 1].set === bound.set;
308
+ return {
309
+ name: bound.pattern.name,
310
+ level: "Constant",
311
+ domain: isDitto
312
+ ? { kind: "Ditto" }
313
+ : { kind: "Domain", expression: convertExpression(bound.set, isExcept) },
314
+ };
315
+ });
316
+
317
+ const getTuplyBounds = (
318
+ bounds: QuantifierExpression["bounds"],
319
+ isExcept: boolean,
320
+ ): TuplyBound[] =>
321
+ bounds.map((bound, index) => {
322
+ const domain = convertExpression(bound.set, isExcept);
323
+ if (bound.pattern.kind === "Tuple")
324
+ return { kind: "Tuple", names: bound.pattern.names, domain };
325
+ const isDitto = index > 0 && bounds[index - 1].set === bound.set;
326
+ return {
327
+ kind: "Name",
328
+ name: bound.pattern.name,
329
+ domain: isDitto ? { kind: "Ditto" } : { kind: "Domain", expression: domain },
330
+ };
331
+ });
332
+
333
+ const expandTuples = (expand: () => Expr, location: Locus) => {
334
+ if (!isExpanded)
335
+ throw new TlaProofError("Unexpected tuple declaration", { failure: TUPLY_FAILURE });
336
+ return at(expand(), { location });
337
+ };
338
+
339
+ const hasTuple = (bounds: QuantifierExpression["bounds"]) =>
340
+ bounds.some((bound) => bound.pattern.kind === "Tuple");
341
+
342
+ const convertExpression = (expression: Expression, isExcept: boolean): Expr => {
343
+ const location = getLocus(expression.location);
344
+ const recurse = (inner: Expression) => convertExpression(inner, isExcept);
345
+ // tlapm locates an operator application by its operator and operands, and a parenthesized
346
+ // operand by what is inside the parentheses.
347
+ const getOperatorLocus = (args: Expr[], fixity: Fixity) => {
348
+ if (args.length === 2) return mergeLoci(args[0].location, args[1].location);
349
+ if (args.length !== 1 || !args[0].location) return location;
350
+ if (fixity === "Prefix") {
351
+ return mergeLoci(
352
+ { ...location, endLine: location.startLine, endColumn: location.startColumn + 1 },
353
+ args[0].location,
354
+ );
355
+ }
356
+ if (fixity === "Postfix") {
357
+ return mergeLoci(
358
+ { ...location, startLine: location.endLine, startColumn: location.endColumn - 1 },
359
+ args[0].location,
360
+ );
361
+ }
362
+ return location;
363
+ };
364
+ const applyPrefix = (builtin: Builtin, operand: Expr): Expr =>
365
+ at(
366
+ { kind: "Apply", operator: { kind: "Internal", builtin }, args: [operand] },
367
+ { location: getOperatorLocus([operand], "Prefix") },
368
+ );
369
+ switch (expression.kind) {
370
+ case "Number":
371
+ return at(
372
+ { kind: "Num", integer: expression.text ?? String(expression.value), fraction: "" },
373
+ { location },
374
+ );
375
+ case "Decimal": {
376
+ const [integer, fraction] = expression.text.split(".");
377
+ return at({ kind: "Num", integer, fraction }, { location });
378
+ }
379
+ case "String":
380
+ return at({ kind: "String", value: expression.value }, { location });
381
+ case "Identifier":
382
+ return getName(expression.name, [], location);
383
+ case "Apply": {
384
+ if (expression.args.length === 2) {
385
+ const regrouped = regroup(expression);
386
+ if (regrouped !== expression) return recurse(regrouped);
387
+ }
388
+ const args = expression.args.map(recurse);
389
+ const operator = lookupOperator(expression.operator);
390
+ if (operator.fixity !== "Nonfix") {
391
+ return at(
392
+ { kind: "Apply", operator: getOperator(expression.operator, undefined), args },
393
+ { location: getOperatorLocus(args, operator.fixity) },
394
+ );
395
+ }
396
+ return getName(expression.operator, args, location, expression.argumentCounts);
397
+ }
398
+ case "OperatorReference":
399
+ return getOperator(expression.operator, location);
400
+ case "Lambda":
401
+ return at(
402
+ {
403
+ kind: "Lambda",
404
+ params: expression.params.map((name) => ({ name, shape: 0 })),
405
+ body: recurse(expression.body),
406
+ },
407
+ { location },
408
+ );
409
+ case "And":
410
+ case "Or": {
411
+ const items = expression.operands.map(recurse);
412
+ if (expression.isBulleted)
413
+ return at({ kind: "List", bullet: expression.kind, items }, { location });
414
+ const builtin = expression.kind === "And" ? "Conj" : "Disj";
415
+ return items.reduce((left, right) =>
416
+ at(
417
+ { kind: "Apply", operator: { kind: "Internal", builtin }, args: [left, right] },
418
+ { location: mergeLoci(left.location, right.location) },
419
+ ),
420
+ );
421
+ }
422
+ case "Prime": {
423
+ const operand = recurse(expression.expression);
424
+ return at(
425
+ { kind: "Apply", operator: { kind: "Internal", builtin: "Prime" }, args: [operand] },
426
+ { location: getOperatorLocus([operand], "Postfix") },
427
+ );
428
+ }
429
+ case "Unchanged":
430
+ return applyPrefix("UNCHANGED", recurse(expression.expression));
431
+ case "Enabled":
432
+ return applyPrefix("ENABLED", recurse(expression.expression));
433
+ case "If":
434
+ return at(
435
+ {
436
+ kind: "If",
437
+ condition: recurse(expression.condition),
438
+ consequent: recurse(expression.consequent),
439
+ alternative: recurse(expression.alternative),
440
+ },
441
+ { location },
442
+ );
443
+ case "Case":
444
+ return at(
445
+ {
446
+ kind: "Case",
447
+ arms: expression.arms.map((arm) => ({
448
+ condition: recurse(arm.condition),
449
+ body: recurse(arm.body),
450
+ })),
451
+ other: expression.other && recurse(expression.other),
452
+ },
453
+ { location },
454
+ );
455
+ case "Let":
456
+ return at(
457
+ {
458
+ kind: "Let",
459
+ definitions: [...expression.definitions, ...(expression.instances ?? [])]
460
+ .sort(
461
+ (left, right) =>
462
+ left.location.line - right.location.line ||
463
+ left.location.column - right.location.column,
464
+ )
465
+ .map((definition) => convertDefinition(definition, isExcept)),
466
+ body: recurse(expression.body),
467
+ },
468
+ { location },
469
+ );
470
+ case "Forall":
471
+ case "Exists":
472
+ if (hasTuple(expression.bounds)) {
473
+ const bounds = getTuplyBounds(expression.bounds, isExcept);
474
+ const body = recurse(expression.body);
475
+ return expandTuples(() => expandQuant(expression.kind, bounds, body), location);
476
+ }
477
+ return at(
478
+ {
479
+ kind: "Quant",
480
+ quantifier: expression.kind,
481
+ bounds: convertBounds(expression.bounds, isExcept),
482
+ body: recurse(expression.body),
483
+ },
484
+ { location },
485
+ );
486
+ case "UnboundedQuantifier": {
487
+ const body = recurse(expression.body);
488
+ const quantifier = expression.quantifier;
489
+ return expression.isTemporal
490
+ ? at({ kind: "Tquant", quantifier, names: expression.names, body }, { location })
491
+ : at(
492
+ { kind: "Quant", quantifier, bounds: getUnbounded(expression.names), body },
493
+ { location },
494
+ );
495
+ }
496
+ case "Choose":
497
+ if (expression.pattern.kind === "Tuple") {
498
+ const names = expression.pattern.names;
499
+ const domain = expression.set && recurse(expression.set);
500
+ const body = recurse(expression.body);
501
+ return expandTuples(() => expandChoose(names, domain, body), location);
502
+ }
503
+ return at(
504
+ {
505
+ kind: "Choose",
506
+ name: expression.pattern.name,
507
+ domain: expression.set && recurse(expression.set),
508
+ body: recurse(expression.body),
509
+ },
510
+ { location },
511
+ );
512
+ case "SetEnumeration":
513
+ return at({ kind: "SetEnum", elements: expression.elements.map(recurse) }, { location });
514
+ case "SetFilter":
515
+ if (expression.bound.pattern.kind === "Tuple") {
516
+ const names = expression.bound.pattern.names;
517
+ const domain = recurse(expression.bound.set);
518
+ const body = recurse(expression.predicate);
519
+ return expandTuples(() => expandSetSt(names, domain, body), location);
520
+ }
521
+ return at(
522
+ {
523
+ kind: "SetSt",
524
+ name: expression.bound.pattern.name,
525
+ domain: recurse(expression.bound.set),
526
+ body: recurse(expression.predicate),
527
+ },
528
+ { location },
529
+ );
530
+ case "SetMap":
531
+ if (hasTuple(expression.bounds)) {
532
+ const element = recurse(expression.expression);
533
+ const bounds = getTuplyBounds(expression.bounds, isExcept);
534
+ return expandTuples(() => expandSetOf(element, bounds), location);
535
+ }
536
+ return at(
537
+ {
538
+ kind: "SetOf",
539
+ element: recurse(expression.expression),
540
+ bounds: convertBounds(expression.bounds, isExcept),
541
+ },
542
+ { location },
543
+ );
544
+ case "Tuple":
545
+ // tlapm's tuple parser binds as (<< >>> elements) >>= ">>", which keeps only the location
546
+ // of the closing >>.
547
+ return at(
548
+ { kind: "Tuple", elements: expression.elements.map(recurse) },
549
+ {
550
+ location: {
551
+ ...location,
552
+ startLine: location.endLine,
553
+ startColumn: location.endColumn - 2,
554
+ },
555
+ },
556
+ );
557
+ case "FunctionConstruction":
558
+ if (hasTuple(expression.bounds)) {
559
+ const bounds = getTuplyBounds(expression.bounds, isExcept);
560
+ const body = recurse(expression.body);
561
+ return expandTuples(() => expandFcn(bounds, body), location);
562
+ }
563
+ return at(
564
+ {
565
+ kind: "Fcn",
566
+ bounds: convertBounds(expression.bounds, isExcept),
567
+ body: recurse(expression.body),
568
+ },
569
+ { location },
570
+ );
571
+ case "FunctionSet":
572
+ return at(
573
+ { kind: "Arrow", domain: recurse(expression.domain), range: recurse(expression.range) },
574
+ { location },
575
+ );
576
+ case "Record":
577
+ case "RecordSet":
578
+ return at(
579
+ {
580
+ kind: expression.kind === "Record" ? "Record" : "Rect",
581
+ fields: expression.fields.map((field) => ({
582
+ name: field.name,
583
+ value: recurse(field.value),
584
+ })),
585
+ },
586
+ { location },
587
+ );
588
+ case "Except":
589
+ return at(
590
+ {
591
+ kind: "Except",
592
+ function: recurse(expression.function),
593
+ specs: expression.updates.map((update) => ({
594
+ trail: update.path.map((point) =>
595
+ point.kind === "Field"
596
+ ? { kind: "Dot", field: point.name }
597
+ : {
598
+ kind: "Apply",
599
+ expression:
600
+ point.args.length === 1
601
+ ? recurse(point.args[0])
602
+ : { kind: "Tuple", elements: point.args.map(recurse) },
603
+ },
604
+ ),
605
+ value: convertExpression(update.value, true),
606
+ })),
607
+ },
608
+ { location },
609
+ );
610
+ case "ExceptAt":
611
+ return at({ kind: "At", isExcept }, { location });
612
+ case "FunctionApplication":
613
+ return at(
614
+ {
615
+ kind: "FcnApp",
616
+ function: recurse(expression.function),
617
+ args: expression.args.map(recurse),
618
+ },
619
+ { location },
620
+ );
621
+ case "FieldAccess":
622
+ return at(
623
+ { kind: "Dot", expression: recurse(expression.record), field: expression.field },
624
+ { location },
625
+ );
626
+ case "Product":
627
+ return at({ kind: "Product", factors: expression.factors.map(recurse) }, { location });
628
+ case "Always":
629
+ case "Eventually": {
630
+ const inner = expression.expression;
631
+ if (expression.kind === "Always" && inner.kind === "BoxAction") {
632
+ return at(
633
+ {
634
+ kind: "Tsub",
635
+ modal: "Box",
636
+ action: recurse(inner.action),
637
+ subscript: recurse(inner.subscript),
638
+ },
639
+ { location },
640
+ );
641
+ }
642
+ if (expression.kind === "Eventually" && inner.kind === "AngleAction") {
643
+ return at(
644
+ {
645
+ kind: "Tsub",
646
+ modal: "Dia",
647
+ action: recurse(inner.action),
648
+ subscript: recurse(inner.subscript),
649
+ },
650
+ { location },
651
+ );
652
+ }
653
+ return applyPrefix(expression.kind === "Always" ? "BoxTrue" : "Diamond", recurse(inner));
654
+ }
655
+ case "BoxAction":
656
+ case "AngleAction":
657
+ return at(
658
+ {
659
+ kind: "Sub",
660
+ modal: expression.kind === "BoxAction" ? "Box" : "Dia",
661
+ action: recurse(expression.action),
662
+ subscript: recurse(expression.subscript),
663
+ },
664
+ { location },
665
+ );
666
+ case "WeakFairness":
667
+ case "StrongFairness":
668
+ return at(
669
+ {
670
+ kind: "Fair",
671
+ fairness: expression.kind === "WeakFairness" ? "Weak" : "Strong",
672
+ subscript: recurse(expression.subscript),
673
+ action: recurse(expression.action),
674
+ },
675
+ { location },
676
+ );
677
+ case "Label":
678
+ return at(
679
+ {
680
+ kind: "Parens",
681
+ expression: recurse(expression.body),
682
+ form: { kind: "Nlabel", label: expression.name, names: expression.params },
683
+ },
684
+ { location },
685
+ );
686
+ case "Subexpression": {
687
+ const [head, ...selectors] = expression.selectors;
688
+ if (head.kind !== "Name") throw new TlaProofError("Unexpected subexpression reference");
689
+ const operator = at({ kind: "Opaque", name: head.name }, { location });
690
+ const base =
691
+ head.args.length === 0
692
+ ? operator
693
+ : at({ kind: "Apply", operator, args: head.args.map(recurse) }, { location });
694
+ return at(
695
+ {
696
+ kind: "Bang",
697
+ expression: base,
698
+ selectors: selectors.map((selector): Selector => {
699
+ switch (selector.kind) {
700
+ case "Name":
701
+ return {
702
+ kind: "Label",
703
+ label: getCanonicalName(selector.name),
704
+ args: selector.args.map(recurse),
705
+ };
706
+ case "Operand":
707
+ if (selector.operand === "<<") return { kind: "Left" };
708
+ if (selector.operand === ">>") return { kind: "Right" };
709
+ return { kind: "Number", value: selector.operand };
710
+ case "Bound":
711
+ return { kind: "Instance", args: selector.args.map(recurse) };
712
+ case "At":
713
+ return { kind: "At" };
714
+ case "Colon":
715
+ return { kind: "Down" };
716
+ }
717
+ }),
718
+ },
719
+ { location },
720
+ );
721
+ }
722
+ }
723
+ };
724
+
725
+ const convertInstance = (instance: Instance): InstanceDefinition => ({
726
+ location: getRangeLocus(instance.definitionRange ?? instance.instanceRange),
727
+ args: instance.params.map((param) => param.name),
728
+ module: instance.module,
729
+ substitutions: instance.substitutions.map((substitution) => ({
730
+ name: getCanonicalName(substitution.name),
731
+ expression: convertExpression(substitution.expression, false),
732
+ })),
733
+ });
734
+
735
+ const convertDefinition = (definition: Definition | Instance, isExcept = false): Defn => {
736
+ const location = getLocus(definition.location);
737
+ if (definition.kind === "Instance") {
738
+ if (definition.name === undefined) throw new TlaProofError("Unexpected keyword INSTANCE");
739
+ return {
740
+ kind: "Instance",
741
+ name: definition.name,
742
+ instance: convertInstance(definition),
743
+ location,
744
+ };
745
+ }
746
+ const name = getCanonicalName(
747
+ definition.name,
748
+ definition.kind === "Operator" && definition.params.length === 1 && definition.name === "-",
749
+ );
750
+ const body = convertExpression(definition.body, isExcept);
751
+ if (definition.kind === "Function" && hasTuple(definition.bounds)) {
752
+ const bounds = getTuplyBounds(definition.bounds, isExcept);
753
+ return {
754
+ kind: "Operator",
755
+ name,
756
+ body: expandTuples(() => expandFcn(bounds, body), location),
757
+ location,
758
+ };
759
+ }
760
+ if (definition.kind === "Function") {
761
+ const bounds = convertBounds(definition.bounds, isExcept);
762
+ return {
763
+ kind: "Operator",
764
+ name,
765
+ body: at({ kind: "Fcn", bounds, body }, { location }),
766
+ location,
767
+ };
768
+ }
769
+ if (definition.params.length === 0) return { kind: "Operator", name, body, location };
770
+ return {
771
+ kind: "Operator",
772
+ name,
773
+ body: at(
774
+ {
775
+ kind: "Lambda",
776
+ params: definition.params.map((param) => ({
777
+ name: getCanonicalName(param.name),
778
+ shape: param.arity,
779
+ ...(param.location && { location: getNameLocus(param.name, param.location) }),
780
+ })),
781
+ body,
782
+ },
783
+ { location },
784
+ ),
785
+ location,
786
+ };
787
+ };
788
+
789
+ const convertNewSymbol = (symbol: NewSymbol): Hyp => {
790
+ const location = getLocus(symbol.location);
791
+ if (symbol.level === "Variable") return { kind: "Flex", name: symbol.name, location };
792
+ return {
793
+ kind: "Fresh",
794
+ name: symbol.name,
795
+ shape: symbol.arity,
796
+ level: symbol.level,
797
+ domain: symbol.set && convertExpression(symbol.set, false),
798
+ domainVisibility: "Visible",
799
+ location,
800
+ };
801
+ };
802
+
803
+ const convertSequent = (statement: Expression | AssumeProve): Sequent => {
804
+ if (statement.kind !== "AssumeProve")
805
+ return { context: [], active: convertExpression(statement, false) };
806
+ return {
807
+ context: statement.assumptions.map((assumption): Hyp => {
808
+ if (assumption.kind === "NewSymbol") return convertNewSymbol(assumption);
809
+ if (assumption.kind === "AssumeProve") {
810
+ const location = getLocus(assumption.location);
811
+ return {
812
+ kind: "Fact",
813
+ expression: at({ kind: "Sequent", sequent: convertSequent(assumption) }, { location }),
814
+ visibility: "Visible",
815
+ location,
816
+ };
817
+ }
818
+ const expression = convertFactLabel(assumption);
819
+ return { kind: "Fact", expression, visibility: "Visible", location: expression.location };
820
+ }),
821
+ active: convertExpression(statement.conclusion, false),
822
+ };
823
+ };
824
+
825
+ // A labeled hypothesis is an Xlabel without parameters, not an Nlabel like a labeled expression.
826
+ const convertFactLabel = (expression: Expression): Expr => {
827
+ if (expression.kind !== "Label" || expression.params.length > 0)
828
+ return convertExpression(expression, false);
829
+ const location = getLocus(expression.location);
830
+ return at(
831
+ {
832
+ kind: "Parens",
833
+ expression: convertExpression(expression.body, false),
834
+ form: { kind: "Xlabel", label: expression.name, indices: [] },
835
+ },
836
+ { location },
837
+ );
838
+ };
839
+
840
+ const convertFact = (fact: Fact): Expr => {
841
+ if (fact.kind === "ModuleFact") {
842
+ throw new TlaUnsupportedError("MODULE facts in proof obligations are not supported");
843
+ }
844
+ const expression = convertExpression(fact, false);
845
+ return expression.kind === "Bang" && expression.selectors.length === 0
846
+ ? expression.expression
847
+ : expression;
848
+ };
849
+
850
+ const convertDef = (fact: Fact): UseDef => {
851
+ const location = getLocus(fact.location);
852
+ if (fact.kind === "ModuleFact") return { kind: "Dvar", name: fact.name, location };
853
+ if (fact.kind === "Identifier")
854
+ return { kind: "Dvar", name: getCanonicalName(fact.name), location };
855
+ if (fact.kind === "OperatorReference")
856
+ return { kind: "Dvar", name: getCanonicalName(fact.operator), location };
857
+ throw new TlaUnsupportedError("This DEF clause in proof obligations is not supported");
858
+ };
859
+
860
+ const convertUsable = (use: { facts: Fact[]; defs: Fact[] }): Usable => ({
861
+ facts: use.facts.map(convertFact),
862
+ defs: use.defs.map(convertDef),
863
+ });
864
+
865
+ const getStepNumber = (token: RegExpExecArray | null, level: number): StepNumber => {
866
+ const label = token ? token[2] : "";
867
+ return label === ""
868
+ ? { kind: "Unnamed", level, id: getUnique(counter) }
869
+ : { kind: "Named", level, label, isAnonymous: false };
870
+ };
871
+
872
+ // to_proof: a missing proof is implicitly omitted where the enclosing step is.
873
+ const convertProof = (
874
+ proof: ProofStep["proof"],
875
+ level: number,
876
+ enclosing: Locus | undefined,
877
+ ): Proof => {
878
+ if (!proof) return { kind: "Omitted", omission: "Implicit", location: enclosing };
879
+ switch (proof.kind) {
880
+ case "Obvious":
881
+ return { kind: "Obvious", location: withPragma(getRangeLocus(proof.keyword)) };
882
+ case "Omitted":
883
+ return {
884
+ kind: "Omitted",
885
+ omission: "Explicit",
886
+ location: getRangeLocus(proof.keyword),
887
+ };
888
+ case "By":
889
+ return {
890
+ kind: "By",
891
+ usable: convertUsable(proof),
892
+ isOnly: proof.isOnly,
893
+ location: getRangeLocus(proof.keyword),
894
+ step: { kind: "Unnamed", level, id: 0 },
895
+ };
896
+ case "Steps": {
897
+ // A <*> first step takes the enclosing level only after PROOF, and the top level
898
+ // always counts as PROOF.
899
+ const tokens = proof.steps.map((step) => STEP_PATTERN.exec(step.token));
900
+ const first = tokens[0];
901
+ const stepLevel = first && first[1] !== "*" && first[1] !== "+" ? Number(first[1]) : level;
902
+ // The parser ends the steps at the first QED.
903
+ const steps = proof.steps
904
+ .slice(0, -1)
905
+ .map((step, index) =>
906
+ convertStep(step, getStepNumber(tokens[index], stepLevel), stepLevel),
907
+ );
908
+ const last = proof.steps[proof.steps.length - 1];
909
+ const number = getStepNumber(tokens[tokens.length - 1], stepLevel);
910
+ const head = getRangeLocus(last.head);
911
+ const qedProof = convertProof(last.proof, stepLevel + 1, head);
912
+ const qed = { proof: qedProof, location: mergeLoci(head, qedProof.location), step: number };
913
+ const location = steps.reduce<Locus | undefined>(
914
+ (merged, step) => mergeLoci(merged, step.location),
915
+ qed.location,
916
+ );
917
+ return { kind: "Steps", steps, qed, location, step: qed.step };
918
+ }
919
+ }
920
+ };
921
+
922
+ // tlapm locates a USE or HIDE by its keywords and DEF clause, since its parser drops the
923
+ // facts' location.
924
+ const getUseLocus = (use: UseOrHide) =>
925
+ mergeLoci(
926
+ getRangeLocus(use.keyword),
927
+ use.defs.length > 0 ? getLocus(use.defs[use.defs.length - 1].location) : undefined,
928
+ );
929
+
930
+ const convertStep = (step: ProofStep, number: StepNumber, level: number): Step => {
931
+ const head = getRangeLocus(step.head);
932
+ const body = step.body;
933
+ const withProof = (make: (proof: Proof) => Step): Step => {
934
+ const proof = convertProof(step.proof, level + 1, head);
935
+ return { ...make(proof), location: mergeLoci(head, proof.location), step: number };
936
+ };
937
+ const plain = (converted: Step): Step => ({ ...converted, location: head, step: number });
938
+ switch (body.kind) {
939
+ case "Use":
940
+ case "Hide": {
941
+ const location = mergeLoci(
942
+ getRangeLocus({ ...step.head, end: step.head.start }),
943
+ getUseLocus(body),
944
+ );
945
+ const usable = convertUsable(body);
946
+ return body.kind === "Use"
947
+ ? { kind: "Use", usable, isOnly: body.isOnly, location, step: number }
948
+ : { kind: "Hide", usable, location, step: number };
949
+ }
950
+ case "Instance":
951
+ throw new TlaProofError("Unexpected keyword INSTANCE");
952
+ case "Define":
953
+ return plain({
954
+ kind: "Define",
955
+ definitions: body.definitions.map((definition) => convertDefinition(definition)),
956
+ });
957
+ case "Have":
958
+ return plain({ kind: "Have", expression: convertExpression(body.expression, false) });
959
+ case "Take":
960
+ return plain({
961
+ kind: "Take",
962
+ bounds:
963
+ body.names.length > 0 ? getUnbounded(body.names) : convertBounds(body.bounds, false),
964
+ });
965
+ case "Witness":
966
+ return plain({
967
+ kind: "Witness",
968
+ expressions: body.expressions.map((expression) => convertExpression(expression, false)),
969
+ });
970
+ case "Pick": {
971
+ const bounds =
972
+ body.names.length > 0 ? getUnbounded(body.names) : convertBounds(body.bounds, false);
973
+ const expression = convertExpression(body.body, false);
974
+ return withProof((proof) => ({ kind: "Pick", bounds, expression, proof }));
975
+ }
976
+ case "Case": {
977
+ const expression = convertExpression(body.expression, false);
978
+ return withProof((proof) => ({ kind: "Pcase", expression, proof }));
979
+ }
980
+ case "Assert": {
981
+ const sequent = convertSequent(body.statement);
982
+ return withProof((proof) =>
983
+ body.isSuffices
984
+ ? { kind: "Suffices", sequent, proof }
985
+ : { kind: "Assert", sequent, proof },
986
+ );
987
+ }
988
+ case "Qed":
989
+ throw new TlaUnsupportedError("QED in the middle of a proof");
990
+ }
991
+ };
992
+
993
+ const convertModule = (module: Module): Mule => ({
994
+ name: module.name,
995
+ extendees: module.extends,
996
+ extendeeLocations: module.extendsRanges.map(getRangeLocus),
997
+ isImportant: false,
998
+ location: getLocus(module.location),
999
+ body: module.units.map((unit): ModuleUnit => {
1000
+ switch (unit.kind) {
1001
+ case "Constants":
1002
+ case "Recursive":
1003
+ return {
1004
+ kind: unit.kind === "Constants" ? "Constants" : "Recursives",
1005
+ declarations: unit.declarations.map((declaration) => ({
1006
+ name: getCanonicalName(declaration.name),
1007
+ shape: declaration.arity,
1008
+ })),
1009
+ location: getLocus(unit.declarations[0].location),
1010
+ };
1011
+ case "Variables":
1012
+ return {
1013
+ kind: "Variables",
1014
+ names: unit.declarations.map((declaration) => declaration.name),
1015
+ location: getLocus(unit.declarations[0].location),
1016
+ };
1017
+ case "Definition":
1018
+ return {
1019
+ kind: "Definition",
1020
+ definition: convertDefinition(unit.definition),
1021
+ whereDefined: "User",
1022
+ visibility: "Hidden",
1023
+ isLocal: unit.definition.isLocal,
1024
+ location: getLocus(unit.definition.location),
1025
+ };
1026
+ case "Instance": {
1027
+ const instance = unit.instance;
1028
+ const location = getLocus(instance.location);
1029
+ return instance.name === undefined
1030
+ ? {
1031
+ kind: "Anoninst",
1032
+ instance: convertInstance(instance),
1033
+ isLocal: instance.isLocal,
1034
+ location,
1035
+ }
1036
+ : {
1037
+ kind: "Definition",
1038
+ definition: convertDefinition(instance),
1039
+ whereDefined: "User",
1040
+ visibility: "Hidden",
1041
+ isLocal: instance.isLocal,
1042
+ location: getRangeLocus(instance.instanceRange),
1043
+ };
1044
+ }
1045
+ case "Assumption":
1046
+ return {
1047
+ kind: "Axiom",
1048
+ name: unit.assumption.name,
1049
+ expression: convertExpression(unit.assumption.expression, false),
1050
+ location: getLocus(unit.assumption.location),
1051
+ };
1052
+ case "Theorem": {
1053
+ const theorem = unit.theorem;
1054
+ const location = getLocus(theorem.location);
1055
+ const end = withPragma(location);
1056
+ return {
1057
+ kind: "Theorem",
1058
+ name: theorem.name,
1059
+ sequent: convertSequent(theorem.statement),
1060
+ axiomCount: 0,
1061
+ proof: convertProof(theorem.proof, 0, {
1062
+ ...end,
1063
+ startLine: end.endLine,
1064
+ startColumn: end.endColumn,
1065
+ }),
1066
+ location,
1067
+ };
1068
+ }
1069
+ case "Use":
1070
+ return {
1071
+ kind: "Mutate",
1072
+ mode: unit.use.kind,
1073
+ isSuppressed: false,
1074
+ usable: convertUsable(unit.use),
1075
+ location: getUseLocus(unit.use),
1076
+ };
1077
+ case "Module":
1078
+ return { kind: "Submod", module: { ...convertModule(unit.module), isImportant: true } };
1079
+ }
1080
+ }),
1081
+ });
1082
+
1083
+ return { convertModule };
1084
+ };