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/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();
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 permuted = computePermutedValue(value, permutation);
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
- return createFunction(domain.map((key, index): [Value, Value] => [key, range[index]]));
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 compareStrings(left.name, right.name);
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) ? compareStrings(left.name, right.name) : -1;
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 compareStrings(left.name, right.name);
580
+ return left.rank - right.rank;
578
581
  };
579
582
 
580
583
  const equalsModelValue = (left: ModelValue, right: Value) => {