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,1042 @@
1
+ // A port of tlapm's module pipeline: flattening EXTENDS (Module.Flatten), normalization
2
+ // (Module.Elab.normalize) and obligation generation (Module.Gen).
3
+
4
+ import { TlaProofError, TlapmBugError } from "./errors";
5
+ import { lambdify } from "./obligation-action";
6
+ import type { Anonymizer } from "./obligation-anon";
7
+ import {
8
+ adjoinAll,
9
+ at,
10
+ bumpBy,
11
+ consAll,
12
+ consSubstitution,
13
+ exprifySequent,
14
+ findNamed,
15
+ getAtPrefix,
16
+ getLocusText,
17
+ getLocusPrefix,
18
+ getScopeHyps,
19
+ mapAccumulate,
20
+ getFresh,
21
+ isEqualHyp,
22
+ shiftExpr,
23
+ substituteDefn,
24
+ substituteExpr,
25
+ substituteExprs,
26
+ substituteHyp,
27
+ substituteSequent,
28
+ shift,
29
+ type Builtin,
30
+ type Defn,
31
+ type Expr,
32
+ type Hyp,
33
+ type InstanceDefinition,
34
+ type Locus,
35
+ type ObligationWarning,
36
+ type Scope,
37
+ type Sequent,
38
+ type Substitution,
39
+ type Visibility,
40
+ type WhereDefined,
41
+ } from "./obligation-expression";
42
+ import {
43
+ EMPTY_SUMMARY,
44
+ getProofSummary,
45
+ joinSummaries,
46
+ type Generator,
47
+ type ProofSummary,
48
+ } from "./obligation-generate";
49
+ import {
50
+ getUnique,
51
+ type Counter,
52
+ type Obligation,
53
+ type Proof,
54
+ type Usable,
55
+ } from "./obligation-proof";
56
+
57
+ export type ModuleUnit = ModuleUnitCore & { location?: Locus };
58
+
59
+ type ModuleUnitCore =
60
+ | { kind: "Constants" | "Recursives"; declarations: { name: string; shape: number }[] }
61
+ | { kind: "Variables"; names: string[] }
62
+ | {
63
+ kind: "Definition";
64
+ definition: Defn;
65
+ whereDefined: WhereDefined;
66
+ visibility: Visibility;
67
+ isLocal: boolean;
68
+ }
69
+ | { kind: "Axiom"; name: string | undefined; expression: Expr }
70
+ | {
71
+ kind: "Theorem";
72
+ name: string | undefined;
73
+ sequent: Sequent;
74
+ axiomCount: number;
75
+ proof: Proof;
76
+ }
77
+ | { kind: "Submod"; module: Mule }
78
+ | { kind: "Mutate"; mode: "Use" | "Hide"; isSuppressed: boolean; usable: Usable }
79
+ | { kind: "Anoninst"; instance: InstanceDefinition; isLocal: boolean };
80
+
81
+ // depth is the size of the context the module was normalized in, which instantiation shifts
82
+ // its body by.
83
+ export interface Mule {
84
+ name: string;
85
+ extendees: string[];
86
+ extendeeLocations?: Locus[];
87
+ body: ModuleUnit[];
88
+ isImportant: boolean;
89
+ isStandard?: boolean;
90
+ depth?: number;
91
+ location?: Locus;
92
+ }
93
+
94
+ const getBuiltinConstant = (name: string, builtin: Builtin): ModuleUnit => ({
95
+ kind: "Definition",
96
+ definition: { kind: "Operator", name, body: { kind: "Internal", builtin } },
97
+ whereDefined: "Builtin",
98
+ visibility: "Visible",
99
+ isLocal: false,
100
+ });
101
+
102
+ const getBuiltinOperator = (arity: number, name: string, builtin: Builtin): ModuleUnit => ({
103
+ kind: "Definition",
104
+ definition: {
105
+ kind: "Operator",
106
+ name,
107
+ body: {
108
+ kind: "Lambda",
109
+ params: Array.from({ length: arity }, (_item, index) => ({
110
+ name: `x${index + 1}`,
111
+ shape: 0,
112
+ })),
113
+ body: {
114
+ kind: "Apply",
115
+ operator: { kind: "Internal", builtin },
116
+ args: Array.from({ length: arity }, (_item, index) => ({
117
+ kind: "Ix",
118
+ index: arity - index,
119
+ })),
120
+ },
121
+ },
122
+ },
123
+ whereDefined: "Builtin",
124
+ visibility: "Visible",
125
+ isLocal: false,
126
+ });
127
+
128
+ const NATURALS: ModuleUnit[] = [
129
+ getBuiltinConstant("Nat", "Nat"),
130
+ getBuiltinOperator(2, "+", "Plus"),
131
+ getBuiltinOperator(2, "-", "Minus"),
132
+ getBuiltinOperator(2, "*", "Times"),
133
+ getBuiltinOperator(2, "^", "Exp"),
134
+ getBuiltinOperator(2, "<", "Lt"),
135
+ getBuiltinOperator(2, ">", "Gt"),
136
+ getBuiltinOperator(2, "=<", "Lteq"),
137
+ getBuiltinOperator(2, ">=", "Gteq"),
138
+ getBuiltinOperator(2, "%", "Remainder"),
139
+ getBuiltinOperator(2, "\\div", "Quotient"),
140
+ getBuiltinOperator(2, "..", "Range"),
141
+ ];
142
+
143
+ const INTEGERS = [
144
+ ...NATURALS,
145
+ getBuiltinConstant("Int", "Int"),
146
+ getBuiltinOperator(1, "-.", "Uminus"),
147
+ ];
148
+
149
+ const REALS = [
150
+ ...INTEGERS,
151
+ getBuiltinConstant("Real", "Real"),
152
+ getBuiltinOperator(2, "/", "Ratio"),
153
+ getBuiltinConstant("Infinity", "Infinity"),
154
+ ];
155
+
156
+ const SEQUENCES = [
157
+ getBuiltinOperator(1, "Seq", "Seq"),
158
+ getBuiltinOperator(1, "Len", "Len"),
159
+ getBuiltinOperator(2, "BSeq", "BSeq"),
160
+ getBuiltinOperator(2, "\\o", "Cat"),
161
+ getBuiltinOperator(2, "Append", "Append"),
162
+ getBuiltinOperator(1, "Head", "Head"),
163
+ getBuiltinOperator(1, "Tail", "Tail"),
164
+ getBuiltinOperator(3, "SubSeq", "SubSeq"),
165
+ getBuiltinOperator(2, "SelectSeq", "SelectSeq"),
166
+ ];
167
+
168
+ const TLC = [
169
+ getBuiltinOperator(2, "Print", "Print"),
170
+ getBuiltinOperator(1, "PrintT", "PrintT"),
171
+ getBuiltinOperator(2, "Assert", "Assert"),
172
+ getBuiltinConstant("JavaTime", "JavaTime"),
173
+ getBuiltinOperator(1, "TLCGet", "TLCGet"),
174
+ getBuiltinOperator(2, "TLCSet", "TLCSet"),
175
+ getBuiltinOperator(2, ":>", "OneArg"),
176
+ getBuiltinOperator(2, "@@", "Extend"),
177
+ getBuiltinOperator(1, "Permutations", "Permutations"),
178
+ getBuiltinOperator(2, "SortSeq", "SortSeq"),
179
+ getBuiltinOperator(1, "RandomElement", "RandomElement"),
180
+ getBuiltinConstant("Any", "Any"),
181
+ getBuiltinOperator(1, "ToString", "ToString"),
182
+ ];
183
+
184
+ const getStandard = (name: string, body: ModuleUnit[]): Mule => ({
185
+ name,
186
+ extendees: [],
187
+ body,
188
+ isImportant: false,
189
+ isStandard: true,
190
+ });
191
+
192
+ // Module.Standard.initctx, apart from $TLAPM, whose Builtins! names no module of the corpora uses.
193
+ export const STANDARD_MODULES = new Map(
194
+ [
195
+ getStandard("Naturals", NATURALS),
196
+ getStandard("Integers", INTEGERS),
197
+ getStandard("Reals", REALS),
198
+ getStandard("Sequences", SEQUENCES),
199
+ getStandard("TLC", TLC),
200
+ ].map((module) => [module.name, module]),
201
+ );
202
+
203
+ const getUnitHyps = (unit: ModuleUnit): Hyp[] => {
204
+ const location = unit.location;
205
+ switch (unit.kind) {
206
+ case "Constants":
207
+ return unit.declarations.map((declaration) => ({
208
+ kind: "Fresh",
209
+ name: declaration.name,
210
+ shape: declaration.shape,
211
+ level: "Constant",
212
+ domain: undefined,
213
+ domainVisibility: "Visible",
214
+ location,
215
+ }));
216
+ case "Recursives":
217
+ return unit.declarations.map((declaration) => ({
218
+ kind: "Defn",
219
+ definition: {
220
+ kind: "Recursive",
221
+ name: declaration.name,
222
+ shape: declaration.shape,
223
+ location,
224
+ },
225
+ whereDefined: "User",
226
+ visibility: "Hidden",
227
+ isLocal: false,
228
+ location,
229
+ }));
230
+ case "Variables":
231
+ return unit.names.map((name) => ({ kind: "Flex", name, location }));
232
+ case "Definition":
233
+ return [
234
+ {
235
+ kind: "Defn",
236
+ definition: unit.definition,
237
+ whereDefined: unit.whereDefined,
238
+ visibility: unit.visibility,
239
+ isLocal: unit.isLocal,
240
+ location,
241
+ },
242
+ ];
243
+ case "Axiom":
244
+ return unit.name === undefined
245
+ ? [{ kind: "Fact", expression: unit.expression, visibility: "Hidden", location }]
246
+ : getNamedFact(unit.name, unit.expression, location);
247
+ case "Theorem": {
248
+ const sequent = substituteSequent(shift(-unit.axiomCount), {
249
+ ...unit.sequent,
250
+ context: unit.sequent.context.slice(unit.axiomCount),
251
+ });
252
+ const body = at(exprifySequent(sequent), unit);
253
+ return unit.name === undefined
254
+ ? [{ kind: "Fact", expression: body, visibility: "Hidden", location }]
255
+ : getNamedFact(unit.name, body, location);
256
+ }
257
+ case "Mutate":
258
+ return unit.mode === "Use"
259
+ ? unit.usable.facts.map((fact, index) => ({
260
+ kind: "Fact",
261
+ expression: shiftExpr(index, fact),
262
+ visibility: "Visible",
263
+ location,
264
+ }))
265
+ : [];
266
+ case "Submod":
267
+ case "Anoninst":
268
+ return [];
269
+ }
270
+ };
271
+
272
+ const getHypSize = (unit: ModuleUnit) =>
273
+ unit.kind === "Axiom" || unit.kind === "Theorem"
274
+ ? unit.name === undefined
275
+ ? 1
276
+ : 2
277
+ : getUnitHyps(unit).length;
278
+
279
+ const getNamedFact = (name: string, expression: Expr, location: Locus | undefined): Hyp[] => [
280
+ {
281
+ kind: "Defn",
282
+ definition: { kind: "Operator", name, body: expression, location },
283
+ whereDefined: "User",
284
+ visibility: "Visible",
285
+ isLocal: false,
286
+ location,
287
+ },
288
+ { kind: "Fact", expression: { kind: "Ix", index: 1, location }, visibility: "Hidden", location },
289
+ ];
290
+
291
+ // Module.Flatten.exports: what an extending module sees, with local definitions inlined like
292
+ // builtins and proofs omitted.
293
+ const getExports = (module: Mule): ModuleUnit[] =>
294
+ module.body.flatMap((unit): ModuleUnit[] => {
295
+ switch (unit.kind) {
296
+ case "Definition":
297
+ return [{ ...unit, whereDefined: unit.isLocal ? "Builtin" : unit.whereDefined }];
298
+ case "Mutate":
299
+ return [];
300
+ case "Theorem":
301
+ return [
302
+ { ...unit, proof: { kind: "Omitted", omission: "Elsewhere", location: unit.location } },
303
+ ];
304
+ case "Submod":
305
+ return [{ ...unit, module: { ...unit.module, body: getExports(unit.module) } }];
306
+ default:
307
+ return [unit];
308
+ }
309
+ });
310
+
311
+ // tlapm recurses on a module that extends itself until the stack overflows.
312
+ export const flatten = (
313
+ modules: Map<string, Mule>,
314
+ module: Mule,
315
+ seen: Set<string>,
316
+ extending = new Set([module.name]),
317
+ ): Mule => {
318
+ const extension: ModuleUnit[] = [];
319
+ for (const name of module.extendees) {
320
+ if (seen.has(name)) continue;
321
+ if (extending.has(name)) throw new TlapmBugError("Stack overflow");
322
+ const extended = modules.get(name);
323
+ if (!extended) throw new TlaProofError(`Module "${name}" unknown`);
324
+ extension.push(...getExports(flatten(modules, extended, seen, new Set([...extending, name]))));
325
+ seen.add(name);
326
+ }
327
+ return {
328
+ ...module,
329
+ body: [...extension, ...module.body].map((unit) =>
330
+ unit.kind === "Submod" ? { ...unit, module: flatten(modules, unit.module, new Set()) } : unit,
331
+ ),
332
+ };
333
+ };
334
+
335
+ // Module.Dep.external_deps: the modules a module extends or instantiates, including those of its
336
+ // submodules.
337
+ export const getDependencies = (module: Mule) => {
338
+ const dependencies = new Map<string, Locus | undefined>();
339
+ const addDependency = (name: string, location: Locus | undefined) => {
340
+ if (!dependencies.has(name)) dependencies.set(name, location);
341
+ };
342
+ const addExtendees = (extending: Mule) =>
343
+ extending.extendees.forEach((name, index) =>
344
+ addDependency(name, extending.extendeeLocations?.[index]),
345
+ );
346
+ const locals = new Set<string>();
347
+ const submodules = new Map<string, Mule>();
348
+ // Every INSTANCE definition inside a value, such as one in a LET.
349
+ const addInstances = (value: unknown) => {
350
+ if (typeof value !== "object" || value === null) return;
351
+ if ("instance" in value && isInstanceDefinition(value.instance)) {
352
+ addDependency(value.instance.module, value.instance.location);
353
+ }
354
+ Object.values(value).forEach(addInstances);
355
+ };
356
+ // The hypotheses of each unit, leaving out the proofs of theorems.
357
+ const visit = (unit: ModuleUnit) => {
358
+ if (unit.kind === "Theorem") addInstances(unit.sequent);
359
+ else if (unit.kind !== "Submod") addInstances(unit);
360
+ if (unit.kind !== "Submod") return;
361
+ locals.add(unit.module.name);
362
+ submodules.set(unit.module.name, unit.module);
363
+ addExtendees(unit.module);
364
+ unit.module.body.forEach(visit);
365
+ };
366
+ module.body.forEach(visit);
367
+ locals.forEach((name) => dependencies.delete(name));
368
+ addExtendees(module);
369
+ return {
370
+ dependencies: [...dependencies.keys()].sort(compareNames),
371
+ locations: dependencies,
372
+ submodules,
373
+ };
374
+ };
375
+
376
+ const isInstanceDefinition = (value: unknown): value is InstanceDefinition =>
377
+ typeof value === "object" &&
378
+ value !== null &&
379
+ "module" in value &&
380
+ typeof value.module === "string" &&
381
+ "substitutions" in value;
382
+
383
+ export const compareNames = (left: string, right: string) =>
384
+ left < right ? -1 : left > right ? 1 : 0;
385
+
386
+ // latestModules is the module context of the latest anonymization of an axiom, theorem or USE,
387
+ // which tlapm's anonymizer keeps using for the instances inside later definitions.
388
+ export interface Pipeline {
389
+ anonymizer: Anonymizer;
390
+ generator: Generator;
391
+ salts: Counter;
392
+ latestModules: Map<string, Mule>;
393
+ onWarning: (warning: ObligationWarning) => void;
394
+ }
395
+
396
+ const substituteUsable = (substitution: Substitution, usable: Usable): Usable => ({
397
+ facts: substituteExprs(substitution, usable.facts),
398
+ defs: usable.defs.map((def) => {
399
+ if (def.kind === "Dvar") return def;
400
+ const substituted = substituteExpr(substitution, { kind: "Ix", index: def.index });
401
+ if (substituted.kind !== "Ix") throw new TlaProofError("substitution produced bad definition");
402
+ return { ...def, index: substituted.index };
403
+ }),
404
+ });
405
+
406
+ // Module.Subst.app_modunit. Instantiated theorems carry omitted proofs, which a substitution leaves
407
+ // alone.
408
+ const substituteUnit = (
409
+ substitution: Substitution,
410
+ unit: ModuleUnit,
411
+ ): [Substitution, ModuleUnit] => {
412
+ switch (unit.kind) {
413
+ case "Constants":
414
+ case "Recursives":
415
+ return [bumpBy(unit.declarations.length, substitution), unit];
416
+ case "Variables":
417
+ return [bumpBy(unit.names.length, substitution), unit];
418
+ case "Definition":
419
+ return [
420
+ bumpBy(1, substitution),
421
+ { ...unit, definition: substituteDefn(substitution, unit.definition) },
422
+ ];
423
+ case "Axiom":
424
+ return [
425
+ bumpBy(getHypSize(unit), substitution),
426
+ { ...unit, expression: substituteExpr(substitution, unit.expression) },
427
+ ];
428
+ case "Theorem":
429
+ return [
430
+ bumpBy(getHypSize(unit), substitution),
431
+ { ...unit, sequent: substituteSequent(substitution, unit.sequent) },
432
+ ];
433
+ case "Submod":
434
+ return [
435
+ substitution,
436
+ {
437
+ ...unit,
438
+ module: { ...unit.module, body: substituteUnits(substitution, unit.module.body) },
439
+ },
440
+ ];
441
+ case "Mutate":
442
+ return [
443
+ unit.mode === "Hide" ? substitution : bumpBy(unit.usable.facts.length, substitution),
444
+ { ...unit, usable: substituteUsable(substitution, unit.usable) },
445
+ ];
446
+ case "Anoninst":
447
+ return [
448
+ substitution,
449
+ {
450
+ ...unit,
451
+ instance: {
452
+ ...unit.instance,
453
+ substitutions: unit.instance.substitutions.map((entry) => ({
454
+ name: entry.name,
455
+ expression: substituteExpr(substitution, entry.expression),
456
+ })),
457
+ },
458
+ },
459
+ ];
460
+ }
461
+ };
462
+
463
+ const substituteUnits = (substitution: Substitution, units: ModuleUnit[]) =>
464
+ mapAccumulate(substitution, units, substituteUnit)[1];
465
+
466
+ const lambdifyTheorem = (
467
+ pipeline: Pipeline,
468
+ scope: Scope | undefined,
469
+ name: string | undefined,
470
+ sequent: Sequent,
471
+ ) => {
472
+ try {
473
+ const lambdified = lambdify(pipeline.anonymizer, pipeline.onWarning, scope, {
474
+ kind: "Sequent",
475
+ sequent,
476
+ });
477
+ if (lambdified.kind !== "Sequent") throw new TlapmBugError();
478
+ return lambdified.sequent;
479
+ } catch (error) {
480
+ if (!(error instanceof TlaProofError) || error.failure === undefined) throw error;
481
+ const location = sequent.active.location;
482
+ const message = `${error.failure}\nCould not instantiate the theorem ${name === undefined ? "" : `named: ${name}, `}at: ${location ? getLocusText(location) : "<unknown location>"}`;
483
+ pipeline.onWarning({ text: `${getAtPrefix(location)}[ERROR]${message}`, channel: "stdout" });
484
+ pipeline.onWarning({ text: message, channel: "toolboxOnly" });
485
+ return { context: [], active: at({ kind: "Internal", builtin: "TRUE" }, sequent.active) };
486
+ }
487
+ };
488
+
489
+ const lambdifyUnit = (pipeline: Pipeline, scope: Scope | undefined, unit: ModuleUnit) => {
490
+ if (unit.kind === "Theorem") {
491
+ return { ...unit, sequent: lambdifyTheorem(pipeline, scope, unit.name, unit.sequent) };
492
+ }
493
+ if (unit.kind !== "Definition") return unit;
494
+ const definition = unit.definition;
495
+ if (definition.kind !== "Operator" && definition.kind !== "Bpragma") return unit;
496
+ const body = lambdify(pipeline.anonymizer, pipeline.onWarning, scope, definition.body);
497
+ return { ...unit, definition: { ...definition, body } };
498
+ };
499
+
500
+ // lambdify_enabled_cdot: the ENABLED and \cdot of each definition and theorem statement bind
501
+ // their variables before instantiation substitutes for them. A theorem that cannot be lambdified
502
+ // states TRUE.
503
+ const lambdifyUnits = (
504
+ pipeline: Pipeline,
505
+ scope: Scope | undefined,
506
+ units: ModuleUnit[],
507
+ ): [Scope | undefined, ModuleUnit[]] =>
508
+ mapAccumulate(scope, units, (current, unit): [Scope | undefined, ModuleUnit] => {
509
+ if (unit.kind === "Submod") {
510
+ const [inner, body] = lambdifyUnits(pipeline, current, unit.module.body);
511
+ return [inner, { ...unit, module: { ...unit.module, body } }];
512
+ }
513
+ const lambdified = lambdifyUnit(pipeline, current, unit);
514
+ return [adjoinAll(current, getUnitHyps(lambdified)), lambdified];
515
+ });
516
+
517
+ // apply_subst: each parameter declaration of the instancee gives way to its substitute.
518
+ const applyParameters = (units: ModuleUnit[], substitutes: Map<string, Expr>) => {
519
+ const body: ModuleUnit[] = [];
520
+ let size = 0;
521
+ let rest = units;
522
+ while (rest.length > 0) {
523
+ const [unit, ...tail] = rest;
524
+ const split =
525
+ unit.kind === "Constants"
526
+ ? {
527
+ name: unit.declarations[0].name,
528
+ remaining: { ...unit, declarations: unit.declarations.slice(1) },
529
+ }
530
+ : unit.kind === "Variables"
531
+ ? { name: unit.names[0], remaining: { ...unit, names: unit.names.slice(1) } }
532
+ : undefined;
533
+ if (split) {
534
+ const substitute = substitutes.get(split.name);
535
+ if (!substitute) throw new TlaProofError(`Parameter ${split.name} has no substitute`);
536
+ const isEmpty = getHypSize(split.remaining) === 0;
537
+ rest = substituteUnits(
538
+ consSubstitution(shiftExpr(size, substitute), shift(0)),
539
+ isEmpty ? tail : [split.remaining, ...tail],
540
+ );
541
+ continue;
542
+ }
543
+ body.push(unit);
544
+ size += getHypSize(unit);
545
+ rest = tail;
546
+ }
547
+ return body;
548
+ };
549
+
550
+ // resub_for: references to the instancee's own units become applications to the instance
551
+ // arguments, which the definitions now bind themselves.
552
+ const getLocalSubstitution = (
553
+ count: number,
554
+ argumentCount: number,
555
+ location: Locus | undefined,
556
+ ): Substitution => {
557
+ const args = Array.from({ length: argumentCount }, (_item, index) =>
558
+ at({ kind: "Ix", index: argumentCount - index }, { location }),
559
+ );
560
+ const parameters = Array.from({ length: argumentCount }, (_item, index) =>
561
+ at({ kind: "Ix", index: index + 1 }, { location }),
562
+ );
563
+ const locals = Array.from({ length: count }, (_item, index): Expr => {
564
+ const operator = at({ kind: "Ix", index: index + argumentCount + 1 }, { location });
565
+ return argumentCount > 0 ? at({ kind: "Apply", operator, args }, { location }) : operator;
566
+ });
567
+ return consAll([...locals, ...parameters].reverse(), shift(count + argumentCount));
568
+ };
569
+
570
+ const addParameters = (expression: Expr, args: string[]): Expr => {
571
+ const params = args.map((name) => ({ name, shape: 0 }));
572
+ if (expression.kind === "Lambda")
573
+ return { ...expression, params: [...params, ...expression.params] };
574
+ return args.length === 0
575
+ ? expression
576
+ : at({ kind: "Lambda", params, body: expression }, expression);
577
+ };
578
+
579
+ // localize: renames the instancee's units under the instance name, salts its local definitions
580
+ const localize = (
581
+ pipeline: Pipeline,
582
+ units: ModuleUnit[],
583
+ prefix: string,
584
+ instance: InstanceDefinition,
585
+ isLocal: boolean,
586
+ location: Locus | undefined,
587
+ ) => {
588
+ const getName = (name: string) => (prefix === "" ? name : `${prefix}!${name}`);
589
+ const argumentCount = instance.args.length;
590
+ const body: ModuleUnit[] = [];
591
+ let size = 0;
592
+ let rest = units;
593
+ let isComplained = argumentCount === 0;
594
+ const complain = (text: string) => {
595
+ if (isComplained) return;
596
+ isComplained = true;
597
+ pipeline.onWarning({ text, channel: "stderr" });
598
+ };
599
+ while (rest.length > 0) {
600
+ const [unit, ...tail] = rest;
601
+ rest = tail;
602
+ const localized = (expression: Expr) =>
603
+ addParameters(
604
+ substituteExpr(getLocalSubstitution(size, argumentCount, location), expression),
605
+ instance.args,
606
+ );
607
+ switch (unit.kind) {
608
+ case "Definition": {
609
+ const definition = unit.definition;
610
+ if (definition.kind !== "Operator" && definition.kind !== "Bpragma") {
611
+ throw new TlaProofError("Module.Elab.localize");
612
+ }
613
+ const name = unit.isLocal
614
+ ? `${definition.name}$${getUnique(pipeline.salts)}`
615
+ : definition.name;
616
+ body.push({
617
+ ...unit,
618
+ definition: { ...definition, name: getName(name), body: localized(definition.body) },
619
+ whereDefined: unit.isLocal ? "Builtin" : unit.whereDefined,
620
+ isLocal: unit.isLocal || isLocal,
621
+ });
622
+ size += 1;
623
+ continue;
624
+ }
625
+ case "Axiom": {
626
+ complain(
627
+ `${getLocusPrefix(location)}Instancing produces parametric axioms.\nSuch axioms will be forcibly hidden and cannot be cited.\n(This warning appears at-most once per INSTANCE declaration.)`,
628
+ );
629
+ const axiom: ModuleUnit = {
630
+ ...unit,
631
+ name: unit.name === undefined ? undefined : getName(unit.name),
632
+ expression: localized(unit.expression),
633
+ };
634
+ body.push(axiom);
635
+ size += getHypSize(axiom);
636
+ continue;
637
+ }
638
+ case "Theorem": {
639
+ complain(
640
+ `${getAtPrefix(location)}Warning: Instancing produces parametric theorems.\nSuch theorems will be forcibly hidden and cannot be cited.\n(This warning appears at-most once per INSTANCE declaration.)`,
641
+ );
642
+ const expression = localized(at(exprifySequent(unit.sequent), unit));
643
+ body.push({
644
+ ...unit,
645
+ name: unit.name === undefined ? undefined : getName(unit.name),
646
+ sequent:
647
+ expression.kind === "Sequent"
648
+ ? expression.sequent
649
+ : { context: [], active: expression },
650
+ proof: { kind: "Omitted", omission: "Elsewhere", location: unit.location },
651
+ });
652
+ size += 1;
653
+ continue;
654
+ }
655
+ case "Mutate":
656
+ if (unit.mode === "Use") rest = substituteUnits(shift(-unit.usable.facts.length), rest);
657
+ continue;
658
+ case "Submod":
659
+ continue;
660
+ default:
661
+ throw new TlaProofError("Module.Elab.localize");
662
+ }
663
+ }
664
+ return body;
665
+ };
666
+
667
+ // localize_axioms: an axiom becomes a hypothesis of each theorem after it, and a named one also a
668
+ // definition.
669
+ const localizeAxioms = (units: ModuleUnit[]) => {
670
+ const body: ModuleUnit[] = [];
671
+ let rest = units;
672
+ while (rest.length > 0) {
673
+ const [unit, ...tail] = rest;
674
+ if (unit.kind !== "Axiom") {
675
+ body.push(unit);
676
+ rest = tail;
677
+ continue;
678
+ }
679
+ const location = unit.location;
680
+ if (unit.name !== undefined) {
681
+ body.push({
682
+ kind: "Definition",
683
+ definition: { kind: "Operator", name: unit.name, body: unit.expression, location },
684
+ whereDefined: "Proof",
685
+ visibility: "Visible",
686
+ isLocal: false,
687
+ location,
688
+ });
689
+ }
690
+ const hyp: Hyp = {
691
+ kind: "Fact",
692
+ expression:
693
+ unit.name === undefined
694
+ ? shiftExpr(1, unit.expression)
695
+ : at({ kind: "Ix", index: 2 }, unit),
696
+ visibility: "Visible",
697
+ location,
698
+ };
699
+ let distance = 0;
700
+ const inserted = tail.map((next): ModuleUnit => {
701
+ const size = distance;
702
+ distance += getHypSize(next);
703
+ if (next.kind !== "Theorem") return next;
704
+ const sequent = substituteSequent(shift(1), next.sequent);
705
+ return {
706
+ ...next,
707
+ sequent: { ...sequent, context: [substituteHyp(shift(size), hyp), ...sequent.context] },
708
+ axiomCount: next.axiomCount + 1,
709
+ };
710
+ });
711
+ rest = substituteUnits(shift(-1), inserted);
712
+ }
713
+ return body;
714
+ };
715
+
716
+ const getParameters = (module: Mule) =>
717
+ new Set(
718
+ module.body.flatMap((unit) =>
719
+ unit.kind === "Constants"
720
+ ? unit.declarations.map((declaration) => declaration.name)
721
+ : unit.kind === "Variables"
722
+ ? unit.names
723
+ : [],
724
+ ),
725
+ );
726
+
727
+ // Module.Elab.instantiate: the units of an instantiated module, in the context of the instance.
728
+ const instantiate = (
729
+ pipeline: Pipeline,
730
+ modules: Map<string, Mule>,
731
+ scope: Scope | undefined,
732
+ instance: InstanceDefinition,
733
+ isLocal: boolean,
734
+ prefix: string,
735
+ location: Locus | undefined,
736
+ ) => {
737
+ const module = modules.get(instance.module);
738
+ if (!module) throw new TlaProofError(`Module "${instance.module}" unknown`);
739
+ const parameters = getParameters(module);
740
+ for (const substitution of instance.substitutions) {
741
+ if (!parameters.has(substitution.name)) {
742
+ throw new TlaProofError(
743
+ `Parameter ${substitution.name} occurs in the statement \`INSTANCE ${instance.module} WITH\`, and is not declared in the module ${instance.module}`,
744
+ );
745
+ }
746
+ }
747
+ const instanceScope = adjoinAll(
748
+ scope,
749
+ instance.args.map((name) => getFresh(name, "Unknown")),
750
+ );
751
+ const substitutes = new Map(
752
+ instance.substitutions.map((entry) => [entry.name, entry.expression]),
753
+ );
754
+ for (const name of [...parameters].sort(compareNames)) {
755
+ if (substitutes.has(name)) continue;
756
+ if (!findNamed(instanceScope, name)) {
757
+ throw new TlaProofError(
758
+ `Required parameter ${name} for module ${instance.module} could not be inferred.`,
759
+ );
760
+ }
761
+ substitutes.set(name, at({ kind: "Opaque", name }, { location }));
762
+ }
763
+ const anonymized = new Map(
764
+ [...substitutes].map(([name, expression]) => [
765
+ name,
766
+ pipeline.anonymizer.expr(instanceScope, expression),
767
+ ]),
768
+ );
769
+ const body = substituteUnits(
770
+ shift((instanceScope?.size ?? 0) - (module.depth ?? 0)),
771
+ module.body.map((unit): ModuleUnit =>
772
+ unit.kind === "Theorem"
773
+ ? {
774
+ ...unit,
775
+ proof: { kind: "Omitted", omission: "Implicit", location: unit.proof.location },
776
+ }
777
+ : unit,
778
+ ),
779
+ );
780
+ const [, lambdified] = lambdifyUnits(pipeline, instanceScope, body);
781
+ return localizeAxioms(
782
+ localize(
783
+ pipeline,
784
+ applyParameters(lambdified, anonymized),
785
+ prefix,
786
+ instance,
787
+ isLocal,
788
+ location,
789
+ ),
790
+ );
791
+ };
792
+
793
+ // A named theorem binds its name as a proof definition, which its statement shifts past.
794
+ const bindTheoremName = (
795
+ name: string | undefined,
796
+ sequent: Sequent,
797
+ location: Locus | undefined,
798
+ ): { hyps: Hyp[]; sequent: Sequent } =>
799
+ name === undefined
800
+ ? { hyps: [], sequent }
801
+ : {
802
+ hyps: [
803
+ {
804
+ kind: "Defn",
805
+ definition: {
806
+ kind: "Operator",
807
+ name,
808
+ body: at(exprifySequent(sequent), { location }),
809
+ location,
810
+ },
811
+ whereDefined: "Proof",
812
+ visibility: "Visible",
813
+ isLocal: false,
814
+ location,
815
+ },
816
+ ],
817
+ sequent: substituteSequent(shift(1), sequent),
818
+ };
819
+
820
+ // make_defs: the definitions among the units an INSTANCE inside a LET or DEFINE stands for.
821
+ export const expandInstance = (
822
+ pipeline: Pipeline,
823
+ scope: Scope | undefined,
824
+ definition: Extract<Defn, { kind: "Instance" }>,
825
+ ): Defn[] => {
826
+ const name = definition.instance.module;
827
+ if (!pipeline.latestModules.has(name)) {
828
+ pipeline.onWarning({
829
+ text: `${getLocusPrefix(definition.instance.location)}Module ${JSON.stringify(name)} unknown\n`,
830
+ channel: "toolbox",
831
+ });
832
+ throw new TlaProofError(`Module ${JSON.stringify(name)} unknown`, {
833
+ failure: "Module.Elab.anon#defns",
834
+ });
835
+ }
836
+ const units = instantiate(
837
+ pipeline,
838
+ pipeline.latestModules,
839
+ scope,
840
+ definition.instance,
841
+ true,
842
+ definition.name,
843
+ definition.location,
844
+ );
845
+ const definitions: Defn[] = [];
846
+ let rest = units;
847
+ while (rest.length > 0) {
848
+ const [unit, ...tail] = rest;
849
+ if (unit.kind !== "Definition") {
850
+ rest = substituteUnits(shift(-getHypSize(unit)), tail);
851
+ continue;
852
+ }
853
+ definitions.push(unit.definition);
854
+ rest = tail;
855
+ }
856
+ return definitions;
857
+ };
858
+
859
+ // Module.Elab.normalize: anonymizes each unit in the context of the ones before it, expands
860
+ // instances, merges repeated definitions, and simplifies the proofs of important modules. The
861
+ // result also joins the modules that later instances can name.
862
+ export const normalize = (
863
+ pipeline: Pipeline,
864
+ modules: Map<string, Mule>,
865
+ scope: Scope | undefined,
866
+ module: Mule,
867
+ ): Mule => {
868
+ const body: ModuleUnit[] = [];
869
+ let current = scope;
870
+ const emit = (unit: ModuleUnit) => {
871
+ body.push(unit);
872
+ current = adjoinAll(current, getUnitHyps(unit));
873
+ };
874
+ const isRedundant = (name: string | undefined) =>
875
+ name !== undefined && findNamed(current, name) !== undefined;
876
+ // Instantiated units arrive anonymized.
877
+ const queue = module.body.map((unit) => ({ unit, isAnonymized: false }));
878
+ for (let index = 0; index < queue.length; index++) {
879
+ const unit = queue[index].unit;
880
+ const expand = (instance: InstanceDefinition, isLocal: boolean, prefix: string) => {
881
+ const units = instantiate(
882
+ pipeline,
883
+ modules,
884
+ current,
885
+ instance,
886
+ isLocal,
887
+ prefix,
888
+ unit.location,
889
+ );
890
+ queue.splice(index + 1, 0, ...units.map((inner) => ({ unit: inner, isAnonymized: true })));
891
+ };
892
+ switch (unit.kind) {
893
+ case "Anoninst":
894
+ expand(unit.instance, unit.isLocal, "");
895
+ continue;
896
+ case "Definition": {
897
+ const definition = unit.definition;
898
+ if (definition.kind === "Instance") {
899
+ expand(definition.instance, unit.isLocal, definition.name);
900
+ continue;
901
+ }
902
+ const anonymized =
903
+ definition.kind === "Recursive" || queue[index].isAnonymized
904
+ ? definition
905
+ : pipeline.anonymizer.defn(current, definition);
906
+ const normalized: ModuleUnit = { ...unit, definition: anonymized };
907
+ if (definition.kind === "Recursive") {
908
+ emit(normalized);
909
+ continue;
910
+ }
911
+ const hyp = getUnitHyps(normalized)[0];
912
+ const found = findNamed(current, definition.name);
913
+ if (found?.hyp.kind === "Defn") {
914
+ const isMerged =
915
+ found.hyp.definition.kind === "Recursive" ||
916
+ isEqualHyp(substituteHyp(shift(found.depth + 1), found.hyp), hyp);
917
+ if (isMerged) {
918
+ const rest = queue.slice(index + 1);
919
+ const substituted = substituteUnits(
920
+ consSubstitution(at({ kind: "Ix", index: found.depth + 1 }, unit), shift(0)),
921
+ rest.map((entry) => entry.unit),
922
+ );
923
+ queue.splice(
924
+ index + 1,
925
+ rest.length,
926
+ ...rest.map((entry, offset) => ({ ...entry, unit: substituted[offset] })),
927
+ );
928
+ continue;
929
+ }
930
+ }
931
+ emit(normalized);
932
+ continue;
933
+ }
934
+ case "Recursives":
935
+ case "Constants":
936
+ case "Variables":
937
+ emit(unit);
938
+ continue;
939
+ case "Axiom":
940
+ if (isRedundant(unit.name)) continue;
941
+ pipeline.latestModules = new Map(modules);
942
+ emit({ ...unit, expression: pipeline.anonymizer.expr(current, unit.expression) });
943
+ continue;
944
+ case "Theorem": {
945
+ const isDuplicate = isRedundant(unit.name);
946
+ const name = isDuplicate ? `(redundant)${unit.name}` : unit.name;
947
+ const proof: Proof = isDuplicate
948
+ ? { kind: "Obvious", location: unit.proof.location }
949
+ : unit.proof;
950
+ pipeline.latestModules = new Map(modules);
951
+ const sequent = pipeline.anonymizer.sequent(current, unit.sequent);
952
+ const binding = bindTheoremName(name, sequent, unit.location);
953
+ const proofScope = adjoinAll(adjoinAll(current, binding.hyps), binding.sequent.context);
954
+ const anonymized = pipeline.anonymizer.proof(proofScope, proof);
955
+ const simplified = module.isImportant
956
+ ? pipeline.generator.simplify(
957
+ getScopeHyps(proofScope),
958
+ binding.sequent.active,
959
+ anonymized,
960
+ )
961
+ : anonymized;
962
+ emit({ ...unit, name, sequent, proof: simplified });
963
+ continue;
964
+ }
965
+ case "Mutate":
966
+ pipeline.latestModules = new Map(modules);
967
+ emit({ ...unit, usable: pipeline.anonymizer.usable(current, unit.usable) });
968
+ continue;
969
+ case "Submod":
970
+ emit({ ...unit, module: normalize(pipeline, modules, current, unit.module) });
971
+ continue;
972
+ }
973
+ }
974
+ const normalized = { ...module, body, depth: scope?.size ?? 0 };
975
+ modules.set(module.name, normalized);
976
+ return normalized;
977
+ };
978
+
979
+ export interface TheoremSummary extends ProofSummary {
980
+ location: Locus | undefined;
981
+ }
982
+
983
+ export interface ModuleSummary extends ProofSummary {
984
+ theorems: TheoremSummary[];
985
+ }
986
+
987
+ // Module.Gen.generate: a named theorem binds its name as a proof definition for what follows.
988
+ export const generate = (
989
+ pipeline: Pipeline,
990
+ context: Hyp[],
991
+ module: Mule,
992
+ ): { obligations: Obligation[]; summary: ModuleSummary } => {
993
+ const obligations: Obligation[] = [];
994
+ const theorems: TheoremSummary[] = [];
995
+ let summary = EMPTY_SUMMARY;
996
+ let current = context;
997
+ for (const unit of module.body) {
998
+ switch (unit.kind) {
999
+ case "Theorem": {
1000
+ const binding = bindTheoremName(unit.name, unit.sequent, unit.location);
1001
+ const named = [...current, ...binding.hyps];
1002
+ const proof = pipeline.generator.generate(
1003
+ { context: [...named, ...binding.sequent.context], active: binding.sequent.active },
1004
+ unit.proof,
1005
+ );
1006
+ obligations.push(...pipeline.generator.collect(proof));
1007
+ const theorem = getProofSummary(proof);
1008
+ theorems.push({ ...theorem, location: unit.location });
1009
+ summary = joinSummaries(summary, theorem);
1010
+ const fact: Expr =
1011
+ unit.name === undefined
1012
+ ? at(exprifySequent(unit.sequent), unit)
1013
+ : { kind: "Ix", index: 1, location: unit.location };
1014
+ current = [
1015
+ ...named,
1016
+ { kind: "Fact", expression: fact, visibility: "Hidden", location: unit.location },
1017
+ ];
1018
+ continue;
1019
+ }
1020
+ case "Submod": {
1021
+ const generated = generate(pipeline, current, unit.module);
1022
+ obligations.push(...generated.obligations);
1023
+ summary = joinSummaries(summary, generated.summary);
1024
+ continue;
1025
+ }
1026
+ case "Mutate": {
1027
+ const [next, generated] = pipeline.generator.mutate(
1028
+ current,
1029
+ unit.mode,
1030
+ unit.usable,
1031
+ unit.location,
1032
+ );
1033
+ obligations.push(...generated);
1034
+ current = next;
1035
+ continue;
1036
+ }
1037
+ default:
1038
+ current = [...current, ...getUnitHyps(unit)];
1039
+ }
1040
+ }
1041
+ return { obligations, summary: { ...summary, theorems } };
1042
+ };