tla-ts 0.0.1 → 0.0.3-dev.a50fc89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +81 -42
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +215 -30
  4. package/dist/index.d.ts +215 -30
  5. package/dist/index.js +1 -1
  6. package/dist/parallel-worker.cjs +2 -2
  7. package/dist/parallel-worker.js +2 -2
  8. package/dist/parallel.cjs +1 -1
  9. package/dist/parallel.js +2 -2
  10. package/dist/run-cli.cjs +20 -1
  11. package/dist/run-cli.js +20 -1
  12. package/dist/transfer.cjs +388 -39
  13. package/dist/transfer.js +388 -39
  14. package/package.json +6 -2
  15. package/src/ast.ts +122 -5
  16. package/src/bags.ts +161 -71
  17. package/src/builtins.ts +203 -23
  18. package/src/bundled-modules.ts +63 -32
  19. package/src/check.ts +945 -238
  20. package/src/cli-options.ts +58 -0
  21. package/src/codegen.ts +52 -18
  22. package/src/config.ts +39 -11
  23. package/src/environment.ts +485 -140
  24. package/src/errors.ts +59 -1
  25. package/src/evaluate.ts +489 -137
  26. package/src/exit-codes.ts +75 -0
  27. package/src/fingerprints.ts +2 -2
  28. package/src/index.ts +9 -4
  29. package/src/instances.ts +221 -66
  30. package/src/java-overrides.ts +241 -0
  31. package/src/json.ts +153 -72
  32. package/src/levels.ts +850 -0
  33. package/src/lexer.ts +103 -28
  34. package/src/liveness.ts +47 -91
  35. package/src/obligation-action.ts +384 -0
  36. package/src/obligation-anon.ts +575 -0
  37. package/src/obligation-deref.ts +637 -0
  38. package/src/obligation-expression.ts +1060 -0
  39. package/src/obligation-format.ts +406 -0
  40. package/src/obligation-generate.ts +1074 -0
  41. package/src/obligation-levels.ts +329 -0
  42. package/src/obligation-module.ts +1042 -0
  43. package/src/obligation-printer.ts +889 -0
  44. package/src/obligation-proof.ts +76 -0
  45. package/src/obligation-source.ts +1084 -0
  46. package/src/obligation-tuples.ts +184 -0
  47. package/src/obligations.ts +316 -0
  48. package/src/parallel-worker.ts +57 -28
  49. package/src/parallel.ts +116 -62
  50. package/src/parser.ts +958 -205
  51. package/src/pretty-print.ts +259 -0
  52. package/src/randomization.ts +24 -113
  53. package/src/resolve.ts +472 -41
  54. package/src/run-cli.ts +427 -61
  55. package/src/runtime.ts +63 -28
  56. package/src/spec-description.ts +509 -0
  57. package/src/standard-levels.ts +105 -0
  58. package/src/standard-modules.ts +223 -92
  59. package/src/subexpressions.ts +21 -35
  60. package/src/substitution-levels.ts +184 -0
  61. package/src/successors.ts +261 -46
  62. package/src/symmetry.ts +65 -21
  63. package/src/tableau.ts +43 -37
  64. package/src/tlc-ext.ts +120 -33
  65. package/src/tlc-module.ts +71 -47
  66. package/src/tlc-modules.ts +1022 -0
  67. package/src/tlc-order.ts +497 -0
  68. package/src/tlc-random.ts +411 -0
  69. package/src/tlc-summary.ts +109 -0
  70. package/src/transfer.ts +26 -5
  71. package/src/values.ts +1231 -176
package/src/parser.ts CHANGED
@@ -1,24 +1,29 @@
1
1
  import type {
2
+ AssumeProve,
2
3
  Assumption,
3
4
  Bound,
4
5
  BoundPattern,
5
6
  CaseArm,
6
7
  Declaration,
8
+ DefineStep,
7
9
  Definition,
8
10
  ExceptPathStep,
9
- ExceptUpdate,
10
11
  Expression,
12
+ Fact,
11
13
  Module,
12
14
  Instance,
13
- OuterScope,
14
- InstanceSubstitution,
15
+ NewSymbol,
15
16
  OperatorParam,
16
- RecordField,
17
+ Proof,
18
+ ProofStep,
17
19
  Selector,
18
20
  SourceLocation,
21
+ SourceRange,
22
+ StepBody,
23
+ UseOrHide,
19
24
  } from "./ast";
20
25
  import { TlaSyntaxError } from "./errors";
21
- import { tokenize, type Token } from "./lexer";
26
+ import { getNumeralValue, tokenize, type Token } from "./lexer";
22
27
 
23
28
  interface InfixOperator {
24
29
  low: number;
@@ -26,7 +31,7 @@ interface InfixOperator {
26
31
  isAssociative: boolean;
27
32
  }
28
33
 
29
- const createInfix = (low: number, high: number, isAssociative: boolean): InfixOperator => ({
34
+ const createInfix = (low: number, high: number, isAssociative: boolean) => ({
30
35
  low,
31
36
  high,
32
37
  isAssociative,
@@ -134,6 +139,22 @@ const POSTFIX_OPERATORS = new Set(["^+", "^*", "^#"]);
134
139
 
135
140
  const PREFIX_OPERATOR_REFERENCES = new Set(["-", "~", "-."]);
136
141
 
142
+ const EXPRESSION_PREFIXES = new Set([
143
+ "-",
144
+ "~",
145
+ "/\\",
146
+ "\\/",
147
+ "SUBSET",
148
+ "UNION",
149
+ "DOMAIN",
150
+ "ENABLED",
151
+ "UNCHANGED",
152
+ "[]",
153
+ "<>",
154
+ ]);
155
+
156
+ const CIRCLED_OPERATORS = new Set(["+", "-", "/", "\\X"]);
157
+
137
158
  const OPERATOR_ARGUMENTS = new Set([
138
159
  "SUBSET",
139
160
  "UNION",
@@ -146,6 +167,9 @@ const OPERATOR_ARGUMENTS = new Set([
146
167
  ...POSTFIX_OPERATORS,
147
168
  ]);
148
169
 
170
+ const isOperatorName = (text: string) =>
171
+ INFIX_OPERATORS.has(text) || POSTFIX_OPERATORS.has(text) || text === "-.";
172
+
149
173
  const getOperatorApplication = (
150
174
  operator: string,
151
175
  argument: Expression,
@@ -187,27 +211,148 @@ const UNIT_KEYWORDS = new Set([
187
211
 
188
212
  const THEOREM_KEYWORDS = new Set(["THEOREM", "LEMMA", "COROLLARY", "PROPOSITION"]);
189
213
 
190
- const PROOF_WORDS = new Set(["BY", "OBVIOUS", "OMITTED", "QED", "USE", "HIDE"]);
214
+ const PROOF_STARTS = new Set(["BY", "PROOF", "OBVIOUS", "OMITTED"]);
215
+
216
+ const PROVABLE_STEPS = new Set(["Qed", "Pick", "Case", "Assert"]);
217
+
218
+ const TLAPM_PUNCTUATION = new Set([
219
+ ",",
220
+ ".",
221
+ "_",
222
+ "(",
223
+ ")",
224
+ "[",
225
+ "]",
226
+ "{",
227
+ "}",
228
+ "<<",
229
+ ">>",
230
+ "]_",
231
+ ">>_",
232
+ "==",
233
+ "!",
234
+ "@",
235
+ ":",
236
+ "::",
237
+ ";",
238
+ "->",
239
+ "<-",
240
+ "|->",
241
+ "\\A",
242
+ "\\AA",
243
+ "\\E",
244
+ "\\EE",
245
+ "\\forall",
246
+ "\\exists",
247
+ "WF_",
248
+ "SF_",
249
+ "(*{",
250
+ "}*)",
251
+ ]);
252
+
253
+ const UNNAMED_STEP_LABELS = new Set(["", "*", "-"]);
254
+
255
+ const FACT_KEYWORDS = new Set([
256
+ "CASE",
257
+ "CHOOSE",
258
+ "DOMAIN",
259
+ "ENABLED",
260
+ "IF",
261
+ "LAMBDA",
262
+ "LET",
263
+ "SF_",
264
+ "SUBSET",
265
+ "UNCHANGED",
266
+ "UNION",
267
+ "WF_",
268
+ ]);
269
+
270
+ const NEW_SYMBOL_LEVELS = new Map<string, NewSymbol["level"]>([
271
+ ["CONSTANT", "Constant"],
272
+ ["CONSTANTS", "Constant"],
273
+ ["VARIABLE", "Variable"],
274
+ ["VARIABLES", "Variable"],
275
+ ["STATE", "State"],
276
+ ["ACTION", "Action"],
277
+ ["ACTIONS", "Action"],
278
+ ["TEMPORAL", "Temporal"],
279
+ ["TEMPORALS", "Temporal"],
280
+ ]);
281
+
282
+ const NEW_SYMBOL_STARTS = new Set(["NEW", ...NEW_SYMBOL_LEVELS.keys()]);
283
+
284
+ const OPENING_BRACKETS = new Set(["(", "[", "{", "<<"]);
285
+
286
+ const CLOSING_BRACKETS = new Set([")", "]", "}", ">>", "]_", ">>_"]);
287
+
288
+ const BUILTIN_CONSTANTS = new Set(["TRUE", "FALSE", "BOOLEAN", "STRING"]);
191
289
 
192
290
  const NO_OFFSIDE = -1;
193
291
 
194
- export const parseModule = (source: string): Module => {
292
+ const getModuleHeader = (name: string) => new RegExp(`^-{4,}\\s*MODULE\\s+${name}\\s*-{3,}$`);
293
+
294
+ // The text of a module that starts on its own line in a file, as TLC's monolith extractor reads
295
+ // modules that follow the root module.
296
+ export const extractModule = (source: string, name: string) => {
297
+ const lines = source.split(/\r?\n/);
298
+ const start = lines.findIndex((line) => getModuleHeader(name).test(line));
299
+ if (start === -1) return undefined;
300
+ const end = lines.findIndex((line, index) => index > start && line.startsWith("===="));
301
+ return lines.slice(start, end === -1 ? undefined : end + 1).join("\n");
302
+ };
303
+
304
+ // isTlapm parses as tlapm does for proof obligations, which accepts and rejects some modules that
305
+ // SANY does not.
306
+ const parse = (source: string, isTlapm: boolean): Module => {
195
307
  const tokens = tokenize(source);
196
- const cursor = { index: 0, limit: tokens.length, offsideColumns: [] as number[] };
197
- const recursiveNames = new Set<string>();
308
+ const cursor = { index: 0, offsideColumns: [] as number[] };
309
+ // A RECURSIVE declaration covers the definitions of its own LET or module.
310
+ const recursiveScopes: Set<string>[] = [];
311
+ const factNames = new Set<string>();
312
+ // The NEW, TAKE, and PICK names in scope, which a DEF clause cannot cite.
313
+ const proofScopes: Set<string>[] = [];
314
+ // A fact may cite an operator of a parameterized instance without the instance's arguments.
315
+ const factStart = { index: -1 };
316
+ const openModules: Module[] = [];
317
+
318
+ const isRecursiveName = (name: string) => recursiveScopes[recursiveScopes.length - 1].has(name);
198
319
 
199
320
  const getLocation = (token: Token): SourceLocation => ({
200
321
  line: token.line,
201
- column: token.column,
322
+ column: isTlapm && token.kind === "String" ? token.endColumn : token.column,
323
+ range: undefined,
202
324
  });
203
325
 
326
+ const tokenStarts: SourceLocation[] = [];
327
+ const tokenEnds: SourceLocation[] = [];
328
+ const tokenRanges: SourceRange[] = [];
329
+
330
+ const getRange = (startIndex: number) => {
331
+ const endIndex = cursor.index - 1;
332
+ const cached = startIndex === endIndex ? tokenRanges[startIndex] : undefined;
333
+ if (cached) return cached;
334
+ const last = tokens[endIndex];
335
+ tokenStarts[startIndex] ??= getLocation(tokens[startIndex]);
336
+ tokenEnds[endIndex] ??= { line: last.line, column: last.endColumn };
337
+ const range = {
338
+ module: openModules[openModules.length - 1].name,
339
+ start: tokenStarts[startIndex],
340
+ end: tokenEnds[endIndex],
341
+ };
342
+ if (startIndex === endIndex) tokenRanges[startIndex] = range;
343
+ return range;
344
+ };
345
+
346
+ // Each node gets its own location object from getLocation, so the range can be set in place.
347
+ const withRange = <Node extends { location: SourceLocation }>(node: Node, startIndex: number) => {
348
+ node.location.range = getRange(startIndex);
349
+ return node;
350
+ };
351
+
204
352
  const peekRaw = (offset = 0) => tokens[Math.min(cursor.index + offset, tokens.length - 1)];
205
353
 
206
354
  const peek = (offset = 0) => {
207
- const token =
208
- cursor.index + offset >= cursor.limit
209
- ? { ...peekRaw(offset), kind: "End" as const, text: "" }
210
- : peekRaw(offset);
355
+ const token = peekRaw(offset);
211
356
  const offsideColumn = cursor.offsideColumns[cursor.offsideColumns.length - 1];
212
357
  if (offsideColumn === undefined || offsideColumn === NO_OFFSIDE || token.kind === "End") {
213
358
  return token;
@@ -216,12 +361,80 @@ export const parseModule = (source: string): Module => {
216
361
  };
217
362
 
218
363
  const describe = (token: Token) =>
219
- token.kind === "End" ? "end of input" : `${JSON.stringify(token.text)}`;
364
+ token.kind === "End" ? "end of input" : JSON.stringify(token.text);
365
+
366
+ const unitStart = { index: 0 };
367
+
368
+ const getDefinitionEnd = (fallback: { line: number; column: number }) => {
369
+ const equals = tokens
370
+ .slice(unitStart.index, cursor.index)
371
+ .find((token) => token.text === "==" && token.kind !== "String");
372
+ return equals ? { line: equals.line, column: equals.endColumn + 1 } : fallback;
373
+ };
374
+
375
+ // tlapm reads TRUE, FALSE, BOOLEAN, STRING and the other keywords only as keywords.
376
+ const isTlapmKeyword = (token: Token) =>
377
+ isTlapm && (token.kind === "Keyword" || BUILTIN_CONSTANTS.has(token.text));
378
+
379
+ const getPreviousEnd = (position: { line: number; column: number }) => {
380
+ const previous = tokens.findLast(
381
+ (token) =>
382
+ token.kind !== "End" &&
383
+ (token.line < position.line ||
384
+ (token.line === position.line && token.column < position.column)),
385
+ );
386
+ return previous ? { line: previous.line, column: previous.endColumn + 1 } : position;
387
+ };
220
388
 
221
- const createError = (message: string, token = peekRaw()) =>
222
- new TlaSyntaxError(`${message} at line ${token.line}, column ${token.column}`);
389
+ const getTlapmName = (token: Token) => {
390
+ if (/^-{4,}$/.test(token.text)) return "----";
391
+ if (/^={4,}$/.test(token.text)) return "====";
392
+ if (TLAPM_PUNCTUATION.has(token.text) || token.kind === "Number" || token.kind === "Decimal")
393
+ return token.text;
394
+ if (token.kind === "String") return JSON.stringify(token.text);
395
+ if (isTlapmKeyword(token)) return `keyword ${token.text}`;
396
+ return token.kind === "Identifier" ? `identifier ${token.text}` : `operator ${token.text}`;
397
+ };
223
398
 
224
- const isFieldName = (token: Token) => token.kind === "Identifier" || token.kind === "Keyword";
399
+ const createError = (
400
+ message: string,
401
+ token: { line: number; column: number } = peekRaw(),
402
+ unexpected?: Token,
403
+ ) => {
404
+ const end = tokens[tokens.length - 1];
405
+ const isEnd = token.line === end.line && token.column === end.column;
406
+ return new TlaSyntaxError(
407
+ `${message} at line ${token.line}, column ${token.column}`,
408
+ undefined,
409
+ getFailure(message, token, isEnd, unexpected),
410
+ );
411
+ };
412
+
413
+ const getFailure = (
414
+ message: string,
415
+ token: { line: number; column: number },
416
+ isEnd: boolean,
417
+ unexpected: Token | undefined,
418
+ ) => {
419
+ if (isTlapm && isEnd) {
420
+ const end = tokens[tokens.length - 1];
421
+ return { reason: "Unexpected EOF", ...getDefinitionEnd(getPreviousEnd(end)), isEnd };
422
+ }
423
+ if (isTlapm && unexpected) {
424
+ return {
425
+ reason: `Unexpected ${getTlapmName(unexpected)}`,
426
+ ...getPreviousEnd(unexpected),
427
+ isEnd,
428
+ };
429
+ }
430
+ return { reason: message, line: token.line, column: token.column, isEnd };
431
+ };
432
+
433
+ const createUnexpected = (message: string, token: Token = peekRaw()) =>
434
+ createError(message, token, token);
435
+
436
+ const isFieldName = (token: Token) =>
437
+ (token.kind === "Identifier" || token.kind === "Keyword") && !isTlapmKeyword(token);
225
438
 
226
439
  const isAt = (text: string, offset = 0) => {
227
440
  const token = peek(offset);
@@ -243,15 +456,16 @@ export const parseModule = (source: string): Module => {
243
456
 
244
457
  const expect = (text: string) => {
245
458
  if (!isAt(text))
246
- throw createError(`Expected ${JSON.stringify(text)} but found ${describe(peek())}`);
459
+ throw createUnexpected(`Expected ${JSON.stringify(text)} but found ${describe(peek())}`);
247
460
  return advance();
248
461
  };
249
462
 
250
463
  const expectIdentifier = () => {
251
464
  const token = peek();
252
465
  if (token.kind !== "Identifier") {
253
- throw createError(`Expected an identifier but found ${describe(token)}`);
466
+ throw createUnexpected(`Expected an identifier but found ${describe(token)}`);
254
467
  }
468
+ if (isTlapmKeyword(token)) throw createUnexpected(`Unexpected keyword ${token.text}`);
255
469
  cursor.index++;
256
470
  return token.text;
257
471
  };
@@ -304,7 +518,7 @@ export const parseModule = (source: string): Module => {
304
518
  return { kind: "Tuple", names };
305
519
  };
306
520
 
307
- const parseBounds = (): Bound[] =>
521
+ const parseBounds = () =>
308
522
  parseSeparated(() => {
309
523
  const patterns = parseSeparated(parsePattern);
310
524
  expect("\\in");
@@ -315,26 +529,81 @@ export const parseModule = (source: string): Module => {
315
529
  const parseSubscript = (): Expression => {
316
530
  const token = peek();
317
531
  if (token.kind === "Identifier") {
318
- cursor.index++;
319
- return { kind: "Identifier", name: token.text, location: getLocation(token) };
532
+ const start = cursor.index;
533
+ const names = [advance().text];
534
+ while (isAt("!") && peek(1).kind === "Identifier") {
535
+ advance();
536
+ names.push(expectIdentifier());
537
+ }
538
+ return withRange(
539
+ { kind: "Identifier", name: names.join("!"), location: getLocation(token) },
540
+ start,
541
+ );
320
542
  }
321
543
  if (isAt("<<") || isAt("(")) return parsePrimary();
322
- throw createError(`Expected a subscript but found ${describe(token)}`);
544
+ throw createUnexpected(`Expected a subscript but found ${describe(token)}`);
545
+ };
546
+
547
+ const isDefinitionAhead = (operator: Token) => {
548
+ const token = peek(1);
549
+ const next = peek(2);
550
+ if (token.kind === "Identifier" && next.text === "==") return true;
551
+ if (token.line === operator.line) return false;
552
+ if (token.text === "-.") return true;
553
+ return (
554
+ token.kind === "Identifier" &&
555
+ (next.text === "(" ||
556
+ next.text === "[" ||
557
+ (next.kind === "Operator" && isAt("==", POSTFIX_OPERATORS.has(next.text) ? 3 : 4)))
558
+ );
559
+ };
560
+
561
+ const isStandalone = (operator: Token) => {
562
+ if (!EXPRESSION_PREFIXES.has(operator.text)) return true;
563
+ const token = peek(1);
564
+ return (
565
+ token.kind === "End" ||
566
+ token.kind === "ModuleEnd" ||
567
+ token.kind === "Separator" ||
568
+ isAt(",", 1) ||
569
+ isAt(")", 1) ||
570
+ (token.kind === "Keyword" && UNIT_KEYWORDS.has(token.text)) ||
571
+ isDefinitionAhead(operator)
572
+ );
323
573
  };
324
574
 
325
575
  const parseArgument = (): Expression => {
326
576
  const token = peek();
327
- const next = peek(1);
328
577
  const isOperatorAlone =
329
578
  token.kind === "Operator" &&
330
579
  (INFIX_OPERATORS.has(token.text) || PREFIX_OPERATOR_REFERENCES.has(token.text)) &&
331
- (next.text === "," || next.text === ")");
580
+ isStandalone(token);
581
+ // tlapm reads a prefix operator as the start of an expression, and (+), (-), (/) and (\X) as
582
+ // the operators \oplus, \ominus, \oslash and \otimes.
583
+ const isTlapmIncomplete =
584
+ isTlapm &&
585
+ token.kind !== "String" &&
586
+ (PREFIX_OPERATOR_REFERENCES.has(token.text) ||
587
+ (OPERATOR_ARGUMENTS.has(token.text) &&
588
+ token.text !== "'" &&
589
+ !POSTFIX_OPERATORS.has(token.text)) ||
590
+ (CIRCLED_OPERATORS.has(token.text) &&
591
+ peek(-1).text === "(" &&
592
+ peek(-1).line === token.line &&
593
+ peek(-1).endColumn + 1 === token.column &&
594
+ peek(1).text === ")" &&
595
+ peek(1).line === token.line &&
596
+ peek(1).column === token.endColumn + 1));
597
+ if (isTlapmIncomplete && isStandalone(token)) throw createError("incomplete expression", token);
332
598
  if (isOperatorAlone) {
333
599
  cursor.index++;
334
- return { kind: "OperatorReference", operator: token.text, location: getLocation(token) };
600
+ return withRange(
601
+ { kind: "OperatorReference", operator: token.text, location: getLocation(token) },
602
+ cursor.index - 1,
603
+ );
335
604
  }
336
605
  const isPrefixAlone =
337
- OPERATOR_ARGUMENTS.has(token.text) && (next.text === "," || next.text === ")");
606
+ token.kind !== "String" && OPERATOR_ARGUMENTS.has(token.text) && isStandalone(token);
338
607
  if (isPrefixAlone) {
339
608
  cursor.index++;
340
609
  const location = getLocation(token);
@@ -350,10 +619,12 @@ export const parseModule = (source: string): Module => {
350
619
  };
351
620
  }
352
621
  if (isAt("LAMBDA")) {
622
+ const start = cursor.index;
353
623
  advance();
354
624
  const params = parseSeparated(expectIdentifier);
355
625
  expect(":");
356
- return { kind: "Lambda", params, body: parseExpression(), location: getLocation(token) };
626
+ const body = parseExpression();
627
+ return withRange({ kind: "Lambda", params, body, location: getLocation(token) }, start);
357
628
  }
358
629
  return parseExpression();
359
630
  };
@@ -378,6 +649,7 @@ export const parseModule = (source: string): Module => {
378
649
  return {
379
650
  kind: bullet.text === "/\\" ? "And" : "Or",
380
651
  operands,
652
+ isBulleted: true,
381
653
  location: getLocation(bullet),
382
654
  };
383
655
  };
@@ -387,7 +659,7 @@ export const parseModule = (source: string): Module => {
387
659
  const location = getLocation(start);
388
660
  if (peek().kind === "Identifier" && (isAt("|->", 1) || isAt(":", 1))) {
389
661
  const kind = isAt("|->", 1) ? "Record" : "RecordSet";
390
- const fields = parseSeparated((): RecordField => {
662
+ const fields = parseSeparated(() => {
391
663
  const name = expectIdentifier();
392
664
  expect(kind === "Record" ? "|->" : ":");
393
665
  return { name, value: parseExpression() };
@@ -409,13 +681,13 @@ export const parseModule = (source: string): Module => {
409
681
  return { kind: "FunctionSet", domain: expression, range, location };
410
682
  }
411
683
  if (accept("EXCEPT")) {
412
- const updates = parseSeparated((): ExceptUpdate => {
684
+ const updates = parseSeparated(() => {
413
685
  expect("!");
414
686
  const path: ExceptPathStep[] = [];
415
687
  while (isAt(".") || isAt("[")) {
416
688
  if (accept(".")) {
417
689
  if (!isFieldName(peek()))
418
- throw createError(`Expected a field name but found ${describe(peek())}`);
690
+ throw createUnexpected(`Expected a field name but found ${describe(peek())}`);
419
691
  path.push({ kind: "Field", name: advance().text });
420
692
  continue;
421
693
  }
@@ -468,10 +740,9 @@ export const parseModule = (source: string): Module => {
468
740
 
469
741
  const parseTuple = (start: Token): Expression => {
470
742
  const location = getLocation(start);
471
- const elements = withoutOffside(() => {
472
- if (isAt(">>") || isAt(">>_")) return [];
473
- return parseSeparated(() => parseExpression());
474
- });
743
+ const elements = withoutOffside(() =>
744
+ isAt(">>") || isAt(">>_") ? [] : parseSeparated(() => parseExpression()),
745
+ );
475
746
  if (accept(">>_")) {
476
747
  if (elements.length !== 1)
477
748
  throw createError("Expected a single action inside <<A>>_v", start);
@@ -492,23 +763,28 @@ export const parseModule = (source: string): Module => {
492
763
  }
493
764
  const names = parseSeparated(expectIdentifier);
494
765
  expect(":");
495
- return { kind: "UnboundedQuantifier", names, body: parseExpression(), isTemporal, location };
766
+ return {
767
+ kind: "UnboundedQuantifier",
768
+ quantifier: start.text === "\\A" || start.text === "\\AA" ? "Forall" : "Exists",
769
+ names,
770
+ body: parseExpression(),
771
+ isTemporal,
772
+ location,
773
+ };
496
774
  };
497
775
 
498
776
  const parseCase = (start: Token): Expression => {
499
777
  const arms: CaseArm[] = [];
500
- const other = { value: undefined as Expression | undefined };
501
778
  do {
502
779
  if (accept("OTHER")) {
503
780
  expect("->");
504
- other.value = parseExpression();
505
- break;
781
+ return { kind: "Case", arms, other: parseExpression(), location: getLocation(start) };
506
782
  }
507
783
  const condition = parseExpression();
508
784
  expect("->");
509
785
  arms.push({ condition, body: parseExpression() });
510
786
  } while (accept("[]"));
511
- return { kind: "Case", arms, other: other.value, location: getLocation(start) };
787
+ return { kind: "Case", arms, other: undefined, location: getLocation(start) };
512
788
  };
513
789
 
514
790
  const isSelectorAhead = () => {
@@ -527,7 +803,7 @@ export const parseModule = (source: string): Module => {
527
803
 
528
804
  const parseSelector = (): Selector => {
529
805
  const token = advance();
530
- if (token.kind === "Number") return { kind: "Operand", operand: Number(token.text) };
806
+ if (token.kind === "Number") return { kind: "Operand", operand: getNumeralValue(token.text) };
531
807
  switch (token.text) {
532
808
  case "<<":
533
809
  case ">>":
@@ -545,25 +821,52 @@ export const parseModule = (source: string): Module => {
545
821
  };
546
822
 
547
823
  const parsePrimary = (): Expression => {
824
+ const isFactStart = cursor.index === factStart.index;
548
825
  const token = advance();
549
826
  const location = getLocation(token);
550
- if (token.kind === "Number") return { kind: "Number", value: Number(token.text), location };
827
+ if (isTlapm && token.kind === "Keyword" && token.text === "LAMBDA") {
828
+ const params = parseSeparated(expectIdentifier);
829
+ expect(":");
830
+ return { kind: "Lambda", params, body: parseExpression(), location };
831
+ }
832
+ if (token.kind === "Number") {
833
+ if (isTlapm && /^\\[BOH]/.test(token.text)) throw createError("incomplete expression", token);
834
+ return { kind: "Number", value: getNumeralValue(token.text), text: token.text, location };
835
+ }
836
+ if (token.kind === "Decimal") return { kind: "Decimal", text: token.text, location };
551
837
  if (token.kind === "String") return { kind: "String", value: token.text, location };
552
- if (token.kind === "Identifier") {
838
+ const isStep = token.kind === "Step" && isStepReference(token);
839
+ if (isStep && getProofLevel() < 0 && proofLevels.length <= 1) {
840
+ throw createError("Step number used outside proof", token);
841
+ }
842
+ if (isStep && token.text[1] === "+") {
843
+ throw createError("<+> step number used in an expression", token);
844
+ }
845
+ if (token.kind === "Identifier" || isStep) {
846
+ const name = isStep ? (getStepName(token) ?? token.text) : token.text;
553
847
  if (isLabelAhead()) {
554
848
  const params = accept("(") ? parseSeparated(expectIdentifier) : [];
555
849
  if (params.length > 0) expect(")");
556
850
  expect("::");
557
- return { kind: "Label", name: token.text, params, body: parseExpression(), location };
851
+ return { kind: "Label", name, params, body: parseExpression(), location };
558
852
  }
559
853
  const hasArguments = isAt("(");
560
854
  const args = hasArguments ? parseArguments() : [];
561
855
  if (!isSelectorAhead()) {
562
856
  return hasArguments
563
- ? { kind: "Apply", operator: token.text, args, location }
564
- : { kind: "Identifier", name: token.text, location };
857
+ ? { kind: "Apply", operator: name, args, location }
858
+ : { kind: "Identifier", name, location };
565
859
  }
566
- const selectors: Selector[] = [{ kind: "Name", name: token.text, args }];
860
+ const instance = openModules
861
+ .flatMap((module) => module.instances)
862
+ .find((candidate) => candidate.name === name);
863
+ if (instance && !isFactStart && !isTlapm && instance.params.length !== args.length) {
864
+ throw createError(
865
+ `The operator ${name} requires ${instance.params.length} arguments`,
866
+ token,
867
+ );
868
+ }
869
+ const selectors: Selector[] = [{ kind: "Name", name, args }];
567
870
  while (isSelectorAhead()) {
568
871
  advance();
569
872
  selectors.push(parseSelector());
@@ -576,13 +879,15 @@ export const parseModule = (source: string): Module => {
576
879
  const qualifiedArgs = selectors.flatMap((selector) =>
577
880
  "args" in selector ? selector.args : [],
578
881
  );
579
- return qualifiedArgs.length > 0
580
- ? { kind: "Apply", operator, args: qualifiedArgs, location }
581
- : { kind: "Identifier", name: operator, location };
882
+ if (qualifiedArgs.length === 0) return { kind: "Identifier", name: operator, location };
883
+ const argumentCounts = selectors.map((selector) =>
884
+ "args" in selector ? selector.args.length : 0,
885
+ );
886
+ return argumentCounts.slice(0, -1).some((count) => count > 0)
887
+ ? { kind: "Apply", operator, args: qualifiedArgs, argumentCounts, location }
888
+ : { kind: "Apply", operator, args: qualifiedArgs, location };
582
889
  }
583
- const isOperatorName =
584
- INFIX_OPERATORS.has(token.text) || POSTFIX_OPERATORS.has(token.text) || token.text === "-.";
585
- if (token.kind === "Operator" && isOperatorName && isAt("("))
890
+ if (token.kind === "Operator" && isOperatorName(token.text) && isAt("("))
586
891
  return { kind: "Apply", operator: token.text, args: parseArguments(), location };
587
892
  switch (token.text) {
588
893
  case "(": {
@@ -615,15 +920,19 @@ export const parseModule = (source: string): Module => {
615
920
  case "LET": {
616
921
  const definitions: Definition[] = [];
617
922
  const instances: Instance[] = [];
923
+ recursiveScopes.push(new Set());
618
924
  while (!accept("IN")) {
925
+ if (isTlapm && isAt("RECURSIVE"))
926
+ throw createError("Unexpected keyword RECURSIVE", getDefinitionEnd(peekRaw()));
619
927
  if (accept("RECURSIVE")) {
620
- parseDeclarations().forEach((declaration) => recursiveNames.add(declaration.name));
928
+ declareRecursive();
621
929
  continue;
622
930
  }
623
931
  const unit = parseDefinition(false);
624
932
  if (unit.kind === "Instance") instances.push(unit);
625
933
  else definitions.push(unit);
626
934
  }
935
+ recursiveScopes.pop();
627
936
  const body = parseExpression();
628
937
  return instances.length === 0
629
938
  ? { kind: "Let", definitions, body, location }
@@ -652,7 +961,7 @@ export const parseModule = (source: string): Module => {
652
961
  };
653
962
  }
654
963
  }
655
- throw createError(`Unexpected ${describe(token)}`, token);
964
+ throw createUnexpected(`Unexpected ${describe(token)}`, token);
656
965
  };
657
966
 
658
967
  const parsePrefix = (): Expression => {
@@ -692,8 +1001,10 @@ export const parseModule = (source: string): Module => {
692
1001
  };
693
1002
 
694
1003
  const parseExpression = (minimumPrecedence = 0): Expression => {
1004
+ const start = cursor.index;
1005
+ const prefix = parsePrefix();
695
1006
  const state = {
696
- left: parsePrefix(),
1007
+ left: prefix.location.range ? prefix : withRange(prefix, start),
697
1008
  previous: undefined as (InfixOperator & { text: string }) | undefined,
698
1009
  };
699
1010
  while (true) {
@@ -702,24 +1013,31 @@ export const parseModule = (source: string): Module => {
702
1013
  const location = getLocation(token);
703
1014
  if (token.text === "'" && minimumPrecedence <= 15) {
704
1015
  advance();
705
- state.left = { kind: "Prime", expression: state.left, location };
1016
+ state.left = withRange({ kind: "Prime", expression: state.left, location }, start);
706
1017
  continue;
707
1018
  }
708
1019
  if (POSTFIX_OPERATORS.has(token.text) && minimumPrecedence <= 15) {
709
1020
  advance();
710
- state.left = { kind: "Apply", operator: token.text, args: [state.left], location };
1021
+ state.left = withRange(
1022
+ { kind: "Apply", operator: token.text, args: [state.left], location },
1023
+ start,
1024
+ );
711
1025
  continue;
712
1026
  }
713
1027
  if (token.text === "[") {
714
1028
  advance();
715
1029
  const args = withoutOffside(() => parseSeparated(() => parseExpression()));
716
1030
  withoutOffside(() => expect("]"));
717
- state.left = { kind: "FunctionApplication", function: state.left, args, location };
1031
+ state.left = withRange(
1032
+ { kind: "FunctionApplication", function: state.left, args, location },
1033
+ start,
1034
+ );
718
1035
  continue;
719
1036
  }
720
1037
  if (token.text === "." && isFieldName(peek(1))) {
721
1038
  advance();
722
- state.left = { kind: "FieldAccess", record: state.left, field: advance().text, location };
1039
+ const field = advance().text;
1040
+ state.left = withRange({ kind: "FieldAccess", record: state.left, field, location }, start);
723
1041
  continue;
724
1042
  }
725
1043
  const infix = INFIX_OPERATORS.get(token.text);
@@ -752,44 +1070,59 @@ export const parseModule = (source: string): Module => {
752
1070
  } else {
753
1071
  state.left = { kind: "Apply", operator: token.text, args: [left, right], location };
754
1072
  }
1073
+ state.left = withRange(state.left, start);
755
1074
  state.previous = { ...infix, text: token.text };
756
1075
  }
757
1076
  return state.left;
758
1077
  };
759
1078
 
760
- const parseParams = (): OperatorParam[] => {
1079
+ const parseParams = () => {
761
1080
  if (!accept("(")) return [];
762
1081
  const params = parseDeclarations();
763
1082
  expect(")");
764
1083
  return params;
765
1084
  };
766
1085
 
1086
+ const parseBody = () => {
1087
+ const start = getLocation(peek());
1088
+ const body = parseExpression();
1089
+ const last = tokens[cursor.index - 1];
1090
+ const end = { line: last.line, column: last.endColumn };
1091
+ return { body, bodyRange: { module: openModules[openModules.length - 1].name, start, end } };
1092
+ };
1093
+
767
1094
  const parseDefinition = (isLocal: boolean): Definition | Instance => {
1095
+ const start = cursor.index;
1096
+ return withRange(parseDefinitionUnit(isLocal), start);
1097
+ };
1098
+
1099
+ const parseDefinitionUnit = (isLocal: boolean): Definition | Instance => {
768
1100
  const token = peek();
769
1101
  const location = getLocation(token);
1102
+ const parseOperator = (name: string, params: OperatorParam[]): Definition => ({
1103
+ kind: "Operator",
1104
+ name,
1105
+ params,
1106
+ ...parseBody(),
1107
+ isLocal,
1108
+ isRecursive: isRecursiveName(name),
1109
+ location,
1110
+ });
770
1111
  if (
771
1112
  token.kind === "Identifier" &&
772
1113
  peek(1).kind === "Operator" &&
773
- INFIX_OPERATORS.has(peek(1).text)
1114
+ INFIX_OPERATORS.has(peek(1).text) &&
1115
+ peek(2).kind === "Identifier" &&
1116
+ isAt("==", 3)
774
1117
  ) {
775
- if (peek(2).kind === "Identifier" && isAt("==", 3)) {
776
- const left = expectIdentifier();
777
- const operator = advance().text;
778
- const right = expectIdentifier();
779
- expect("==");
780
- return {
781
- kind: "Operator",
782
- name: operator,
783
- params: [
784
- { name: left, arity: 0 },
785
- { name: right, arity: 0 },
786
- ],
787
- body: parseExpression(),
788
- isLocal,
789
- isRecursive: recursiveNames.has(operator),
790
- location,
791
- };
792
- }
1118
+ const left = expectIdentifier();
1119
+ const operator = advance().text;
1120
+ const right = expectIdentifier();
1121
+ expect("==");
1122
+ return parseOperator(operator, [
1123
+ { name: left, arity: 0 },
1124
+ { name: right, arity: 0 },
1125
+ ]);
793
1126
  }
794
1127
  const isPrefixDefinition = token.kind === "Operator" && token.text === "-.";
795
1128
  const isPostfixDefinition =
@@ -799,15 +1132,7 @@ export const parseModule = (source: string): Module => {
799
1132
  cursor.index += isPrefixDefinition ? 1 : 2;
800
1133
  const param = isPrefixDefinition ? expectIdentifier() : token.text;
801
1134
  expect("==");
802
- return {
803
- kind: "Operator",
804
- name: operator,
805
- params: [{ name: param, arity: 0 }],
806
- body: parseExpression(),
807
- isLocal,
808
- isRecursive: recursiveNames.has(operator),
809
- location,
810
- };
1135
+ return parseOperator(operator, [{ name: param, arity: 0 }]);
811
1136
  }
812
1137
  const name = expectIdentifier();
813
1138
  if (accept("[")) {
@@ -820,22 +1145,18 @@ export const parseModule = (source: string): Module => {
820
1145
  bounds,
821
1146
  body: parseExpression(),
822
1147
  isLocal,
823
- isRecursive: recursiveNames.has(name),
1148
+ isRecursive: isRecursiveName(name),
824
1149
  location,
825
1150
  };
826
1151
  }
1152
+ const definitionStart = cursor.index;
827
1153
  const params = parseParams();
828
1154
  expect("==");
829
- if (isAt("INSTANCE")) return parseInstance(name, params, isLocal, location);
830
- return {
831
- kind: "Operator",
832
- name,
833
- params,
834
- body: parseExpression(),
835
- isLocal,
836
- isRecursive: recursiveNames.has(name),
837
- location,
838
- };
1155
+ if (isAt("INSTANCE")) {
1156
+ const instance = parseInstance(name, params, isLocal, location);
1157
+ return { ...instance, definitionRange: getRange(definitionStart) };
1158
+ }
1159
+ return parseOperator(name, params);
839
1160
  };
840
1161
 
841
1162
  const parseInstance = (
@@ -844,145 +1165,577 @@ export const parseModule = (source: string): Module => {
844
1165
  isLocal: boolean,
845
1166
  location: SourceLocation,
846
1167
  ): Instance => {
1168
+ const start = cursor.index;
847
1169
  expect("INSTANCE");
848
1170
  const module = expectIdentifier();
849
1171
  const substitutions = accept("WITH")
850
- ? parseSeparated((): InstanceSubstitution => {
1172
+ ? parseSeparated(() => {
851
1173
  const target = advance().text;
852
1174
  expect("<-");
853
1175
  return { name: target, expression: parseArgument() };
854
1176
  })
855
1177
  : [];
856
- return { kind: "Instance", name, params, module, substitutions, isLocal, location };
1178
+ return {
1179
+ kind: "Instance",
1180
+ name,
1181
+ params,
1182
+ module,
1183
+ substitutions,
1184
+ isLocal,
1185
+ location,
1186
+ instanceRange: getRange(start),
1187
+ };
857
1188
  };
858
1189
 
859
- const parseDeclarations = (): Declaration[] =>
860
- parseSeparated(() => {
861
- if (isAt("_") && peek(1).kind === "Operator") {
862
- advance();
863
- const operator = advance().text;
864
- return accept("_") ? { name: operator, arity: 2 } : { name: operator, arity: 1 };
865
- }
866
- if (peek().kind === "Operator" && isAt("_", 1)) {
867
- const operator = advance().text;
868
- advance();
869
- return { name: operator === "-" ? "-." : operator, arity: 1 };
1190
+ const parseDeclaration = (): Declaration => {
1191
+ const location = getLocation(peek());
1192
+ if (isAt("_") && peek(1).kind === "Operator") {
1193
+ advance();
1194
+ const name = advance().text;
1195
+ return { name, arity: accept("_") ? 2 : 1, location };
1196
+ }
1197
+ if (peek().kind === "Operator" && isAt("_", 1)) {
1198
+ const operator = advance().text;
1199
+ advance();
1200
+ return { name: operator === "-" ? "-." : operator, arity: 1, location };
1201
+ }
1202
+ const name = expectIdentifier();
1203
+ if (!accept("(")) return { name, arity: 0, location };
1204
+ const placeholders = parseSeparated(() => expect("_"));
1205
+ expect(")");
1206
+ return { name, arity: placeholders.length, location };
1207
+ };
1208
+
1209
+ const parseDeclarations = () => parseSeparated(parseDeclaration);
1210
+
1211
+ const declareRecursive = () => {
1212
+ const declarations = parseDeclarations();
1213
+ declarations.forEach((declaration) =>
1214
+ recursiveScopes[recursiveScopes.length - 1].add(declaration.name),
1215
+ );
1216
+ return declarations;
1217
+ };
1218
+
1219
+ const getClosingOffset = (offset: number) => {
1220
+ const depth = { value: 0, offset };
1221
+ do {
1222
+ const token = peek(depth.offset);
1223
+ if (token.kind === "End") return depth.offset;
1224
+ if (token.kind === "Operator" && OPENING_BRACKETS.has(token.text)) depth.value++;
1225
+ if (token.kind === "Operator" && CLOSING_BRACKETS.has(token.text)) depth.value--;
1226
+ depth.offset++;
1227
+ } while (depth.value > 0);
1228
+ return depth.offset;
1229
+ };
1230
+
1231
+ // SANY inserts a break before each definition, which ends the fact list or step before it.
1232
+ const isDefinitionStart = () => {
1233
+ const token = peek();
1234
+ const next = peek(1);
1235
+ if (token.kind !== "Identifier") {
1236
+ return isAt("-.") && next.kind === "Identifier" && isAt("==", 2);
1237
+ }
1238
+ if (isAt("(", 1) || isAt("[", 1)) return isAt("==", getClosingOffset(1));
1239
+ if (next.kind === "Operator" && POSTFIX_OPERATORS.has(next.text)) return isAt("==", 2);
1240
+ if (next.kind === "Operator" && INFIX_OPERATORS.has(next.text)) {
1241
+ return peek(2).kind === "Identifier" && isAt("==", 3);
1242
+ }
1243
+ return isAt("==", 1);
1244
+ };
1245
+
1246
+ // SANY's proof levels, one for each open proof, which stay -1 until a step sets them.
1247
+ const proofLevels: number[] = [];
1248
+
1249
+ const getProofLevel = () => proofLevels[proofLevels.length - 1] ?? -1;
1250
+
1251
+ const getStepLevel = (step: Token) => {
1252
+ if (step.text[1] === "*") return -1;
1253
+ if (step.text[1] === "+") return -2;
1254
+ return Number(step.text.slice(1, step.text.indexOf(">")));
1255
+ };
1256
+
1257
+ const getStepLabel = (step: Token) => {
1258
+ const label = step.text.slice(step.text.indexOf(">") + 1);
1259
+ const end = { value: label.length };
1260
+ while (label[end.value - 1] === ".") end.value--;
1261
+ return label.slice(0, end.value);
1262
+ };
1263
+
1264
+ const isStepReference = (step: Token) =>
1265
+ !step.text.endsWith(".") && !UNNAMED_STEP_LABELS.has(getStepLabel(step));
1266
+
1267
+ const getStepName = (step: Token) => {
1268
+ const label = getStepLabel(step);
1269
+ if (UNNAMED_STEP_LABELS.has(label)) return undefined;
1270
+ const level = getStepLevel(step);
1271
+ if (level >= 0) return `<${level}>${label}`;
1272
+ const current = getProofLevel();
1273
+ const outer = proofLevels[proofLevels.length - 2];
1274
+ return `<${current < 0 && outer !== undefined ? outer : current}>${label}`;
1275
+ };
1276
+
1277
+ const isProofStart = (token: Token) => {
1278
+ if (token.kind === "Keyword") return PROOF_STARTS.has(token.text);
1279
+ if (token.kind !== "Step") return false;
1280
+ if (token.text[1] === "*") return proofLevels.length === 0;
1281
+ if (token.text[1] === "+" || proofLevels.length === 0) return true;
1282
+ const level = getProofLevel();
1283
+ return level >= 0 && getStepLevel(token) > level;
1284
+ };
1285
+
1286
+ const isCorrectLevel = (step: Token) => {
1287
+ const level = getStepLevel(step);
1288
+ const top = proofLevels.length - 1;
1289
+ const outer = proofLevels[top - 1] ?? -1;
1290
+ const isFirst = proofLevels[top] < 0;
1291
+ if (isFirst) proofLevels[top] = level < 0 ? outer + 1 : level;
1292
+ if (level === -1) return true;
1293
+ if (level === -2) return isFirst;
1294
+ return isFirst ? level > outer : proofLevels[top] === level;
1295
+ };
1296
+
1297
+ const isDeclared = (name: string) =>
1298
+ !factNames.has(name) &&
1299
+ openModules.some(
1300
+ (module) =>
1301
+ module.constants.some((constant) => constant.name === name) ||
1302
+ module.variables.some((variable) => variable.name === name) ||
1303
+ module.definitions.some((definition) => definition.name === name),
1304
+ );
1305
+
1306
+ const isHideFact = (fact: Fact) => {
1307
+ if (fact.kind === "ModuleFact" || fact.kind === "Subexpression") return true;
1308
+ if (fact.kind === "Apply") return fact.operator.includes("!");
1309
+ if (fact.kind !== "Identifier") return false;
1310
+ if (fact.name.startsWith("<") || fact.name.includes("!")) return true;
1311
+ return !BUILTIN_CONSTANTS.has(fact.name) && !isDeclared(fact.name);
1312
+ };
1313
+
1314
+ const isFactAhead = () => {
1315
+ const token = peek();
1316
+ if (token.kind === "Step") return isStepReference(token);
1317
+ if (token.kind === "Keyword") return FACT_KEYWORDS.has(token.text);
1318
+ if (token.kind === "End" || token.kind === "ModuleEnd" || token.kind === "Separator") {
1319
+ return false;
1320
+ }
1321
+ return !isDefinitionStart();
1322
+ };
1323
+
1324
+ // SANY reads an infix or postfix operator alone as its name, but a prefix operator such as - as the
1325
+ // start of an expression.
1326
+ const parseFact = (): Fact => {
1327
+ const start = cursor.index;
1328
+ factStart.index = start;
1329
+ const token = peek();
1330
+ if (
1331
+ token.kind === "Operator" &&
1332
+ !EXPRESSION_PREFIXES.has(token.text) &&
1333
+ isOperatorName(token.text)
1334
+ ) {
1335
+ cursor.index++;
1336
+ return withRange(
1337
+ { kind: "OperatorReference", operator: token.text, location: getLocation(token) },
1338
+ start,
1339
+ );
1340
+ }
1341
+ if (!accept("MODULE")) return parseExpression();
1342
+ return withRange(
1343
+ { kind: "ModuleFact", name: expectIdentifier(), location: getLocation(token) },
1344
+ start,
1345
+ );
1346
+ };
1347
+
1348
+ const declareProofSymbol = (name: string) => proofScopes[proofScopes.length - 1]?.add(name);
1349
+
1350
+ const withProofScope = <Result>(parse: () => Result) => {
1351
+ proofScopes.push(new Set());
1352
+ const result = parse();
1353
+ proofScopes.pop();
1354
+ return result;
1355
+ };
1356
+
1357
+ const isDeclaredSymbol = (name: string) =>
1358
+ proofScopes.some((scope) => scope.has(name)) ||
1359
+ openModules.some(
1360
+ (module) =>
1361
+ module.constants.some((constant) => constant.name === name) ||
1362
+ module.variables.some((variable) => variable.name === name),
1363
+ );
1364
+
1365
+ // tlapm's definable: names and operators joined by !.
1366
+ const parseDefinable = (): Fact => {
1367
+ const start = cursor.index;
1368
+ const token = peek();
1369
+ const parts = parseSeparated(() => {
1370
+ const part = peek();
1371
+ const isName = part.kind === "Identifier" && !isTlapmKeyword(part);
1372
+ if (!isName && part.kind !== "Operator") {
1373
+ throw createUnexpected(`Expected a definition name but found ${describe(part)}`, part);
870
1374
  }
1375
+ cursor.index++;
1376
+ return part.text;
1377
+ }, "!");
1378
+ const location = getLocation(token);
1379
+ return withRange(
1380
+ parts.length === 1 && token.kind === "Operator"
1381
+ ? { kind: "OperatorReference", operator: token.text, location }
1382
+ : { kind: "Identifier", name: parts.join("!"), location },
1383
+ start,
1384
+ );
1385
+ };
1386
+
1387
+ const parseDefs = () => {
1388
+ if (isTlapm) return parseSeparated(parseDefinable);
1389
+ const defs = parseSeparated(parseFact);
1390
+ const invalid = defs.find((def) => def.kind === "Identifier" && isDeclaredSymbol(def.name));
1391
+ if (invalid) {
1392
+ throw createError("DEF clause entry should describe a defined operator", invalid.location);
1393
+ }
1394
+ return defs;
1395
+ };
1396
+
1397
+ const parseFacts = () => ({
1398
+ facts: isFactAhead() ? parseSeparated(parseFact) : [],
1399
+ defs: accept("DEF") || accept("DEFS") ? parseDefs() : [],
1400
+ });
1401
+
1402
+ const parseUseOrHide = (): UseOrHide => {
1403
+ const start = cursor.index;
1404
+ const keyword = advance();
1405
+ const isOnly = keyword.text === "USE" && accept("ONLY");
1406
+ const keywordRange = getRange(start);
1407
+ const use: UseOrHide = {
1408
+ kind: keyword.text === "USE" ? "Use" : "Hide",
1409
+ isOnly,
1410
+ ...parseFacts(),
1411
+ keyword: keywordRange,
1412
+ location: getLocation(keyword),
1413
+ };
1414
+ const invalid =
1415
+ use.kind === "Hide" && !isTlapm ? use.facts.find((fact) => !isHideFact(fact)) : undefined;
1416
+ if (invalid) {
1417
+ throw createError(
1418
+ "The only expression allowed as a fact in a HIDE is the name of a theorem, assumption, or step",
1419
+ invalid.location,
1420
+ );
1421
+ }
1422
+ return withRange(use, start);
1423
+ };
1424
+
1425
+ const parseNewSymbol = (): NewSymbol => {
1426
+ const start = cursor.index;
1427
+ const location = getLocation(peek());
1428
+ const isNew = accept("NEW");
1429
+ const level = peek().kind === "Keyword" ? NEW_SYMBOL_LEVELS.get(peek().text) : undefined;
1430
+ if (!isNew && level === undefined) {
1431
+ throw createUnexpected(`Expected a declaration but found ${describe(peek())}`);
1432
+ }
1433
+ if (level !== undefined) advance();
1434
+ if (level === "Variable") {
871
1435
  const name = expectIdentifier();
872
- if (!accept("(")) return { name, arity: 0 };
873
- const placeholders = parseSeparated(() => expect("_"));
874
- expect(")");
875
- return { name, arity: placeholders.length };
876
- });
1436
+ declareProofSymbol(name);
1437
+ return withRange(
1438
+ { kind: "NewSymbol", name, arity: 0, level, set: undefined, location },
1439
+ start,
1440
+ );
1441
+ }
1442
+ const declaration = parseDeclaration();
1443
+ declareProofSymbol(declaration.name);
1444
+ const isBounded = (level ?? "Constant") === "Constant" && isAt("\\in");
1445
+ if (isBounded && declaration.arity > 0) {
1446
+ throw createError("declared symbol with arguments before \\in");
1447
+ }
1448
+ if (isBounded) advance();
1449
+ return withRange(
1450
+ {
1451
+ kind: "NewSymbol",
1452
+ name: declaration.name,
1453
+ arity: declaration.arity,
1454
+ level: level ?? "Constant",
1455
+ set: isBounded ? parseExpression() : undefined,
1456
+ location,
1457
+ },
1458
+ start,
1459
+ );
1460
+ };
877
1461
 
878
- const isUnitStart = (token: Token, next: Token) => {
879
- if (token.column !== 1) return false;
880
- if (token.kind === "Separator" || token.kind === "ModuleEnd" || token.kind === "End")
881
- return true;
882
- if (token.kind === "Keyword") return UNIT_KEYWORDS.has(token.text);
883
- return (
884
- token.kind === "Identifier" && (next.text === "==" || next.text === "(" || next.text === "[")
1462
+ const parseAssumeProve = (): AssumeProve => {
1463
+ const start = cursor.index;
1464
+ const location = getLocation(expect("ASSUME"));
1465
+ const assumptions = parseSeparated(() => {
1466
+ if (isAt("ASSUME")) return withProofScope(parseAssumeProve);
1467
+ if (peek().kind === "Keyword" && NEW_SYMBOL_STARTS.has(peek().text)) return parseNewSymbol();
1468
+ return parseExpression();
1469
+ });
1470
+ expect("PROVE");
1471
+ return withRange(
1472
+ { kind: "AssumeProve", assumptions, conclusion: parseExpression(), location },
1473
+ start,
885
1474
  );
886
1475
  };
887
1476
 
888
- const skipUnit = (start: Token) => {
889
- while (peekRaw().kind !== "End") {
890
- const token = peekRaw();
891
- if (token.line > start.line && isUnitStart(token, peekRaw(1))) return;
892
- cursor.index++;
1477
+ const isNameListAhead = (terminator: string) => {
1478
+ const offset = { value: 0 };
1479
+ while (peek(offset.value).kind === "Identifier" && isAt(",", offset.value + 1)) {
1480
+ offset.value += 2;
893
1481
  }
1482
+ return peek(offset.value).kind === "Identifier" && isAt(terminator, offset.value + 1);
894
1483
  };
895
1484
 
896
- const isProofStart = (index: number, start: Token) => {
897
- const token = tokens[index];
898
- if (token.kind === "End") return true;
899
- if (token.line > start.line && isUnitStart(token, tokens[index + 1])) return true;
900
- if (token.text === "PROOF" || PROOF_WORDS.has(token.text)) return true;
901
- return (
902
- token.text === "<" && tokens[index + 1]?.kind === "Number" && tokens[index + 2]?.text === ">"
1485
+ const declareProofSymbols = (names: string[], bounds: Bound[]) => {
1486
+ names.forEach(declareProofSymbol);
1487
+ bounds.forEach((bound) =>
1488
+ (bound.pattern.kind === "Name" ? [bound.pattern.name] : bound.pattern.names).forEach(
1489
+ declareProofSymbol,
1490
+ ),
903
1491
  );
904
1492
  };
905
1493
 
906
- const parseTheorem = (): Definition | undefined => {
907
- const start = advance();
908
- const isNamed = peek().kind === "Identifier" && isAt("==", 1) && !isAt("ASSUME", 2);
909
- if (!isNamed) {
910
- skipUnit(start);
911
- return undefined;
1494
+ const parseStepBody = (): StepBody => {
1495
+ const token = peek();
1496
+ if (isAt("USE") || isAt("HIDE")) return parseUseOrHide();
1497
+ if (isAt("INSTANCE")) {
1498
+ return withRange(parseInstance(undefined, [], false, getLocation(token)), cursor.index);
912
1499
  }
913
- const name = expectIdentifier();
914
- advance();
915
- const end = { value: cursor.index };
916
- while (!isProofStart(end.value, start)) end.value++;
917
- cursor.limit = end.value;
918
- const body = parseExpression();
919
- const isComplete = cursor.index === end.value;
920
- cursor.limit = tokens.length;
921
- skipUnit(start);
922
- if (!isComplete) return undefined;
1500
+ if (isAt("DEFINE") || isDefinitionStart()) {
1501
+ const start = cursor.index;
1502
+ accept("DEFINE");
1503
+ const definitions = [parseDefinition(false)];
1504
+ while (isDefinitionStart()) definitions.push(parseDefinition(false));
1505
+ const define: DefineStep = { kind: "Define", definitions, location: getLocation(token) };
1506
+ return withRange(define, start);
1507
+ }
1508
+ if (accept("HAVE")) return { kind: "Have", expression: parseExpression() };
1509
+ if (accept("TAKE")) {
1510
+ const isBounded = isBoundAhead();
1511
+ const names = isBounded ? [] : parseSeparated(expectIdentifier);
1512
+ const bounds = isBounded ? parseBounds() : [];
1513
+ declareProofSymbols(names, bounds);
1514
+ return { kind: "Take", names, bounds };
1515
+ }
1516
+ if (accept("WITNESS")) {
1517
+ return { kind: "Witness", expressions: parseSeparated(() => parseExpression()) };
1518
+ }
1519
+ if (accept("PICK")) {
1520
+ const isNames = isNameListAhead(":");
1521
+ const names = isNames ? parseSeparated(expectIdentifier) : [];
1522
+ const bounds = isNames ? [] : parseBounds();
1523
+ declareProofSymbols(names, bounds);
1524
+ expect(":");
1525
+ return { kind: "Pick", names, bounds, body: parseExpression() };
1526
+ }
1527
+ if (accept("CASE")) return { kind: "Case", expression: parseExpression() };
923
1528
  return {
924
- kind: "Operator",
925
- name,
926
- params: [],
927
- body,
928
- isLocal: false,
929
- isRecursive: false,
930
- location: getLocation(start),
1529
+ kind: "Assert",
1530
+ isSuffices: accept("SUFFICES"),
1531
+ statement: isAt("ASSUME") ? parseAssumeProve() : parseExpression(),
931
1532
  };
932
1533
  };
933
1534
 
1535
+ const parseStepStart = (isQed: boolean) => {
1536
+ const step = peek();
1537
+ if (step.kind !== "Step") {
1538
+ throw createUnexpected(`Expected a step number but found ${describe(step)}`, step);
1539
+ }
1540
+ if (!isCorrectLevel(step)) {
1541
+ throw createError(`${isQed ? "QED step" : "step"}'s number has bad level`, step);
1542
+ }
1543
+ if (getStepLevel(step) < 0 && getStepName(step) !== undefined) {
1544
+ throw createError("<*> and <+> cannot be used for a named step", step);
1545
+ }
1546
+ cursor.index++;
1547
+ return { name: getStepName(step), token: step.text, location: getLocation(step) };
1548
+ };
1549
+
1550
+ // The names an assertion assumes stay in its own proof, and other steps declare theirs for the rest
1551
+ // of the enclosing proof.
1552
+ const parseStep = (): ProofStep => {
1553
+ const start = cursor.index;
1554
+ const step = parseStepStart(isAt("QED", 1));
1555
+ const scope = new Set<string>();
1556
+ proofScopes.push(scope);
1557
+ const body: StepBody = accept("QED") ? { kind: "Qed" } : parseStepBody();
1558
+ const head = getRange(start);
1559
+ const hasProof = isProofStart(peek());
1560
+ if (hasProof && !PROVABLE_STEPS.has(body.kind)) {
1561
+ throw createError("Proof of a step that does not take a proof");
1562
+ }
1563
+ const proof = hasProof ? parseProof() : undefined;
1564
+ proofScopes.pop();
1565
+ if (body.kind !== "Assert" || body.isSuffices) scope.forEach(declareProofSymbol);
1566
+ return withRange({ ...step, body, head, proof }, start);
1567
+ };
1568
+
1569
+ const parseProofBody = (): Proof => {
1570
+ const location = getLocation(peek());
1571
+ if (isAt("BY") || (isAt("PROOF") && isAt("BY", 1))) {
1572
+ accept("PROOF");
1573
+ advance();
1574
+ const keyword = getRange(cursor.index - 1);
1575
+ return { kind: "By", isOnly: accept("ONLY"), ...parseFacts(), keyword, location };
1576
+ }
1577
+ const terminal = peek(isAt("PROOF") ? 1 : 0);
1578
+ if (
1579
+ terminal.kind === "Keyword" &&
1580
+ (terminal.text === "OBVIOUS" || terminal.text === "OMITTED")
1581
+ ) {
1582
+ accept("PROOF");
1583
+ advance();
1584
+ const keyword = getRange(cursor.index - 1);
1585
+ return { kind: terminal.text === "OBVIOUS" ? "Obvious" : "Omitted", keyword, location };
1586
+ }
1587
+ accept("PROOF");
1588
+ const steps = [parseStep()];
1589
+ while (steps[steps.length - 1].body.kind !== "Qed") steps.push(parseStep());
1590
+ return { kind: "Steps", level: getProofLevel(), steps, location };
1591
+ };
1592
+
1593
+ const parseProof = (): Proof => {
1594
+ const start = cursor.index;
1595
+ proofLevels.push(-1);
1596
+ const proof = withProofScope(parseProofBody);
1597
+ proofLevels.pop();
1598
+ return withRange(proof, start);
1599
+ };
1600
+
1601
+ const parseTheorem = (module: Module) => {
1602
+ const start = cursor.index;
1603
+ const keyword = advance();
1604
+ const isNamed = peek().kind === "Identifier" && isAt("==", 1);
1605
+ const name = isNamed ? expectIdentifier() : undefined;
1606
+ if (name !== undefined) {
1607
+ factNames.add(name);
1608
+ advance();
1609
+ }
1610
+ const parsed = withProofScope(() => {
1611
+ const statement = isAt("ASSUME") ? parseAssumeProve() : parseExpression();
1612
+ return { statement, proof: isProofStart(peek()) ? parseProof() : undefined };
1613
+ });
1614
+ const statement = parsed.statement;
1615
+ const theorem = withRange(
1616
+ { name, statement, proof: parsed.proof, location: getLocation(keyword) },
1617
+ start,
1618
+ );
1619
+ module.theorems.push(theorem);
1620
+ module.units.push({ kind: "Theorem", theorem });
1621
+ if (name === undefined || statement.kind === "AssumeProve") return;
1622
+ module.definitions.push(
1623
+ withRange(
1624
+ {
1625
+ kind: "Operator",
1626
+ name,
1627
+ params: [],
1628
+ body: statement,
1629
+ isLocal: false,
1630
+ isRecursive: false,
1631
+ location: getLocation(keyword),
1632
+ },
1633
+ start,
1634
+ ),
1635
+ );
1636
+ };
1637
+
934
1638
  const parseAssumption = (): Assumption => {
1639
+ const start = cursor.index;
1640
+ return withRange(parseAssumptionUnit(), start);
1641
+ };
1642
+
1643
+ const parseAssumptionUnit = (): Assumption => {
935
1644
  const keyword = advance();
936
1645
  const location = getLocation(keyword);
937
1646
  const isAxiom = keyword.text === "AXIOM";
938
1647
  if (peek().kind === "Identifier" && isAt("==", 1)) {
939
1648
  const name = expectIdentifier();
1649
+ factNames.add(name);
940
1650
  advance();
941
1651
  return { name, expression: parseExpression(), isAxiom, location };
942
1652
  }
943
1653
  return { name: undefined, expression: parseExpression(), isAxiom, location };
944
1654
  };
945
1655
 
946
- const parseModuleBody = (outer?: OuterScope): Module => {
947
- expect("----");
1656
+ const parseModuleBody = (): Module => {
1657
+ const location = getLocation(expect("----"));
948
1658
  expect("MODULE");
949
1659
  const name = expectIdentifier();
950
1660
  expect("----");
951
1661
  const module: Module = {
952
1662
  name,
953
- outer,
954
1663
  modules: [],
955
- extends: accept("EXTENDS") ? parseSeparated(expectIdentifier) : [],
1664
+ extends: [],
1665
+ extendsRanges: [],
956
1666
  constants: [],
957
1667
  variables: [],
958
1668
  definitions: [],
959
1669
  instances: [],
960
1670
  assumptions: [],
1671
+ location,
1672
+ theorems: [],
1673
+ uses: [],
1674
+ units: [],
961
1675
  };
1676
+ openModules.push(module);
1677
+ recursiveScopes.push(new Set());
1678
+ if (accept("EXTENDS")) {
1679
+ parseSeparated(() => {
1680
+ const start = cursor.index;
1681
+ module.extends.push(expectIdentifier());
1682
+ module.extendsRanges.push(getRange(start));
1683
+ });
1684
+ }
962
1685
  while (true) {
1686
+ const tokenIndex = cursor.index;
1687
+ unitStart.index = tokenIndex;
963
1688
  const token = peek();
964
- if (token.kind === "ModuleEnd" || token.kind === "End") return module;
1689
+ if (token.kind === "ModuleEnd" || token.kind === "End") {
1690
+ if (token.kind === "ModuleEnd") {
1691
+ location.range = {
1692
+ module: name,
1693
+ start: { line: location.line, column: location.column },
1694
+ end: { line: token.line, column: token.endColumn },
1695
+ };
1696
+ }
1697
+ openModules.pop();
1698
+ recursiveScopes.pop();
1699
+ return module;
1700
+ }
965
1701
  if (token.kind === "Separator") {
966
- if (isAt("MODULE", 1)) module.modules.push(parseNestedModule(getOuterScope(module)));
967
- else advance();
1702
+ if (isAt("MODULE", 1)) {
1703
+ const submodule = parseModuleBody();
1704
+ module.modules.push(submodule);
1705
+ module.units.push({ kind: "Module", module: submodule });
1706
+ expect("====");
1707
+ continue;
1708
+ }
1709
+ advance();
968
1710
  continue;
969
1711
  }
970
1712
  switch (token.text) {
971
1713
  case "CONSTANT":
972
- case "CONSTANTS":
1714
+ case "CONSTANTS": {
973
1715
  advance();
974
- module.constants.push(...parseDeclarations());
1716
+ const declarations = parseDeclarations();
1717
+ module.constants.push(...declarations);
1718
+ module.units.push({ kind: "Constants", declarations });
975
1719
  continue;
1720
+ }
976
1721
  case "VARIABLE":
977
- case "VARIABLES":
1722
+ case "VARIABLES": {
978
1723
  advance();
979
- module.variables.push(...parseSeparated(expectIdentifier));
1724
+ const declarations = parseSeparated(() => {
1725
+ const start = cursor.index;
1726
+ const location = getLocation(peek());
1727
+ return withRange({ name: expectIdentifier(), arity: 0, location }, start);
1728
+ });
1729
+ module.variables.push(...declarations);
1730
+ module.units.push({ kind: "Variables", declarations });
980
1731
  continue;
1732
+ }
981
1733
  case "ASSUME":
982
1734
  case "ASSUMPTION":
983
1735
  case "AXIOM": {
984
1736
  const assumption = parseAssumption();
985
1737
  module.assumptions.push(assumption);
1738
+ module.units.push({ kind: "Assumption", assumption });
986
1739
  if (assumption.name !== undefined) {
987
1740
  module.definitions.push({
988
1741
  kind: "Operator",
@@ -996,57 +1749,57 @@ export const parseModule = (source: string): Module => {
996
1749
  }
997
1750
  continue;
998
1751
  }
1752
+ case "USE":
1753
+ case "HIDE": {
1754
+ const use = parseUseOrHide();
1755
+ module.uses.push(use);
1756
+ module.units.push({ kind: "Use", use });
1757
+ continue;
1758
+ }
999
1759
  case "RECURSIVE":
1000
1760
  advance();
1001
- parseDeclarations().forEach((declaration) => recursiveNames.add(declaration.name));
1761
+ module.units.push({ kind: "Recursive", declarations: declareRecursive() });
1002
1762
  continue;
1003
1763
  case "INSTANCE":
1004
- module.instances.push(parseInstance(undefined, [], false, getLocation(token)));
1764
+ addDefinitionUnit(
1765
+ module,
1766
+ withRange(parseInstance(undefined, [], false, getLocation(token)), tokenIndex),
1767
+ );
1005
1768
  continue;
1006
1769
  case "LOCAL": {
1007
1770
  advance();
1008
1771
  if (isAt("INSTANCE")) {
1009
- module.instances.push(parseInstance(undefined, [], true, getLocation(token)));
1772
+ addDefinitionUnit(
1773
+ module,
1774
+ withRange(parseInstance(undefined, [], true, getLocation(token)), tokenIndex),
1775
+ );
1010
1776
  continue;
1011
1777
  }
1012
- const unit = parseDefinition(true);
1013
- if (unit.kind === "Instance") module.instances.push(unit);
1014
- else module.definitions.push(unit);
1778
+ addDefinitionUnit(module, parseDefinition(true));
1015
1779
  continue;
1016
1780
  }
1017
1781
  }
1018
1782
  if (token.kind === "Keyword" && THEOREM_KEYWORDS.has(token.text)) {
1019
- const theorem = parseTheorem();
1020
- if (theorem) module.definitions.push(theorem);
1783
+ parseTheorem(module);
1021
1784
  continue;
1022
1785
  }
1023
- const unit = parseDefinition(false);
1024
- if (unit.kind === "Instance") module.instances.push(unit);
1025
- else module.definitions.push(unit);
1786
+ addDefinitionUnit(module, parseDefinition(false));
1026
1787
  }
1027
1788
  };
1028
1789
 
1029
- const getOuterScope = (module: Module): OuterScope => ({
1030
- extends: [...(module.outer?.extends ?? []), ...module.extends],
1031
- symbols: [
1032
- ...(module.outer?.symbols ?? []),
1033
- ...module.constants,
1034
- ...module.variables.map((variable) => ({ name: variable, arity: 0 })),
1035
- ...module.definitions.map((definition) => ({
1036
- name: definition.name,
1037
- arity: definition.kind === "Operator" ? definition.params.length : 0,
1038
- })),
1039
- ],
1040
- });
1041
-
1042
- const parseNestedModule = (outer?: OuterScope) => {
1043
- const nested = parseModuleBody(outer);
1044
- accept("====");
1045
- return nested;
1790
+ const addDefinitionUnit = (module: Module, unit: Definition | Instance) => {
1791
+ if (unit.kind === "Instance") {
1792
+ module.instances.push(unit);
1793
+ module.units.push({ kind: "Instance", instance: unit });
1794
+ return;
1795
+ }
1796
+ module.definitions.push(unit);
1797
+ module.units.push({ kind: "Definition", definition: unit });
1046
1798
  };
1047
1799
 
1048
- const root = parseModuleBody();
1049
- accept("====");
1050
- while (peekRaw().kind === "Separator") root.modules.push(parseNestedModule());
1051
- return root;
1800
+ return parseModuleBody();
1052
1801
  };
1802
+
1803
+ export const parseModule = (source: string) => parse(source, false);
1804
+
1805
+ export const parseTlapmModule = (source: string) => parse(source, true);