tla-ts 0.0.1 → 0.0.3-dev.3cf2ad0

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 +216 -30
  4. package/dist/index.d.ts +216 -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 +183 -202
  49. package/src/parallel.ts +163 -84
  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 +70 -22
  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 +1234 -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,13 @@ 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;
47
+ requests: Int32Array;
48
+ remaining: Int32Array;
37
49
  }
38
50
 
39
51
  export interface LevelMessage {
@@ -41,13 +53,19 @@ export interface LevelMessage {
41
53
  level: number;
42
54
  generated: number;
43
55
  added: number;
56
+ outDegrees: number[];
57
+ fingerprints: BigInt64Array;
44
58
  }
45
59
 
46
- export type WorkerMessage = { type: "ready" } | { type: "abort" } | LevelMessage;
60
+ export type WorkerMessage =
61
+ | { type: "ready" }
62
+ | { type: "abort" }
63
+ | { type: "paused" }
64
+ | LevelMessage;
47
65
 
48
- export interface ParallelCheckOptions extends CheckOptions {
49
- workers: number;
50
- }
66
+ export type CoordinatorMessage =
67
+ | { type: "level"; level: number }
68
+ | { type: "resize"; fingerprints: SharedFingerprints };
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,137 @@ 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);
228
+ const requests = new Int32Array(new SharedArrayBuffer(count * 4));
229
+ const remaining = new Int32Array(new SharedArrayBuffer(count * 4));
168
230
  const workers = await Promise.all(Array.from({ length: count }, () => spawnWorker(onMessage)));
169
231
  const terminate = () => workers.forEach((worker) => worker.terminate());
170
232
  const channels = new Map<string, MessageChannel>();
171
- const getPort = (from: number, to: number): Port => {
233
+ const getPort = (from: number, to: number) => {
172
234
  const key = from < to ? `${from}:${to}` : `${to}:${from}`;
173
235
  const channel = channels.get(key) ?? new MessageChannel();
174
236
  channels.set(key, channel);
@@ -178,46 +240,63 @@ export const checkInParallel = async (options: ParallelCheckOptions): Promise<Ch
178
240
  const ports = Array.from({ length: count }, (_, peer) =>
179
241
  peer === index ? undefined : getPort(index, peer),
180
242
  );
181
- const seed = seeds[index].encoder.finish();
182
- const seedFingerprints = Int32Array.from(seeds[index].fingerprints);
243
+ const seed = seeds[index].finish();
183
244
  const message: StartMessage = {
184
245
  type: "start",
185
246
  source: options.source,
186
247
  config,
187
248
  modules: [...modules],
249
+ classes: [...classes],
188
250
  index,
189
- count,
190
251
  ports,
191
252
  seed,
192
- seedFingerprints,
253
+ fingerprints: shared.fingerprints,
254
+ requests,
255
+ remaining,
193
256
  };
194
- worker.postMessage(message, [
195
- ...ports.filter((port) => port !== undefined),
196
- seed.words.buffer,
197
- seedFingerprints.buffer,
198
- ]);
257
+ worker.postMessage(message, [...ports.filter((port) => port !== undefined), seed.words.buffer]);
199
258
  });
200
259
  const isAborted = (messages: WorkerMessage[]) =>
201
260
  messages.some((message) => message.type === "abort");
261
+ const send = (message: CoordinatorMessage) =>
262
+ workers.forEach((worker) => worker.postMessage(message));
263
+ // Workers pause when the shared table needs to grow, and resume once it has.
264
+ const collectLevels = async (messages: WorkerMessage[]): Promise<WorkerMessage[]> => {
265
+ const paused = messages.filter((message) => message.type === "paused").length;
266
+ if (paused === 0 || isAborted(messages)) return messages;
267
+ shared.fingerprints = growSharedFingerprints(shared.fingerprints);
268
+ const resumed = waitForReports(paused);
269
+ send({ type: "resize", fingerprints: shared.fingerprints });
270
+ const reported = messages.filter((message) => message.type !== "paused");
271
+ return [...reported, ...(await collectLevels(await resumed))];
272
+ };
202
273
  if (isAborted(await ready)) {
203
274
  terminate();
204
- return checkSequentially();
275
+ return fallBack();
205
276
  }
206
277
  for (let level = 1; ; level++) {
207
- const reported = waitForReports();
208
- workers.forEach((worker) => worker.postMessage({ type: "level", level }));
209
- const messages = await reported;
278
+ const reported = waitForReports(count);
279
+ // Every worker is idle, so peers can ask any of them for states once the level starts.
280
+ requests.forEach((_, index) => Atomics.store(requests, index, 0));
281
+ send({ type: "level", level });
282
+ const messages = await collectLevels(await reported);
210
283
  if (isAborted(messages)) {
211
284
  terminate();
212
- return checkSequentially();
285
+ return fallBack();
213
286
  }
214
287
  const levels = messages.flatMap((message) => (message.type === "level" ? [message] : []));
215
288
  const added = levels.reduce((sum, message) => sum + message.added, 0);
216
289
  statistics.generatedStates += levels.reduce((sum, message) => sum + message.generated, 0);
217
290
  statistics.distinctStates += added;
291
+ levels.forEach((message) => {
292
+ message.outDegrees.forEach(
293
+ (degreeCount, degree) => (statistics.outDegrees[degree] += degreeCount),
294
+ );
295
+ tlcFingerprints.push(message.fingerprints);
296
+ });
218
297
  if (added === 0) {
219
298
  terminate();
220
- return { status: "success", violation: undefined, ...statistics };
299
+ return createSuccess();
221
300
  }
222
301
  statistics.depth++;
223
302
  }