tla-ts 0.0.2 → 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.
@@ -1,9 +1,20 @@
1
1
  import { createCanonicalizer } from "./canonical";
2
2
  import { createSearch } from "./check";
3
3
  import { createEnvironment } from "./environment";
4
- import { createFingerprintTable, getFingerprintOwner, writeStateFingerprint } from "./fingerprints";
5
- import type { LevelMessage, Port, StartMessage, WorkerMessage } from "./parallel";
6
- import { getSuccessors } from "./successors";
4
+ import {
5
+ createFingerprintTable,
6
+ insertSharedFingerprint,
7
+ isSharedFingerprintsFull,
8
+ writeStateFingerprint,
9
+ } from "./fingerprints";
10
+ import type {
11
+ CoordinatorMessage,
12
+ LevelMessage,
13
+ Port,
14
+ StartMessage,
15
+ WorkerMessage,
16
+ } from "./parallel";
17
+ import { getSuccessors, type Successor } from "./successors";
7
18
  import { getTlcStringOrder, getTlcVariableOrder } from "./tlc-order";
8
19
  import { OUT_DEGREE_BUCKETS } from "./tlc-summary";
9
20
  import { createStateDecoder, createStateEncoder, type EncodedStates } from "./transfer";
@@ -11,21 +22,21 @@ import { getRankedStringCount, setStringOrder, type Value } from "./values";
11
22
 
12
23
  const WORKER_THREADS_MODULE = "node:worker_threads";
13
24
 
14
- interface Pending {
15
- states: Value[][];
16
- origins: number[];
17
- fingerprints: number[];
18
- }
19
-
20
25
  interface PeerMessage {
21
- phase: "query" | "answer" | "sizes" | "balance";
26
+ phase: "balance";
22
27
  level: number;
23
28
  from: number;
24
- fingerprints?: Int32Array;
25
- bits?: Uint8Array;
26
- states?: EncodedStates;
27
- unmarks?: Int32Array;
28
- size?: number;
29
+ states: EncodedStates;
30
+ }
31
+
32
+ interface Expansion {
33
+ origin: number;
34
+ successors: Successor[] | undefined;
35
+ index: number;
36
+ generated: number;
37
+ newStates: Int32Array;
38
+ next: Value[][];
39
+ fingerprintStates: Value[][];
29
40
  }
30
41
 
31
42
  interface Transfer {
@@ -117,12 +128,13 @@ const run = (start: StartMessage, parent: Port) => {
117
128
  const nextAction = search?.spec.next;
118
129
  if (!search || nextAction === undefined)
119
130
  return parent.postMessage({ type: "abort" } satisfies WorkerMessage);
120
- const count = start.count;
121
131
  const self = start.index;
122
132
  const canonicalize = createCanonicalizer();
123
133
  const decode = createStateDecoder(environment.variables.length);
124
- const table = createFingerprintTable();
125
- const known = createFingerprintTable();
134
+ const shared = { fingerprints: start.fingerprints };
135
+ // Under symmetry, a successor the worker generated before has the representative it had then,
136
+ // so the worker skips the search for one.
137
+ const generatedStates = search.isSymmetric ? createFingerprintTable() : undefined;
126
138
  const fingerprint = new Int32Array(2);
127
139
  const inbox = createInbox();
128
140
  const peers = start.ports.flatMap((port, index) => (port ? [{ port, index }] : []));
@@ -130,82 +142,44 @@ const run = (start: StartMessage, parent: Port) => {
130
142
  peer.port.addEventListener("message", (event) => inbox.deliver(event.data as PeerMessage));
131
143
  peer.port.start?.();
132
144
  });
133
- for (let index = 0; index < start.seedFingerprints.length; index += 2) {
134
- table.insert(start.seedFingerprints[index], start.seedFingerprints[index + 1]);
135
- }
136
145
  const frontier = { states: decode(start.seed).map(canonicalize) };
137
146
  const send = (index: number, message: PeerMessage, transfer: unknown[]) =>
138
147
  start.ports[index]?.postMessage(message, transfer);
139
148
 
140
- const expand = (states: Value[][], next: Value[][]) => {
141
- const pending: Pending[] = Array.from({ length: count }, () => ({
142
- states: [],
143
- origins: [],
144
- fingerprints: [],
145
- }));
146
- const statistics = { generated: 0 };
147
- const newStates = new Int32Array(states.length);
148
- for (let origin = 0; origin < states.length; origin++) {
149
- const state = states[origin];
150
- const successors = getSuccessors(nextAction, state, environment, search.spec.scope);
151
- statistics.generated += successors.length;
152
- if (successors.length === 0 && start.config.checkDeadlock) return undefined;
153
- for (const successor of successors) {
149
+ // The worker pauses before a successor when the shared table needs to grow, and resumes there.
150
+ const expand = (expansion: Expansion) => {
151
+ const states = frontier.states;
152
+ for (; expansion.origin < states.length; expansion.origin++) {
153
+ const state = states[expansion.origin];
154
+ if (!expansion.successors) {
155
+ expansion.successors = getSuccessors(nextAction, state, environment, search.spec.scope);
156
+ expansion.generated += expansion.successors.length;
157
+ if (expansion.successors.length === 0 && start.config.checkDeadlock) return undefined;
158
+ }
159
+ const successors = expansion.successors;
160
+ for (; expansion.index < successors.length; expansion.index++) {
161
+ if (isSharedFingerprintsFull(shared.fingerprints)) return "paused";
162
+ const successor = successors[expansion.index];
154
163
  if (search.findViolatedActionProperty(state, successor.state)) return undefined;
155
164
  if (!search.satisfiesActionConstraints(state, successor.state)) continue;
156
- canonicalize(successor.state);
157
- writeStateFingerprint(search.getFingerprintState(successor.state), fingerprint);
158
- const owner = getFingerprintOwner(fingerprint[1], count);
159
- if (owner === self) {
160
- if (!table.insert(fingerprint[0], fingerprint[1])) continue;
161
- if (search.findViolatedInvariant(successor.state, state)) return undefined;
162
- if (!search.satisfiesConstraints(successor.state, state)) {
163
- table.remove(fingerprint[0], fingerprint[1]);
164
- continue;
165
- }
166
- next.push(successor.state);
167
- newStates[origin]++;
168
- continue;
165
+ if (generatedStates) {
166
+ writeStateFingerprint(successor.state, fingerprint);
167
+ if (!generatedStates.insert(fingerprint[0], fingerprint[1])) continue;
169
168
  }
170
- if (!known.insert(fingerprint[0], fingerprint[1])) continue;
171
- pending[owner].states.push(successor.state);
172
- pending[owner].origins.push(origin);
173
- pending[owner].fingerprints.push(fingerprint[0], fingerprint[1]);
174
- }
175
- }
176
- return { pending, generated: statistics.generated, newStates };
177
- };
178
-
179
- const answer = (fingerprints: Int32Array | number[]) => {
180
- const bits = new Uint8Array(fingerprints.length / 2);
181
- for (let index = 0; index < bits.length; index++) {
182
- bits[index] = table.insert(fingerprints[index * 2], fingerprints[index * 2 + 1]) ? 1 : 0;
183
- }
184
- return bits;
185
- };
186
-
187
- const commit = (pending: Pending, bits: Uint8Array, next: Value[][], newStates: Int32Array) => {
188
- const unmarks: number[] = [];
189
- for (let index = 0; index < bits.length; index++) {
190
- if (bits[index] === 0) continue;
191
- const state = pending.states[index];
192
- const parentState = frontier.states[pending.origins[index]];
193
- if (search.findViolatedInvariant(state, parentState)) return undefined;
194
- if (search.satisfiesConstraints(state, parentState)) {
195
- next.push(state);
196
- newStates[pending.origins[index]]++;
197
- continue;
169
+ canonicalize(successor.state);
170
+ const fingerprintState = search.getFingerprintState(successor.state);
171
+ writeStateFingerprint(fingerprintState, fingerprint);
172
+ if (!insertSharedFingerprint(shared.fingerprints, fingerprint[0], fingerprint[1])) continue;
173
+ if (search.findViolatedInvariant(successor.state, state)) return undefined;
174
+ if (!search.satisfiesConstraints(successor.state, state)) continue;
175
+ expansion.next.push(successor.state);
176
+ expansion.fingerprintStates.push(fingerprintState);
177
+ expansion.newStates[expansion.origin]++;
198
178
  }
199
- known.remove(pending.fingerprints[index * 2], pending.fingerprints[index * 2 + 1]);
200
- unmarks.push(pending.fingerprints[index * 2], pending.fingerprints[index * 2 + 1]);
201
- }
202
- return Int32Array.from(unmarks);
203
- };
204
-
205
- const removeAll = (fingerprints: Int32Array) => {
206
- for (let index = 0; index < fingerprints.length; index += 2) {
207
- table.remove(fingerprints[index], fingerprints[index + 1]);
179
+ expansion.successors = undefined;
180
+ expansion.index = 0;
208
181
  }
182
+ return "done";
209
183
  };
210
184
 
211
185
  const balance = async (level: number, next: Value[][], sizes: number[]) => {
@@ -222,60 +196,50 @@ const run = (start: StartMessage, parent: Port) => {
222
196
  send(index, { phase: "balance", level, from: self, states }, [states.words.buffer]);
223
197
  });
224
198
  const incoming = await inbox.receive("balance", level, peers.length);
225
- incoming.forEach((message) => {
226
- if (message.states) next.push(...decode(message.states).map(canonicalize));
227
- });
199
+ incoming.forEach((message) => next.push(...decode(message.states).map(canonicalize)));
228
200
  return true;
229
201
  };
230
202
 
203
+ const waiters = { resume: () => {}, balance: (_sizes: number[]) => {} };
204
+ const report = (message: WorkerMessage) =>
205
+ parent.postMessage(message, message.type === "level" ? [message.fingerprints.buffer] : []);
206
+
231
207
  const runLevel = async (level: number): Promise<LevelMessage | undefined> => {
232
- const next: Value[][] = [];
233
- const expanded = expand(frontier.states, next);
234
- if (!expanded || getRankedStringCount() !== startupStrings) return undefined;
235
- peers.forEach((peer) => {
236
- const fingerprints = Int32Array.from(expanded.pending[peer.index].fingerprints);
237
- send(peer.index, { phase: "query", level, from: self, fingerprints }, [fingerprints.buffer]);
238
- });
239
- const queries = await inbox.receive("query", level, peers.length);
240
- queries.forEach((query) => {
241
- const bits = answer(query.fingerprints ?? []);
242
- send(query.from, { phase: "answer", level, from: self, bits }, [bits.buffer]);
243
- });
244
- const answers = await inbox.receive("answer", level, peers.length);
245
- for (const reply of answers) {
246
- const unmarks = commit(
247
- expanded.pending[reply.from],
248
- reply.bits ?? new Uint8Array(),
249
- next,
250
- expanded.newStates,
251
- );
252
- if (!unmarks) return undefined;
253
- send(reply.from, { phase: "sizes", level, from: self, unmarks }, [unmarks.buffer]);
208
+ const expansion: Expansion = {
209
+ origin: 0,
210
+ successors: undefined,
211
+ index: 0,
212
+ generated: 0,
213
+ newStates: new Int32Array(frontier.states.length),
214
+ next: [],
215
+ fingerprintStates: [],
216
+ };
217
+ for (let status = expand(expansion); status !== "done"; status = expand(expansion)) {
218
+ if (status === undefined) return undefined;
219
+ const resumed = new Promise<void>((resolve) => (waiters.resume = resolve));
220
+ report({ type: "paused" });
221
+ await resumed;
254
222
  }
223
+ if (getRankedStringCount() !== startupStrings) return undefined;
224
+ const next = expansion.next;
255
225
  const added = next.length;
256
226
  const outDegrees = Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0);
257
- expanded.newStates.forEach((degree) => outDegrees[Math.min(degree, OUT_DEGREE_BUCKETS - 1)]++);
258
- const fingerprints = BigInt64Array.from(next, (state) =>
259
- environment.runtime.getFingerprint(state),
260
- );
261
- const sizes = Array.from({ length: count }, () => 0);
262
- sizes[self] = added;
263
- peers.forEach((peer) =>
264
- send(peer.index, { phase: "sizes", level, from: self, size: added }, []),
265
- );
266
- const received = await inbox.receive("sizes", level, peers.length * 2);
267
- received.forEach((message) => {
268
- if (message.unmarks) removeAll(message.unmarks);
269
- else sizes[message.from] = message.size ?? 0;
270
- });
271
- if (getRankedStringCount() !== startupStrings) return undefined;
272
- if (!(await balance(level, next, sizes))) return undefined;
227
+ expansion.newStates.forEach((degree) => outDegrees[Math.min(degree, OUT_DEGREE_BUCKETS - 1)]++);
228
+ const fingerprints = search.getStoredFingerprints(next, expansion.fingerprintStates);
229
+ const sizes = new Promise<number[]>((resolve) => (waiters.balance = resolve));
230
+ report({ type: "expanded", from: self, added });
231
+ if (!(await balance(level, next, await sizes))) return undefined;
273
232
  frontier.states = next;
274
- return { type: "level", level, generated: expanded.generated, added, outDegrees, fingerprints };
233
+ return {
234
+ type: "level",
235
+ level,
236
+ generated: expansion.generated,
237
+ added,
238
+ outDegrees,
239
+ fingerprints,
240
+ };
275
241
  };
276
242
 
277
- const report = (message: WorkerMessage) =>
278
- parent.postMessage(message, message.type === "level" ? [message.fingerprints.buffer] : []);
279
243
  const step = (level: number) => {
280
244
  runLevel(level).then(
281
245
  (message) => report(message ?? { type: "abort" }),
@@ -283,8 +247,13 @@ const run = (start: StartMessage, parent: Port) => {
283
247
  );
284
248
  };
285
249
  parent.addEventListener("message", (event) => {
286
- const message = event.data as { type: string; level: number };
250
+ const message = event.data as CoordinatorMessage;
287
251
  if (message.type === "level") step(message.level);
252
+ if (message.type === "resize") {
253
+ shared.fingerprints = message.fingerprints;
254
+ waiters.resume();
255
+ }
256
+ if (message.type === "balance") waiters.balance(message.sizes);
288
257
  });
289
258
  report({ type: "ready" });
290
259
  };
package/src/parallel.ts CHANGED
@@ -11,7 +11,14 @@ import { parseConfig, type ModelConfig } from "./config";
11
11
  import { createEnvironment } from "./environment";
12
12
  import { TlaEvaluationError } from "./errors";
13
13
  import { isImpure } from "./evaluate";
14
- import { createFingerprintTable, getFingerprintOwner, writeStateFingerprint } from "./fingerprints";
14
+ import {
15
+ createSharedFingerprints,
16
+ growSharedFingerprints,
17
+ insertSharedFingerprint,
18
+ isSharedFingerprintsFull,
19
+ writeStateFingerprint,
20
+ type SharedFingerprints,
21
+ } from "./fingerprints";
15
22
  import { getInitialStates } from "./successors";
16
23
  import { getTlcStringOrder, getTlcVariableOrder } from "./tlc-order";
17
24
  import { getFingerprintDistance, OUT_DEGREE_BUCKETS } from "./tlc-summary";
@@ -34,10 +41,9 @@ export interface StartMessage {
34
41
  modules: [string, string | undefined][];
35
42
  classes: [string, Uint8Array | undefined][];
36
43
  index: number;
37
- count: number;
38
44
  ports: (Port | undefined)[];
39
45
  seed: EncodedStates;
40
- seedFingerprints: Int32Array;
46
+ fingerprints: SharedFingerprints;
41
47
  }
42
48
 
43
49
  export interface LevelMessage {
@@ -49,7 +55,17 @@ export interface LevelMessage {
49
55
  fingerprints: BigInt64Array;
50
56
  }
51
57
 
52
- 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;
64
+
65
+ export type CoordinatorMessage =
66
+ | { type: "level"; level: number }
67
+ | { type: "resize"; fingerprints: SharedFingerprints }
68
+ | { type: "balance"; sizes: number[] };
53
69
 
54
70
  interface WorkerHandle {
55
71
  postMessage: (message: unknown, transfer?: unknown[]) => void;
@@ -90,6 +106,7 @@ export const check = async (options: CheckOptions): Promise<CheckResult> => {
90
106
  const count = options.workers ?? 1;
91
107
  const isSequential =
92
108
  count < 2 ||
109
+ typeof SharedArrayBuffer !== "function" ||
93
110
  config.view !== undefined ||
94
111
  config.postconditions.length > 0 ||
95
112
  config.possible.length > 0;
@@ -131,12 +148,9 @@ export const check = async (options: CheckOptions): Promise<CheckResult> => {
131
148
  }
132
149
 
133
150
  const canonicalize = createCanonicalizer();
134
- const table = createFingerprintTable();
151
+ const shared = { fingerprints: createSharedFingerprints() };
135
152
  const fingerprint = new Int32Array(2);
136
- const seeds = Array.from({ length: count }, () => ({
137
- encoder: createStateEncoder(),
138
- fingerprints: [] as number[],
139
- }));
153
+ const seeds = Array.from({ length: count }, createStateEncoder);
140
154
  const statistics = {
141
155
  generatedStates: 0,
142
156
  distinctStates: 0,
@@ -169,17 +183,15 @@ export const check = async (options: CheckOptions): Promise<CheckResult> => {
169
183
  canonicalize(state);
170
184
  statistics.generatedStates++;
171
185
  writeStateFingerprint(search.getFingerprintState(state), fingerprint);
172
- if (!table.insert(fingerprint[0], fingerprint[1])) continue;
186
+ if (isSharedFingerprintsFull(shared.fingerprints)) {
187
+ shared.fingerprints = growSharedFingerprints(shared.fingerprints);
188
+ }
189
+ if (!insertSharedFingerprint(shared.fingerprints, fingerprint[0], fingerprint[1])) continue;
173
190
  if (search.findViolatedInvariant(state) || search.findFailedInitialPredicate(state)) {
174
191
  return false;
175
192
  }
176
- if (!search.satisfiesConstraints(state)) {
177
- table.remove(fingerprint[0], fingerprint[1]);
178
- continue;
179
- }
180
- const seed = seeds[getFingerprintOwner(fingerprint[1], count)];
181
- if (!seed.encoder.encode(state)) return false;
182
- seed.fingerprints.push(fingerprint[0], fingerprint[1]);
193
+ if (!search.satisfiesConstraints(state)) continue;
194
+ if (!seeds[seeded.length % count].encode(state)) return false;
183
195
  seeded.push(state);
184
196
  statistics.distinctStates++;
185
197
  }
@@ -200,18 +212,19 @@ export const check = async (options: CheckOptions): Promise<CheckResult> => {
200
212
  if (statistics.distinctStates === 0) return createSuccess();
201
213
 
202
214
  const reports: WorkerMessage[] = [];
203
- const pending = { resolve: (_reports: WorkerMessage[]) => {} };
215
+ const pending = { count, resolve: (_reports: WorkerMessage[]) => {} };
204
216
  const onMessage = (message: WorkerMessage) => {
205
217
  reports.push(message);
206
- if (message.type === "abort" || reports.length === count) {
218
+ if (message.type === "abort" || reports.length === pending.count) {
207
219
  pending.resolve(reports.splice(0));
208
220
  }
209
221
  };
210
- const waitForReports = () =>
222
+ const waitForReports = (expected: number) =>
211
223
  new Promise<WorkerMessage[]>((resolve) => {
224
+ pending.count = expected;
212
225
  pending.resolve = resolve;
213
226
  });
214
- const ready = waitForReports();
227
+ const ready = waitForReports(count);
215
228
  const workers = await Promise.all(Array.from({ length: count }, () => spawnWorker(onMessage)));
216
229
  const terminate = () => workers.forEach((worker) => worker.terminate());
217
230
  const channels = new Map<string, MessageChannel>();
@@ -225,8 +238,7 @@ export const check = async (options: CheckOptions): Promise<CheckResult> => {
225
238
  const ports = Array.from({ length: count }, (_, peer) =>
226
239
  peer === index ? undefined : getPort(index, peer),
227
240
  );
228
- const seed = seeds[index].encoder.finish();
229
- const seedFingerprints = Int32Array.from(seeds[index].fingerprints);
241
+ const seed = seeds[index].finish();
230
242
  const message: StartMessage = {
231
243
  type: "start",
232
244
  source: options.source,
@@ -234,26 +246,44 @@ export const check = async (options: CheckOptions): Promise<CheckResult> => {
234
246
  modules: [...modules],
235
247
  classes: [...classes],
236
248
  index,
237
- count,
238
249
  ports,
239
250
  seed,
240
- seedFingerprints,
251
+ fingerprints: shared.fingerprints,
241
252
  };
242
- worker.postMessage(message, [
243
- ...ports.filter((port) => port !== undefined),
244
- seed.words.buffer,
245
- seedFingerprints.buffer,
246
- ]);
253
+ worker.postMessage(message, [...ports.filter((port) => port !== undefined), seed.words.buffer]);
247
254
  });
248
255
  const isAborted = (messages: WorkerMessage[]) =>
249
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
+ };
250
269
  if (isAborted(await ready)) {
251
270
  terminate();
252
271
  return fallBack();
253
272
  }
254
273
  for (let level = 1; ; level++) {
255
- const reported = waitForReports();
256
- 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 });
257
287
  const messages = await reported;
258
288
  if (isAborted(messages)) {
259
289
  terminate();