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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tla-ts",
3
- "version": "0.0.2",
3
+ "version": "0.0.3-dev.a50fc89",
4
4
  "description": "A TLA+ interpreter and model checker in TypeScript, 2× faster than TLC",
5
5
  "keywords": [
6
6
  "formal verification",
@@ -71,6 +71,8 @@
71
71
  "conformance:sany": "tsx scripts/proof-conformance.ts",
72
72
  "conformance:record": "tsx scripts/record-tlc.ts",
73
73
  "conformance:record-sany": "tsx scripts/record-sany.ts",
74
+ "conformance:record-tlapm": "tsx scripts/record-tlapm.ts",
75
+ "conformance:tlapm": "tsx scripts/tlapm-conformance.ts",
74
76
  "publint": "publint",
75
77
  "test": "vp test",
76
78
  "typecheck": "tsc --noEmit"
package/src/ast.ts CHANGED
@@ -44,6 +44,9 @@ export interface ApplyExpression {
44
44
  kind: "Apply";
45
45
  operator: string;
46
46
  args: Expression[];
47
+ // The number of args each part of a name like F(x)!l(y) takes, when a part before the last takes
48
+ // some.
49
+ argumentCounts?: number[];
47
50
  location: SourceLocation;
48
51
  resolution?: LocalResolution;
49
52
  }
@@ -64,6 +67,7 @@ export interface LambdaExpression {
64
67
  export interface JunctionExpression {
65
68
  kind: "And" | "Or";
66
69
  operands: Expression[];
70
+ isBulleted?: boolean;
67
71
  location: SourceLocation;
68
72
  }
69
73
 
@@ -129,6 +133,7 @@ export interface QuantifierExpression {
129
133
 
130
134
  export interface UnboundedQuantifierExpression {
131
135
  kind: "UnboundedQuantifier";
136
+ quantifier: "Forall" | "Exists";
132
137
  names: string[];
133
138
  isTemporal?: boolean;
134
139
  body: Expression;
@@ -317,6 +322,7 @@ export type Expression =
317
322
  export interface OperatorParam {
318
323
  name: string;
319
324
  arity: number;
325
+ location?: SourceLocation;
320
326
  }
321
327
 
322
328
  export interface OperatorDefinition {
@@ -368,6 +374,8 @@ export interface Instance {
368
374
  substitutions: InstanceSubstitution[];
369
375
  isLocal: boolean;
370
376
  location: SourceLocation;
377
+ instanceRange: SourceRange;
378
+ definitionRange?: SourceRange;
371
379
  }
372
380
 
373
381
  export interface NewSymbol {
@@ -388,18 +396,33 @@ export interface AssumeProve {
388
396
 
389
397
  export type Fact = Expression | { kind: "ModuleFact"; name: string; location: SourceLocation };
390
398
 
399
+ // keyword is the range of the USE or HIDE token and any ONLY after it.
391
400
  export interface UseOrHide {
392
401
  kind: "Use" | "Hide";
393
402
  isOnly: boolean;
394
403
  facts: Fact[];
395
404
  defs: Fact[];
405
+ keyword: SourceRange;
396
406
  location: SourceLocation;
397
407
  }
398
408
 
409
+ // keyword is the range of the BY, OBVIOUS, or OMITTED token.
399
410
  export type Proof =
400
- | { kind: "Obvious" | "Omitted"; location: SourceLocation }
401
- | { kind: "By"; isOnly: boolean; facts: Fact[]; defs: Fact[]; location: SourceLocation }
402
- | { kind: "Steps"; level: number; steps: ProofStep[]; location: SourceLocation };
411
+ | { kind: "Obvious" | "Omitted"; keyword: SourceRange; location: SourceLocation }
412
+ | {
413
+ kind: "By";
414
+ isOnly: boolean;
415
+ facts: Fact[];
416
+ defs: Fact[];
417
+ keyword: SourceRange;
418
+ location: SourceLocation;
419
+ }
420
+ | {
421
+ kind: "Steps";
422
+ level: number;
423
+ steps: ProofStep[];
424
+ location: SourceLocation;
425
+ };
403
426
 
404
427
  export interface DefineStep {
405
428
  kind: "Define";
@@ -419,9 +442,12 @@ export type StepBody =
419
442
  | { kind: "Assert"; isSuffices: boolean; statement: Expression | AssumeProve }
420
443
  | { kind: "Qed" };
421
444
 
445
+ // head is the range of the step number and its statement, without the proof.
422
446
  export interface ProofStep {
423
447
  name: string | undefined;
448
+ token: string;
424
449
  body: StepBody;
450
+ head: SourceRange;
425
451
  proof: Proof | undefined;
426
452
  location: SourceLocation;
427
453
  }
@@ -433,10 +459,23 @@ export interface Theorem {
433
459
  location: SourceLocation;
434
460
  }
435
461
 
462
+ // The units of a module in source order, for consumers that need the order across kinds.
463
+ export type ModuleUnit =
464
+ | { kind: "Constants"; declarations: Declaration[] }
465
+ | { kind: "Variables"; declarations: Declaration[] }
466
+ | { kind: "Recursive"; declarations: Declaration[] }
467
+ | { kind: "Definition"; definition: Definition }
468
+ | { kind: "Instance"; instance: Instance }
469
+ | { kind: "Assumption"; assumption: Assumption }
470
+ | { kind: "Theorem"; theorem: Theorem }
471
+ | { kind: "Use"; use: UseOrHide }
472
+ | { kind: "Module"; module: Module };
473
+
436
474
  export interface Module {
437
475
  name: string;
438
476
  modules: Module[];
439
477
  extends: string[];
478
+ extendsRanges: SourceRange[];
440
479
  constants: Declaration[];
441
480
  variables: Declaration[];
442
481
  definitions: Definition[];
@@ -445,4 +484,5 @@ export interface Module {
445
484
  location: SourceLocation;
446
485
  theorems: Theorem[];
447
486
  uses: UseOrHide[];
487
+ units: ModuleUnit[];
448
488
  }
package/src/check.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  getDefinition,
14
14
  isConstantLevel,
15
15
  isImpure,
16
+ isLocallyResolved,
16
17
  type Context,
17
18
  type Environment,
18
19
  type Scope,
@@ -54,6 +55,7 @@ import {
54
55
  getValueHash,
55
56
  isFunctionValue,
56
57
  isSameValue,
58
+ isUpdatedElementOf,
57
59
  setStringOrder,
58
60
  toFiniteFunction,
59
61
  type Value,
@@ -139,6 +141,13 @@ interface NamedExpression {
139
141
 
140
142
  interface StateCheck extends NamedExpression {
141
143
  variables: number[] | undefined;
144
+ membership: Membership | undefined;
145
+ }
146
+
147
+ // A conjunct `x \in S` whose S reads no variables.
148
+ interface Membership {
149
+ variable: number;
150
+ set: Expression;
142
151
  }
143
152
 
144
153
  interface ActionProperty {
@@ -481,14 +490,32 @@ const getPossiblePredicates = (names: string[], environment: Environment) =>
481
490
  return { name, expression, scope: undefined };
482
491
  });
483
492
 
493
+ const getMembership = (
494
+ expression: Expression,
495
+ variables: number[] | undefined,
496
+ environment: Environment,
497
+ ): Membership | undefined => {
498
+ if (expression.kind !== "Apply" || expression.operator !== "\\in" || variables?.length !== 1)
499
+ return undefined;
500
+ const element = expression.args[0];
501
+ if (element.kind !== "Identifier" || isLocallyResolved(element)) return undefined;
502
+ return environment.variableIndex.get(element.name) === variables[0]
503
+ ? { variable: variables[0], set: expression.args[1] }
504
+ : undefined;
505
+ };
506
+
484
507
  const getStateChecks = (named: NamedExpression[], environment: Environment): StateCheck[] =>
485
508
  named.flatMap((item) =>
486
- getConjuncts(item.expression, environment).map((expression) => ({
487
- name: item.name,
488
- expression,
489
- scope: item.scope,
490
- variables: getReadVariables(expression, environment),
491
- })),
509
+ getConjuncts(item.expression, environment).map((expression) => {
510
+ const variables = getReadVariables(expression, environment);
511
+ return {
512
+ name: item.name,
513
+ expression,
514
+ scope: item.scope,
515
+ variables,
516
+ membership: getMembership(expression, variables, environment),
517
+ };
518
+ }),
492
519
  );
493
520
 
494
521
  const isUnchangedFor = (stateCheck: StateCheck, state: Value[], parent: Value[] | undefined) => {
@@ -743,10 +770,16 @@ export const createSearch = (
743
770
  invariants.find((invariant) => {
744
771
  if (isUnchangedFor(invariant, state, parent)) return false;
745
772
  try {
746
- return !holdsLikeTlc(
747
- invariant.expression,
748
- getContext(environment, state, undefined, invariant.scope),
749
- );
773
+ const context = getContext(environment, state, undefined, invariant.scope);
774
+ const membership = invariant.membership;
775
+ // The parent satisfies every invariant, so only the entries the step changed can fail.
776
+ return parent && membership
777
+ ? !isUpdatedElementOf(
778
+ state[membership.variable],
779
+ parent[membership.variable],
780
+ evaluate(membership.set, context),
781
+ )
782
+ : !holdsLikeTlc(invariant.expression, context);
750
783
  } catch (error) {
751
784
  // TLC reports a failure only on a state after the initial states as an invariant failure.
752
785
  if (!parent) throw error;
@@ -0,0 +1,58 @@
1
+ export interface OptionSpec {
2
+ flag: string;
3
+ isRequired?: boolean;
4
+ isRepeatable?: boolean;
5
+ }
6
+
7
+ export interface ParsedOption {
8
+ name: string;
9
+ values: string[];
10
+ }
11
+
12
+ export interface ParsedArgs {
13
+ options: ParsedOption[];
14
+ positionals: string[];
15
+ unknown: string[];
16
+ incomplete: ParsedOption | undefined;
17
+ has: (name: string) => boolean;
18
+ get: (name: string) => string[] | undefined;
19
+ }
20
+
21
+ const formatOption = (spec: OptionSpec) => {
22
+ const text = spec.isRequired ? spec.flag : `[${spec.flag}]`;
23
+ return spec.isRepeatable ? `${text}...` : text;
24
+ };
25
+
26
+ // Each <name> in a flag such as "--toolbox <start> <end>" is one value the option takes, and
27
+ // {options} in the usage stands for the options.
28
+ export const defineCommand = (usage: string, specs: OptionSpec[]) => {
29
+ const arities = new Map(
30
+ specs.map((spec) => {
31
+ const [name, ...values] = spec.flag.split(" ");
32
+ return [name, values.length];
33
+ }),
34
+ );
35
+ const parse = (args: string[]): ParsedArgs => {
36
+ const options: ParsedOption[] = [];
37
+ const positionals: string[] = [];
38
+ const unknown: string[] = [];
39
+ for (let index = 0; index < args.length; index++) {
40
+ const arity = arities.get(args[index]);
41
+ if (arity === undefined) {
42
+ (args[index].startsWith("-") ? unknown : positionals).push(args[index]);
43
+ continue;
44
+ }
45
+ options.push({ name: args[index], values: args.slice(index + 1, index + 1 + arity) });
46
+ index += arity;
47
+ }
48
+ return {
49
+ options,
50
+ positionals,
51
+ unknown,
52
+ incomplete: options.find((option) => option.values.length < (arities.get(option.name) ?? 0)),
53
+ has: (name) => options.some((option) => option.name === name),
54
+ get: (name) => options.findLast((option) => option.name === name)?.values,
55
+ };
56
+ };
57
+ return { usage: usage.replace("{options}", specs.map(formatOption).join(" ")), parse };
58
+ };
package/src/errors.ts CHANGED
@@ -7,10 +7,20 @@ export class TlaError extends Error {
7
7
  }
8
8
  }
9
9
 
10
+ export interface SyntaxFailure {
11
+ reason: string;
12
+ line: number;
13
+ column: number;
14
+ isEnd: boolean;
15
+ }
16
+
10
17
  export class TlaSyntaxError extends TlaError {
11
- constructor(message: string, cause?: unknown) {
18
+ failure: SyntaxFailure | undefined;
19
+
20
+ constructor(message: string, cause?: unknown, failure?: SyntaxFailure) {
12
21
  super(message, cause);
13
22
  this.name = "TlaSyntaxError";
23
+ this.failure = failure;
14
24
  }
15
25
  }
16
26
 
@@ -38,6 +48,32 @@ export class TlaModuleError extends TlaEvaluationError {
38
48
  }
39
49
  }
40
50
 
51
+ export interface TlapmReport {
52
+ failure?: string;
53
+ exception?: string;
54
+ }
55
+
56
+ export class TlaProofError extends TlaError {
57
+ failure: string | undefined;
58
+ exception: string;
59
+
60
+ constructor(message: string, report: TlapmReport = {}) {
61
+ super(message);
62
+ this.name = "TlaProofError";
63
+ this.failure = report.exception === undefined ? (report.failure ?? message) : undefined;
64
+ this.exception = report.exception ?? `Failure(${JSON.stringify(this.failure)})`;
65
+ }
66
+ }
67
+
68
+ // tlapm fails one of its own assertions, which it reports as a bug in itself. It keeps the name
69
+ // TlaProofError, and index.ts doesn't export it.
70
+ export class TlapmBugError extends TlaProofError {
71
+ constructor(exception = "Assert_failure") {
72
+ super("Oops, this seems to be a bug in TLAPM.", { exception });
73
+ this.name = "TlaProofError";
74
+ }
75
+ }
76
+
41
77
  export class TlaUnsupportedError extends TlaError {
42
78
  constructor(message: string, cause?: unknown) {
43
79
  super(message, cause);
package/src/evaluate.ts CHANGED
@@ -734,7 +734,9 @@ const compileArgumentBinder = (argument: Expression, environment: Environment):
734
734
  if (global === undefined) {
735
735
  return (context) => {
736
736
  const binding = lookupScope(context.scope, argument.name);
737
- return binding?.kind === "Value" ? binding : createThunk(argument, context);
737
+ return binding?.kind === "Value" || binding?.kind === "Thunk"
738
+ ? binding
739
+ : createThunk(argument, context);
738
740
  };
739
741
  }
740
742
  }
package/src/lexer.ts CHANGED
@@ -380,7 +380,12 @@ export const tokenize = (source: string): Token[] => {
380
380
  }
381
381
  };
382
382
  const createError = (message: string) =>
383
- new TlaSyntaxError(`${message} at line ${cursor.line}, column ${getColumn()}`);
383
+ new TlaSyntaxError(`${message} at line ${cursor.line}, column ${getColumn()}`, undefined, {
384
+ reason: message,
385
+ line: cursor.line,
386
+ column: getColumn(),
387
+ isEnd: cursor.index >= source.length,
388
+ });
384
389
 
385
390
  while (cursor.index < source.length) {
386
391
  const character = source[cursor.index];