@fragno-dev/test 1.0.1 → 2.0.0
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/.turbo/turbo-build.log +31 -15
- package/CHANGELOG.md +54 -0
- package/dist/adapters.d.ts +21 -3
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +125 -31
- package/dist/adapters.js.map +1 -1
- package/dist/db-test.d.ts.map +1 -1
- package/dist/db-test.js +33 -2
- package/dist/db-test.js.map +1 -1
- package/dist/durable-hooks.d.ts +7 -0
- package/dist/durable-hooks.d.ts.map +1 -0
- package/dist/durable-hooks.js +12 -0
- package/dist/durable-hooks.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/model-checker-actors.d.ts +41 -0
- package/dist/model-checker-actors.d.ts.map +1 -0
- package/dist/model-checker-actors.js +406 -0
- package/dist/model-checker-actors.js.map +1 -0
- package/dist/model-checker-adapter.d.ts +32 -0
- package/dist/model-checker-adapter.d.ts.map +1 -0
- package/dist/model-checker-adapter.js +109 -0
- package/dist/model-checker-adapter.js.map +1 -0
- package/dist/model-checker.d.ts +128 -0
- package/dist/model-checker.d.ts.map +1 -0
- package/dist/model-checker.js +443 -0
- package/dist/model-checker.js.map +1 -0
- package/package.json +13 -12
- package/src/adapter-conformance.test.ts +322 -0
- package/src/adapters.ts +199 -36
- package/src/db-test.test.ts +2 -2
- package/src/db-test.ts +53 -3
- package/src/durable-hooks.ts +13 -0
- package/src/index.test.ts +84 -7
- package/src/index.ts +39 -4
- package/src/model-checker-actors.test.ts +78 -0
- package/src/model-checker-actors.ts +642 -0
- package/src/model-checker-adapter.ts +200 -0
- package/src/model-checker.test.ts +399 -0
- package/src/model-checker.ts +799 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
import type { AnySchema } from "@fragno-dev/db/schema";
|
|
2
|
+
import type { SimpleQueryInterface } from "@fragno-dev/db/query";
|
|
3
|
+
import type { UOWInstrumentationContext } from "@fragno-dev/db/unit-of-work";
|
|
4
|
+
import {
|
|
5
|
+
defaultStateHasher,
|
|
6
|
+
type ModelCheckerBounds,
|
|
7
|
+
type ModelCheckerHistory,
|
|
8
|
+
type ModelCheckerMode,
|
|
9
|
+
type ModelCheckerPhase,
|
|
10
|
+
type ModelCheckerRunResult,
|
|
11
|
+
type ModelCheckerScheduleResult,
|
|
12
|
+
type ModelCheckerStateHasherContext,
|
|
13
|
+
type ModelCheckerStep,
|
|
14
|
+
} from "./model-checker";
|
|
15
|
+
import { ModelCheckerAdapter, type ModelCheckerScheduler } from "./model-checker-adapter";
|
|
16
|
+
|
|
17
|
+
type DeferredPromise<T> = {
|
|
18
|
+
promise: Promise<T>;
|
|
19
|
+
resolve: (value: T) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const createDeferred = <T>(): DeferredPromise<T> => {
|
|
23
|
+
const { promise, resolve } = Promise.withResolvers<T>();
|
|
24
|
+
return { promise, resolve };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type PendingGate = {
|
|
28
|
+
uowId: number;
|
|
29
|
+
phase: ModelCheckerPhase;
|
|
30
|
+
release: () => void;
|
|
31
|
+
completion: Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type UowPlan = {
|
|
35
|
+
hasRetrieve: boolean;
|
|
36
|
+
hasMutate: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type SchedulePlan = {
|
|
40
|
+
phasesPerTransaction: ModelCheckerPhase[][];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type SchedulerMode = "record" | "schedule";
|
|
44
|
+
|
|
45
|
+
class PhaseScheduler implements ModelCheckerScheduler {
|
|
46
|
+
#mode: SchedulerMode;
|
|
47
|
+
#plan: SchedulePlan | null;
|
|
48
|
+
#uowIds = new WeakMap<object, number>();
|
|
49
|
+
#uowPlans: UowPlan[] = [];
|
|
50
|
+
#pending: PendingGate[] = [];
|
|
51
|
+
#pendingSignal = createDeferred<void>();
|
|
52
|
+
#inFlight = new Map<string, DeferredPromise<void>>();
|
|
53
|
+
#actorsDone = false;
|
|
54
|
+
|
|
55
|
+
constructor(mode: SchedulerMode, plan?: SchedulePlan) {
|
|
56
|
+
this.#mode = mode;
|
|
57
|
+
this.#plan = plan ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
markActorsDone(): void {
|
|
61
|
+
this.#actorsDone = true;
|
|
62
|
+
this.#pendingSignal.resolve();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getPlan(): SchedulePlan {
|
|
66
|
+
return {
|
|
67
|
+
phasesPerTransaction: this.#uowPlans.map((entry) => {
|
|
68
|
+
const phases: ModelCheckerPhase[] = [];
|
|
69
|
+
if (entry.hasRetrieve) {
|
|
70
|
+
phases.push("retrieve");
|
|
71
|
+
}
|
|
72
|
+
if (entry.hasMutate) {
|
|
73
|
+
phases.push("mutate");
|
|
74
|
+
}
|
|
75
|
+
return phases;
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async beforePhase(ctx: UOWInstrumentationContext, phase: ModelCheckerPhase): Promise<void> {
|
|
81
|
+
const uow = ctx.uow as object;
|
|
82
|
+
const uowId = this.#getOrCreateUowId(uow);
|
|
83
|
+
|
|
84
|
+
if (phase === "mutate") {
|
|
85
|
+
const plan = this.#uowPlans[uowId];
|
|
86
|
+
if (plan) {
|
|
87
|
+
plan.hasMutate = true;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
const plan = this.#uowPlans[uowId];
|
|
91
|
+
if (plan) {
|
|
92
|
+
plan.hasRetrieve = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this.#mode === "record") {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.#assertPlanned(uowId, phase);
|
|
101
|
+
const release = createDeferred<void>();
|
|
102
|
+
const completion = createDeferred<void>();
|
|
103
|
+
this.#inFlight.set(`${uowId}:${phase}`, completion);
|
|
104
|
+
const gate: PendingGate = {
|
|
105
|
+
uowId,
|
|
106
|
+
phase,
|
|
107
|
+
release: () => {
|
|
108
|
+
release.resolve();
|
|
109
|
+
},
|
|
110
|
+
completion: completion.promise,
|
|
111
|
+
};
|
|
112
|
+
this.#pending.push(gate);
|
|
113
|
+
this.#pendingSignal.resolve();
|
|
114
|
+
this.#pendingSignal = createDeferred<void>();
|
|
115
|
+
await release.promise;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async afterPhase(ctx: UOWInstrumentationContext, phase: ModelCheckerPhase): Promise<void> {
|
|
119
|
+
const uow = ctx.uow as object;
|
|
120
|
+
const uowId = this.#getOrCreateUowId(uow);
|
|
121
|
+
const key = `${uowId}:${phase}`;
|
|
122
|
+
const completion = this.#inFlight.get(key);
|
|
123
|
+
if (completion) {
|
|
124
|
+
completion.resolve();
|
|
125
|
+
this.#inFlight.delete(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async releaseStep(step: ModelCheckerStep, actorsPromise: Promise<void>): Promise<void> {
|
|
130
|
+
for (;;) {
|
|
131
|
+
const matchIndex = this.#pending.findIndex(
|
|
132
|
+
(gate) => gate.uowId === step.txId && gate.phase === step.phase,
|
|
133
|
+
);
|
|
134
|
+
if (matchIndex !== -1) {
|
|
135
|
+
const [gate] = this.#pending.splice(matchIndex, 1);
|
|
136
|
+
gate.release();
|
|
137
|
+
await gate.completion;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.#pending.length > 0) {
|
|
142
|
+
const pendingSummary = this.#pending
|
|
143
|
+
.map((gate) => `${gate.uowId}:${gate.phase}`)
|
|
144
|
+
.join(", ");
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Model checker schedule mismatch. Expected ${step.txId}:${step.phase}, pending [${pendingSummary}]`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await Promise.race([this.#pendingSignal.promise, actorsPromise]);
|
|
151
|
+
if (this.#actorsDone) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Model checker schedule expects ${step.phase} for UOW ${step.txId}, but no gate appeared.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async releaseAny(
|
|
160
|
+
pickIndex: (pending: PendingGate[]) => number,
|
|
161
|
+
actorsPromise: Promise<void>,
|
|
162
|
+
): Promise<ModelCheckerStep | null> {
|
|
163
|
+
for (;;) {
|
|
164
|
+
if (this.#pending.length > 0) {
|
|
165
|
+
const index = Math.max(0, Math.min(pickIndex(this.#pending), this.#pending.length - 1));
|
|
166
|
+
const [gate] = this.#pending.splice(index, 1);
|
|
167
|
+
gate.release();
|
|
168
|
+
await gate.completion;
|
|
169
|
+
return { txId: gate.uowId, phase: gate.phase };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await Promise.race([this.#pendingSignal.promise, actorsPromise]);
|
|
173
|
+
if (this.#actorsDone) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#getOrCreateUowId(uow: object): number {
|
|
180
|
+
const existing = this.#uowIds.get(uow);
|
|
181
|
+
if (existing !== undefined) {
|
|
182
|
+
return existing;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const next = this.#uowPlans.length;
|
|
186
|
+
if (this.#plan && next >= this.#plan.phasesPerTransaction.length) {
|
|
187
|
+
throw new Error("Model checker saw more UOWs than planned.");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.#uowIds.set(uow, next);
|
|
191
|
+
this.#uowPlans.push({ hasRetrieve: false, hasMutate: false });
|
|
192
|
+
return next;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#assertPlanned(uowId: number, phase: ModelCheckerPhase): void {
|
|
196
|
+
const plan = this.#plan;
|
|
197
|
+
if (!plan) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const phases = plan.phasesPerTransaction[uowId];
|
|
202
|
+
if (!phases || phases.length === 0) {
|
|
203
|
+
throw new Error(`Model checker saw unexpected UOW ${uowId}.`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!phases.includes(phase)) {
|
|
207
|
+
throw new Error(`Model checker saw unexpected ${phase} phase for UOW ${uowId}.`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const mulberry32 = (seed: number) => {
|
|
213
|
+
let t = seed >>> 0;
|
|
214
|
+
return () => {
|
|
215
|
+
t += 1831565813;
|
|
216
|
+
let r = Math.imul(t ^ (t >>> 15), t | 1);
|
|
217
|
+
r ^= r + Math.imul(r ^ (r >>> 7), r | 61);
|
|
218
|
+
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const generateAllSchedules = (
|
|
223
|
+
phasesPerTransaction: ModelCheckerPhase[][],
|
|
224
|
+
): ModelCheckerStep[][] => {
|
|
225
|
+
const schedules: ModelCheckerStep[][] = [];
|
|
226
|
+
const totalSteps = phasesPerTransaction.reduce((sum, phases) => sum + phases.length, 0);
|
|
227
|
+
const progress = phasesPerTransaction.map(() => 0);
|
|
228
|
+
const current: ModelCheckerStep[] = [];
|
|
229
|
+
|
|
230
|
+
const walk = () => {
|
|
231
|
+
if (current.length === totalSteps) {
|
|
232
|
+
schedules.push([...current]);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (let txId = 0; txId < phasesPerTransaction.length; txId += 1) {
|
|
237
|
+
if (progress[txId] >= phasesPerTransaction[txId].length) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const phase = phasesPerTransaction[txId][progress[txId]];
|
|
241
|
+
if (!phase) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
current.push({ txId, phase });
|
|
245
|
+
progress[txId] += 1;
|
|
246
|
+
walk();
|
|
247
|
+
progress[txId] -= 1;
|
|
248
|
+
current.pop();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
walk();
|
|
253
|
+
return schedules;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
type HistoryTracker = {
|
|
257
|
+
add: (key: string) => boolean;
|
|
258
|
+
size: () => number;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const createHistoryTracker = (history: ModelCheckerHistory): HistoryTracker | null => {
|
|
262
|
+
if (!history) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (history.type === "unbounded") {
|
|
267
|
+
const set = new Set<string>();
|
|
268
|
+
return {
|
|
269
|
+
add: (key: string) => {
|
|
270
|
+
const has = set.has(key);
|
|
271
|
+
if (!has) {
|
|
272
|
+
set.add(key);
|
|
273
|
+
}
|
|
274
|
+
return !has;
|
|
275
|
+
},
|
|
276
|
+
size: () => set.size,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const maxEntries = Math.max(history.maxEntries, 1);
|
|
281
|
+
const map = new Map<string, true>();
|
|
282
|
+
return {
|
|
283
|
+
add: (key: string) => {
|
|
284
|
+
const existed = map.delete(key);
|
|
285
|
+
map.set(key, true);
|
|
286
|
+
if (map.size > maxEntries) {
|
|
287
|
+
const first = map.keys().next().value;
|
|
288
|
+
if (first) {
|
|
289
|
+
map.delete(first);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return !existed;
|
|
293
|
+
},
|
|
294
|
+
size: () => map.size,
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const serializeSchedulePrefix = (schedule: ModelCheckerStep[]): string =>
|
|
299
|
+
schedule.map((step) => `${step.txId}:${step.phase}`).join("|");
|
|
300
|
+
|
|
301
|
+
const buildPathKey = (stateHash: string, prefix: ModelCheckerStep[]): string =>
|
|
302
|
+
`${stateHash}::${serializeSchedulePrefix(prefix)}`;
|
|
303
|
+
|
|
304
|
+
export type ModelCheckerActor<TContext> = {
|
|
305
|
+
name?: string;
|
|
306
|
+
run: (ctx: TContext) => Promise<void> | void;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export type ModelCheckerInvariantContext<TSchema extends AnySchema, TUowConfig, TContext> = {
|
|
310
|
+
schema: TSchema;
|
|
311
|
+
queryEngine: SimpleQueryInterface<TSchema, TUowConfig>;
|
|
312
|
+
context: TContext;
|
|
313
|
+
schedule: ModelCheckerStep[];
|
|
314
|
+
stepIndex: number;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export type ModelCheckerInvariant<TSchema extends AnySchema, TUowConfig, TContext> = (
|
|
318
|
+
ctx: ModelCheckerInvariantContext<TSchema, TUowConfig, TContext>,
|
|
319
|
+
) => Promise<void> | void;
|
|
320
|
+
|
|
321
|
+
export type ModelCheckerActorsConfig<TSchema extends AnySchema, TUowConfig, TContext> = {
|
|
322
|
+
schema: TSchema;
|
|
323
|
+
mode?: ModelCheckerMode;
|
|
324
|
+
seed?: number;
|
|
325
|
+
maxSchedules?: number;
|
|
326
|
+
bounds?: ModelCheckerBounds;
|
|
327
|
+
history?: ModelCheckerHistory;
|
|
328
|
+
stopWhenFrontierExhausted?: boolean;
|
|
329
|
+
stateHasher?: (ctx: ModelCheckerStateHasherContext<TSchema, TUowConfig>) => Promise<string>;
|
|
330
|
+
createContext: () => Promise<{
|
|
331
|
+
adapter: ModelCheckerAdapter<TUowConfig>;
|
|
332
|
+
ctx: TContext;
|
|
333
|
+
queryEngine: SimpleQueryInterface<TSchema, TUowConfig>;
|
|
334
|
+
cleanup?: () => Promise<void>;
|
|
335
|
+
}>;
|
|
336
|
+
setup?: (ctx: TContext) => Promise<void>;
|
|
337
|
+
buildActors: (ctx: TContext) => ModelCheckerActor<TContext>[];
|
|
338
|
+
invariants?: ModelCheckerInvariant<TSchema, TUowConfig, TContext>[];
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const defaultHistoryConfig: ModelCheckerHistory = { type: "lru", maxEntries: 5000 };
|
|
342
|
+
|
|
343
|
+
const planTransactions = (phasesPerTransaction: ModelCheckerPhase[][]): SchedulePlan => {
|
|
344
|
+
if (phasesPerTransaction.length === 0) {
|
|
345
|
+
throw new Error("Model checker requires at least one transaction.");
|
|
346
|
+
}
|
|
347
|
+
return { phasesPerTransaction };
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const recordPlan = async <TSchema extends AnySchema, TUowConfig, TContext>(
|
|
351
|
+
config: ModelCheckerActorsConfig<TSchema, TUowConfig, TContext>,
|
|
352
|
+
): Promise<SchedulePlan> => {
|
|
353
|
+
const { adapter, ctx, cleanup } = await config.createContext();
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
if (config.setup) {
|
|
357
|
+
await config.setup(ctx);
|
|
358
|
+
}
|
|
359
|
+
const scheduler = new PhaseScheduler("record");
|
|
360
|
+
adapter.setScheduler(scheduler);
|
|
361
|
+
const actors = config.buildActors(ctx);
|
|
362
|
+
await Promise.all(actors.map((actor) => actor.run(ctx)));
|
|
363
|
+
return planTransactions(scheduler.getPlan().phasesPerTransaction);
|
|
364
|
+
} finally {
|
|
365
|
+
adapter.setScheduler(null);
|
|
366
|
+
if (cleanup) {
|
|
367
|
+
await cleanup();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const executeSchedule = async <TSchema extends AnySchema, TUowConfig, TContext>(
|
|
373
|
+
schedule: ModelCheckerStep[],
|
|
374
|
+
config: ModelCheckerActorsConfig<TSchema, TUowConfig, TContext>,
|
|
375
|
+
plan: SchedulePlan,
|
|
376
|
+
history: HistoryTracker | null,
|
|
377
|
+
stateHasher: (ctx: ModelCheckerStateHasherContext<TSchema, TUowConfig>) => Promise<string>,
|
|
378
|
+
): Promise<{ stateHash: string; newPathsAdded: boolean }> => {
|
|
379
|
+
const { adapter, ctx, queryEngine, cleanup } = await config.createContext();
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
if (config.setup) {
|
|
383
|
+
await config.setup(ctx);
|
|
384
|
+
}
|
|
385
|
+
const scheduler = new PhaseScheduler("schedule", plan);
|
|
386
|
+
adapter.setScheduler(scheduler);
|
|
387
|
+
|
|
388
|
+
const actors = config.buildActors(ctx);
|
|
389
|
+
const actorsPromise = Promise.all(actors.map((actor) => actor.run(ctx))).then(() => {
|
|
390
|
+
scheduler.markActorsDone();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
let lastStateHash: string | null = null;
|
|
394
|
+
let newPathsAdded = false;
|
|
395
|
+
|
|
396
|
+
for (let stepIndex = 0; stepIndex < schedule.length; stepIndex += 1) {
|
|
397
|
+
const step = schedule[stepIndex];
|
|
398
|
+
if (!step) {
|
|
399
|
+
throw new Error("Schedule step is missing.");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
await scheduler.releaseStep(step, actorsPromise);
|
|
403
|
+
|
|
404
|
+
if (config.invariants) {
|
|
405
|
+
for (const invariant of config.invariants) {
|
|
406
|
+
await invariant({
|
|
407
|
+
schema: config.schema,
|
|
408
|
+
queryEngine,
|
|
409
|
+
context: ctx,
|
|
410
|
+
schedule,
|
|
411
|
+
stepIndex,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (history) {
|
|
417
|
+
lastStateHash = await stateHasher({
|
|
418
|
+
schema: config.schema,
|
|
419
|
+
queryEngine,
|
|
420
|
+
});
|
|
421
|
+
const prefix = schedule.slice(0, stepIndex + 1);
|
|
422
|
+
const added = history.add(buildPathKey(lastStateHash, prefix));
|
|
423
|
+
if (added) {
|
|
424
|
+
newPathsAdded = true;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
await actorsPromise;
|
|
430
|
+
|
|
431
|
+
if (!lastStateHash) {
|
|
432
|
+
lastStateHash = await stateHasher({ schema: config.schema, queryEngine });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return { stateHash: lastStateHash, newPathsAdded };
|
|
436
|
+
} finally {
|
|
437
|
+
adapter.setScheduler(null);
|
|
438
|
+
if (cleanup) {
|
|
439
|
+
await cleanup();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const executeScheduleDynamic = async <TSchema extends AnySchema, TUowConfig, TContext>(
|
|
445
|
+
config: ModelCheckerActorsConfig<TSchema, TUowConfig, TContext>,
|
|
446
|
+
history: HistoryTracker | null,
|
|
447
|
+
stateHasher: (ctx: ModelCheckerStateHasherContext<TSchema, TUowConfig>) => Promise<string>,
|
|
448
|
+
pickIndex: (pending: PendingGate[]) => number,
|
|
449
|
+
): Promise<{ schedule: ModelCheckerStep[]; stateHash: string; newPathsAdded: boolean }> => {
|
|
450
|
+
const { adapter, ctx, queryEngine, cleanup } = await config.createContext();
|
|
451
|
+
|
|
452
|
+
try {
|
|
453
|
+
if (config.setup) {
|
|
454
|
+
await config.setup(ctx);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const scheduler = new PhaseScheduler("schedule");
|
|
458
|
+
adapter.setScheduler(scheduler);
|
|
459
|
+
|
|
460
|
+
const actors = config.buildActors(ctx);
|
|
461
|
+
const actorsPromise = Promise.all(actors.map((actor) => actor.run(ctx))).then(() => {
|
|
462
|
+
scheduler.markActorsDone();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const schedule: ModelCheckerStep[] = [];
|
|
466
|
+
let lastStateHash: string | null = null;
|
|
467
|
+
let newPathsAdded = false;
|
|
468
|
+
|
|
469
|
+
for (;;) {
|
|
470
|
+
const step = await scheduler.releaseAny(pickIndex, actorsPromise);
|
|
471
|
+
if (!step) {
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
schedule.push(step);
|
|
476
|
+
const stepIndex = schedule.length - 1;
|
|
477
|
+
|
|
478
|
+
if (config.invariants) {
|
|
479
|
+
for (const invariant of config.invariants) {
|
|
480
|
+
await invariant({
|
|
481
|
+
schema: config.schema,
|
|
482
|
+
queryEngine,
|
|
483
|
+
context: ctx,
|
|
484
|
+
schedule,
|
|
485
|
+
stepIndex,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (history) {
|
|
491
|
+
lastStateHash = await stateHasher({
|
|
492
|
+
schema: config.schema,
|
|
493
|
+
queryEngine,
|
|
494
|
+
});
|
|
495
|
+
const prefix = schedule.slice(0, stepIndex + 1);
|
|
496
|
+
const added = history.add(buildPathKey(lastStateHash, prefix));
|
|
497
|
+
if (added) {
|
|
498
|
+
newPathsAdded = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
await actorsPromise;
|
|
504
|
+
|
|
505
|
+
if (!lastStateHash) {
|
|
506
|
+
lastStateHash = await stateHasher({ schema: config.schema, queryEngine });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return { schedule, stateHash: lastStateHash, newPathsAdded };
|
|
510
|
+
} finally {
|
|
511
|
+
adapter.setScheduler(null);
|
|
512
|
+
if (cleanup) {
|
|
513
|
+
await cleanup();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
const executeScheduleDynamicWithChoices = async <TSchema extends AnySchema, TUowConfig, TContext>(
|
|
519
|
+
config: ModelCheckerActorsConfig<TSchema, TUowConfig, TContext>,
|
|
520
|
+
history: HistoryTracker | null,
|
|
521
|
+
stateHasher: (ctx: ModelCheckerStateHasherContext<TSchema, TUowConfig>) => Promise<string>,
|
|
522
|
+
choices: number[],
|
|
523
|
+
): Promise<{
|
|
524
|
+
schedule: ModelCheckerStep[];
|
|
525
|
+
stateHash: string;
|
|
526
|
+
newPathsAdded: boolean;
|
|
527
|
+
choicePoints: number[];
|
|
528
|
+
}> => {
|
|
529
|
+
const choicePoints: number[] = [];
|
|
530
|
+
const result = await executeScheduleDynamic(config, history, stateHasher, (pending) => {
|
|
531
|
+
const stepIndex = choicePoints.length;
|
|
532
|
+
choicePoints.push(pending.length);
|
|
533
|
+
const choice = choices[stepIndex];
|
|
534
|
+
if (choice === undefined || choice < 0 || choice >= pending.length) {
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
return choice;
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
...result,
|
|
542
|
+
choicePoints,
|
|
543
|
+
};
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const serializeChoices = (choices: number[]): string => choices.join(",");
|
|
547
|
+
|
|
548
|
+
export const runModelCheckerWithActors = async <TSchema extends AnySchema, TUowConfig, TContext>(
|
|
549
|
+
config: ModelCheckerActorsConfig<TSchema, TUowConfig, TContext>,
|
|
550
|
+
): Promise<ModelCheckerRunResult> => {
|
|
551
|
+
const mode = config.mode ?? "exhaustive";
|
|
552
|
+
const historyConfig = config.history ?? defaultHistoryConfig;
|
|
553
|
+
const history = createHistoryTracker(historyConfig);
|
|
554
|
+
const stateHasher = config.stateHasher ?? defaultStateHasher;
|
|
555
|
+
const schedules: ModelCheckerScheduleResult[] = [];
|
|
556
|
+
const maxSchedules = config.maxSchedules;
|
|
557
|
+
const seed = config.seed ?? 1;
|
|
558
|
+
|
|
559
|
+
if (mode === "exhaustive") {
|
|
560
|
+
const plan = await recordPlan(config);
|
|
561
|
+
const allSchedules = generateAllSchedules(plan.phasesPerTransaction);
|
|
562
|
+
for (const schedule of allSchedules) {
|
|
563
|
+
if (maxSchedules !== undefined && schedules.length >= maxSchedules) {
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
const result = await executeSchedule(schedule, config, plan, history, stateHasher);
|
|
567
|
+
schedules.push({ schedule, stateHash: result.stateHash });
|
|
568
|
+
}
|
|
569
|
+
} else if (mode === "bounded-exhaustive") {
|
|
570
|
+
const maxSteps = config.bounds?.maxSteps;
|
|
571
|
+
if (!maxSteps || maxSteps < 1) {
|
|
572
|
+
throw new Error("bounded-exhaustive mode requires bounds.maxSteps >= 1.");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const stack: number[][] = [[]];
|
|
576
|
+
const seen = new Set<string>();
|
|
577
|
+
seen.add("");
|
|
578
|
+
|
|
579
|
+
while (stack.length > 0) {
|
|
580
|
+
if (maxSchedules !== undefined && schedules.length >= maxSchedules) {
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const choices = stack.pop();
|
|
585
|
+
if (!choices) {
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const result = await executeScheduleDynamicWithChoices(config, history, stateHasher, choices);
|
|
590
|
+
schedules.push({ schedule: result.schedule, stateHash: result.stateHash });
|
|
591
|
+
|
|
592
|
+
const limit = Math.min(result.choicePoints.length, maxSteps);
|
|
593
|
+
for (let stepIndex = 0; stepIndex < limit; stepIndex += 1) {
|
|
594
|
+
const count = result.choicePoints[stepIndex];
|
|
595
|
+
const current = choices[stepIndex] ?? 0;
|
|
596
|
+
for (let alt = current + 1; alt < count; alt += 1) {
|
|
597
|
+
const next = choices.slice(0, stepIndex);
|
|
598
|
+
next[stepIndex] = alt;
|
|
599
|
+
const key = serializeChoices(next);
|
|
600
|
+
if (!seen.has(key)) {
|
|
601
|
+
seen.add(key);
|
|
602
|
+
stack.push(next);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} else if (mode === "random") {
|
|
608
|
+
const rng = mulberry32(seed);
|
|
609
|
+
const total = maxSchedules ?? 1;
|
|
610
|
+
for (let i = 0; i < total; i += 1) {
|
|
611
|
+
const result = await executeScheduleDynamic(config, history, stateHasher, (pending) =>
|
|
612
|
+
Math.floor(rng() * pending.length),
|
|
613
|
+
);
|
|
614
|
+
schedules.push({ schedule: result.schedule, stateHash: result.stateHash });
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
const rng = mulberry32(seed);
|
|
618
|
+
const stopWhenFrontierExhausted = config.stopWhenFrontierExhausted ?? false;
|
|
619
|
+
let iterations = 0;
|
|
620
|
+
let frontierExhausted = false;
|
|
621
|
+
|
|
622
|
+
while (!frontierExhausted) {
|
|
623
|
+
if (maxSchedules !== undefined && iterations >= maxSchedules) {
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
const result = await executeScheduleDynamic(config, history, stateHasher, (pending) =>
|
|
627
|
+
Math.floor(rng() * pending.length),
|
|
628
|
+
);
|
|
629
|
+
schedules.push({ schedule: result.schedule, stateHash: result.stateHash });
|
|
630
|
+
iterations += 1;
|
|
631
|
+
|
|
632
|
+
if (stopWhenFrontierExhausted && history) {
|
|
633
|
+
frontierExhausted = !result.newPathsAdded;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
schedules,
|
|
640
|
+
visitedPaths: history?.size() ?? 0,
|
|
641
|
+
};
|
|
642
|
+
};
|