tla-ts 0.0.1 → 0.0.3-dev.6979965

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.
Files changed (71) hide show
  1. package/README.md +85 -40
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.cts +215 -30
  4. package/dist/index.d.ts +215 -30
  5. package/dist/index.js +1 -1
  6. package/dist/parallel-worker.cjs +2 -2
  7. package/dist/parallel-worker.js +2 -2
  8. package/dist/parallel.cjs +1 -1
  9. package/dist/parallel.js +2 -2
  10. package/dist/run-cli.cjs +20 -1
  11. package/dist/run-cli.js +20 -1
  12. package/dist/transfer.cjs +388 -39
  13. package/dist/transfer.js +388 -39
  14. package/package.json +6 -2
  15. package/src/ast.ts +122 -5
  16. package/src/bags.ts +161 -71
  17. package/src/builtins.ts +203 -23
  18. package/src/bundled-modules.ts +63 -32
  19. package/src/check.ts +945 -238
  20. package/src/cli-options.ts +58 -0
  21. package/src/codegen.ts +52 -18
  22. package/src/config.ts +39 -11
  23. package/src/environment.ts +485 -140
  24. package/src/errors.ts +59 -1
  25. package/src/evaluate.ts +489 -137
  26. package/src/exit-codes.ts +75 -0
  27. package/src/fingerprints.ts +48 -13
  28. package/src/index.ts +9 -4
  29. package/src/instances.ts +221 -66
  30. package/src/java-overrides.ts +241 -0
  31. package/src/json.ts +153 -72
  32. package/src/levels.ts +850 -0
  33. package/src/lexer.ts +103 -28
  34. package/src/liveness.ts +47 -91
  35. package/src/obligation-action.ts +384 -0
  36. package/src/obligation-anon.ts +575 -0
  37. package/src/obligation-deref.ts +637 -0
  38. package/src/obligation-expression.ts +1060 -0
  39. package/src/obligation-format.ts +406 -0
  40. package/src/obligation-generate.ts +1074 -0
  41. package/src/obligation-levels.ts +329 -0
  42. package/src/obligation-module.ts +1042 -0
  43. package/src/obligation-printer.ts +889 -0
  44. package/src/obligation-proof.ts +76 -0
  45. package/src/obligation-source.ts +1084 -0
  46. package/src/obligation-tuples.ts +184 -0
  47. package/src/obligations.ts +316 -0
  48. package/src/parallel-worker.ts +121 -123
  49. package/src/parallel.ts +167 -83
  50. package/src/parser.ts +958 -205
  51. package/src/pretty-print.ts +259 -0
  52. package/src/randomization.ts +24 -113
  53. package/src/resolve.ts +472 -41
  54. package/src/run-cli.ts +427 -61
  55. package/src/runtime.ts +63 -28
  56. package/src/spec-description.ts +509 -0
  57. package/src/standard-levels.ts +105 -0
  58. package/src/standard-modules.ts +223 -92
  59. package/src/subexpressions.ts +21 -35
  60. package/src/substitution-levels.ts +184 -0
  61. package/src/successors.ts +261 -46
  62. package/src/symmetry.ts +65 -21
  63. package/src/tableau.ts +43 -37
  64. package/src/tlc-ext.ts +120 -33
  65. package/src/tlc-module.ts +71 -47
  66. package/src/tlc-modules.ts +1022 -0
  67. package/src/tlc-order.ts +497 -0
  68. package/src/tlc-random.ts +411 -0
  69. package/src/tlc-summary.ts +109 -0
  70. package/src/transfer.ts +26 -5
  71. package/src/values.ts +1231 -176
@@ -1,30 +1,42 @@
1
1
  import { createCanonicalizer } from "./canonical";
2
2
  import { createSearch } from "./check";
3
3
  import { createEnvironment } from "./environment";
4
- import { createFingerprintTable, getFingerprintOwner, getStateFingerprint } from "./fingerprints";
5
- import type { LevelMessage, Port, StartMessage, WorkerMessage } from "./parallel";
6
- import { parseModule } from "./parser";
7
- 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";
18
+ import { getTlcStringOrder, getTlcVariableOrder } from "./tlc-order";
19
+ import { OUT_DEGREE_BUCKETS } from "./tlc-summary";
8
20
  import { createStateDecoder, createStateEncoder, type EncodedStates } from "./transfer";
9
- import type { Value } from "./values";
21
+ import { getRankedStringCount, setStringOrder, type Value } from "./values";
10
22
 
11
23
  const WORKER_THREADS_MODULE = "node:worker_threads";
12
24
 
13
- interface Pending {
14
- states: Value[][];
15
- parents: Value[][];
16
- fingerprints: number[];
17
- }
18
-
19
25
  interface PeerMessage {
20
- phase: "query" | "answer" | "sizes" | "balance";
26
+ phase: "balance";
21
27
  level: number;
22
28
  from: number;
23
- fingerprints?: Int32Array;
24
- bits?: Uint8Array;
25
- states?: EncodedStates;
26
- unmarks?: Int32Array;
27
- size?: number;
29
+ states: EncodedStates;
30
+ }
31
+
32
+ interface Expansion {
33
+ origin: number;
34
+ successors: Successor[] | undefined;
35
+ index: number;
36
+ generated: number;
37
+ newStates: Int32Array;
38
+ next: Value[][];
39
+ fingerprintStates: Value[][];
28
40
  }
29
41
 
30
42
  interface Transfer {
@@ -33,12 +45,12 @@ interface Transfer {
33
45
  count: number;
34
46
  }
35
47
 
36
- const getBalancePlan = (sizes: number[]): Transfer[] => {
48
+ const getBalancePlan = (sizes: number[]) => {
37
49
  const total = sizes.reduce((sum, size) => sum + size, 0);
38
50
  const average = total / sizes.length;
39
51
  if (Math.max(...sizes) - Math.min(...sizes) <= Math.max(64, average / 4)) return [];
40
52
  const targets = sizes.map(
41
- (_, index) => Math.floor(total / sizes.length) + (index < total % sizes.length ? 1 : 0),
53
+ (_, index) => Math.floor(average) + (index < total % sizes.length ? 1 : 0),
42
54
  );
43
55
  const donors = sizes.flatMap((size, index) =>
44
56
  size > targets[index] ? [{ index, amount: size - targets[index] }] : [],
@@ -59,7 +71,7 @@ const getBalancePlan = (sizes: number[]): Transfer[] => {
59
71
  return transfers;
60
72
  };
61
73
 
62
- const getParentPort = async (): Promise<Port> => {
74
+ const getParentPort = async () => {
63
75
  if (typeof (globalThis as { postMessage?: unknown }).postMessage === "function") {
64
76
  return globalThis as unknown as Port;
65
77
  }
@@ -98,21 +110,31 @@ const createInbox = () => {
98
110
 
99
111
  const run = (start: StartMessage, parent: Port) => {
100
112
  const modules = new Map(start.modules);
101
- const specification = createEnvironment(parseModule(start.source), start.config, {
102
- loadModule: (name) => modules.get(name),
113
+ const loadModule = (name: string) => modules.get(name);
114
+ const classes = new Map(start.classes);
115
+ setStringOrder(getTlcStringOrder(start.source, start.config, loadModule));
116
+ const startupStrings = getRankedStringCount();
117
+ const specification = createEnvironment(start.config, {
118
+ source: start.source,
119
+ loadModule,
120
+ loadClass: (name) => classes.get(name),
103
121
  print: () => {},
104
122
  files: { readFile: undefined, writeFile: undefined },
105
123
  });
106
124
  const environment = specification.environment;
107
- const search = createSearch(start.config, environment);
108
- if (!search) return parent.postMessage({ type: "abort" } satisfies WorkerMessage);
109
- const width = environment.variables.length;
110
- const count = start.count;
125
+ const search = createSearch(start.config, environment, () =>
126
+ getTlcVariableOrder(start.source, loadModule),
127
+ );
128
+ const nextAction = search?.spec.next;
129
+ if (!search || nextAction === undefined)
130
+ return parent.postMessage({ type: "abort" } satisfies WorkerMessage);
111
131
  const self = start.index;
112
132
  const canonicalize = createCanonicalizer();
113
- const decode = createStateDecoder(width);
114
- const table = createFingerprintTable();
115
- const known = createFingerprintTable();
133
+ const decode = createStateDecoder(environment.variables.length);
134
+ const shared = { fingerprints: start.fingerprints };
135
+ // Under symmetry, a successor the worker generated before has the representative it had then,
136
+ // so the worker skips the search for one.
137
+ const generatedStates = search.isSymmetric ? createFingerprintTable() : undefined;
116
138
  const fingerprint = new Int32Array(2);
117
139
  const inbox = createInbox();
118
140
  const peers = start.ports.flatMap((port, index) => (port ? [{ port, index }] : []));
@@ -120,75 +142,44 @@ const run = (start: StartMessage, parent: Port) => {
120
142
  peer.port.addEventListener("message", (event) => inbox.deliver(event.data as PeerMessage));
121
143
  peer.port.start?.();
122
144
  });
123
- for (let index = 0; index < start.seedFingerprints.length; index += 2) {
124
- table.insert(start.seedFingerprints[index], start.seedFingerprints[index + 1]);
125
- }
126
145
  const frontier = { states: decode(start.seed).map(canonicalize) };
127
146
  const send = (index: number, message: PeerMessage, transfer: unknown[]) =>
128
147
  start.ports[index]?.postMessage(message, transfer);
129
148
 
130
- const expand = (states: Value[][], next: Value[][]) => {
131
- const pending: Pending[] = Array.from({ length: count }, () => ({
132
- states: [],
133
- parents: [],
134
- fingerprints: [],
135
- }));
136
- const statistics = { generated: 0 };
137
- for (const state of states) {
138
- const successors = getSuccessors(search.spec.next, state, environment);
139
- statistics.generated += successors.length;
140
- if (successors.length === 0 && start.config.checkDeadlock) return undefined;
141
- for (const successor of successors) {
149
+ // The worker pauses before a successor when the shared table needs to grow, and resumes there.
150
+ const expand = (expansion: Expansion) => {
151
+ const states = frontier.states;
152
+ for (; expansion.origin < states.length; expansion.origin++) {
153
+ const state = states[expansion.origin];
154
+ if (!expansion.successors) {
155
+ expansion.successors = getSuccessors(nextAction, state, environment, search.spec.scope);
156
+ expansion.generated += expansion.successors.length;
157
+ if (expansion.successors.length === 0 && start.config.checkDeadlock) return undefined;
158
+ }
159
+ const successors = expansion.successors;
160
+ for (; expansion.index < successors.length; expansion.index++) {
161
+ if (isSharedFingerprintsFull(shared.fingerprints)) return "paused";
162
+ const successor = successors[expansion.index];
142
163
  if (search.findViolatedActionProperty(state, successor.state)) return undefined;
143
164
  if (!search.satisfiesActionConstraints(state, successor.state)) continue;
144
- canonicalize(successor.state);
145
- getStateFingerprint(search.getFingerprintState(successor.state), fingerprint);
146
- const owner = getFingerprintOwner(fingerprint[1], count);
147
- if (owner === self) {
148
- if (!table.insert(fingerprint[0], fingerprint[1])) continue;
149
- if (search.findViolatedInvariant(successor.state, state)) return undefined;
150
- if (search.satisfiesConstraints(successor.state, state)) next.push(successor.state);
151
- else table.remove(fingerprint[0], fingerprint[1]);
152
- continue;
165
+ if (generatedStates) {
166
+ writeStateFingerprint(successor.state, fingerprint);
167
+ if (!generatedStates.insert(fingerprint[0], fingerprint[1])) continue;
153
168
  }
154
- if (!known.insert(fingerprint[0], fingerprint[1])) continue;
155
- pending[owner].states.push(successor.state);
156
- pending[owner].parents.push(state);
157
- pending[owner].fingerprints.push(fingerprint[0], fingerprint[1]);
158
- }
159
- }
160
- return { pending, generated: statistics.generated };
161
- };
162
-
163
- const answer = (fingerprints: Int32Array | number[]) => {
164
- const bits = new Uint8Array(fingerprints.length / 2);
165
- for (let index = 0; index < bits.length; index++) {
166
- bits[index] = table.insert(fingerprints[index * 2], fingerprints[index * 2 + 1]) ? 1 : 0;
167
- }
168
- return bits;
169
- };
170
-
171
- const commit = (pending: Pending, bits: Uint8Array, next: Value[][]) => {
172
- const unmarks: number[] = [];
173
- for (let index = 0; index < bits.length; index++) {
174
- if (bits[index] === 0) continue;
175
- const state = pending.states[index];
176
- const parentState = pending.parents[index];
177
- if (search.findViolatedInvariant(state, parentState)) return undefined;
178
- if (search.satisfiesConstraints(state, parentState)) {
179
- next.push(state);
180
- continue;
169
+ canonicalize(successor.state);
170
+ const fingerprintState = search.getFingerprintState(successor.state);
171
+ writeStateFingerprint(fingerprintState, fingerprint);
172
+ if (!insertSharedFingerprint(shared.fingerprints, fingerprint[0], fingerprint[1])) continue;
173
+ if (search.findViolatedInvariant(successor.state, state)) return undefined;
174
+ if (!search.satisfiesConstraints(successor.state, state)) continue;
175
+ expansion.next.push(successor.state);
176
+ expansion.fingerprintStates.push(fingerprintState);
177
+ expansion.newStates[expansion.origin]++;
181
178
  }
182
- known.remove(pending.fingerprints[index * 2], pending.fingerprints[index * 2 + 1]);
183
- unmarks.push(pending.fingerprints[index * 2], pending.fingerprints[index * 2 + 1]);
184
- }
185
- return Int32Array.from(unmarks);
186
- };
187
-
188
- const removeAll = (fingerprints: Int32Array) => {
189
- for (let index = 0; index < fingerprints.length; index += 2) {
190
- table.remove(fingerprints[index], fingerprints[index + 1]);
179
+ expansion.successors = undefined;
180
+ expansion.index = 0;
191
181
  }
182
+ return "done";
192
183
  };
193
184
 
194
185
  const balance = async (level: number, next: Value[][], sizes: number[]) => {
@@ -205,48 +196,50 @@ const run = (start: StartMessage, parent: Port) => {
205
196
  send(index, { phase: "balance", level, from: self, states }, [states.words.buffer]);
206
197
  });
207
198
  const incoming = await inbox.receive("balance", level, peers.length);
208
- incoming.forEach((message) => {
209
- if (message.states) next.push(...decode(message.states).map(canonicalize));
210
- });
199
+ incoming.forEach((message) => next.push(...decode(message.states).map(canonicalize)));
211
200
  return true;
212
201
  };
213
202
 
203
+ const waiters = { resume: () => {}, balance: (_sizes: number[]) => {} };
204
+ const report = (message: WorkerMessage) =>
205
+ parent.postMessage(message, message.type === "level" ? [message.fingerprints.buffer] : []);
206
+
214
207
  const runLevel = async (level: number): Promise<LevelMessage | undefined> => {
215
- const next: Value[][] = [];
216
- const expanded = expand(frontier.states, next);
217
- if (!expanded) return undefined;
218
- peers.forEach((peer) => {
219
- const fingerprints = Int32Array.from(expanded.pending[peer.index].fingerprints);
220
- send(peer.index, { phase: "query", level, from: self, fingerprints }, [fingerprints.buffer]);
221
- });
222
- const queries = await inbox.receive("query", level, peers.length);
223
- queries.forEach((query) => {
224
- const bits = answer(query.fingerprints ?? []);
225
- send(query.from, { phase: "answer", level, from: self, bits }, [bits.buffer]);
226
- });
227
- const answers = await inbox.receive("answer", level, peers.length);
228
- for (const reply of answers) {
229
- const unmarks = commit(expanded.pending[reply.from], reply.bits ?? new Uint8Array(), next);
230
- if (!unmarks) return undefined;
231
- send(reply.from, { phase: "sizes", level, from: self, unmarks, size: 0 }, [unmarks.buffer]);
208
+ const expansion: Expansion = {
209
+ origin: 0,
210
+ successors: undefined,
211
+ index: 0,
212
+ generated: 0,
213
+ newStates: new Int32Array(frontier.states.length),
214
+ next: [],
215
+ fingerprintStates: [],
216
+ };
217
+ for (let status = expand(expansion); status !== "done"; status = expand(expansion)) {
218
+ if (status === undefined) return undefined;
219
+ const resumed = new Promise<void>((resolve) => (waiters.resume = resolve));
220
+ report({ type: "paused" });
221
+ await resumed;
232
222
  }
223
+ if (getRankedStringCount() !== startupStrings) return undefined;
224
+ const next = expansion.next;
233
225
  const added = next.length;
234
- const sizes = Array.from({ length: count }, () => 0);
235
- sizes[self] = added;
236
- peers.forEach((peer) =>
237
- send(peer.index, { phase: "sizes", level, from: self, size: added }, []),
238
- );
239
- const received = await inbox.receive("sizes", level, peers.length * 2);
240
- received.forEach((message) => {
241
- if (message.unmarks) removeAll(message.unmarks);
242
- else sizes[message.from] = message.size ?? 0;
243
- });
244
- if (!(await balance(level, next, sizes))) return undefined;
226
+ const outDegrees = Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0);
227
+ expansion.newStates.forEach((degree) => outDegrees[Math.min(degree, OUT_DEGREE_BUCKETS - 1)]++);
228
+ const fingerprints = search.getStoredFingerprints(next, expansion.fingerprintStates);
229
+ const sizes = new Promise<number[]>((resolve) => (waiters.balance = resolve));
230
+ report({ type: "expanded", from: self, added });
231
+ if (!(await balance(level, next, await sizes))) return undefined;
245
232
  frontier.states = next;
246
- return { type: "level", level, generated: expanded.generated, added };
233
+ return {
234
+ type: "level",
235
+ level,
236
+ generated: expansion.generated,
237
+ added,
238
+ outDegrees,
239
+ fingerprints,
240
+ };
247
241
  };
248
242
 
249
- const report = (message: WorkerMessage) => parent.postMessage(message);
250
243
  const step = (level: number) => {
251
244
  runLevel(level).then(
252
245
  (message) => report(message ?? { type: "abort" }),
@@ -254,8 +247,13 @@ const run = (start: StartMessage, parent: Port) => {
254
247
  );
255
248
  };
256
249
  parent.addEventListener("message", (event) => {
257
- const message = event.data as { type: string; level: number };
250
+ const message = event.data as CoordinatorMessage;
258
251
  if (message.type === "level") step(message.level);
252
+ if (message.type === "resize") {
253
+ shared.fingerprints = message.fingerprints;
254
+ waiters.resume();
255
+ }
256
+ if (message.type === "balance") waiters.balance(message.sizes);
259
257
  });
260
258
  report({ type: "ready" });
261
259
  };