tla-ts 0.0.3-dev.a50fc89 → 0.0.3-dev.b74f28c
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.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- 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 +3 -3
- package/dist/transfer.js +3 -3
- package/package.json +1 -1
- package/src/fingerprints.ts +46 -11
- package/src/parallel-worker.ts +163 -211
- package/src/parallel.ts +58 -33
- package/src/symmetry.ts +14 -2
- package/src/values.ts +7 -4
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();
|
package/src/symmetry.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
isEnumerable,
|
|
10
10
|
isFunctionValue,
|
|
11
11
|
isModelValue,
|
|
12
|
+
isSameValue,
|
|
12
13
|
isSetValue,
|
|
13
14
|
toFiniteFunction,
|
|
14
15
|
toFiniteSet,
|
|
@@ -163,7 +164,10 @@ const permuteValue = (value: Value, permutation: Permutation): Value => {
|
|
|
163
164
|
if (size === 0 || size > CACHED_SIZE_LIMIT) return computePermutedValue(value, permutation);
|
|
164
165
|
const cached = permutation.cache.get(value);
|
|
165
166
|
if (cached !== undefined) return cached;
|
|
166
|
-
const
|
|
167
|
+
const computed = computePermutedValue(value, permutation);
|
|
168
|
+
// A permutation often maps a value to an equal one. Keeping the original lets the comparison
|
|
169
|
+
// with the smallest state so far stop at identity.
|
|
170
|
+
const permuted = computed !== value && isSameValue(computed, value) ? value : computed;
|
|
167
171
|
if (permutation.cacheSize >= permutation.cacheLimit) {
|
|
168
172
|
permutation.cache = new WeakMap();
|
|
169
173
|
permutation.cacheSize = 0;
|
|
@@ -184,7 +188,15 @@ const computePermutedValue = (value: ObjectValue, permutation: Permutation) => {
|
|
|
184
188
|
const domain = permuteElements(functionValue.domain, permutation);
|
|
185
189
|
const range = permuteElements(functionValue.range, permutation) ?? functionValue.range;
|
|
186
190
|
if (domain) {
|
|
187
|
-
|
|
191
|
+
const permuted = createFunction(
|
|
192
|
+
domain.map((key, index): [Value, Value] => [key, range[index]]),
|
|
193
|
+
);
|
|
194
|
+
// A permutation often maps the domain to itself, such as a function over the symmetric set.
|
|
195
|
+
// Sharing the original domain array lets comparisons with other images skip the domain.
|
|
196
|
+
const isSameDomain = permuted.domain.every(
|
|
197
|
+
(key, index) => key === functionValue.domain[index],
|
|
198
|
+
);
|
|
199
|
+
return isSameDomain ? createSortedFunction(functionValue.domain, permuted.range) : permuted;
|
|
188
200
|
}
|
|
189
201
|
return range === functionValue.range
|
|
190
202
|
? functionValue
|
package/src/values.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface ModelValue {
|
|
|
5
5
|
kind: "ModelValue";
|
|
6
6
|
name: string;
|
|
7
7
|
type: string | undefined;
|
|
8
|
+
rank: number;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export interface SetValue {
|
|
@@ -264,6 +265,7 @@ const stringOrder = { ranks: Object.create(null) as Record<string, number>, size
|
|
|
264
265
|
export const setStringOrder = (texts: Iterable<string>) => {
|
|
265
266
|
stringOrder.ranks = Object.create(null);
|
|
266
267
|
stringOrder.size = 0;
|
|
268
|
+
modelValues.clear();
|
|
267
269
|
for (const text of texts) rankString(text);
|
|
268
270
|
};
|
|
269
271
|
|
|
@@ -353,7 +355,7 @@ export const compareValues = (left: Value, right: Value): number => {
|
|
|
353
355
|
if (left.type !== undefined && right.type !== undefined) {
|
|
354
356
|
checkModelValueTypes(left, right, false);
|
|
355
357
|
}
|
|
356
|
-
return
|
|
358
|
+
return left.rank - right.rank;
|
|
357
359
|
}
|
|
358
360
|
if (leftKind === "ModelValue" || rightKind === "ModelValue") {
|
|
359
361
|
checkModelValueTypes(left, right, false);
|
|
@@ -385,14 +387,15 @@ export const compareValues = (left: Value, right: Value): number => {
|
|
|
385
387
|
|
|
386
388
|
const modelValues = new Map<string, ModelValue>();
|
|
387
389
|
|
|
390
|
+
// A check ranks every model value's name when it creates the value, and creates its own values.
|
|
388
391
|
export const createModelValue = (name: string): ModelValue => {
|
|
389
|
-
rankString(name);
|
|
390
392
|
const known = modelValues.get(name);
|
|
391
393
|
if (known) return known;
|
|
392
394
|
const value: ModelValue = {
|
|
393
395
|
kind: "ModelValue",
|
|
394
396
|
name,
|
|
395
397
|
type: name.length > 2 && name[1] === "_" ? name[0] : undefined,
|
|
398
|
+
rank: getStringRank(name),
|
|
396
399
|
};
|
|
397
400
|
modelValues.set(name, value);
|
|
398
401
|
return value;
|
|
@@ -562,7 +565,7 @@ const equalsModelValueWith = (modelValue: ModelValue, other: Value) => {
|
|
|
562
565
|
|
|
563
566
|
const compareModelValue = (left: ModelValue, right: Value) => {
|
|
564
567
|
const type = getModelValueType(left);
|
|
565
|
-
if (type === undefined) return isModelValue(right) ?
|
|
568
|
+
if (type === undefined) return isModelValue(right) ? left.rank - right.rank : -1;
|
|
566
569
|
if (!isModelValue(right)) {
|
|
567
570
|
throw new TlaEvaluationError(
|
|
568
571
|
`Attempted to compare the typed model value ${formatValue(left)} and non-model value\n${formatValue(right)}`,
|
|
@@ -574,7 +577,7 @@ const compareModelValue = (left: ModelValue, right: Value) => {
|
|
|
574
577
|
`Attempted to compare the differently-typed model values ${formatValue(left)} and ${formatValue(right)}`,
|
|
575
578
|
);
|
|
576
579
|
}
|
|
577
|
-
return
|
|
580
|
+
return left.rank - right.rank;
|
|
578
581
|
};
|
|
579
582
|
|
|
580
583
|
const equalsModelValue = (left: ModelValue, right: Value) => {
|