tla-ts 0.0.1 → 0.0.3-dev.763cc71

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 +78 -30
  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
@@ -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/codegen.ts CHANGED
@@ -1,16 +1,24 @@
1
1
  import type { Expression } from "./ast";
2
2
  import { TlaEvaluationError } from "./errors";
3
3
  import {
4
+ describeLocation,
4
5
  bindValue,
5
6
  expectBoolean,
6
7
  findGlobalName,
7
8
  getChildren,
8
9
  getEnumeratedElements,
10
+ getConditionError,
11
+ getConjunctError,
12
+ getDomainOperator,
13
+ getSubsetOperator,
9
14
  getEvaluator,
15
+ getFieldError,
10
16
  isClosedConstant,
11
17
  isLocallyResolved,
18
+ isTlcInteger,
12
19
  isValueArgument,
13
20
  isWorthCaching,
21
+ keepLazy,
14
22
  resolveGlobalOperator,
15
23
  type Context,
16
24
  type Environment,
@@ -18,9 +26,11 @@ import {
18
26
  } from "./evaluate";
19
27
  import {
20
28
  applyFunction,
21
- createSet,
29
+ compareValues,
30
+ createEnumeratedSet,
22
31
  createSortedFunction,
23
32
  createTuple,
33
+ createWrittenRecord,
24
34
  valuesEqual,
25
35
  type Value,
26
36
  } from "./values";
@@ -56,15 +66,21 @@ const GENERATED_KINDS = new Set([
56
66
  "Exists",
57
67
  ]);
58
68
 
59
- const describeLocation = (expression: Expression) =>
60
- `line ${expression.location.line}, column ${expression.location.column}`;
61
-
62
69
  const createRuntime = () => ({
63
70
  bool: expectBoolean,
71
+ condition: (value: Value, expression: Expression) => {
72
+ if (typeof value !== "boolean") throw getConditionError(expression, value);
73
+ return value;
74
+ },
75
+ conjunct: (value: Value, expression: Expression & { kind: "And" | "Or" }, index: number) => {
76
+ if (typeof value !== "boolean") throw getConjunctError(expression, index, value);
77
+ return value;
78
+ },
64
79
  apply: applyFunction,
65
80
  tuple: createTuple,
66
- set: createSet,
81
+ set: createEnumeratedSet,
67
82
  record: createSortedFunction,
83
+ writtenRecord: createWrittenRecord,
68
84
  equal: valuesEqual,
69
85
  enumerate: getEnumeratedElements,
70
86
  unassigned: (name: string, expression: Expression): never => {
@@ -103,7 +119,7 @@ const addReference = (generator: Generator, value: unknown) => {
103
119
 
104
120
  const createLocal = (generator: Generator) => `local${generator.counter++}`;
105
121
 
106
- const getFreeNames = (expression: Expression, names = new Set<string>()): Set<string> => {
122
+ const getFreeNames = (expression: Expression, names = new Set<string>()) => {
107
123
  if (expression.kind === "Identifier") names.add(expression.name);
108
124
  if (expression.kind === "Apply") names.add(expression.operator);
109
125
  getChildren(expression).forEach((child) => getFreeNames(child, names));
@@ -131,7 +147,7 @@ const generateQuantifier = (
131
147
  const description = JSON.stringify(isForall ? "the body of \\A" : "the body of \\E");
132
148
  const name = `quantifier${generator.counter++}`;
133
149
  const outer = [...frame.locals.values()];
134
- const inner: Frame = {
150
+ const inner = {
135
151
  context: "context",
136
152
  state: "state",
137
153
  next: "next",
@@ -174,6 +190,15 @@ const generateApply = (
174
190
  if (isLocallyResolved(expression)) return generateDelegate(generator, expression, frame);
175
191
  const builtin = resolveGlobalOperator(expression.operator, generator.environment);
176
192
  if (builtin?.kind !== "Builtin") return generateDelegate(generator, expression, frame);
193
+ if (builtin.name === "DOMAIN") {
194
+ const domain = addReference(generator, getDomainOperator(expression));
195
+ return `${domain}(${generateExpression(generator, args[0], frame)})`;
196
+ }
197
+ if (builtin.name === "TLCEval") return generateDelegate(generator, expression, frame);
198
+ if (builtin.name === "\\subseteq" && args.length === 2) {
199
+ const subset = addReference(generator, getSubsetOperator(expression, builtin));
200
+ return `${subset}(${generateExpression(generator, args[0], frame)}, ${generateExpression(generator, args[1], frame)})`;
201
+ }
177
202
  const environment = generator.environment;
178
203
  if (builtin.evaluateUnary && args.length === 1 && isValueArgument(args[0], environment)) {
179
204
  const operation = addReference(generator, builtin.evaluateUnary);
@@ -198,7 +223,7 @@ const generateExpression = (generator: Generator, expression: Expression, frame:
198
223
  }
199
224
  switch (expression.kind) {
200
225
  case "Number":
201
- if (Number.isInteger(expression.value) && Math.abs(expression.value) <= 2147483647) {
226
+ if (isTlcInteger(expression.value)) {
202
227
  return String(expression.value);
203
228
  }
204
229
  return generateDelegate(generator, expression, frame);
@@ -219,22 +244,26 @@ const generateExpression = (generator: Generator, expression: Expression, frame:
219
244
  }
220
245
  case "Apply":
221
246
  return generateApply(generator, expression, frame);
222
- case "And":
223
- return `(${expression.operands.map((operand) => `bool(${generate(operand)}, "a conjunct")`).join(" && ")})`;
247
+ case "And": {
248
+ const node = addReference(generator, expression);
249
+ return `(${expression.operands.map((operand, index) => `conjunct(${generate(operand)}, ${node}, ${index})`).join(" && ")})`;
250
+ }
224
251
  case "Or":
225
252
  return `(${expression.operands.map((operand) => `bool(${generate(operand)}, "a disjunct")`).join(" || ")})`;
226
253
  case "If":
227
- return `(bool(${generate(expression.condition)}, "an IF condition") ? ${generate(expression.consequent)} : ${generate(expression.alternative)})`;
254
+ return `(condition(${generate(expression.condition)}, ${addReference(generator, expression)}) ? ${generate(expression.consequent)} : ${generate(expression.alternative)})`;
228
255
  case "FunctionApplication": {
229
- const fn = generate(expression.function);
230
- if (expression.args.length === 1) return `apply(${fn}, ${generate(expression.args[0])})`;
231
- return `apply(${fn}, tuple([${expression.args.map((arg) => generate(arg)).join(", ")}]))`;
256
+ keepLazy(expression.function);
257
+ const functionCode = generate(expression.function);
258
+ if (expression.args.length === 1)
259
+ return `apply(${functionCode}, ${generate(expression.args[0])})`;
260
+ return `apply(${functionCode}, tuple([${expression.args.map((arg) => generate(arg)).join(", ")}]))`;
232
261
  }
233
262
  case "FieldAccess":
234
- return `apply(${generate(expression.record)}, ${JSON.stringify(expression.field)})`;
263
+ return `apply(${generate(expression.record)}, ${JSON.stringify(expression.field)}, ${addReference(generator, getFieldError(expression))})`;
235
264
  case "Record": {
236
265
  const fields = [...expression.fields].sort((left, right) =>
237
- left.name < right.name ? -1 : 1,
266
+ compareValues(left.name, right.name),
238
267
  );
239
268
  const hasDuplicates = fields.some(
240
269
  (field, index) => index > 0 && fields[index - 1].name === field.name,
@@ -244,7 +273,12 @@ const generateExpression = (generator: Generator, expression: Expression, frame:
244
273
  generator,
245
274
  fields.map((field) => field.name),
246
275
  );
247
- return `record(${names}, [${fields.map((field) => generate(field.value)).join(", ")}])`;
276
+ const values = `[${fields.map((field) => generate(field.value)).join(", ")}]`;
277
+ const writtenNames = expression.fields.map((field) => field.name);
278
+ if (writtenNames.every((name, index) => name === fields[index].name)) {
279
+ return `record(${names}, ${values})`;
280
+ }
281
+ return `writtenRecord(${names}, ${values}, ${addReference(generator, writtenNames)})`;
248
282
  }
249
283
  case "Tuple":
250
284
  return `tuple([${expression.elements.map((element) => generate(element)).join(", ")}])`;
@@ -253,7 +287,7 @@ const generateExpression = (generator: Generator, expression: Expression, frame:
253
287
  case "Prime": {
254
288
  const node = addReference(generator, expression);
255
289
  if (frame.isPrimed) return `primeOutsideAction(${node})`;
256
- const primed: Frame = {
290
+ const primed = {
257
291
  context: `(primed ??= prime(${frame.context}))`,
258
292
  state: frame.next,
259
293
  next: "undefined",
package/src/config.ts CHANGED
@@ -34,8 +34,11 @@ export interface ModelConfig {
34
34
  checkDeadlock: boolean;
35
35
  symmetry: string | undefined;
36
36
  view: string | undefined;
37
+ alias: string | undefined;
37
38
  postconditions: string[];
38
39
  possible: string[];
40
+ periodic: string | undefined;
41
+ rlReward: string | undefined;
39
42
  }
40
43
 
41
44
  interface ConfigToken {
@@ -65,11 +68,13 @@ const KEYWORDS = new Set([
65
68
  "POSTCONDITION",
66
69
  "POSTCONDITIONS",
67
70
  "_POSSIBLE",
71
+ "_PERIODIC",
72
+ "_RL_REWARD",
68
73
  ]);
69
74
 
70
75
  const SYMBOLS = ["<-", "<<", ">>", "=", "{", "}", ",", "[", "]", "(", ")"];
71
76
 
72
- const tokenizeConfig = (source: string): ConfigToken[] => {
77
+ const tokenizeConfig = (source: string) => {
73
78
  const tokens: ConfigToken[] = [];
74
79
  const cursor = { index: 0, line: 1 };
75
80
  while (cursor.index < source.length) {
@@ -92,7 +97,7 @@ const tokenizeConfig = (source: string): ConfigToken[] => {
92
97
  cursor.index += end + 2;
93
98
  continue;
94
99
  }
95
- const number = /^-?\d+(?![A-Za-z_])/.exec(rest)?.[0];
100
+ const number = /^\d+(?![A-Za-z_])/.exec(rest)?.[0];
96
101
  if (number) {
97
102
  tokens.push({ kind: "Number", text: number, line: cursor.line });
98
103
  cursor.index += number.length;
@@ -106,7 +111,7 @@ const tokenizeConfig = (source: string): ConfigToken[] => {
106
111
  }
107
112
  const string = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
108
113
  if (string) {
109
- tokens.push({ kind: "String", text: JSON.parse(`"${string[1]}"`), line: cursor.line });
114
+ tokens.push({ kind: "String", text: string[1], line: cursor.line });
110
115
  cursor.index += string[0].length;
111
116
  continue;
112
117
  }
@@ -144,10 +149,14 @@ export const parseConfig = (source: string): ModelConfig => {
144
149
  checkDeadlock: true,
145
150
  symmetry: undefined,
146
151
  view: undefined,
152
+ alias: undefined,
147
153
  postconditions: [],
148
154
  possible: [],
155
+ periodic: undefined,
156
+ rlReward: undefined,
149
157
  };
150
158
 
159
+ const deadlock = { isSet: false };
151
160
  const peek = () => tokens[cursor.index];
152
161
  const advance = () => tokens[cursor.index++];
153
162
  const isKeyword = (token: ConfigToken) => token.kind === "Word" && KEYWORDS.has(token.text);
@@ -168,13 +177,22 @@ export const parseConfig = (source: string): ModelConfig => {
168
177
  }
169
178
  return token.text;
170
179
  };
180
+ const isNameAhead = () => peek().kind === "Word" && !isKeyword(peek());
171
181
  const parseNames = () => {
172
182
  const names: string[] = [];
173
- while (peek().kind === "Word" && !isKeyword(peek())) names.push(advance().text);
183
+ while (isNameAhead()) names.push(advance().text);
174
184
  return names;
175
185
  };
176
186
  const setSingle = (
177
- key: "init" | "next" | "specification" | "symmetry" | "view",
187
+ key:
188
+ | "init"
189
+ | "next"
190
+ | "specification"
191
+ | "symmetry"
192
+ | "view"
193
+ | "alias"
194
+ | "periodic"
195
+ | "rlReward",
178
196
  keyword: string,
179
197
  ) => {
180
198
  if (config[key] !== undefined) throw new TlaConfigError(`The keyword ${keyword} appears twice`);
@@ -189,11 +207,12 @@ export const parseConfig = (source: string): ModelConfig => {
189
207
  return { kind: "Boolean", value: token.text === "TRUE" };
190
208
  if (token.text === "{") {
191
209
  const elements: ConfigValue[] = [];
192
- while (peek().text !== "}") {
210
+ if (peek().text !== "}") elements.push(parseValue());
211
+ while (peek().text === ",") {
212
+ advance();
193
213
  elements.push(parseValue());
194
- if (peek().text !== "}") expect(",");
195
214
  }
196
- advance();
215
+ expect("}");
197
216
  return { kind: "Set", elements };
198
217
  }
199
218
  if (token.kind === "Word" && !isKeyword(token)) return { kind: "ModelValue", name: token.text };
@@ -211,7 +230,7 @@ export const parseConfig = (source: string): ModelConfig => {
211
230
  };
212
231
 
213
232
  const parseConstants = () => {
214
- while (peek().kind === "Word" && !isKeyword(peek())) {
233
+ while (isNameAhead()) {
215
234
  const name = advance().text;
216
235
  if (peek().text === "(") {
217
236
  advance();
@@ -222,7 +241,8 @@ export const parseConfig = (source: string): ModelConfig => {
222
241
  }
223
242
  advance();
224
243
  expect("=");
225
- config.constants.push({ name, value: parseValue(), args });
244
+ const module = parseModuleScope();
245
+ config.constants.push({ name, value: parseValue(), args, ...(module && { module }) });
226
246
  continue;
227
247
  }
228
248
  if (peek().text === "<-") {
@@ -276,7 +296,7 @@ export const parseConfig = (source: string): ModelConfig => {
276
296
  config.actionConstraints.push(...parseNames());
277
297
  break;
278
298
  case "ALIAS":
279
- expectName();
299
+ setSingle("alias", token.text);
280
300
  break;
281
301
  case "POSTCONDITION":
282
302
  case "POSTCONDITIONS":
@@ -285,7 +305,15 @@ export const parseConfig = (source: string): ModelConfig => {
285
305
  case "_POSSIBLE":
286
306
  config.possible.push(...parseNames());
287
307
  break;
308
+ case "_PERIODIC":
309
+ setSingle("periodic", token.text);
310
+ break;
311
+ case "_RL_REWARD":
312
+ setSingle("rlReward", token.text);
313
+ break;
288
314
  case "CHECK_DEADLOCK": {
315
+ if (deadlock.isSet) throw new TlaConfigError("The keyword CHECK_DEADLOCK appears twice");
316
+ deadlock.isSet = true;
289
317
  const value = advance().text;
290
318
  if (value !== "TRUE" && value !== "FALSE") {
291
319
  throw new TlaConfigError(