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.
- package/README.md +85 -40
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +215 -30
- package/dist/index.d.ts +215 -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 +121 -123
- package/src/parallel.ts +167 -83
- 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 +65 -21
- 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 +1231 -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,11 @@ 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;
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
export interface LevelMessage {
|
|
@@ -41,13 +51,21 @@ export interface LevelMessage {
|
|
|
41
51
|
level: number;
|
|
42
52
|
generated: number;
|
|
43
53
|
added: number;
|
|
54
|
+
outDegrees: number[];
|
|
55
|
+
fingerprints: BigInt64Array;
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
export type WorkerMessage =
|
|
58
|
+
export type WorkerMessage =
|
|
59
|
+
| { type: "ready" }
|
|
60
|
+
| { type: "abort" }
|
|
61
|
+
| { type: "paused" }
|
|
62
|
+
| { type: "expanded"; from: number; added: number }
|
|
63
|
+
| LevelMessage;
|
|
47
64
|
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
}
|
|
65
|
+
export type CoordinatorMessage =
|
|
66
|
+
| { type: "level"; level: number }
|
|
67
|
+
| { type: "resize"; fingerprints: SharedFingerprints }
|
|
68
|
+
| { type: "balance"; sizes: number[] };
|
|
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,135 @@ 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);
|
|
168
228
|
const workers = await Promise.all(Array.from({ length: count }, () => spawnWorker(onMessage)));
|
|
169
229
|
const terminate = () => workers.forEach((worker) => worker.terminate());
|
|
170
230
|
const channels = new Map<string, MessageChannel>();
|
|
171
|
-
const getPort = (from: number, to: number)
|
|
231
|
+
const getPort = (from: number, to: number) => {
|
|
172
232
|
const key = from < to ? `${from}:${to}` : `${to}:${from}`;
|
|
173
233
|
const channel = channels.get(key) ?? new MessageChannel();
|
|
174
234
|
channels.set(key, channel);
|
|
@@ -178,46 +238,70 @@ export const checkInParallel = async (options: ParallelCheckOptions): Promise<Ch
|
|
|
178
238
|
const ports = Array.from({ length: count }, (_, peer) =>
|
|
179
239
|
peer === index ? undefined : getPort(index, peer),
|
|
180
240
|
);
|
|
181
|
-
const seed = seeds[index].
|
|
182
|
-
const seedFingerprints = Int32Array.from(seeds[index].fingerprints);
|
|
241
|
+
const seed = seeds[index].finish();
|
|
183
242
|
const message: StartMessage = {
|
|
184
243
|
type: "start",
|
|
185
244
|
source: options.source,
|
|
186
245
|
config,
|
|
187
246
|
modules: [...modules],
|
|
247
|
+
classes: [...classes],
|
|
188
248
|
index,
|
|
189
|
-
count,
|
|
190
249
|
ports,
|
|
191
250
|
seed,
|
|
192
|
-
|
|
251
|
+
fingerprints: shared.fingerprints,
|
|
193
252
|
};
|
|
194
|
-
worker.postMessage(message, [
|
|
195
|
-
...ports.filter((port) => port !== undefined),
|
|
196
|
-
seed.words.buffer,
|
|
197
|
-
seedFingerprints.buffer,
|
|
198
|
-
]);
|
|
253
|
+
worker.postMessage(message, [...ports.filter((port) => port !== undefined), seed.words.buffer]);
|
|
199
254
|
});
|
|
200
255
|
const isAborted = (messages: WorkerMessage[]) =>
|
|
201
256
|
messages.some((message) => message.type === "abort");
|
|
257
|
+
const send = (message: CoordinatorMessage) =>
|
|
258
|
+
workers.forEach((worker) => worker.postMessage(message));
|
|
259
|
+
// Workers pause when the shared table needs to grow, and resume once it has.
|
|
260
|
+
const collectExpansions = async (messages: WorkerMessage[]): Promise<WorkerMessage[]> => {
|
|
261
|
+
const paused = messages.filter((message) => message.type === "paused").length;
|
|
262
|
+
if (paused === 0 || isAborted(messages)) return messages;
|
|
263
|
+
shared.fingerprints = growSharedFingerprints(shared.fingerprints);
|
|
264
|
+
const resumed = waitForReports(paused);
|
|
265
|
+
send({ type: "resize", fingerprints: shared.fingerprints });
|
|
266
|
+
const expanded = messages.filter((message) => message.type !== "paused");
|
|
267
|
+
return [...expanded, ...(await collectExpansions(await resumed))];
|
|
268
|
+
};
|
|
202
269
|
if (isAborted(await ready)) {
|
|
203
270
|
terminate();
|
|
204
|
-
return
|
|
271
|
+
return fallBack();
|
|
205
272
|
}
|
|
206
273
|
for (let level = 1; ; level++) {
|
|
207
|
-
const
|
|
208
|
-
|
|
274
|
+
const expanding = waitForReports(count);
|
|
275
|
+
send({ type: "level", level });
|
|
276
|
+
const expansions = await collectExpansions(await expanding);
|
|
277
|
+
if (isAborted(expansions)) {
|
|
278
|
+
terminate();
|
|
279
|
+
return fallBack();
|
|
280
|
+
}
|
|
281
|
+
const sizes = Array.from({ length: count }, () => 0);
|
|
282
|
+
expansions.forEach((message) => {
|
|
283
|
+
if (message.type === "expanded") sizes[message.from] = message.added;
|
|
284
|
+
});
|
|
285
|
+
const reported = waitForReports(count);
|
|
286
|
+
send({ type: "balance", sizes });
|
|
209
287
|
const messages = await reported;
|
|
210
288
|
if (isAborted(messages)) {
|
|
211
289
|
terminate();
|
|
212
|
-
return
|
|
290
|
+
return fallBack();
|
|
213
291
|
}
|
|
214
292
|
const levels = messages.flatMap((message) => (message.type === "level" ? [message] : []));
|
|
215
293
|
const added = levels.reduce((sum, message) => sum + message.added, 0);
|
|
216
294
|
statistics.generatedStates += levels.reduce((sum, message) => sum + message.generated, 0);
|
|
217
295
|
statistics.distinctStates += added;
|
|
296
|
+
levels.forEach((message) => {
|
|
297
|
+
message.outDegrees.forEach(
|
|
298
|
+
(degreeCount, degree) => (statistics.outDegrees[degree] += degreeCount),
|
|
299
|
+
);
|
|
300
|
+
tlcFingerprints.push(message.fingerprints);
|
|
301
|
+
});
|
|
218
302
|
if (added === 0) {
|
|
219
303
|
terminate();
|
|
220
|
-
return
|
|
304
|
+
return createSuccess();
|
|
221
305
|
}
|
|
222
306
|
statistics.depth++;
|
|
223
307
|
}
|