tla-ts 0.0.3-dev.763cc71 → 0.0.3-dev.79f7ed7

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,66 +1,41 @@
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";
10
21
  import { getRankedStringCount, setStringOrder, type Value } from "./values";
11
22
 
12
23
  const WORKER_THREADS_MODULE = "node:worker_threads";
24
+ const LOCKED = -1;
13
25
 
14
- interface Pending {
26
+ interface Expansion {
15
27
  states: Value[][];
16
- origins: number[];
17
- fingerprints: number[];
28
+ origin: number;
29
+ end: number;
30
+ successors: Successor[] | undefined;
31
+ index: number;
32
+ added: number;
33
+ generated: number;
34
+ outDegrees: number[];
35
+ next: Value[][];
18
36
  fingerprintStates: Value[][];
19
37
  }
20
38
 
21
- interface PeerMessage {
22
- phase: "query" | "answer" | "sizes" | "balance";
23
- level: number;
24
- from: number;
25
- fingerprints?: Int32Array;
26
- bits?: Uint8Array;
27
- states?: EncodedStates;
28
- unmarks?: Int32Array;
29
- size?: number;
30
- }
31
-
32
- interface Transfer {
33
- from: number;
34
- to: number;
35
- count: number;
36
- }
37
-
38
- const getBalancePlan = (sizes: number[]) => {
39
- const total = sizes.reduce((sum, size) => sum + size, 0);
40
- const average = total / sizes.length;
41
- if (Math.max(...sizes) - Math.min(...sizes) <= Math.max(64, average / 4)) return [];
42
- const targets = sizes.map(
43
- (_, index) => Math.floor(average) + (index < total % sizes.length ? 1 : 0),
44
- );
45
- const donors = sizes.flatMap((size, index) =>
46
- size > targets[index] ? [{ index, amount: size - targets[index] }] : [],
47
- );
48
- const receivers = sizes.flatMap((size, index) =>
49
- size < targets[index] ? [{ index, amount: targets[index] - size }] : [],
50
- );
51
- const transfers: Transfer[] = [];
52
- for (const donor of donors) {
53
- for (const receiver of receivers) {
54
- const moved = Math.min(donor.amount, receiver.amount);
55
- if (moved === 0) continue;
56
- transfers.push({ from: donor.index, to: receiver.index, count: moved });
57
- donor.amount -= moved;
58
- receiver.amount -= moved;
59
- }
60
- }
61
- return transfers;
62
- };
63
-
64
39
  const getParentPort = async () => {
65
40
  if (typeof (globalThis as { postMessage?: unknown }).postMessage === "function") {
66
41
  return globalThis as unknown as Port;
@@ -71,33 +46,6 @@ const getParentPort = async () => {
71
46
  return threads.parentPort as unknown as Port;
72
47
  };
73
48
 
74
- const createInbox = () => {
75
- const messages = new Map<string, PeerMessage[]>();
76
- const waiters = new Map<string, { count: number; resolve: (messages: PeerMessage[]) => void }>();
77
- const getKey = (phase: PeerMessage["phase"], level: number) => `${phase}:${level}`;
78
- const settle = (key: string) => {
79
- const waiter = waiters.get(key);
80
- const received = messages.get(key) ?? [];
81
- if (!waiter || received.length < waiter.count) return;
82
- waiters.delete(key);
83
- messages.delete(key);
84
- waiter.resolve(received);
85
- };
86
- return {
87
- deliver: (message: PeerMessage) => {
88
- const key = getKey(message.phase, message.level);
89
- messages.set(key, [...(messages.get(key) ?? []), message]);
90
- settle(key);
91
- },
92
- receive: (phase: PeerMessage["phase"], level: number, count: number) =>
93
- new Promise<PeerMessage[]>((resolve) => {
94
- const key = getKey(phase, level);
95
- waiters.set(key, { count, resolve });
96
- settle(key);
97
- }),
98
- };
99
- };
100
-
101
49
  const run = (start: StartMessage, parent: Port) => {
102
50
  const modules = new Map(start.modules);
103
51
  const loadModule = (name: string) => modules.get(name);
@@ -118,44 +66,67 @@ const run = (start: StartMessage, parent: Port) => {
118
66
  const nextAction = search?.spec.next;
119
67
  if (!search || nextAction === undefined)
120
68
  return parent.postMessage({ type: "abort" } satisfies WorkerMessage);
121
- const count = start.count;
122
69
  const self = start.index;
123
70
  const canonicalize = createCanonicalizer();
124
71
  const decode = createStateDecoder(environment.variables.length);
125
- const table = createFingerprintTable();
126
- const known = createFingerprintTable();
72
+ const shared = { fingerprints: start.fingerprints };
73
+ const requests = start.requests;
74
+ const remaining = start.remaining;
127
75
  // Under symmetry, a successor the worker generated before has the representative it had then,
128
76
  // so the worker skips the search for one.
129
77
  const generatedStates = search.isSymmetric ? createFingerprintTable() : undefined;
130
78
  const fingerprint = new Int32Array(2);
131
- const inbox = createInbox();
132
- const peers = start.ports.flatMap((port, index) => (port ? [{ port, index }] : []));
133
- peers.forEach((peer) => {
134
- peer.port.addEventListener("message", (event) => inbox.deliver(event.data as PeerMessage));
135
- peer.port.start?.();
79
+ const waiters = { resume: () => {}, loot: (_states: EncodedStates) => {} };
80
+ start.ports.forEach((port) => {
81
+ port?.addEventListener("message", (event) => waiters.loot(event.data as EncodedStates));
82
+ port?.start?.();
136
83
  });
137
- for (let index = 0; index < start.seedFingerprints.length; index += 2) {
138
- table.insert(start.seedFingerprints[index], start.seedFingerprints[index + 1]);
139
- }
140
84
  const frontier = { states: decode(start.seed).map(canonicalize) };
141
- const send = (index: number, message: PeerMessage, transfer: unknown[]) =>
142
- start.ports[index]?.postMessage(message, transfer);
143
85
 
144
- const expand = (states: Value[][], next: Value[][], nextFingerprintStates: Value[][]) => {
145
- const pending: Pending[] = Array.from({ length: count }, () => ({
146
- states: [],
147
- origins: [],
148
- fingerprints: [],
149
- fingerprintStates: [],
150
- }));
151
- const statistics = { generated: 0 };
152
- const newStates = new Int32Array(states.length);
153
- for (let origin = 0; origin < states.length; origin++) {
154
- const state = states[origin];
155
- const successors = getSuccessors(nextAction, state, environment, search.spec.scope);
156
- statistics.generated += successors.length;
157
- if (successors.length === 0 && start.config.checkDeadlock) return undefined;
158
- for (const successor of successors) {
86
+ // A worker that runs out of states asks a peer for some by writing its index + 1 to the peer's
87
+ // request word. The peer sends half of the states it hasn't started, keeps expanding, and wakes
88
+ // the workers waiting to ask it next.
89
+ const answer = (expansion: Expansion, request: number) => {
90
+ const thief = Atomics.load(requests, self) - 1;
91
+ const given = Math.floor((expansion.end - expansion.origin - 1) / 2);
92
+ const encoder = createStateEncoder();
93
+ const states = expansion.states.slice(expansion.end - given, expansion.end);
94
+ const isEncoded = given > 0 && states.every(encoder.encode);
95
+ if (isEncoded) expansion.end -= given;
96
+ const loot = isEncoded ? encoder.finish() : createStateEncoder().finish();
97
+ start.ports[thief]?.postMessage(loot, [loot.words.buffer]);
98
+ Atomics.store(requests, self, request);
99
+ Atomics.notify(requests, self);
100
+ };
101
+
102
+ // A worker answers any request before it pauses or runs out of states, and takes no more until
103
+ // it resumes.
104
+ const lock = (expansion: Expansion) => {
105
+ Atomics.store(remaining, self, 0);
106
+ if (Atomics.compareExchange(requests, self, 0, LOCKED) === 0) return;
107
+ answer(expansion, LOCKED);
108
+ };
109
+
110
+ const unlock = (expansion: Expansion) => {
111
+ Atomics.store(remaining, self, expansion.end - expansion.origin);
112
+ Atomics.store(requests, self, 0);
113
+ };
114
+
115
+ // The worker pauses before a successor when the shared table needs to grow, and resumes there.
116
+ const expand = (expansion: Expansion) => {
117
+ for (; expansion.origin < expansion.end; expansion.origin++) {
118
+ const state = expansion.states[expansion.origin];
119
+ if (!expansion.successors) {
120
+ if (Atomics.load(requests, self) > 0) answer(expansion, 0);
121
+ Atomics.store(remaining, self, expansion.end - expansion.origin - 1);
122
+ expansion.successors = getSuccessors(nextAction, state, environment, search.spec.scope);
123
+ expansion.generated += expansion.successors.length;
124
+ if (expansion.successors.length === 0 && start.config.checkDeadlock) return undefined;
125
+ }
126
+ const successors = expansion.successors;
127
+ for (; expansion.index < successors.length; expansion.index++) {
128
+ if (isSharedFingerprintsFull(shared.fingerprints)) return "paused";
129
+ const successor = successors[expansion.index];
159
130
  if (search.findViolatedActionProperty(state, successor.state)) return undefined;
160
131
  if (!search.satisfiesActionConstraints(state, successor.state)) continue;
161
132
  if (generatedStates) {
@@ -165,136 +136,93 @@ const run = (start: StartMessage, parent: Port) => {
165
136
  canonicalize(successor.state);
166
137
  const fingerprintState = search.getFingerprintState(successor.state);
167
138
  writeStateFingerprint(fingerprintState, fingerprint);
168
- const owner = getFingerprintOwner(fingerprint[1], count);
169
- if (owner === self) {
170
- if (!table.insert(fingerprint[0], fingerprint[1])) continue;
171
- if (search.findViolatedInvariant(successor.state, state)) return undefined;
172
- if (!search.satisfiesConstraints(successor.state, state)) {
173
- table.remove(fingerprint[0], fingerprint[1]);
174
- continue;
175
- }
176
- next.push(successor.state);
177
- nextFingerprintStates.push(fingerprintState);
178
- newStates[origin]++;
179
- continue;
180
- }
181
- if (!known.insert(fingerprint[0], fingerprint[1])) continue;
182
- pending[owner].states.push(successor.state);
183
- pending[owner].origins.push(origin);
184
- pending[owner].fingerprints.push(fingerprint[0], fingerprint[1]);
185
- pending[owner].fingerprintStates.push(fingerprintState);
139
+ if (!insertSharedFingerprint(shared.fingerprints, fingerprint[0], fingerprint[1])) continue;
140
+ if (search.findViolatedInvariant(successor.state, state)) return undefined;
141
+ if (!search.satisfiesConstraints(successor.state, state)) continue;
142
+ expansion.next.push(successor.state);
143
+ expansion.fingerprintStates.push(fingerprintState);
144
+ expansion.added++;
186
145
  }
146
+ expansion.outDegrees[Math.min(expansion.added, OUT_DEGREE_BUCKETS - 1)]++;
147
+ expansion.successors = undefined;
148
+ expansion.index = 0;
149
+ expansion.added = 0;
187
150
  }
188
- return { pending, generated: statistics.generated, newStates };
151
+ return "done";
189
152
  };
190
153
 
191
- const answer = (fingerprints: Int32Array | number[]) => {
192
- const bits = new Uint8Array(fingerprints.length / 2);
193
- for (let index = 0; index < bits.length; index++) {
194
- bits[index] = table.insert(fingerprints[index * 2], fingerprints[index * 2 + 1]) ? 1 : 0;
195
- }
196
- return bits;
197
- };
198
-
199
- const commit = (
200
- pending: Pending,
201
- bits: Uint8Array,
202
- next: Value[][],
203
- nextFingerprintStates: Value[][],
204
- newStates: Int32Array,
205
- ) => {
206
- const unmarks: number[] = [];
207
- for (let index = 0; index < bits.length; index++) {
208
- if (bits[index] === 0) continue;
209
- const state = pending.states[index];
210
- const parentState = frontier.states[pending.origins[index]];
211
- if (search.findViolatedInvariant(state, parentState)) return undefined;
212
- if (search.satisfiesConstraints(state, parentState)) {
213
- next.push(state);
214
- nextFingerprintStates.push(pending.fingerprintStates[index]);
215
- newStates[pending.origins[index]]++;
154
+ // Each attempt asks the peer with the most states left. While another worker's request to that
155
+ // peer is open, the worker waits for the peer to answer it. The worker stops once no peer that
156
+ // takes requests has two states left.
157
+ const steal = async (expansion: Expansion) => {
158
+ const skipped = new Set<number>();
159
+ for (;;) {
160
+ const victim = start.ports.reduce<number | undefined>((best, port, index) => {
161
+ if (!port || skipped.has(index) || Atomics.load(remaining, index) < 2) return best;
162
+ return best === undefined || Atomics.load(remaining, index) > Atomics.load(remaining, best)
163
+ ? index
164
+ : best;
165
+ }, undefined);
166
+ if (victim === undefined) return false;
167
+ const request = Atomics.compareExchange(requests, victim, 0, self + 1);
168
+ if (request === LOCKED) skipped.add(victim);
169
+ if (request > 0) Atomics.wait(requests, victim, request);
170
+ if (request !== 0) continue;
171
+ const loot = await new Promise<EncodedStates>((resolve) => (waiters.loot = resolve));
172
+ if (loot.count === 0) {
173
+ skipped.add(victim);
216
174
  continue;
217
175
  }
218
- known.remove(pending.fingerprints[index * 2], pending.fingerprints[index * 2 + 1]);
219
- unmarks.push(pending.fingerprints[index * 2], pending.fingerprints[index * 2 + 1]);
176
+ expansion.states = decode(loot).map(canonicalize);
177
+ expansion.origin = 0;
178
+ expansion.end = expansion.states.length;
179
+ unlock(expansion);
180
+ return true;
220
181
  }
221
- return Int32Array.from(unmarks);
222
182
  };
223
183
 
224
- const removeAll = (fingerprints: Int32Array) => {
225
- for (let index = 0; index < fingerprints.length; index += 2) {
226
- table.remove(fingerprints[index], fingerprints[index + 1]);
227
- }
228
- };
229
-
230
- const balance = async (level: number, next: Value[][], sizes: number[]) => {
231
- const transfers = getBalancePlan(sizes);
232
- if (transfers.length === 0) return true;
233
- const outgoing = new Map(peers.map((peer) => [peer.index, createStateEncoder()]));
234
- for (const transfer of transfers.filter((candidate) => candidate.from === self)) {
235
- const encoder = outgoing.get(transfer.to);
236
- const states = next.splice(next.length - transfer.count, transfer.count);
237
- if (!encoder || !states.every(encoder.encode)) return false;
238
- }
239
- outgoing.forEach((encoder, index) => {
240
- const states = encoder.finish();
241
- send(index, { phase: "balance", level, from: self, states }, [states.words.buffer]);
242
- });
243
- const incoming = await inbox.receive("balance", level, peers.length);
244
- incoming.forEach((message) => {
245
- if (message.states) next.push(...decode(message.states).map(canonicalize));
246
- });
247
- return true;
248
- };
184
+ const report = (message: WorkerMessage) =>
185
+ parent.postMessage(message, message.type === "level" ? [message.fingerprints.buffer] : []);
249
186
 
250
187
  const runLevel = async (level: number): Promise<LevelMessage | undefined> => {
251
- const next: Value[][] = [];
252
- const nextFingerprintStates: Value[][] = [];
253
- const expanded = expand(frontier.states, next, nextFingerprintStates);
254
- if (!expanded || getRankedStringCount() !== startupStrings) return undefined;
255
- peers.forEach((peer) => {
256
- const fingerprints = Int32Array.from(expanded.pending[peer.index].fingerprints);
257
- send(peer.index, { phase: "query", level, from: self, fingerprints }, [fingerprints.buffer]);
258
- });
259
- const queries = await inbox.receive("query", level, peers.length);
260
- queries.forEach((query) => {
261
- const bits = answer(query.fingerprints ?? []);
262
- send(query.from, { phase: "answer", level, from: self, bits }, [bits.buffer]);
263
- });
264
- const answers = await inbox.receive("answer", level, peers.length);
265
- for (const reply of answers) {
266
- const unmarks = commit(
267
- expanded.pending[reply.from],
268
- reply.bits ?? new Uint8Array(),
269
- next,
270
- nextFingerprintStates,
271
- expanded.newStates,
272
- );
273
- if (!unmarks) return undefined;
274
- send(reply.from, { phase: "sizes", level, from: self, unmarks }, [unmarks.buffer]);
188
+ const expansion: Expansion = {
189
+ states: frontier.states,
190
+ origin: 0,
191
+ end: frontier.states.length,
192
+ successors: undefined,
193
+ index: 0,
194
+ added: 0,
195
+ generated: 0,
196
+ outDegrees: Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0),
197
+ next: [],
198
+ fingerprintStates: [],
199
+ };
200
+ for (;;) {
201
+ const status = expand(expansion);
202
+ if (status === undefined) return undefined;
203
+ lock(expansion);
204
+ if (status === "paused") {
205
+ const resumed = new Promise<void>((resolve) => (waiters.resume = resolve));
206
+ report({ type: "paused" });
207
+ await resumed;
208
+ unlock(expansion);
209
+ continue;
210
+ }
211
+ if (!(await steal(expansion))) break;
275
212
  }
276
- const added = next.length;
277
- const outDegrees = Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0);
278
- expanded.newStates.forEach((degree) => outDegrees[Math.min(degree, OUT_DEGREE_BUCKETS - 1)]++);
279
- const fingerprints = search.getStoredFingerprints(next, nextFingerprintStates);
280
- const sizes = Array.from({ length: count }, () => 0);
281
- sizes[self] = added;
282
- peers.forEach((peer) =>
283
- send(peer.index, { phase: "sizes", level, from: self, size: added }, []),
284
- );
285
- const received = await inbox.receive("sizes", level, peers.length * 2);
286
- received.forEach((message) => {
287
- if (message.unmarks) removeAll(message.unmarks);
288
- else sizes[message.from] = message.size ?? 0;
289
- });
290
213
  if (getRankedStringCount() !== startupStrings) return undefined;
291
- if (!(await balance(level, next, sizes))) return undefined;
292
- frontier.states = next;
293
- return { type: "level", level, generated: expanded.generated, added, outDegrees, fingerprints };
214
+ frontier.states = expansion.next;
215
+ Atomics.store(remaining, self, frontier.states.length);
216
+ return {
217
+ type: "level",
218
+ level,
219
+ generated: expansion.generated,
220
+ added: expansion.next.length,
221
+ outDegrees: expansion.outDegrees,
222
+ fingerprints: search.getStoredFingerprints(expansion.next, expansion.fingerprintStates),
223
+ };
294
224
  };
295
225
 
296
- const report = (message: WorkerMessage) =>
297
- parent.postMessage(message, message.type === "level" ? [message.fingerprints.buffer] : []);
298
226
  const step = (level: number) => {
299
227
  runLevel(level).then(
300
228
  (message) => report(message ?? { type: "abort" }),
@@ -302,9 +230,14 @@ const run = (start: StartMessage, parent: Port) => {
302
230
  );
303
231
  };
304
232
  parent.addEventListener("message", (event) => {
305
- const message = event.data as { type: string; level: number };
233
+ const message = event.data as CoordinatorMessage;
306
234
  if (message.type === "level") step(message.level);
235
+ if (message.type === "resize") {
236
+ shared.fingerprints = message.fingerprints;
237
+ waiters.resume();
238
+ }
307
239
  });
240
+ Atomics.store(remaining, self, frontier.states.length);
308
241
  report({ type: "ready" });
309
242
  };
310
243
 
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,11 @@ 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;
47
+ requests: Int32Array;
48
+ remaining: Int32Array;
41
49
  }
42
50
 
43
51
  export interface LevelMessage {
@@ -49,7 +57,15 @@ export interface LevelMessage {
49
57
  fingerprints: BigInt64Array;
50
58
  }
51
59
 
52
- export type WorkerMessage = { type: "ready" } | { type: "abort" } | LevelMessage;
60
+ export type WorkerMessage =
61
+ | { type: "ready" }
62
+ | { type: "abort" }
63
+ | { type: "paused" }
64
+ | LevelMessage;
65
+
66
+ export type CoordinatorMessage =
67
+ | { type: "level"; level: number }
68
+ | { type: "resize"; fingerprints: SharedFingerprints };
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,21 @@ 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);
228
+ const requests = new Int32Array(new SharedArrayBuffer(count * 4));
229
+ const remaining = new Int32Array(new SharedArrayBuffer(count * 4));
215
230
  const workers = await Promise.all(Array.from({ length: count }, () => spawnWorker(onMessage)));
216
231
  const terminate = () => workers.forEach((worker) => worker.terminate());
217
232
  const channels = new Map<string, MessageChannel>();
@@ -225,8 +240,7 @@ export const check = async (options: CheckOptions): Promise<CheckResult> => {
225
240
  const ports = Array.from({ length: count }, (_, peer) =>
226
241
  peer === index ? undefined : getPort(index, peer),
227
242
  );
228
- const seed = seeds[index].encoder.finish();
229
- const seedFingerprints = Int32Array.from(seeds[index].fingerprints);
243
+ const seed = seeds[index].finish();
230
244
  const message: StartMessage = {
231
245
  type: "start",
232
246
  source: options.source,
@@ -234,27 +248,38 @@ export const check = async (options: CheckOptions): Promise<CheckResult> => {
234
248
  modules: [...modules],
235
249
  classes: [...classes],
236
250
  index,
237
- count,
238
251
  ports,
239
252
  seed,
240
- seedFingerprints,
253
+ fingerprints: shared.fingerprints,
254
+ requests,
255
+ remaining,
241
256
  };
242
- worker.postMessage(message, [
243
- ...ports.filter((port) => port !== undefined),
244
- seed.words.buffer,
245
- seedFingerprints.buffer,
246
- ]);
257
+ worker.postMessage(message, [...ports.filter((port) => port !== undefined), seed.words.buffer]);
247
258
  });
248
259
  const isAborted = (messages: WorkerMessage[]) =>
249
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
+ };
250
273
  if (isAborted(await ready)) {
251
274
  terminate();
252
275
  return fallBack();
253
276
  }
254
277
  for (let level = 1; ; level++) {
255
- const reported = waitForReports();
256
- workers.forEach((worker) => worker.postMessage({ type: "level", level }));
257
- 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);
258
283
  if (isAborted(messages)) {
259
284
  terminate();
260
285
  return fallBack();