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,259 @@
1
+ // A port of TLC's tlc2.pprint package, which re-parses a printed value and breaks it across
2
+ // lines of at most 80 columns. TLC prints the original text whenever the parse fails.
3
+
4
+ import { skipWhile } from "./lexer";
5
+
6
+ interface PrintedNode {
7
+ kind: "Constant" | "Set" | "Sequence" | "Record" | "Function" | "Subset" | "Pair";
8
+ first: number;
9
+ last: number;
10
+ children: PrintedNode[];
11
+ }
12
+
13
+ interface Delimiters {
14
+ open: string;
15
+ close: string;
16
+ pad: string;
17
+ separator: string;
18
+ divider: string;
19
+ dividerPad: string;
20
+ }
21
+
22
+ const WIDTH = 80;
23
+
24
+ const DELIMITERS: Record<string, Delimiters> = {
25
+ Set: { open: "{ ", close: " }", pad: " ", separator: ",", divider: "", dividerPad: "" },
26
+ Sequence: { open: "<< ", close: " >>", pad: " ", separator: ",", divider: "", dividerPad: "" },
27
+ Subset: { open: "SUBSET ", close: " ", pad: " ", separator: ",", divider: "", dividerPad: "" },
28
+ Function: {
29
+ open: "( ",
30
+ close: " )",
31
+ pad: " ",
32
+ separator: " @@",
33
+ divider: " :>",
34
+ dividerPad: " ",
35
+ },
36
+ Record: {
37
+ open: "[ ",
38
+ close: " ]",
39
+ pad: " ",
40
+ separator: ",",
41
+ divider: " |->",
42
+ dividerPad: " ",
43
+ },
44
+ };
45
+
46
+ const isWhitespace = (character: string | undefined) => {
47
+ if (character === undefined) return false;
48
+ const code = character.charCodeAt(0);
49
+ return (
50
+ (code >= 0x09 && code <= 0x0d) ||
51
+ (code >= 0x1c && code <= 0x20) ||
52
+ code === 0x1680 ||
53
+ (code >= 0x2000 && code <= 0x200a && code !== 0x2007) ||
54
+ code === 0x2028 ||
55
+ code === 0x2029 ||
56
+ code === 0x205f ||
57
+ code === 0x3000
58
+ );
59
+ };
60
+
61
+ const isDigit = (character: string | undefined) =>
62
+ character !== undefined && /^\p{Nd}$/u.test(character);
63
+
64
+ const isLetterOrDigit = (character: string | undefined) =>
65
+ character !== undefined && /^[\p{L}\p{Nd}]$/u.test(character);
66
+
67
+ const parsePrinted = (text: string) => {
68
+ const skipWhitespace = (index: number) => {
69
+ const end = skipWhile(text, index, isWhitespace);
70
+ return text[end] === undefined ? undefined : end;
71
+ };
72
+
73
+ const parseItems = (
74
+ kind: PrintedNode["kind"],
75
+ first: number,
76
+ start: number,
77
+ close: string,
78
+ parseItem: (index: number) => PrintedNode | undefined,
79
+ separator: string,
80
+ ) => {
81
+ const node: PrintedNode = { kind, first, last: first, children: [] };
82
+ const position = { index: skipWhitespace(start) };
83
+ while (position.index !== undefined && text[position.index] !== close) {
84
+ const item = parseItem(position.index);
85
+ if (!item) return undefined;
86
+ node.children.push(item);
87
+ const afterItem = skipWhitespace(item.last + 1);
88
+ if (afterItem === undefined) return undefined;
89
+ const afterSeparator = text.startsWith(separator, afterItem)
90
+ ? afterItem + separator.length
91
+ : afterItem;
92
+ position.index = skipWhitespace(afterSeparator);
93
+ }
94
+ if (position.index === undefined) return undefined;
95
+ node.last = position.index;
96
+ return node;
97
+ };
98
+
99
+ const parsePair = (index: number, divider: string): PrintedNode | undefined => {
100
+ const left = parse(index);
101
+ if (!left) return undefined;
102
+ const dividerIndex = skipWhitespace(left.last + 1);
103
+ if (dividerIndex === undefined || !text.startsWith(divider, dividerIndex)) return undefined;
104
+ const rightIndex = skipWhitespace(dividerIndex + divider.length);
105
+ const right = rightIndex === undefined ? undefined : parse(rightIndex);
106
+ if (!right) return undefined;
107
+ return { kind: "Pair", first: index, last: right.last, children: [left, right] };
108
+ };
109
+
110
+ const parseInterval = (first: number): PrintedNode | undefined => {
111
+ const lowEnd = skipWhile(text, first, isDigit);
112
+ if (!text.startsWith("..", lowEnd) || !isDigit(text[lowEnd + 2])) return undefined;
113
+ return {
114
+ kind: "Constant",
115
+ first,
116
+ last: skipWhile(text, lowEnd + 2, isDigit) - 1,
117
+ children: [],
118
+ };
119
+ };
120
+
121
+ const parseConstant = (first: number): PrintedNode | undefined => {
122
+ const character = text[first];
123
+ if (character === '"') {
124
+ const closing = text.indexOf('"', first + 1);
125
+ return closing === -1 ? undefined : { kind: "Constant", first, last: closing, children: [] };
126
+ }
127
+ const interval = isDigit(character) ? parseInterval(first) : undefined;
128
+ if (interval) return interval;
129
+ if (character === "-") {
130
+ if (first + 1 >= text.length) return undefined;
131
+ return {
132
+ kind: "Constant",
133
+ first,
134
+ last: skipWhile(text, first + 1, isDigit) - 1,
135
+ children: [],
136
+ };
137
+ }
138
+ if (!isLetterOrDigit(character)) return undefined;
139
+ return {
140
+ kind: "Constant",
141
+ first,
142
+ last: skipWhile(text, first, isLetterOrDigit) - 1,
143
+ children: [],
144
+ };
145
+ };
146
+
147
+ const parse = (index: number): PrintedNode | undefined => {
148
+ const first = skipWhitespace(index);
149
+ if (first === undefined) return undefined;
150
+ switch (text[first]) {
151
+ case "{":
152
+ return parseItems("Set", first, first + 1, "}", parse, ",");
153
+ case "<": {
154
+ const node = parseItems("Sequence", first, first + 2, ">", parse, ",");
155
+ if (!node || node.last + 1 >= text.length) return undefined;
156
+ return { ...node, last: node.last + 1 };
157
+ }
158
+ case "[":
159
+ return parseItems("Record", first, first + 1, "]", (item) => parsePair(item, "|->"), ",");
160
+ case "(":
161
+ return parseItems("Function", first, first + 1, ")", (item) => parsePair(item, ":>"), "@@");
162
+ default: {
163
+ if (!text.startsWith("SUBSET", first)) return parseConstant(first);
164
+ const elementIndex = skipWhitespace(first + 6);
165
+ const element = elementIndex === undefined ? undefined : parse(elementIndex);
166
+ return element
167
+ ? { kind: "Subset", first, last: element.last, children: [element] }
168
+ : undefined;
169
+ }
170
+ }
171
+ };
172
+
173
+ return parse(0);
174
+ };
175
+
176
+ export const prettyPrint = (text: string) => {
177
+ const tree = text.length === 0 ? undefined : parsePrinted(text);
178
+ if (!tree || tree.last < text.length - 1) return text;
179
+
180
+ const getText = (node: PrintedNode) => text.slice(node.first, node.last + 1);
181
+ const getLength = (node: PrintedNode) => node.last - node.first + 1;
182
+
183
+ const format = (
184
+ node: PrintedNode,
185
+ columnWidth: number,
186
+ trailerWidth: number,
187
+ leftPad: string,
188
+ ): string | undefined => {
189
+ if (getLength(node) <= columnWidth - trailerWidth || node.kind === "Constant") {
190
+ return getText(node);
191
+ }
192
+ if (node.kind === "Pair") return undefined;
193
+ const delimiters = DELIMITERS[node.kind];
194
+ const innerWidth = columnWidth - Math.max(delimiters.open.length, delimiters.pad.length);
195
+ const innerTrailer = trailerWidth + delimiters.close.length;
196
+ const innerPad = leftPad + delimiters.pad;
197
+ const inner =
198
+ node.kind === "Function" || node.kind === "Record"
199
+ ? formatPairs(node.children, innerWidth, innerTrailer, innerPad, delimiters)
200
+ : formatItems(node.children, innerWidth, innerTrailer, innerPad, delimiters.separator);
201
+ return inner === undefined ? undefined : delimiters.open + inner + delimiters.close;
202
+ };
203
+
204
+ const formatItems = (
205
+ items: PrintedNode[],
206
+ columnWidth: number,
207
+ trailerWidth: number,
208
+ leftPad: string,
209
+ separator: string,
210
+ ) => {
211
+ const parts = items.map((item, index) => {
212
+ const isLast = index === items.length - 1;
213
+ const formatted = format(
214
+ item,
215
+ columnWidth,
216
+ isLast ? trailerWidth : separator.length,
217
+ leftPad,
218
+ );
219
+ return formatted === undefined || isLast ? formatted : `${formatted}${separator}\n${leftPad}`;
220
+ });
221
+ return parts.includes(undefined) ? undefined : parts.join("");
222
+ };
223
+
224
+ const formatPairs = (
225
+ pairs: PrintedNode[],
226
+ columnWidth: number,
227
+ trailerWidth: number,
228
+ leftPad: string,
229
+ delimiters: Delimiters,
230
+ ) => {
231
+ const parts = pairs.map((pair, index) => {
232
+ const isLast = index === pairs.length - 1;
233
+ const left = pair.children[0];
234
+ const right = pair.children[1];
235
+ const ending = isLast ? "" : `${delimiters.separator}\n${leftPad}`;
236
+ const pairLength = getLength(left) + delimiters.divider.length + 1 + getLength(right);
237
+ const fitsOnOneLine = isLast
238
+ ? pairLength <= columnWidth - trailerWidth
239
+ : pairLength + delimiters.separator.length <= columnWidth;
240
+ if (fitsOnOneLine) return `${getText(left)}${delimiters.divider} ${getText(right)}${ending}`;
241
+ const formattedLeft =
242
+ getLength(left) + delimiters.divider.length <= columnWidth
243
+ ? getText(left)
244
+ : format(left, columnWidth, trailerWidth + delimiters.divider.length, leftPad);
245
+ const formattedRight = format(
246
+ right,
247
+ columnWidth - delimiters.dividerPad.length,
248
+ isLast ? trailerWidth : delimiters.separator.length,
249
+ leftPad + delimiters.dividerPad,
250
+ );
251
+ return formattedLeft === undefined || formattedRight === undefined
252
+ ? undefined
253
+ : `${formattedLeft}${delimiters.divider}\n${leftPad}${delimiters.dividerPad}${formattedRight}${ending}`;
254
+ });
255
+ return parts.includes(undefined) ? undefined : parts.join("");
256
+ };
257
+
258
+ return format(tree, WIDTH, 0, "") ?? text;
259
+ };
@@ -1,14 +1,12 @@
1
1
  import { defineStateOperator, getValue } from "./builtins";
2
- import { TlaEvaluationError } from "./errors";
2
+ import { TlaEvaluationError, TlaModuleError } from "./errors";
3
3
  import type { Builtin, BuiltinArgument } from "./evaluate";
4
4
  import type { CheckerRuntime } from "./runtime";
5
+ import { getRandomSubset, type JavaRandom } from "./tlc-random";
5
6
  import {
6
- createRecord,
7
7
  createSet,
8
- createSortedFunction,
9
8
  createTuple,
10
9
  formatValue,
11
- getCardinality,
12
10
  isEmptySet,
13
11
  isEnumerable,
14
12
  isSetValue,
@@ -16,105 +14,48 @@ import {
16
14
  type Value,
17
15
  } from "./values";
18
16
 
19
- const describeArgument = (position: string, name: string, expected: string, value: string) =>
20
- new TlaEvaluationError(
17
+ const createArgumentError = (position: string, name: string, expected: string, value: string) =>
18
+ new TlaModuleError(
21
19
  `The ${position} argument of ${name} should be a ${expected}, but instead it is:\n${value}`,
22
20
  );
23
21
 
24
22
  const getCount = (value: Value, position: string, name: string) => {
25
23
  if (typeof value !== "number" || value < 0) {
26
- throw describeArgument(position, name, "nonnegative integer", formatValue(value));
24
+ throw createArgumentError(position, name, "nonnegative integer", formatValue(value));
27
25
  }
28
26
  return value;
29
27
  };
30
28
 
31
29
  const getFiniteElements = (value: Value, position: string, name: string, expected: string) => {
32
30
  if (!isSetValue(value) || !isEnumerable(value)) {
33
- throw describeArgument(position, name, expected, formatValue(value));
31
+ throw createArgumentError(position, name, expected, formatValue(value));
34
32
  }
35
33
  return toFiniteSet(value).elements;
36
34
  };
37
35
 
38
- const getMixedRadixElement = (sets: Value[], index: number) => {
39
- const lists = sets.map((set) => toFiniteSet(set).elements);
40
- return lists.reduceRight<{ remaining: number; elements: Value[] }>(
41
- (current, list) => ({
42
- remaining: Math.floor(current.remaining / list.length),
43
- elements: [list[current.remaining % list.length], ...current.elements],
44
- }),
45
- { remaining: index, elements: [] },
46
- ).elements;
47
- };
48
-
49
- const getElementAt = (set: Value, index: number): Value => {
50
- if (typeof set === "object" && set.kind === "Interval") return set.low + index;
51
- if (typeof set === "object" && set.kind === "ProductSet") {
52
- return createTuple(getMixedRadixElement(set.factors, index));
53
- }
54
- if (typeof set === "object" && set.kind === "RecordSet") {
55
- const values = getMixedRadixElement(set.sets, index);
56
- return createRecord(
57
- set.names.map((name, position): [string, Value] => [name, values[position]]),
58
- );
59
- }
60
- if (typeof set === "object" && set.kind === "FunctionSet") {
61
- const domain = toFiniteSet(set.domain).elements;
62
- return createSortedFunction(
63
- domain,
64
- getMixedRadixElement(
65
- domain.map(() => set.range),
66
- index,
67
- ),
68
- );
69
- }
70
- return toFiniteSet(set).elements[index];
71
- };
72
-
73
- const getSampleSize = (set: Value): number => {
74
- if (isEmptySet(set)) return 0;
75
- if (typeof set === "object" && set.kind === "ProductSet") {
76
- return set.factors.reduce<number>((size, factor) => size * getSampleSize(factor), 1);
77
- }
78
- if (typeof set === "object" && set.kind === "RecordSet") {
79
- return set.sets.reduce<number>((size, factor) => size * getSampleSize(factor), 1);
80
- }
81
- if (typeof set === "object" && set.kind === "FunctionSet") {
82
- return getSampleSize(set.range) ** getCardinality(set.domain);
83
- }
84
- return getCardinality(set);
85
- };
86
-
87
- const pickSubset = (set: Value, count: number, random: () => number) => {
88
- const size = getSampleSize(set);
89
- const indices = new Set<number>();
90
- while (indices.size < Math.min(count, size)) indices.add(Math.floor(random() * size));
91
- return [...indices].map((index) => getElementAt(set, index));
92
- };
93
-
94
- const tossSubsets = (elements: Value[], picks: number, probability: number, random: () => number) =>
36
+ // SubsetValue.CoinTossingSubsetEnumerator: each pick keeps each element with the probability.
37
+ const tossSubsets = (elements: Value[], picks: number, probability: number, random: JavaRandom) =>
95
38
  createSet(
96
- Array.from({ length: picks }, () => createSet(elements.filter(() => random() < probability))),
39
+ Array.from({ length: picks }, () =>
40
+ createSet(elements.filter(() => random.nextDouble() < probability)),
41
+ ),
97
42
  );
98
43
 
99
- const checkPicks = (picks: number, size: number, name: string) => {
100
- if (size < 31 && picks > 2 ** size) {
101
- throw describeArgument(
102
- "first",
103
- name,
104
- `nonnegative integer that is smaller than the subset's size of 2^${size}`,
105
- String(picks),
106
- );
107
- }
108
- };
109
-
110
44
  const getRandomSetOfSubsets = (args: BuiltinArgument[], runtime: CheckerRuntime) => {
111
45
  const name = "RandomSetOfSubsets";
112
46
  const picks = getCount(getValue(args, 0, name), "first", name);
113
47
  const size = getCount(getValue(args, 1, name), "second", name);
114
48
  const elements = getFiniteElements(getValue(args, 2, name), "third", name, "finite set");
115
- checkPicks(picks, elements.length, name);
49
+ if (elements.length < 31 && picks > 2 ** elements.length) {
50
+ throw createArgumentError(
51
+ "first",
52
+ name,
53
+ `nonnegative integer that is smaller than the subset's size of 2^${elements.length}`,
54
+ String(picks),
55
+ );
56
+ }
116
57
  if (elements.length < size) {
117
- throw describeArgument(
58
+ throw createArgumentError(
118
59
  "second",
119
60
  name,
120
61
  "nonnegative integer in range 0..Cardinality(S)",
@@ -125,7 +66,7 @@ const getRandomSetOfSubsets = (args: BuiltinArgument[], runtime: CheckerRuntime)
125
66
  elements,
126
67
  picks,
127
68
  elements.length === 0 ? 0 : size / elements.length,
128
- runtime.random,
69
+ runtime.getRandom(),
129
70
  );
130
71
  };
131
72
 
@@ -133,7 +74,7 @@ export const createRandomizationOperators = (runtime: CheckerRuntime): Builtin[]
133
74
  defineStateOperator("RandomSubset", 2, (args) => {
134
75
  const count = getValue(args, 0, "RandomSubset");
135
76
  if (typeof count !== "number") {
136
- throw describeArgument("first", "RandomSubset", "nonnegative integer", formatValue(count));
77
+ throw createArgumentError("first", "RandomSubset", "nonnegative integer", formatValue(count));
137
78
  }
138
79
  if (count < 0) {
139
80
  throw new TlaEvaluationError(
@@ -142,9 +83,9 @@ export const createRandomizationOperators = (runtime: CheckerRuntime): Builtin[]
142
83
  }
143
84
  const set = getValue(args, 1, "RandomSubset");
144
85
  if (!isSetValue(set) || (!isEmptySet(set) && !isEnumerable(set))) {
145
- throw describeArgument("second", "RandomSubset", "a finite set", formatValue(set));
86
+ throw createArgumentError("second", "RandomSubset", "a finite set", formatValue(set));
146
87
  }
147
- return createSet(pickSubset(set, count, runtime.random));
88
+ return createSet(getRandomSubset(count, set, runtime.getRandom()));
148
89
  }),
149
90
  defineStateOperator("RandomSetOfSubsets", 3, (args) => getRandomSetOfSubsets(args, runtime)),
150
91
  defineStateOperator("TestRandomSetOfSubsets", 3, (args) =>
@@ -155,34 +96,4 @@ export const createRandomizationOperators = (runtime: CheckerRuntime): Builtin[]
155
96
  ),
156
97
  ),
157
98
  ),
158
- defineStateOperator("RandomSubsetSet", 3, (args) => {
159
- const name = "RandomSubsetSetProbability";
160
- const picks = getCount(getValue(args, 0, "RandomSubsetSet"), "first", name);
161
- const text = getValue(args, 1, "RandomSubsetSet");
162
- if (typeof text !== "string") {
163
- throw describeArgument(
164
- "second",
165
- name,
166
- "string literal representing a probability",
167
- formatValue(text),
168
- );
169
- }
170
- const probability = Number(text);
171
- if (text.trim() === "" || Number.isNaN(probability) || probability < 0 || probability > 1) {
172
- throw describeArgument(
173
- "second",
174
- name,
175
- "string literal does not represent a parsable probability",
176
- formatValue(text),
177
- );
178
- }
179
- const elements = getFiniteElements(
180
- getValue(args, 2, "RandomSubsetSet"),
181
- "third",
182
- name,
183
- "finite set",
184
- );
185
- checkPicks(picks, elements.length, name);
186
- return tossSubsets(elements, picks, probability, runtime.random);
187
- }),
188
99
  ];