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.
- package/README.md +7 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/parallel-worker.cjs +2 -2
- package/dist/parallel-worker.js +2 -2
- package/dist/parallel.cjs +1 -1
- package/dist/parallel.js +2 -2
- package/dist/run-cli.cjs +12 -12
- package/dist/run-cli.js +13 -13
- package/dist/transfer.cjs +1 -1
- package/dist/transfer.js +1 -1
- package/package.json +1 -1
- package/src/fingerprints.ts +46 -11
- package/src/parallel-worker.ts +153 -220
- package/src/parallel.ts +58 -33
package/src/parallel-worker.ts
CHANGED
|
@@ -1,66 +1,41 @@
|
|
|
1
1
|
import { createCanonicalizer } from "./canonical";
|
|
2
2
|
import { createSearch } from "./check";
|
|
3
3
|
import { createEnvironment } from "./environment";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
26
|
+
interface Expansion {
|
|
15
27
|
states: Value[][];
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
126
|
-
const
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
151
|
+
return "done";
|
|
189
152
|
};
|
|
190
153
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
|
225
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
292
|
-
frontier.states
|
|
293
|
-
return {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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].
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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();
|