tla-ts 0.0.1 → 0.0.3-dev.3cf2ad0
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 +85 -40
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +216 -30
- package/dist/index.d.ts +216 -30
- 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 +20 -1
- package/dist/run-cli.js +20 -1
- package/dist/transfer.cjs +388 -39
- package/dist/transfer.js +388 -39
- package/package.json +6 -2
- package/src/ast.ts +122 -5
- package/src/bags.ts +161 -71
- package/src/builtins.ts +203 -23
- package/src/bundled-modules.ts +63 -32
- package/src/check.ts +945 -238
- package/src/cli-options.ts +58 -0
- package/src/codegen.ts +52 -18
- package/src/config.ts +39 -11
- package/src/environment.ts +485 -140
- package/src/errors.ts +59 -1
- package/src/evaluate.ts +489 -137
- package/src/exit-codes.ts +75 -0
- package/src/fingerprints.ts +48 -13
- package/src/index.ts +9 -4
- package/src/instances.ts +221 -66
- package/src/java-overrides.ts +241 -0
- package/src/json.ts +153 -72
- package/src/levels.ts +850 -0
- package/src/lexer.ts +103 -28
- package/src/liveness.ts +47 -91
- package/src/obligation-action.ts +384 -0
- package/src/obligation-anon.ts +575 -0
- package/src/obligation-deref.ts +637 -0
- package/src/obligation-expression.ts +1060 -0
- package/src/obligation-format.ts +406 -0
- package/src/obligation-generate.ts +1074 -0
- package/src/obligation-levels.ts +329 -0
- package/src/obligation-module.ts +1042 -0
- package/src/obligation-printer.ts +889 -0
- package/src/obligation-proof.ts +76 -0
- package/src/obligation-source.ts +1084 -0
- package/src/obligation-tuples.ts +184 -0
- package/src/obligations.ts +316 -0
- package/src/parallel-worker.ts +183 -202
- package/src/parallel.ts +163 -84
- package/src/parser.ts +958 -205
- package/src/pretty-print.ts +259 -0
- package/src/randomization.ts +24 -113
- package/src/resolve.ts +472 -41
- package/src/run-cli.ts +427 -61
- package/src/runtime.ts +63 -28
- package/src/spec-description.ts +509 -0
- package/src/standard-levels.ts +105 -0
- package/src/standard-modules.ts +223 -92
- package/src/subexpressions.ts +21 -35
- package/src/substitution-levels.ts +184 -0
- package/src/successors.ts +261 -46
- package/src/symmetry.ts +70 -22
- package/src/tableau.ts +43 -37
- package/src/tlc-ext.ts +120 -33
- package/src/tlc-module.ts +71 -47
- package/src/tlc-modules.ts +1022 -0
- package/src/tlc-order.ts +497 -0
- package/src/tlc-random.ts +411 -0
- package/src/tlc-summary.ts +109 -0
- package/src/transfer.ts +26 -5
- package/src/values.ts +1234 -176
package/src/parallel.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Expression } from "./ast";
|
|
2
2
|
import { createCanonicalizer } from "./canonical";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
checkSequentially,
|
|
5
5
|
createSearch,
|
|
6
6
|
findAssumptionViolation,
|
|
7
7
|
type CheckOptions,
|
|
@@ -9,11 +9,21 @@ import {
|
|
|
9
9
|
} from "./check";
|
|
10
10
|
import { parseConfig, type ModelConfig } from "./config";
|
|
11
11
|
import { createEnvironment } from "./environment";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { TlaEvaluationError } from "./errors";
|
|
13
|
+
import { isImpure } from "./evaluate";
|
|
14
|
+
import {
|
|
15
|
+
createSharedFingerprints,
|
|
16
|
+
growSharedFingerprints,
|
|
17
|
+
insertSharedFingerprint,
|
|
18
|
+
isSharedFingerprintsFull,
|
|
19
|
+
writeStateFingerprint,
|
|
20
|
+
type SharedFingerprints,
|
|
21
|
+
} from "./fingerprints";
|
|
15
22
|
import { getInitialStates } from "./successors";
|
|
23
|
+
import { getTlcStringOrder, getTlcVariableOrder } from "./tlc-order";
|
|
24
|
+
import { getFingerprintDistance, OUT_DEGREE_BUCKETS } from "./tlc-summary";
|
|
16
25
|
import { createStateEncoder, type EncodedStates } from "./transfer";
|
|
26
|
+
import { getRankedStringCount, setStringOrder, type Value } from "./values";
|
|
17
27
|
|
|
18
28
|
const WORKER_THREADS_MODULE = "node:worker_threads";
|
|
19
29
|
|
|
@@ -29,11 +39,13 @@ export interface StartMessage {
|
|
|
29
39
|
source: string;
|
|
30
40
|
config: ModelConfig;
|
|
31
41
|
modules: [string, string | undefined][];
|
|
42
|
+
classes: [string, Uint8Array | undefined][];
|
|
32
43
|
index: number;
|
|
33
|
-
count: number;
|
|
34
44
|
ports: (Port | undefined)[];
|
|
35
45
|
seed: EncodedStates;
|
|
36
|
-
|
|
46
|
+
fingerprints: SharedFingerprints;
|
|
47
|
+
requests: Int32Array;
|
|
48
|
+
remaining: Int32Array;
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
export interface LevelMessage {
|
|
@@ -41,13 +53,19 @@ export interface LevelMessage {
|
|
|
41
53
|
level: number;
|
|
42
54
|
generated: number;
|
|
43
55
|
added: number;
|
|
56
|
+
outDegrees: number[];
|
|
57
|
+
fingerprints: BigInt64Array;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
export type WorkerMessage =
|
|
60
|
+
export type WorkerMessage =
|
|
61
|
+
| { type: "ready" }
|
|
62
|
+
| { type: "abort" }
|
|
63
|
+
| { type: "paused" }
|
|
64
|
+
| LevelMessage;
|
|
47
65
|
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
}
|
|
66
|
+
export type CoordinatorMessage =
|
|
67
|
+
| { type: "level"; level: number }
|
|
68
|
+
| { type: "resize"; fingerprints: SharedFingerprints };
|
|
51
69
|
|
|
52
70
|
interface WorkerHandle {
|
|
53
71
|
postMessage: (message: unknown, transfer?: unknown[]) => void;
|
|
@@ -73,7 +91,7 @@ const spawnWorker = async (onMessage: (message: WorkerMessage) => void): Promise
|
|
|
73
91
|
/* @vite-ignore */ /* webpackIgnore: true */ WORKER_THREADS_MODULE
|
|
74
92
|
);
|
|
75
93
|
const worker = new threads.Worker(new URL("./parallel-worker.js", import.meta.url));
|
|
76
|
-
worker.on("message",
|
|
94
|
+
worker.on("message", onMessage);
|
|
77
95
|
worker.on("error", () => onMessage({ type: "abort" }));
|
|
78
96
|
return {
|
|
79
97
|
postMessage: (message, transfer) =>
|
|
@@ -82,93 +100,137 @@ const spawnWorker = async (onMessage: (message: WorkerMessage) => void): Promise
|
|
|
82
100
|
};
|
|
83
101
|
};
|
|
84
102
|
|
|
85
|
-
const
|
|
86
|
-
((expression.kind === "Apply" || expression.kind === "OperatorReference") &&
|
|
87
|
-
isImpureBuiltin(expression.operator, environment)) ||
|
|
88
|
-
(expression.kind === "Identifier" && isImpureBuiltin(expression.name, environment)) ||
|
|
89
|
-
getChildren(expression).some((child) => usesImpureBuiltin(child, environment));
|
|
90
|
-
|
|
91
|
-
const isImpure = (environment: Environment) =>
|
|
92
|
-
[...environment.definitions.values()].some(
|
|
93
|
-
(definition) =>
|
|
94
|
-
usesImpureBuiltin(definition.body, environment) ||
|
|
95
|
-
(definition.kind === "Function" &&
|
|
96
|
-
definition.bounds.some((bound) => usesImpureBuiltin(bound.set, environment))),
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
export const checkInParallel = async (options: ParallelCheckOptions): Promise<CheckResult> => {
|
|
103
|
+
export const check = async (options: CheckOptions): Promise<CheckResult> => {
|
|
100
104
|
const config = typeof options.config === "string" ? parseConfig(options.config) : options.config;
|
|
101
|
-
const
|
|
105
|
+
const fallBack = () => checkSequentially({ ...options, config });
|
|
106
|
+
const count = options.workers ?? 1;
|
|
102
107
|
const isSequential =
|
|
103
|
-
|
|
108
|
+
count < 2 ||
|
|
109
|
+
typeof SharedArrayBuffer !== "function" ||
|
|
104
110
|
config.view !== undefined ||
|
|
105
111
|
config.postconditions.length > 0 ||
|
|
106
112
|
config.possible.length > 0;
|
|
107
|
-
if (isSequential) return
|
|
113
|
+
if (isSequential) return fallBack();
|
|
108
114
|
const modules = new Map<string, string | undefined>();
|
|
109
|
-
const
|
|
110
|
-
loadModule
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
+
const loadModule = (name: string) => {
|
|
116
|
+
const source = options.loadModule?.(name);
|
|
117
|
+
modules.set(name, source);
|
|
118
|
+
return source;
|
|
119
|
+
};
|
|
120
|
+
const classes = new Map<string, Uint8Array | undefined>();
|
|
121
|
+
const loadClass = (name: string) => {
|
|
122
|
+
const bytes = options.loadClass?.(name);
|
|
123
|
+
classes.set(name, bytes);
|
|
124
|
+
return bytes;
|
|
125
|
+
};
|
|
126
|
+
setStringOrder(getTlcStringOrder(options.source, config, loadModule));
|
|
127
|
+
const startupStrings = getRankedStringCount();
|
|
128
|
+
const specification = createEnvironment(config, {
|
|
129
|
+
source: options.source,
|
|
130
|
+
loadModule,
|
|
131
|
+
loadClass,
|
|
115
132
|
print: options.onPrint ?? console.log,
|
|
116
133
|
files: { readFile: options.readFile, writeFile: options.writeFile },
|
|
117
134
|
});
|
|
118
135
|
const environment = specification.environment;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
136
|
+
const search = createSearch(config, environment, () =>
|
|
137
|
+
getTlcVariableOrder(options.source, loadModule),
|
|
138
|
+
);
|
|
139
|
+
if (findAssumptionViolation(specification.assumptions, environment)) return fallBack();
|
|
140
|
+
if (
|
|
141
|
+
!search ||
|
|
142
|
+
search.spec.init === undefined ||
|
|
143
|
+
search.spec.next === undefined ||
|
|
144
|
+
search.properties.liveness.length > 0 ||
|
|
145
|
+
isImpure(environment)
|
|
146
|
+
) {
|
|
147
|
+
return fallBack();
|
|
123
148
|
}
|
|
124
149
|
|
|
125
|
-
const count = options.workers;
|
|
126
150
|
const canonicalize = createCanonicalizer();
|
|
127
|
-
const
|
|
151
|
+
const shared = { fingerprints: createSharedFingerprints() };
|
|
128
152
|
const fingerprint = new Int32Array(2);
|
|
129
|
-
const seeds = Array.from({ length: count },
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
153
|
+
const seeds = Array.from({ length: count }, createStateEncoder);
|
|
154
|
+
const statistics = {
|
|
155
|
+
generatedStates: 0,
|
|
156
|
+
distinctStates: 0,
|
|
157
|
+
queuedStates: 0,
|
|
158
|
+
depth: 0,
|
|
159
|
+
outDegrees: Array.from({ length: OUT_DEGREE_BUCKETS }, () => 0),
|
|
160
|
+
};
|
|
161
|
+
const tlcFingerprints: BigInt64Array[] = [];
|
|
162
|
+
const createSuccess = (): CheckResult => ({
|
|
163
|
+
status: "success",
|
|
164
|
+
violation: undefined,
|
|
165
|
+
failure: undefined,
|
|
166
|
+
...statistics,
|
|
167
|
+
fingerprintDistance: getFingerprintDistance(
|
|
168
|
+
statistics.distinctStates,
|
|
169
|
+
statistics.generatedStates,
|
|
170
|
+
() => {
|
|
171
|
+
const fingerprints = new BigInt64Array(statistics.distinctStates);
|
|
172
|
+
tlcFingerprints.reduce((offset, level) => {
|
|
173
|
+
fingerprints.set(level, offset);
|
|
174
|
+
return offset + level.length;
|
|
175
|
+
}, 0);
|
|
176
|
+
return fingerprints;
|
|
177
|
+
},
|
|
178
|
+
),
|
|
179
|
+
});
|
|
180
|
+
const seedInitialStates = (init: Expression) => {
|
|
181
|
+
const seeded: Value[][] = [];
|
|
182
|
+
for (const state of getInitialStates(init, environment, search.spec.scope)) {
|
|
183
|
+
canonicalize(state);
|
|
184
|
+
statistics.generatedStates++;
|
|
185
|
+
writeStateFingerprint(search.getFingerprintState(state), fingerprint);
|
|
186
|
+
if (isSharedFingerprintsFull(shared.fingerprints)) {
|
|
187
|
+
shared.fingerprints = growSharedFingerprints(shared.fingerprints);
|
|
188
|
+
}
|
|
189
|
+
if (!insertSharedFingerprint(shared.fingerprints, fingerprint[0], fingerprint[1])) continue;
|
|
190
|
+
if (search.findViolatedInvariant(state) || search.findFailedInitialPredicate(state)) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
if (!search.satisfiesConstraints(state)) continue;
|
|
194
|
+
if (!seeds[seeded.length % count].encode(state)) return false;
|
|
195
|
+
seeded.push(state);
|
|
196
|
+
statistics.distinctStates++;
|
|
145
197
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
198
|
+
tlcFingerprints.push(
|
|
199
|
+
BigInt64Array.from(seeded, (state) => environment.runtime.getFingerprint(state)),
|
|
200
|
+
);
|
|
201
|
+
return true;
|
|
202
|
+
};
|
|
203
|
+
// The sequential search reports a violation or a failed evaluation in the initial states.
|
|
204
|
+
try {
|
|
205
|
+
if (!seedInitialStates(search.spec.init)) return fallBack();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (!(error instanceof TlaEvaluationError)) throw error;
|
|
208
|
+
return fallBack();
|
|
150
209
|
}
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
statistics.
|
|
210
|
+
if (getRankedStringCount() !== startupStrings) return fallBack();
|
|
211
|
+
statistics.depth = statistics.generatedStates === 0 ? 0 : 1;
|
|
212
|
+
if (statistics.distinctStates === 0) return createSuccess();
|
|
154
213
|
|
|
155
214
|
const reports: WorkerMessage[] = [];
|
|
156
|
-
const pending = { resolve: (
|
|
215
|
+
const pending = { count, resolve: (_reports: WorkerMessage[]) => {} };
|
|
157
216
|
const onMessage = (message: WorkerMessage) => {
|
|
158
217
|
reports.push(message);
|
|
159
|
-
if (message.type === "abort" || reports.length === count) {
|
|
218
|
+
if (message.type === "abort" || reports.length === pending.count) {
|
|
160
219
|
pending.resolve(reports.splice(0));
|
|
161
220
|
}
|
|
162
221
|
};
|
|
163
|
-
const waitForReports = () =>
|
|
222
|
+
const waitForReports = (expected: number) =>
|
|
164
223
|
new Promise<WorkerMessage[]>((resolve) => {
|
|
224
|
+
pending.count = expected;
|
|
165
225
|
pending.resolve = resolve;
|
|
166
226
|
});
|
|
167
|
-
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));
|
|
168
230
|
const workers = await Promise.all(Array.from({ length: count }, () => spawnWorker(onMessage)));
|
|
169
231
|
const terminate = () => workers.forEach((worker) => worker.terminate());
|
|
170
232
|
const channels = new Map<string, MessageChannel>();
|
|
171
|
-
const getPort = (from: number, to: number)
|
|
233
|
+
const getPort = (from: number, to: number) => {
|
|
172
234
|
const key = from < to ? `${from}:${to}` : `${to}:${from}`;
|
|
173
235
|
const channel = channels.get(key) ?? new MessageChannel();
|
|
174
236
|
channels.set(key, channel);
|
|
@@ -178,46 +240,63 @@ export const checkInParallel = async (options: ParallelCheckOptions): Promise<Ch
|
|
|
178
240
|
const ports = Array.from({ length: count }, (_, peer) =>
|
|
179
241
|
peer === index ? undefined : getPort(index, peer),
|
|
180
242
|
);
|
|
181
|
-
const seed = seeds[index].
|
|
182
|
-
const seedFingerprints = Int32Array.from(seeds[index].fingerprints);
|
|
243
|
+
const seed = seeds[index].finish();
|
|
183
244
|
const message: StartMessage = {
|
|
184
245
|
type: "start",
|
|
185
246
|
source: options.source,
|
|
186
247
|
config,
|
|
187
248
|
modules: [...modules],
|
|
249
|
+
classes: [...classes],
|
|
188
250
|
index,
|
|
189
|
-
count,
|
|
190
251
|
ports,
|
|
191
252
|
seed,
|
|
192
|
-
|
|
253
|
+
fingerprints: shared.fingerprints,
|
|
254
|
+
requests,
|
|
255
|
+
remaining,
|
|
193
256
|
};
|
|
194
|
-
worker.postMessage(message, [
|
|
195
|
-
...ports.filter((port) => port !== undefined),
|
|
196
|
-
seed.words.buffer,
|
|
197
|
-
seedFingerprints.buffer,
|
|
198
|
-
]);
|
|
257
|
+
worker.postMessage(message, [...ports.filter((port) => port !== undefined), seed.words.buffer]);
|
|
199
258
|
});
|
|
200
259
|
const isAborted = (messages: WorkerMessage[]) =>
|
|
201
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
|
+
};
|
|
202
273
|
if (isAborted(await ready)) {
|
|
203
274
|
terminate();
|
|
204
|
-
return
|
|
275
|
+
return fallBack();
|
|
205
276
|
}
|
|
206
277
|
for (let level = 1; ; level++) {
|
|
207
|
-
const reported = waitForReports();
|
|
208
|
-
|
|
209
|
-
|
|
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);
|
|
210
283
|
if (isAborted(messages)) {
|
|
211
284
|
terminate();
|
|
212
|
-
return
|
|
285
|
+
return fallBack();
|
|
213
286
|
}
|
|
214
287
|
const levels = messages.flatMap((message) => (message.type === "level" ? [message] : []));
|
|
215
288
|
const added = levels.reduce((sum, message) => sum + message.added, 0);
|
|
216
289
|
statistics.generatedStates += levels.reduce((sum, message) => sum + message.generated, 0);
|
|
217
290
|
statistics.distinctStates += added;
|
|
291
|
+
levels.forEach((message) => {
|
|
292
|
+
message.outDegrees.forEach(
|
|
293
|
+
(degreeCount, degree) => (statistics.outDegrees[degree] += degreeCount),
|
|
294
|
+
);
|
|
295
|
+
tlcFingerprints.push(message.fingerprints);
|
|
296
|
+
});
|
|
218
297
|
if (added === 0) {
|
|
219
298
|
terminate();
|
|
220
|
-
return
|
|
299
|
+
return createSuccess();
|
|
221
300
|
}
|
|
222
301
|
statistics.depth++;
|
|
223
302
|
}
|