tla-ts 0.0.2 → 0.0.3-dev.763cc71

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.
@@ -0,0 +1,184 @@
1
+ // tlapm's E_tuply_declarations: declarations such as <<x, y>> \in S become declarations of names.
2
+
3
+ import type { Bound, BoundDomain, Defn, Expr } from "./obligation-expression";
4
+
5
+ export type TuplyBound =
6
+ | { kind: "Name"; name: string; domain: BoundDomain }
7
+ | { kind: "Tuple"; names: string[]; domain: Expr };
8
+
9
+ const getOpaque = (name: string): Expr => ({ kind: "Opaque", name });
10
+
11
+ const getNamesTuple = (names: string[]): Expr => ({
12
+ kind: "Tuple",
13
+ elements: names.map(getOpaque),
14
+ });
15
+
16
+ const getEquality = (left: Expr, right: Expr): Expr => ({
17
+ kind: "Apply",
18
+ operator: { kind: "Internal", builtin: "Eq" },
19
+ args: [left, right],
20
+ });
21
+
22
+ export const getUnbounded = (names: string[]): Bound[] =>
23
+ names.map((name) => ({ name, level: "Constant", domain: { kind: "NoDomain" } }));
24
+
25
+ const getFactors = (domain: Expr | undefined, names: string[]) =>
26
+ domain?.kind === "Product" && domain.factors.length === names.length ? domain.factors : undefined;
27
+
28
+ // Each name is the matching item of the tuple: x == t[1], y == t[2].
29
+ const getItemDefinitions = (tuple: string, names: string[]): Defn[] =>
30
+ names.map((name, index) => ({
31
+ kind: "Operator",
32
+ name,
33
+ body: {
34
+ kind: "FcnApp",
35
+ function: getOpaque(tuple),
36
+ args: [{ kind: "Num", integer: String(index + 1), fraction: "" }],
37
+ },
38
+ }));
39
+
40
+ const getFactorBounds = (names: string[], factors: Expr[]): Bound[] =>
41
+ names.map((name, index) => ({
42
+ name,
43
+ level: "Constant",
44
+ domain: { kind: "Domain", expression: factors[index] },
45
+ }));
46
+
47
+ const getNamedBound = (bound: Extract<TuplyBound, { kind: "Name" }>): Bound => ({
48
+ name: bound.name,
49
+ level: "Constant",
50
+ domain: bound.domain,
51
+ });
52
+
53
+ // \E <<x, y>> \in A \X B: P becomes \E x \in A, y \in B: P, and \E <<x, y>> \in S: P becomes
54
+ // \E x, y: <<x, y>> \in S /\ P. Each domain quantifies separately, the last one innermost.
55
+ export const expandQuant = (
56
+ quantifier: "Forall" | "Exists",
57
+ bounds: TuplyBound[],
58
+ body: Expr,
59
+ ): Expr => {
60
+ const groups: TuplyBound[][] = [];
61
+ bounds.forEach((bound) => {
62
+ if (bound.kind === "Name" && bound.domain.kind === "Ditto") groups.at(-1)?.push(bound);
63
+ else groups.push([bound]);
64
+ });
65
+ return groups.reduceRight((inner: Expr, group): Expr => {
66
+ const first = group[0];
67
+ if (first.kind === "Name") {
68
+ return {
69
+ kind: "Quant",
70
+ quantifier,
71
+ bounds: group.flatMap((bound) => (bound.kind === "Name" ? [getNamedBound(bound)] : [])),
72
+ body: inner,
73
+ location: inner.location,
74
+ };
75
+ }
76
+ const factors = getFactors(first.domain, first.names);
77
+ if (factors) {
78
+ return {
79
+ kind: "Quant",
80
+ quantifier,
81
+ bounds: getFactorBounds(first.names, factors),
82
+ body: inner,
83
+ location: inner.location,
84
+ };
85
+ }
86
+ const membership: Expr = {
87
+ kind: "Apply",
88
+ operator: { kind: "Internal", builtin: quantifier === "Exists" ? "Mem" : "Notmem" },
89
+ args: [getNamesTuple(first.names), first.domain],
90
+ };
91
+ return {
92
+ kind: "Quant",
93
+ quantifier,
94
+ bounds: getUnbounded(first.names),
95
+ body: {
96
+ kind: "List",
97
+ bullet: quantifier === "Exists" ? "And" : "Or",
98
+ items: [membership, inner],
99
+ },
100
+ location: inner.location,
101
+ };
102
+ }, body);
103
+ };
104
+
105
+ // CHOOSE <<x, y>> \in A \X B: P binds t \in A \X B with x == t[1], y == t[2], and any other
106
+ // domain binds t with \E x, y: t = <<x, y>> /\ P.
107
+ const getChooseBody = (
108
+ name: string,
109
+ names: string[],
110
+ domain: Expr | undefined,
111
+ body: Expr,
112
+ ): Expr =>
113
+ getFactors(domain, names)
114
+ ? { kind: "Let", definitions: getItemDefinitions(name, names), body }
115
+ : {
116
+ kind: "Quant",
117
+ quantifier: "Exists",
118
+ bounds: getUnbounded(names),
119
+ body: {
120
+ kind: "List",
121
+ bullet: "And",
122
+ items: [getEquality(getOpaque(name), getNamesTuple(names)), body],
123
+ },
124
+ };
125
+
126
+ export const expandChoose = (names: string[], domain: Expr | undefined, body: Expr): Expr => {
127
+ const name = `choose#${names[0]}`;
128
+ return { kind: "Choose", name, domain, body: getChooseBody(name, names, domain, body) };
129
+ };
130
+
131
+ export const expandSetSt = (names: string[], domain: Expr, body: Expr): Expr => {
132
+ const name = `setst#${names[0]}`;
133
+ return { kind: "SetSt", name, domain, body: getChooseBody(name, names, domain, body) };
134
+ };
135
+
136
+ // A set or function constructor binds each tuple as fcn#x, with its names defined in a LET around
137
+ // the value. A set constructor over a matching product binds the names to its factors instead.
138
+ const expandConstructorBounds = (bounds: TuplyBound[], value: Expr, isSetOf: boolean) => {
139
+ const expanded = bounds.map((bound): { bounds: Bound[]; definitions: Defn[] } => {
140
+ if (bound.kind === "Name") return { bounds: [getNamedBound(bound)], definitions: [] };
141
+ const factors = getFactors(bound.domain, bound.names);
142
+ if (factors && isSetOf) {
143
+ return { bounds: getFactorBounds(bound.names, factors), definitions: [] };
144
+ }
145
+ const name = `fcn#${bound.names[0]}`;
146
+ const fcnBound: Bound = {
147
+ name,
148
+ level: "Constant",
149
+ domain: { kind: "Domain", expression: bound.domain },
150
+ };
151
+ if (factors) return { bounds: [fcnBound], definitions: getItemDefinitions(name, bound.names) };
152
+ const choice = `choose#${name}`;
153
+ return {
154
+ bounds: [fcnBound],
155
+ definitions: [
156
+ {
157
+ kind: "Operator",
158
+ name: choice,
159
+ body: expandChoose(
160
+ bound.names,
161
+ undefined,
162
+ getEquality(getOpaque(name), getNamesTuple(bound.names)),
163
+ ),
164
+ },
165
+ ...getItemDefinitions(choice, bound.names),
166
+ ],
167
+ };
168
+ });
169
+ const definitions = expanded.flatMap((item) => item.definitions);
170
+ return {
171
+ bounds: expanded.flatMap((item) => item.bounds),
172
+ value: definitions.length === 0 ? value : { kind: "Let" as const, definitions, body: value },
173
+ };
174
+ };
175
+
176
+ export const expandSetOf = (element: Expr, bounds: TuplyBound[]): Expr => {
177
+ const expanded = expandConstructorBounds(bounds, element, true);
178
+ return { kind: "SetOf", element: expanded.value, bounds: expanded.bounds };
179
+ };
180
+
181
+ export const expandFcn = (bounds: TuplyBound[], body: Expr): Expr => {
182
+ const expanded = expandConstructorBounds(bounds, body, false);
183
+ return { kind: "Fcn", bounds: expanded.bounds, body: expanded.value };
184
+ };
@@ -0,0 +1,316 @@
1
+ // Generates the proof obligations tlapm prints for a module, in tlapm's order and layout.
2
+
3
+ import { TlaProofError, TlaSyntaxError, type SyntaxFailure } from "./errors";
4
+ import { createAnonymizer } from "./obligation-anon";
5
+ import {
6
+ getAtPrefix,
7
+ getErrorNote,
8
+ getLocusPosition,
9
+ getLocusPrefix,
10
+ type Locus,
11
+ type ObligationWarning,
12
+ } from "./obligation-expression";
13
+ import { createGenerator, type LineRange } from "./obligation-generate";
14
+ import {
15
+ compareNames,
16
+ expandInstance,
17
+ flatten,
18
+ generate,
19
+ getDependencies,
20
+ normalize,
21
+ STANDARD_MODULES,
22
+ type ModuleSummary,
23
+ type Mule,
24
+ type TheoremSummary,
25
+ type Pipeline,
26
+ } from "./obligation-module";
27
+ import { formatObligation } from "./obligation-printer";
28
+ import type { Counter } from "./obligation-proof";
29
+ import { createSource } from "./obligation-source";
30
+ import { tokenize } from "./lexer";
31
+ import { parseTlapmModule } from "./parser";
32
+ import type { Module } from "./ast";
33
+
34
+ export interface ObligationSource {
35
+ file: string;
36
+ source: string;
37
+ }
38
+
39
+ export interface PrintedObligation {
40
+ id: number;
41
+ location: string;
42
+ text: string;
43
+ }
44
+
45
+ export interface ModuleObligations {
46
+ name: string;
47
+ location: Locus | undefined;
48
+ obligations: PrintedObligation[];
49
+ summary: ModuleSummary;
50
+ }
51
+
52
+ export const WHOLE_MODULE: LineRange = { start: 0, end: Infinity };
53
+
54
+ interface ObligationOptions {
55
+ lines?: LineRange;
56
+ onWarning?: (warning: ObligationWarning) => void;
57
+ }
58
+
59
+ const MODULE_HEADER = /-{4,} *MODULE/;
60
+
61
+ const END_FAILURE = "Unexpected EOF";
62
+
63
+ const formatLocus = (locus: Locus | undefined) =>
64
+ locus ? `${locus.startLine}:${locus.startColumn}:${locus.endLine}:${locus.endColumn}` : "";
65
+
66
+ const findTab = (source: string) => {
67
+ const tokens = tokenize(source);
68
+ const lines = source.split("\n");
69
+ const endLine = tokens.at(-1)?.line ?? lines.length;
70
+ const moduleEnd =
71
+ lines.slice(0, endLine - 1).reduce((total, line) => total + line.length + 1, 0) +
72
+ lines[endLine - 1].indexOf("====");
73
+ const offset = source.indexOf("\t", source.search(MODULE_HEADER));
74
+ if (offset === -1 || offset >= moduleEnd) return undefined;
75
+ const line = source.slice(0, offset).split("\n").length;
76
+ const column = offset - source.lastIndexOf("\n", offset - 1);
77
+ const last = tokens.findLast(
78
+ (token) =>
79
+ token.kind !== "End" && (token.line < line || (token.line === line && token.column < column)),
80
+ );
81
+ const end: SyntaxFailure = {
82
+ reason: END_FAILURE,
83
+ line: last?.line ?? line,
84
+ column: last ? last.endColumn + 1 : column,
85
+ isEnd: true,
86
+ };
87
+ return { offset, line, column, end };
88
+ };
89
+
90
+ const readModule = <Value>(read: () => Value): Value | SyntaxFailure => {
91
+ try {
92
+ return read();
93
+ } catch (error) {
94
+ if (!(error instanceof TlaSyntaxError) || !error.failure) throw error;
95
+ return error.failure;
96
+ }
97
+ };
98
+
99
+ const isSyntaxFailure = (value: unknown): value is SyntaxFailure =>
100
+ typeof value === "object" && value !== null && "reason" in value;
101
+
102
+ const parseSource = (source: string): Module | SyntaxFailure[] => {
103
+ const tab = readModule(() => findTab(source));
104
+ if (isSyntaxFailure(tab)) return [tab];
105
+ const parsed = readModule(() => parseTlapmModule(tab ? source.slice(0, tab.offset) : source));
106
+ if (tab && (!isSyntaxFailure(parsed) || parsed.isEnd)) {
107
+ return [
108
+ {
109
+ reason: "Unexpected TAB character. TLAPS does not handle TAB characters in source files.",
110
+ line: tab.line,
111
+ column: tab.column,
112
+ isEnd: false,
113
+ },
114
+ isSyntaxFailure(parsed) && parsed.reason === END_FAILURE ? parsed : tab.end,
115
+ ];
116
+ }
117
+ return isSyntaxFailure(parsed) ? [parsed] : parsed;
118
+ };
119
+
120
+ // Module.Save.parse_file warns when a module's name differs from its file's, then renames it.
121
+ const loadMule = (
122
+ input: ObligationSource,
123
+ counter: Counter,
124
+ isMain: boolean,
125
+ onWarning: (warning: ObligationWarning) => void,
126
+ citation: string,
127
+ ): Mule => {
128
+ if (!MODULE_HEADER.test(input.source)) throw new TlaProofError("No tokens in file");
129
+ const parsed = parseSource(input.source);
130
+ if (Array.isArray(parsed)) {
131
+ parsed.forEach((failure) => {
132
+ const text = `File ${JSON.stringify(input.file)}, line ${failure.line}, character ${failure.column}\n${failure.reason}`;
133
+ onWarning({ text, channel: "stderr" });
134
+ onWarning({ text: `${text}\n`, channel: "toolboxOnly" });
135
+ });
136
+ const message = `Could not parse ${JSON.stringify(input.file)} successfully.`;
137
+ onWarning({ text: `${citation}Error: ${message}`, channel: "stderr" });
138
+ throw new TlaProofError(message, { failure: "Module.Parser.parse_file" });
139
+ }
140
+ const mule = createSource(input.file, input.source, counter, isMain).convertModule(parsed);
141
+ const fileName = input.file.slice(input.file.lastIndexOf("/") + 1);
142
+ const name = fileName.replace(/\.tla$/, "");
143
+ if (mule.name === name) return mule;
144
+ onWarning({
145
+ text: `${getLocusPrefix(mule.location)}Warning: module name ${JSON.stringify(mule.name)} does not match file name ${JSON.stringify(fileName)}.`,
146
+ channel: "toolbox",
147
+ });
148
+ return { ...mule, name };
149
+ };
150
+
151
+ // Module.Dep.schedule: dependencies first, visiting modules and their dependencies by name.
152
+ const schedule = (modules: Map<string, Mule>) => {
153
+ const dependencies = new Map(
154
+ [...modules].map(([name, module]) => [name, getDependencies(module).dependencies]),
155
+ );
156
+ const seen = new Set<string>();
157
+ const order: Mule[] = [];
158
+ const visit = (name: string) => {
159
+ if (seen.has(name)) return;
160
+ seen.add(name);
161
+ const module = modules.get(name);
162
+ if (!module) throw new TlaProofError(`Module "${name}" not found`);
163
+ (dependencies.get(name) ?? []).forEach(visit);
164
+ order.push(module);
165
+ };
166
+ [...modules.keys()].sort(compareNames).forEach(visit);
167
+ return order;
168
+ };
169
+
170
+ const formatProofs = (kind: "missing" | "omitted", loci: Locus[], before: number) =>
171
+ loci.length === 0
172
+ ? []
173
+ : [
174
+ ` ${kind}_proofs_count = ${loci.length}`,
175
+ ...loci.map(
176
+ (locus, index) => ` ${kind}_proof_${before + index + 1} at ${getLocusPosition(locus)}`,
177
+ ),
178
+ ];
179
+
180
+ const formatTheoremSummary = (theorem: TheoremSummary, missing: number, omitted: number) => {
181
+ const location = theorem.location;
182
+ const start = location
183
+ ? getLocusPosition({
184
+ ...location,
185
+ endLine: location.startLine,
186
+ endColumn: location.startColumn,
187
+ })
188
+ : "<unknown location>";
189
+ return [
190
+ ` ---- incomplete proof of theorem at ${start} ----`,
191
+ ...formatProofs("missing", theorem.absent, missing),
192
+ ...formatProofs("omitted", theorem.omitted, omitted),
193
+ " ====",
194
+ ];
195
+ };
196
+
197
+ export const formatSummary = (result: ModuleObligations) => {
198
+ const summary = result.summary;
199
+ const theorems = summary.theorems.reduce(
200
+ (state, theorem) =>
201
+ theorem.absent.length + theorem.omitted.length === 0
202
+ ? state
203
+ : {
204
+ lines: [...state.lines, ...formatTheoremSummary(theorem, state.missing, state.omitted)],
205
+ missing: state.missing + theorem.absent.length,
206
+ omitted: state.omitted + theorem.omitted.length,
207
+ },
208
+ { lines: new Array<string>(), missing: 0, omitted: 0 },
209
+ );
210
+ return [
211
+ `---- summary of module ${JSON.stringify(result.name)} ----`,
212
+ ` obligations_count = ${summary.total}`,
213
+ ...(summary.absent.length > 0 ? [` missing_proofs_count = ${summary.absent.length}`] : []),
214
+ ...(summary.omitted.length > 0 ? [` omitted_proofs_count = ${summary.omitted.length}`] : []),
215
+ ...theorems.lines,
216
+ "====",
217
+ ].join("\n");
218
+ };
219
+
220
+ // Each main module is one file given to tlapm. They load into one context and report in schedule
221
+ // order, each numbering its obligations from 1.
222
+ export const getObligations = (
223
+ mains: ObligationSource[],
224
+ loadModule: (name: string) => ObligationSource | undefined,
225
+ options: ObligationOptions = {},
226
+ ): ModuleObligations[] => {
227
+ const lines = options.lines ?? WHOLE_MODULE;
228
+ const onWarning = options.onWarning ?? (() => {});
229
+ const counter = { value: 0 };
230
+ const modules = new Map<string, Mule>(STANDARD_MODULES);
231
+ mains.forEach((main) => {
232
+ const module = {
233
+ ...loadMule(main, counter, true, onWarning, 'File "<unknown>":\n'),
234
+ isImportant: true,
235
+ };
236
+ modules.set(module.name, module);
237
+ });
238
+ // Module.Save.complete_load: breadth first from every known module, by name.
239
+ const queue = [...modules.values()].sort((left, right) => compareNames(left.name, right.name));
240
+ // A missing module is fine only if some loaded module declares it as a submodule.
241
+ const requested = new Map<string, Locus | undefined>();
242
+ const found = new Set<string>();
243
+ for (let index = 0; index < queue.length; index++) {
244
+ const result = getDependencies(queue[index]);
245
+ result.submodules.forEach((submodule, name) => {
246
+ found.add(name);
247
+ if (!modules.has(name)) modules.set(name, submodule);
248
+ });
249
+ // A file named like a standard module replaces it, and is read again for each module citing it.
250
+ for (const name of result.dependencies) {
251
+ if (modules.has(name) && !STANDARD_MODULES.has(name)) continue;
252
+ const input = loadModule(name);
253
+ if (!input) {
254
+ if (!modules.has(name) && !requested.has(name))
255
+ requested.set(name, result.locations.get(name));
256
+ continue;
257
+ }
258
+ const loaded = loadMule(
259
+ input,
260
+ counter,
261
+ false,
262
+ onWarning,
263
+ getAtPrefix(result.locations.get(name)),
264
+ );
265
+ modules.set(name, loaded);
266
+ queue.push(loaded);
267
+ }
268
+ }
269
+ const missing = [...requested.keys()].filter((name) => !found.has(name)).sort(compareNames);
270
+ missing.forEach((name) =>
271
+ onWarning(getErrorNote(requested.get(name), `Unknown module ${JSON.stringify(name)}`)),
272
+ );
273
+ if (missing.length > 0)
274
+ throw new TlaProofError(`Unknown module ${JSON.stringify(missing[0])}`, {
275
+ failure: "Module.Parser.load_module",
276
+ });
277
+ const pipeline: Pipeline = {
278
+ anonymizer: createAnonymizer(
279
+ counter,
280
+ (scope, definition) => expandInstance(pipeline, scope, definition),
281
+ onWarning,
282
+ ),
283
+ generator: createGenerator(counter, new Set(mains.map((main) => main.file)), lines, onWarning),
284
+ salts: { value: 0 },
285
+ latestModules: new Map(),
286
+ onWarning,
287
+ };
288
+ const order = schedule(modules).map((module) =>
289
+ module.isStandard ? module : flatten(modules, module, new Set()),
290
+ );
291
+ // Instances see each module as flattened, and as normalized once its turn has come.
292
+ const normalized = new Map(order.map((module) => [module.name, module]));
293
+ return order.flatMap((module): ModuleObligations[] => {
294
+ if (module.isStandard) return [];
295
+ const result = normalize(pipeline, normalized, undefined, module);
296
+ if (!module.isImportant) return [];
297
+ const generated = generate(pipeline, [], result);
298
+ // tlapm numbers the obligations, then drops the omitted ones and those outside the lines.
299
+ const obligations = generated.obligations.flatMap((obligation, index): PrintedObligation[] => {
300
+ const location = obligation.location;
301
+ if (
302
+ obligation.kind.kind === "Omitted" ||
303
+ (location && (location.startLine < lines.start || location.endLine > lines.end))
304
+ )
305
+ return [];
306
+ const text =
307
+ obligation.kind.kind === "Error"
308
+ ? obligation.kind.message
309
+ : `${formatObligation(obligation.sequent)}\n`;
310
+ return [{ id: index + 1, location: formatLocus(location), text }];
311
+ });
312
+ return [
313
+ { name: module.name, location: module.location, obligations, summary: generated.summary },
314
+ ];
315
+ });
316
+ };
@@ -15,6 +15,7 @@ interface Pending {
15
15
  states: Value[][];
16
16
  origins: number[];
17
17
  fingerprints: number[];
18
+ fingerprintStates: Value[][];
18
19
  }
19
20
 
20
21
  interface PeerMessage {
@@ -123,6 +124,9 @@ const run = (start: StartMessage, parent: Port) => {
123
124
  const decode = createStateDecoder(environment.variables.length);
124
125
  const table = createFingerprintTable();
125
126
  const known = createFingerprintTable();
127
+ // Under symmetry, a successor the worker generated before has the representative it had then,
128
+ // so the worker skips the search for one.
129
+ const generatedStates = search.isSymmetric ? createFingerprintTable() : undefined;
126
130
  const fingerprint = new Int32Array(2);
127
131
  const inbox = createInbox();
128
132
  const peers = start.ports.flatMap((port, index) => (port ? [{ port, index }] : []));
@@ -137,11 +141,12 @@ const run = (start: StartMessage, parent: Port) => {
137
141
  const send = (index: number, message: PeerMessage, transfer: unknown[]) =>
138
142
  start.ports[index]?.postMessage(message, transfer);
139
143
 
140
- const expand = (states: Value[][], next: Value[][]) => {
144
+ const expand = (states: Value[][], next: Value[][], nextFingerprintStates: Value[][]) => {
141
145
  const pending: Pending[] = Array.from({ length: count }, () => ({
142
146
  states: [],
143
147
  origins: [],
144
148
  fingerprints: [],
149
+ fingerprintStates: [],
145
150
  }));
146
151
  const statistics = { generated: 0 };
147
152
  const newStates = new Int32Array(states.length);
@@ -153,8 +158,13 @@ const run = (start: StartMessage, parent: Port) => {
153
158
  for (const successor of successors) {
154
159
  if (search.findViolatedActionProperty(state, successor.state)) return undefined;
155
160
  if (!search.satisfiesActionConstraints(state, successor.state)) continue;
161
+ if (generatedStates) {
162
+ writeStateFingerprint(successor.state, fingerprint);
163
+ if (!generatedStates.insert(fingerprint[0], fingerprint[1])) continue;
164
+ }
156
165
  canonicalize(successor.state);
157
- writeStateFingerprint(search.getFingerprintState(successor.state), fingerprint);
166
+ const fingerprintState = search.getFingerprintState(successor.state);
167
+ writeStateFingerprint(fingerprintState, fingerprint);
158
168
  const owner = getFingerprintOwner(fingerprint[1], count);
159
169
  if (owner === self) {
160
170
  if (!table.insert(fingerprint[0], fingerprint[1])) continue;
@@ -164,6 +174,7 @@ const run = (start: StartMessage, parent: Port) => {
164
174
  continue;
165
175
  }
166
176
  next.push(successor.state);
177
+ nextFingerprintStates.push(fingerprintState);
167
178
  newStates[origin]++;
168
179
  continue;
169
180
  }
@@ -171,6 +182,7 @@ const run = (start: StartMessage, parent: Port) => {
171
182
  pending[owner].states.push(successor.state);
172
183
  pending[owner].origins.push(origin);
173
184
  pending[owner].fingerprints.push(fingerprint[0], fingerprint[1]);
185
+ pending[owner].fingerprintStates.push(fingerprintState);
174
186
  }
175
187
  }
176
188
  return { pending, generated: statistics.generated, newStates };
@@ -184,7 +196,13 @@ const run = (start: StartMessage, parent: Port) => {
184
196
  return bits;
185
197
  };
186
198
 
187
- const commit = (pending: Pending, bits: Uint8Array, next: Value[][], newStates: Int32Array) => {
199
+ const commit = (
200
+ pending: Pending,
201
+ bits: Uint8Array,
202
+ next: Value[][],
203
+ nextFingerprintStates: Value[][],
204
+ newStates: Int32Array,
205
+ ) => {
188
206
  const unmarks: number[] = [];
189
207
  for (let index = 0; index < bits.length; index++) {
190
208
  if (bits[index] === 0) continue;
@@ -193,6 +211,7 @@ const run = (start: StartMessage, parent: Port) => {
193
211
  if (search.findViolatedInvariant(state, parentState)) return undefined;
194
212
  if (search.satisfiesConstraints(state, parentState)) {
195
213
  next.push(state);
214
+ nextFingerprintStates.push(pending.fingerprintStates[index]);
196
215
  newStates[pending.origins[index]]++;
197
216
  continue;
198
217
  }
@@ -230,7 +249,8 @@ const run = (start: StartMessage, parent: Port) => {
230
249
 
231
250
  const runLevel = async (level: number): Promise<LevelMessage | undefined> => {
232
251
  const next: Value[][] = [];
233
- const expanded = expand(frontier.states, next);
252
+ const nextFingerprintStates: Value[][] = [];
253
+ const expanded = expand(frontier.states, next, nextFingerprintStates);
234
254
  if (!expanded || getRankedStringCount() !== startupStrings) return undefined;
235
255
  peers.forEach((peer) => {
236
256
  const fingerprints = Int32Array.from(expanded.pending[peer.index].fingerprints);
@@ -247,6 +267,7 @@ const run = (start: StartMessage, parent: Port) => {
247
267
  expanded.pending[reply.from],
248
268
  reply.bits ?? new Uint8Array(),
249
269
  next,
270
+ nextFingerprintStates,
250
271
  expanded.newStates,
251
272
  );
252
273
  if (!unmarks) return undefined;
@@ -255,9 +276,7 @@ const run = (start: StartMessage, parent: Port) => {
255
276
  const added = next.length;
256
277
  const outDegrees = Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0);
257
278
  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
- );
279
+ const fingerprints = search.getStoredFingerprints(next, nextFingerprintStates);
261
280
  const sizes = Array.from({ length: count }, () => 0);
262
281
  sizes[self] = added;
263
282
  peers.forEach((peer) =>