tla-ts 0.0.1 → 0.0.3-dev.6979965

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 +85 -40
  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 +48 -13
  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 +121 -123
  49. package/src/parallel.ts +167 -83
  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/parallel.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Expression } from "./ast";
2
2
  import { createCanonicalizer } from "./canonical";
3
3
  import {
4
- check,
4
+ checkSequentially,
5
5
  createSearch,
6
6
  findAssumptionViolation,
7
7
  type CheckOptions,
@@ -9,11 +9,21 @@ import {
9
9
  } from "./check";
10
10
  import { parseConfig, type ModelConfig } from "./config";
11
11
  import { createEnvironment } from "./environment";
12
- import { getChildren, isImpureBuiltin, type Environment } from "./evaluate";
13
- import { createFingerprintTable, getFingerprintOwner, getStateFingerprint } from "./fingerprints";
14
- import { parseModule } from "./parser";
12
+ import { TlaEvaluationError } from "./errors";
13
+ import { isImpure } from "./evaluate";
14
+ import {
15
+ createSharedFingerprints,
16
+ growSharedFingerprints,
17
+ insertSharedFingerprint,
18
+ isSharedFingerprintsFull,
19
+ writeStateFingerprint,
20
+ type SharedFingerprints,
21
+ } from "./fingerprints";
15
22
  import { getInitialStates } from "./successors";
23
+ import { getTlcStringOrder, getTlcVariableOrder } from "./tlc-order";
24
+ import { getFingerprintDistance, OUT_DEGREE_BUCKETS } from "./tlc-summary";
16
25
  import { createStateEncoder, type EncodedStates } from "./transfer";
26
+ import { getRankedStringCount, setStringOrder, type Value } from "./values";
17
27
 
18
28
  const WORKER_THREADS_MODULE = "node:worker_threads";
19
29
 
@@ -29,11 +39,11 @@ export interface StartMessage {
29
39
  source: string;
30
40
  config: ModelConfig;
31
41
  modules: [string, string | undefined][];
42
+ classes: [string, Uint8Array | undefined][];
32
43
  index: number;
33
- count: number;
34
44
  ports: (Port | undefined)[];
35
45
  seed: EncodedStates;
36
- seedFingerprints: Int32Array;
46
+ fingerprints: SharedFingerprints;
37
47
  }
38
48
 
39
49
  export interface LevelMessage {
@@ -41,13 +51,21 @@ export interface LevelMessage {
41
51
  level: number;
42
52
  generated: number;
43
53
  added: number;
54
+ outDegrees: number[];
55
+ fingerprints: BigInt64Array;
44
56
  }
45
57
 
46
- export type WorkerMessage = { type: "ready" } | { type: "abort" } | LevelMessage;
58
+ export type WorkerMessage =
59
+ | { type: "ready" }
60
+ | { type: "abort" }
61
+ | { type: "paused" }
62
+ | { type: "expanded"; from: number; added: number }
63
+ | LevelMessage;
47
64
 
48
- export interface ParallelCheckOptions extends CheckOptions {
49
- workers: number;
50
- }
65
+ export type CoordinatorMessage =
66
+ | { type: "level"; level: number }
67
+ | { type: "resize"; fingerprints: SharedFingerprints }
68
+ | { type: "balance"; sizes: number[] };
51
69
 
52
70
  interface WorkerHandle {
53
71
  postMessage: (message: unknown, transfer?: unknown[]) => void;
@@ -73,7 +91,7 @@ const spawnWorker = async (onMessage: (message: WorkerMessage) => void): Promise
73
91
  /* @vite-ignore */ /* webpackIgnore: true */ WORKER_THREADS_MODULE
74
92
  );
75
93
  const worker = new threads.Worker(new URL("./parallel-worker.js", import.meta.url));
76
- worker.on("message", (message: WorkerMessage) => onMessage(message));
94
+ worker.on("message", onMessage);
77
95
  worker.on("error", () => onMessage({ type: "abort" }));
78
96
  return {
79
97
  postMessage: (message, transfer) =>
@@ -82,93 +100,135 @@ const spawnWorker = async (onMessage: (message: WorkerMessage) => void): Promise
82
100
  };
83
101
  };
84
102
 
85
- const usesImpureBuiltin = (expression: Expression, environment: Environment): boolean =>
86
- ((expression.kind === "Apply" || expression.kind === "OperatorReference") &&
87
- isImpureBuiltin(expression.operator, environment)) ||
88
- (expression.kind === "Identifier" && isImpureBuiltin(expression.name, environment)) ||
89
- getChildren(expression).some((child) => usesImpureBuiltin(child, environment));
90
-
91
- const isImpure = (environment: Environment) =>
92
- [...environment.definitions.values()].some(
93
- (definition) =>
94
- usesImpureBuiltin(definition.body, environment) ||
95
- (definition.kind === "Function" &&
96
- definition.bounds.some((bound) => usesImpureBuiltin(bound.set, environment))),
97
- );
98
-
99
- export const checkInParallel = async (options: ParallelCheckOptions): Promise<CheckResult> => {
103
+ export const check = async (options: CheckOptions): Promise<CheckResult> => {
100
104
  const config = typeof options.config === "string" ? parseConfig(options.config) : options.config;
101
- const checkSequentially = () => check({ ...options, config });
105
+ const fallBack = () => checkSequentially({ ...options, config });
106
+ const count = options.workers ?? 1;
102
107
  const isSequential =
103
- options.workers < 2 ||
108
+ count < 2 ||
109
+ typeof SharedArrayBuffer !== "function" ||
104
110
  config.view !== undefined ||
105
111
  config.postconditions.length > 0 ||
106
112
  config.possible.length > 0;
107
- if (isSequential) return checkSequentially();
113
+ if (isSequential) return fallBack();
108
114
  const modules = new Map<string, string | undefined>();
109
- const specification = createEnvironment(parseModule(options.source), config, {
110
- loadModule: (name) => {
111
- const source = options.loadModule?.(name);
112
- modules.set(name, source);
113
- return source;
114
- },
115
+ const loadModule = (name: string) => {
116
+ const source = options.loadModule?.(name);
117
+ modules.set(name, source);
118
+ return source;
119
+ };
120
+ const classes = new Map<string, Uint8Array | undefined>();
121
+ const loadClass = (name: string) => {
122
+ const bytes = options.loadClass?.(name);
123
+ classes.set(name, bytes);
124
+ return bytes;
125
+ };
126
+ setStringOrder(getTlcStringOrder(options.source, config, loadModule));
127
+ const startupStrings = getRankedStringCount();
128
+ const specification = createEnvironment(config, {
129
+ source: options.source,
130
+ loadModule,
131
+ loadClass,
115
132
  print: options.onPrint ?? console.log,
116
133
  files: { readFile: options.readFile, writeFile: options.writeFile },
117
134
  });
118
135
  const environment = specification.environment;
119
- if (findAssumptionViolation(specification.assumptions, environment)) return checkSequentially();
120
- const search = createSearch(config, environment);
121
- if (!search || search.properties.liveness.length > 0 || isImpure(environment)) {
122
- return checkSequentially();
136
+ const search = createSearch(config, environment, () =>
137
+ getTlcVariableOrder(options.source, loadModule),
138
+ );
139
+ if (findAssumptionViolation(specification.assumptions, environment)) return fallBack();
140
+ if (
141
+ !search ||
142
+ search.spec.init === undefined ||
143
+ search.spec.next === undefined ||
144
+ search.properties.liveness.length > 0 ||
145
+ isImpure(environment)
146
+ ) {
147
+ return fallBack();
123
148
  }
124
149
 
125
- const count = options.workers;
126
150
  const canonicalize = createCanonicalizer();
127
- const table = createFingerprintTable();
151
+ const shared = { fingerprints: createSharedFingerprints() };
128
152
  const fingerprint = new Int32Array(2);
129
- const seeds = Array.from({ length: count }, () => ({
130
- encoder: createStateEncoder(),
131
- fingerprints: [] as number[],
132
- }));
133
- const statistics = { generatedStates: 0, distinctStates: 0, depth: 0 };
134
- for (const state of getInitialStates(search.spec.init, environment)) {
135
- canonicalize(state);
136
- statistics.generatedStates++;
137
- getStateFingerprint(search.getFingerprintState(state), fingerprint);
138
- if (!table.insert(fingerprint[0], fingerprint[1])) continue;
139
- if (search.findViolatedInvariant(state) || search.findFailedInitialPredicate(state)) {
140
- return checkSequentially();
141
- }
142
- if (!search.satisfiesConstraints(state)) {
143
- table.remove(fingerprint[0], fingerprint[1]);
144
- continue;
153
+ const seeds = Array.from({ length: count }, createStateEncoder);
154
+ const statistics = {
155
+ generatedStates: 0,
156
+ distinctStates: 0,
157
+ queuedStates: 0,
158
+ depth: 0,
159
+ outDegrees: Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0),
160
+ };
161
+ const tlcFingerprints: BigInt64Array[] = [];
162
+ const createSuccess = (): CheckResult => ({
163
+ status: "success",
164
+ violation: undefined,
165
+ failure: undefined,
166
+ ...statistics,
167
+ fingerprintDistance: getFingerprintDistance(
168
+ statistics.distinctStates,
169
+ statistics.generatedStates,
170
+ () => {
171
+ const fingerprints = new BigInt64Array(statistics.distinctStates);
172
+ tlcFingerprints.reduce((offset, level) => {
173
+ fingerprints.set(level, offset);
174
+ return offset + level.length;
175
+ }, 0);
176
+ return fingerprints;
177
+ },
178
+ ),
179
+ });
180
+ const seedInitialStates = (init: Expression) => {
181
+ const seeded: Value[][] = [];
182
+ for (const state of getInitialStates(init, environment, search.spec.scope)) {
183
+ canonicalize(state);
184
+ statistics.generatedStates++;
185
+ writeStateFingerprint(search.getFingerprintState(state), fingerprint);
186
+ if (isSharedFingerprintsFull(shared.fingerprints)) {
187
+ shared.fingerprints = growSharedFingerprints(shared.fingerprints);
188
+ }
189
+ if (!insertSharedFingerprint(shared.fingerprints, fingerprint[0], fingerprint[1])) continue;
190
+ if (search.findViolatedInvariant(state) || search.findFailedInitialPredicate(state)) {
191
+ return false;
192
+ }
193
+ if (!search.satisfiesConstraints(state)) continue;
194
+ if (!seeds[seeded.length % count].encode(state)) return false;
195
+ seeded.push(state);
196
+ statistics.distinctStates++;
145
197
  }
146
- const seed = seeds[getFingerprintOwner(fingerprint[1], count)];
147
- if (!seed.encoder.encode(state)) return checkSequentially();
148
- seed.fingerprints.push(fingerprint[0], fingerprint[1]);
149
- statistics.distinctStates++;
198
+ tlcFingerprints.push(
199
+ BigInt64Array.from(seeded, (state) => environment.runtime.getFingerprint(state)),
200
+ );
201
+ return true;
202
+ };
203
+ // The sequential search reports a violation or a failed evaluation in the initial states.
204
+ try {
205
+ if (!seedInitialStates(search.spec.init)) return fallBack();
206
+ } catch (error) {
207
+ if (!(error instanceof TlaEvaluationError)) throw error;
208
+ return fallBack();
150
209
  }
151
- if (statistics.distinctStates === 0)
152
- return { status: "success", violation: undefined, ...statistics };
153
- statistics.depth = 1;
210
+ if (getRankedStringCount() !== startupStrings) return fallBack();
211
+ statistics.depth = statistics.generatedStates === 0 ? 0 : 1;
212
+ if (statistics.distinctStates === 0) return createSuccess();
154
213
 
155
214
  const reports: WorkerMessage[] = [];
156
- const pending = { resolve: (_: WorkerMessage[]) => {} };
215
+ const pending = { count, resolve: (_reports: WorkerMessage[]) => {} };
157
216
  const onMessage = (message: WorkerMessage) => {
158
217
  reports.push(message);
159
- if (message.type === "abort" || reports.length === count) {
218
+ if (message.type === "abort" || reports.length === pending.count) {
160
219
  pending.resolve(reports.splice(0));
161
220
  }
162
221
  };
163
- const waitForReports = () =>
222
+ const waitForReports = (expected: number) =>
164
223
  new Promise<WorkerMessage[]>((resolve) => {
224
+ pending.count = expected;
165
225
  pending.resolve = resolve;
166
226
  });
167
- const ready = waitForReports();
227
+ const ready = waitForReports(count);
168
228
  const workers = await Promise.all(Array.from({ length: count }, () => spawnWorker(onMessage)));
169
229
  const terminate = () => workers.forEach((worker) => worker.terminate());
170
230
  const channels = new Map<string, MessageChannel>();
171
- const getPort = (from: number, to: number): Port => {
231
+ const getPort = (from: number, to: number) => {
172
232
  const key = from < to ? `${from}:${to}` : `${to}:${from}`;
173
233
  const channel = channels.get(key) ?? new MessageChannel();
174
234
  channels.set(key, channel);
@@ -178,46 +238,70 @@ export const checkInParallel = async (options: ParallelCheckOptions): Promise<Ch
178
238
  const ports = Array.from({ length: count }, (_, peer) =>
179
239
  peer === index ? undefined : getPort(index, peer),
180
240
  );
181
- const seed = seeds[index].encoder.finish();
182
- const seedFingerprints = Int32Array.from(seeds[index].fingerprints);
241
+ const seed = seeds[index].finish();
183
242
  const message: StartMessage = {
184
243
  type: "start",
185
244
  source: options.source,
186
245
  config,
187
246
  modules: [...modules],
247
+ classes: [...classes],
188
248
  index,
189
- count,
190
249
  ports,
191
250
  seed,
192
- seedFingerprints,
251
+ fingerprints: shared.fingerprints,
193
252
  };
194
- worker.postMessage(message, [
195
- ...ports.filter((port) => port !== undefined),
196
- seed.words.buffer,
197
- seedFingerprints.buffer,
198
- ]);
253
+ worker.postMessage(message, [...ports.filter((port) => port !== undefined), seed.words.buffer]);
199
254
  });
200
255
  const isAborted = (messages: WorkerMessage[]) =>
201
256
  messages.some((message) => message.type === "abort");
257
+ const send = (message: CoordinatorMessage) =>
258
+ workers.forEach((worker) => worker.postMessage(message));
259
+ // Workers pause when the shared table needs to grow, and resume once it has.
260
+ const collectExpansions = async (messages: WorkerMessage[]): Promise<WorkerMessage[]> => {
261
+ const paused = messages.filter((message) => message.type === "paused").length;
262
+ if (paused === 0 || isAborted(messages)) return messages;
263
+ shared.fingerprints = growSharedFingerprints(shared.fingerprints);
264
+ const resumed = waitForReports(paused);
265
+ send({ type: "resize", fingerprints: shared.fingerprints });
266
+ const expanded = messages.filter((message) => message.type !== "paused");
267
+ return [...expanded, ...(await collectExpansions(await resumed))];
268
+ };
202
269
  if (isAborted(await ready)) {
203
270
  terminate();
204
- return checkSequentially();
271
+ return fallBack();
205
272
  }
206
273
  for (let level = 1; ; level++) {
207
- const reported = waitForReports();
208
- workers.forEach((worker) => worker.postMessage({ type: "level", level }));
274
+ const expanding = waitForReports(count);
275
+ send({ type: "level", level });
276
+ const expansions = await collectExpansions(await expanding);
277
+ if (isAborted(expansions)) {
278
+ terminate();
279
+ return fallBack();
280
+ }
281
+ const sizes = Array.from({ length: count }, () => 0);
282
+ expansions.forEach((message) => {
283
+ if (message.type === "expanded") sizes[message.from] = message.added;
284
+ });
285
+ const reported = waitForReports(count);
286
+ send({ type: "balance", sizes });
209
287
  const messages = await reported;
210
288
  if (isAborted(messages)) {
211
289
  terminate();
212
- return checkSequentially();
290
+ return fallBack();
213
291
  }
214
292
  const levels = messages.flatMap((message) => (message.type === "level" ? [message] : []));
215
293
  const added = levels.reduce((sum, message) => sum + message.added, 0);
216
294
  statistics.generatedStates += levels.reduce((sum, message) => sum + message.generated, 0);
217
295
  statistics.distinctStates += added;
296
+ levels.forEach((message) => {
297
+ message.outDegrees.forEach(
298
+ (degreeCount, degree) => (statistics.outDegrees[degree] += degreeCount),
299
+ );
300
+ tlcFingerprints.push(message.fingerprints);
301
+ });
218
302
  if (added === 0) {
219
303
  terminate();
220
- return { status: "success", violation: undefined, ...statistics };
304
+ return createSuccess();
221
305
  }
222
306
  statistics.depth++;
223
307
  }