@fairfox/polly 0.16.0 → 0.18.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/dist/tools/verify/src/cli.js +1061 -32
- package/dist/tools/verify/src/cli.js.map +12 -8
- package/dist/tools/verify/src/config.d.ts +6 -1
- package/dist/tools/verify/src/config.js +3 -1
- package/dist/tools/verify/src/config.js.map +4 -4
- package/dist/tools/verify/src/primitives/index.d.ts +19 -0
- package/dist/tools/visualize/src/cli.js +446 -33
- package/dist/tools/visualize/src/cli.js.map +7 -7
- package/package.json +1 -1
|
@@ -13,6 +13,48 @@ var __export = (target, all) => {
|
|
|
13
13
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
14
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
15
15
|
|
|
16
|
+
// tools/verify/src/analysis/non-interference.ts
|
|
17
|
+
var exports_non_interference = {};
|
|
18
|
+
__export(exports_non_interference, {
|
|
19
|
+
checkNonInterference: () => checkNonInterference
|
|
20
|
+
});
|
|
21
|
+
function checkNonInterference(subsystems, handlers) {
|
|
22
|
+
const violations = [];
|
|
23
|
+
const fieldOwner = new Map;
|
|
24
|
+
for (const [name, sub] of Object.entries(subsystems)) {
|
|
25
|
+
for (const field of sub.state) {
|
|
26
|
+
fieldOwner.set(field, name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const handlerSubsystem = new Map;
|
|
30
|
+
for (const [name, sub] of Object.entries(subsystems)) {
|
|
31
|
+
for (const h of sub.handlers) {
|
|
32
|
+
handlerSubsystem.set(h, name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const handler of handlers) {
|
|
36
|
+
const subsystemName = handlerSubsystem.get(handler.messageType);
|
|
37
|
+
if (!subsystemName)
|
|
38
|
+
continue;
|
|
39
|
+
for (const assignment of handler.assignments) {
|
|
40
|
+
const fieldName = assignment.field;
|
|
41
|
+
const owner = fieldOwner.get(fieldName);
|
|
42
|
+
if (owner && owner !== subsystemName) {
|
|
43
|
+
violations.push({
|
|
44
|
+
handler: handler.messageType,
|
|
45
|
+
subsystem: subsystemName,
|
|
46
|
+
writesTo: fieldName,
|
|
47
|
+
ownedBy: owner
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
valid: violations.length === 0,
|
|
54
|
+
violations
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
16
58
|
// tools/verify/src/codegen/invariants.ts
|
|
17
59
|
import { Node as Node3, Project as Project3 } from "ts-morph";
|
|
18
60
|
|
|
@@ -341,6 +383,7 @@ class TemporalTLAGenerator {
|
|
|
341
383
|
var exports_tla = {};
|
|
342
384
|
__export(exports_tla, {
|
|
343
385
|
generateTLA: () => generateTLA,
|
|
386
|
+
generateSubsystemTLA: () => generateSubsystemTLA,
|
|
344
387
|
TLAValidationError: () => TLAValidationError,
|
|
345
388
|
TLAGenerator: () => TLAGenerator
|
|
346
389
|
});
|
|
@@ -351,6 +394,49 @@ async function generateTLA(config, analysis) {
|
|
|
351
394
|
const generator = new TLAGenerator;
|
|
352
395
|
return await generator.generate(config, analysis);
|
|
353
396
|
}
|
|
397
|
+
async function generateSubsystemTLA(_subsystemName, subsystem, config, analysis) {
|
|
398
|
+
const stateFields = new Set(subsystem.state);
|
|
399
|
+
const handlerNames = new Set(subsystem.handlers);
|
|
400
|
+
const filteredState = {};
|
|
401
|
+
for (const [field, fieldConfig] of Object.entries(config.state)) {
|
|
402
|
+
if (stateFields.has(field)) {
|
|
403
|
+
filteredState[field] = fieldConfig;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const filteredMessages = {
|
|
407
|
+
...config.messages,
|
|
408
|
+
include: subsystem.handlers
|
|
409
|
+
};
|
|
410
|
+
filteredMessages.exclude = undefined;
|
|
411
|
+
if (filteredMessages.perMessageBounds) {
|
|
412
|
+
const filteredBounds = {};
|
|
413
|
+
for (const [msg, bound] of Object.entries(filteredMessages.perMessageBounds)) {
|
|
414
|
+
if (handlerNames.has(msg)) {
|
|
415
|
+
filteredBounds[msg] = bound;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
filteredMessages.perMessageBounds = filteredBounds;
|
|
419
|
+
}
|
|
420
|
+
const filteredConfig = {
|
|
421
|
+
...config,
|
|
422
|
+
state: filteredState,
|
|
423
|
+
messages: filteredMessages,
|
|
424
|
+
subsystems: undefined
|
|
425
|
+
};
|
|
426
|
+
if (filteredConfig.tier2?.temporalConstraints) {
|
|
427
|
+
filteredConfig.tier2 = {
|
|
428
|
+
...filteredConfig.tier2,
|
|
429
|
+
temporalConstraints: filteredConfig.tier2.temporalConstraints.filter((tc) => handlerNames.has(tc.before) && handlerNames.has(tc.after))
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const filteredAnalysis = {
|
|
433
|
+
...analysis,
|
|
434
|
+
messageTypes: analysis.messageTypes.filter((mt) => handlerNames.has(mt)),
|
|
435
|
+
handlers: analysis.handlers.filter((h) => handlerNames.has(h.messageType))
|
|
436
|
+
};
|
|
437
|
+
const generator = new TLAGenerator;
|
|
438
|
+
return await generator.generate(filteredConfig, filteredAnalysis);
|
|
439
|
+
}
|
|
354
440
|
var TLAValidationError, TLAGenerator;
|
|
355
441
|
var init_tla = __esm(() => {
|
|
356
442
|
init_invariants();
|
|
@@ -1832,7 +1918,7 @@ var init_tla = __esm(() => {
|
|
|
1832
1918
|
this.addTemporalConstraints(config.tier2.temporalConstraints);
|
|
1833
1919
|
}
|
|
1834
1920
|
this.line("\\* State constraint to bound state space");
|
|
1835
|
-
this.addStateConstraint(config);
|
|
1921
|
+
this.addStateConstraint(config, _analysis);
|
|
1836
1922
|
this.line("=============================================================================");
|
|
1837
1923
|
}
|
|
1838
1924
|
addTemporalConstraints(constraints) {
|
|
@@ -1862,10 +1948,11 @@ var init_tla = __esm(() => {
|
|
|
1862
1948
|
});
|
|
1863
1949
|
}
|
|
1864
1950
|
}
|
|
1865
|
-
addStateConstraint(config) {
|
|
1951
|
+
addStateConstraint(config, analysis) {
|
|
1866
1952
|
const hasPerMessageBounds = config.messages.perMessageBounds && Object.keys(config.messages.perMessageBounds).length > 0;
|
|
1867
1953
|
const hasBoundedExploration = config.tier2?.boundedExploration?.maxDepth !== undefined;
|
|
1868
|
-
const
|
|
1954
|
+
const hasUserConstraints = (analysis.globalStateConstraints?.length ?? 0) > 0;
|
|
1955
|
+
const needsConjunction = hasPerMessageBounds || hasBoundedExploration || hasUserConstraints;
|
|
1869
1956
|
this.line("StateConstraint ==");
|
|
1870
1957
|
this.indent++;
|
|
1871
1958
|
if (needsConjunction) {
|
|
@@ -1879,6 +1966,13 @@ var init_tla = __esm(() => {
|
|
|
1879
1966
|
if (hasBoundedExploration && config.tier2?.boundedExploration?.maxDepth) {
|
|
1880
1967
|
this.line(`/\\ TLCGet("level") <= ${config.tier2.boundedExploration.maxDepth} \\* Tier 2: Bounded exploration`);
|
|
1881
1968
|
}
|
|
1969
|
+
if (hasUserConstraints && analysis.globalStateConstraints) {
|
|
1970
|
+
for (const constraint of analysis.globalStateConstraints) {
|
|
1971
|
+
const tlaExpr = this.tsExpressionToTLA(constraint.expression, false);
|
|
1972
|
+
this.line(`\\* ${constraint.name}${constraint.message ? `: ${constraint.message}` : ""}`);
|
|
1973
|
+
this.line(`/\\ \\A ctx \\in Contexts : (${tlaExpr})`);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1882
1976
|
} else {
|
|
1883
1977
|
this.line("Len(messages) <= MaxMessages");
|
|
1884
1978
|
}
|
|
@@ -1887,6 +1981,9 @@ var init_tla = __esm(() => {
|
|
|
1887
1981
|
if (hasBoundedExploration && config.tier2?.boundedExploration?.maxDepth) {
|
|
1888
1982
|
console.log(`[INFO] [TLAGenerator] Tier 2: Bounded exploration with maxDepth = ${config.tier2.boundedExploration.maxDepth}`);
|
|
1889
1983
|
}
|
|
1984
|
+
if (hasUserConstraints && analysis.globalStateConstraints) {
|
|
1985
|
+
console.log(`[INFO] [TLAGenerator] ${analysis.globalStateConstraints.length} user-defined state constraint(s) added to CONSTRAINT clause`);
|
|
1986
|
+
}
|
|
1890
1987
|
}
|
|
1891
1988
|
addDeliveredTracking() {
|
|
1892
1989
|
this.line("");
|
|
@@ -2366,6 +2463,400 @@ var init_docker = () => {};
|
|
|
2366
2463
|
import * as fs4 from "node:fs";
|
|
2367
2464
|
import * as path4 from "node:path";
|
|
2368
2465
|
|
|
2466
|
+
// tools/verify/src/analysis/expression-validator.ts
|
|
2467
|
+
var SIGNAL_VALUE_FIELD = /([a-zA-Z_]\w*)\.value\.([a-zA-Z_][\w.]*)/g;
|
|
2468
|
+
var SIGNAL_VALUE_BARE = /([a-zA-Z_]\w*)\.value\b(?!\.)/g;
|
|
2469
|
+
var STATE_DOT_FIELD = /state\.([a-zA-Z_][\w.]*)/g;
|
|
2470
|
+
var PAYLOAD_REF = /payload\.\w+/;
|
|
2471
|
+
function extractFieldRefs(expression) {
|
|
2472
|
+
const refs = [];
|
|
2473
|
+
for (const m of expression.matchAll(SIGNAL_VALUE_FIELD)) {
|
|
2474
|
+
const prefix = m[1];
|
|
2475
|
+
const suffix = m[2];
|
|
2476
|
+
refs.push(`${prefix}.${suffix}`);
|
|
2477
|
+
}
|
|
2478
|
+
for (const m of expression.matchAll(SIGNAL_VALUE_BARE)) {
|
|
2479
|
+
refs.push(m[1]);
|
|
2480
|
+
}
|
|
2481
|
+
for (const m of expression.matchAll(STATE_DOT_FIELD)) {
|
|
2482
|
+
refs.push(m[1]);
|
|
2483
|
+
}
|
|
2484
|
+
return [...new Set(refs)];
|
|
2485
|
+
}
|
|
2486
|
+
function fieldInConfig(fieldRef, configKeys, stateConfig) {
|
|
2487
|
+
if (configKeys.has(fieldRef))
|
|
2488
|
+
return true;
|
|
2489
|
+
const underscored = fieldRef.replace(/\./g, "_");
|
|
2490
|
+
if (configKeys.has(underscored))
|
|
2491
|
+
return true;
|
|
2492
|
+
const dotted = fieldRef.replace(/_/g, ".");
|
|
2493
|
+
if (configKeys.has(dotted))
|
|
2494
|
+
return true;
|
|
2495
|
+
if (stateConfig && fieldRef.endsWith(".length")) {
|
|
2496
|
+
const parent = fieldRef.slice(0, -".length".length);
|
|
2497
|
+
const parentEntry = resolveConfigEntry(parent, stateConfig);
|
|
2498
|
+
if (parentEntry !== undefined && isArrayLike(parentEntry))
|
|
2499
|
+
return true;
|
|
2500
|
+
}
|
|
2501
|
+
return false;
|
|
2502
|
+
}
|
|
2503
|
+
function resolveConfigEntry(fieldRef, stateConfig) {
|
|
2504
|
+
if (fieldRef in stateConfig)
|
|
2505
|
+
return stateConfig[fieldRef];
|
|
2506
|
+
const underscored = fieldRef.replace(/\./g, "_");
|
|
2507
|
+
if (underscored in stateConfig)
|
|
2508
|
+
return stateConfig[underscored];
|
|
2509
|
+
const dotted = fieldRef.replace(/_/g, ".");
|
|
2510
|
+
if (dotted in stateConfig)
|
|
2511
|
+
return stateConfig[dotted];
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
function isArrayLike(configEntry) {
|
|
2515
|
+
if (configEntry && typeof configEntry === "object") {
|
|
2516
|
+
const obj = configEntry;
|
|
2517
|
+
if (obj.type === "array")
|
|
2518
|
+
return true;
|
|
2519
|
+
if ("maxLength" in obj)
|
|
2520
|
+
return true;
|
|
2521
|
+
if ("maxSize" in obj)
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2524
|
+
return false;
|
|
2525
|
+
}
|
|
2526
|
+
function isNullable(configEntry) {
|
|
2527
|
+
if (Array.isArray(configEntry)) {
|
|
2528
|
+
return configEntry.includes(null);
|
|
2529
|
+
}
|
|
2530
|
+
if (configEntry && typeof configEntry === "object") {
|
|
2531
|
+
const obj = configEntry;
|
|
2532
|
+
if (obj.abstract === true)
|
|
2533
|
+
return true;
|
|
2534
|
+
if (Array.isArray(obj.values)) {
|
|
2535
|
+
return obj.values.includes(null);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
return false;
|
|
2539
|
+
}
|
|
2540
|
+
var UNSUPPORTED_METHODS = /\.(some|every|find|filter)\s*\(/;
|
|
2541
|
+
var OPTIONAL_CHAIN = /\?\./;
|
|
2542
|
+
var NULL_COMPARISON = /(===?\s*null|!==?\s*null|null\s*===?|null\s*!==?)/;
|
|
2543
|
+
function checkUnmodeledFields(expression, configKeys, stateConfig, messageType, conditionType, location) {
|
|
2544
|
+
const warnings = [];
|
|
2545
|
+
const refs = extractFieldRefs(expression);
|
|
2546
|
+
for (const ref of refs) {
|
|
2547
|
+
if (!fieldInConfig(ref, configKeys, stateConfig)) {
|
|
2548
|
+
warnings.push({
|
|
2549
|
+
kind: "unmodeled_field",
|
|
2550
|
+
message: `${conditionType}() in ${messageType} references unmodeled field '${ref}'`,
|
|
2551
|
+
messageType,
|
|
2552
|
+
conditionType,
|
|
2553
|
+
expression,
|
|
2554
|
+
location,
|
|
2555
|
+
suggestion: `Add '${ref}' to state config or remove from ${conditionType}()`
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return warnings;
|
|
2560
|
+
}
|
|
2561
|
+
function checkUnsupportedMethods(expression, configKeys, stateConfig, messageType, conditionType, location) {
|
|
2562
|
+
const match = expression.match(UNSUPPORTED_METHODS);
|
|
2563
|
+
if (!match)
|
|
2564
|
+
return [];
|
|
2565
|
+
const refs = extractFieldRefs(expression);
|
|
2566
|
+
const allUnmodeled = refs.length > 0 && refs.every((r) => !fieldInConfig(r, configKeys, stateConfig));
|
|
2567
|
+
if (allUnmodeled)
|
|
2568
|
+
return [];
|
|
2569
|
+
return [
|
|
2570
|
+
{
|
|
2571
|
+
kind: "unsupported_method",
|
|
2572
|
+
message: `${conditionType}() in ${messageType} uses .${match[1]}() which cannot be translated to TLA+`,
|
|
2573
|
+
messageType,
|
|
2574
|
+
conditionType,
|
|
2575
|
+
expression,
|
|
2576
|
+
location,
|
|
2577
|
+
suggestion: `Rewrite without .${match[1]}() or model the check as a boolean state field`
|
|
2578
|
+
}
|
|
2579
|
+
];
|
|
2580
|
+
}
|
|
2581
|
+
function checkOptionalChaining(expression, messageType, conditionType, location) {
|
|
2582
|
+
if (!OPTIONAL_CHAIN.test(expression))
|
|
2583
|
+
return [];
|
|
2584
|
+
return [
|
|
2585
|
+
{
|
|
2586
|
+
kind: "optional_chaining",
|
|
2587
|
+
message: `${conditionType}() in ${messageType} uses optional chaining (?.)`,
|
|
2588
|
+
messageType,
|
|
2589
|
+
conditionType,
|
|
2590
|
+
expression,
|
|
2591
|
+
location,
|
|
2592
|
+
suggestion: "Optional chaining translation is fragile — consider explicit null checks or restructuring"
|
|
2593
|
+
}
|
|
2594
|
+
];
|
|
2595
|
+
}
|
|
2596
|
+
function checkNullComparisons(expression, stateConfig, configKeys, messageType, conditionType, location) {
|
|
2597
|
+
if (!NULL_COMPARISON.test(expression))
|
|
2598
|
+
return [];
|
|
2599
|
+
const warnings = [];
|
|
2600
|
+
const refs = extractFieldRefs(expression);
|
|
2601
|
+
for (const ref of refs) {
|
|
2602
|
+
if (!fieldInConfig(ref, configKeys, stateConfig))
|
|
2603
|
+
continue;
|
|
2604
|
+
const entry = resolveConfigEntry(ref, stateConfig);
|
|
2605
|
+
if (entry !== undefined && !isNullable(entry)) {
|
|
2606
|
+
warnings.push({
|
|
2607
|
+
kind: "null_comparison",
|
|
2608
|
+
message: `${conditionType}() in ${messageType} compares '${ref}' to null but field is not nullable`,
|
|
2609
|
+
messageType,
|
|
2610
|
+
conditionType,
|
|
2611
|
+
expression,
|
|
2612
|
+
location,
|
|
2613
|
+
suggestion: `Add null to '${ref}' values in state config, or remove the null check`
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
return warnings;
|
|
2618
|
+
}
|
|
2619
|
+
var WEAK_NEGATION = /([a-zA-Z_][\w.]*(?:\.value\.[\w.]+|\.value\b|))?\s*!==?\s*("[^"]*"|'[^']*'|\d+|true|false|null)/;
|
|
2620
|
+
function checkWeakPostconditions(expression, handler, messageType, conditionType, location) {
|
|
2621
|
+
if (conditionType !== "ensures")
|
|
2622
|
+
return [];
|
|
2623
|
+
const match = expression.match(WEAK_NEGATION);
|
|
2624
|
+
if (!match)
|
|
2625
|
+
return [];
|
|
2626
|
+
const refs = extractFieldRefs(expression);
|
|
2627
|
+
if (refs.length === 0)
|
|
2628
|
+
return [];
|
|
2629
|
+
for (const ref of refs) {
|
|
2630
|
+
const underscoredRef = ref.replace(/\./g, "_");
|
|
2631
|
+
const hasAssignment = handler.assignments.some((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
|
|
2632
|
+
if (hasAssignment) {
|
|
2633
|
+
const assignment = handler.assignments.find((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
|
|
2634
|
+
const assignedValue = assignment ? JSON.stringify(assignment.value) : "<value>";
|
|
2635
|
+
return [
|
|
2636
|
+
{
|
|
2637
|
+
kind: "weak_postcondition",
|
|
2638
|
+
message: `ensures() in ${messageType} uses !== for '${ref}' which has a concrete assignment`,
|
|
2639
|
+
messageType,
|
|
2640
|
+
conditionType,
|
|
2641
|
+
expression,
|
|
2642
|
+
location,
|
|
2643
|
+
suggestion: `Consider ensures(${ref} === ${assignedValue}) for a stronger postcondition`
|
|
2644
|
+
}
|
|
2645
|
+
];
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
return [];
|
|
2649
|
+
}
|
|
2650
|
+
function validateExpressions(handlers, stateConfig) {
|
|
2651
|
+
const warnings = [];
|
|
2652
|
+
let validCount = 0;
|
|
2653
|
+
const configKeys = new Set(Object.keys(stateConfig));
|
|
2654
|
+
for (const handler of handlers) {
|
|
2655
|
+
const conditions = [
|
|
2656
|
+
...handler.preconditions.map((c) => ({
|
|
2657
|
+
cond: c,
|
|
2658
|
+
type: "requires"
|
|
2659
|
+
})),
|
|
2660
|
+
...handler.postconditions.map((c) => ({
|
|
2661
|
+
cond: c,
|
|
2662
|
+
type: "ensures"
|
|
2663
|
+
}))
|
|
2664
|
+
];
|
|
2665
|
+
for (const { cond, type } of conditions) {
|
|
2666
|
+
const refs = extractFieldRefs(cond.expression);
|
|
2667
|
+
const isPayloadOnly = refs.length === 0 && PAYLOAD_REF.test(cond.expression);
|
|
2668
|
+
const loc = {
|
|
2669
|
+
file: handler.location.file,
|
|
2670
|
+
line: cond.location.line,
|
|
2671
|
+
column: cond.location.column
|
|
2672
|
+
};
|
|
2673
|
+
const condWarnings = [];
|
|
2674
|
+
if (!isPayloadOnly) {
|
|
2675
|
+
condWarnings.push(...checkUnmodeledFields(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
|
|
2676
|
+
}
|
|
2677
|
+
condWarnings.push(...checkUnsupportedMethods(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
|
|
2678
|
+
condWarnings.push(...checkOptionalChaining(cond.expression, handler.messageType, type, loc));
|
|
2679
|
+
condWarnings.push(...checkNullComparisons(cond.expression, stateConfig, configKeys, handler.messageType, type, loc));
|
|
2680
|
+
condWarnings.push(...checkWeakPostconditions(cond.expression, handler, handler.messageType, type, loc));
|
|
2681
|
+
if (condWarnings.length === 0) {
|
|
2682
|
+
validCount++;
|
|
2683
|
+
} else {
|
|
2684
|
+
warnings.push(...condWarnings);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
return { warnings, validCount, warnCount: warnings.length };
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// tools/verify/src/config/types.ts
|
|
2692
|
+
function isAdapterConfig(config) {
|
|
2693
|
+
return "adapter" in config;
|
|
2694
|
+
}
|
|
2695
|
+
function isLegacyConfig(config) {
|
|
2696
|
+
return "messages" in config && !("adapter" in config);
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
// tools/verify/src/analysis/state-space-estimator.ts
|
|
2700
|
+
function typedFieldCardinality(name, obj) {
|
|
2701
|
+
if (!("type" in obj))
|
|
2702
|
+
return null;
|
|
2703
|
+
switch (obj.type) {
|
|
2704
|
+
case "boolean":
|
|
2705
|
+
return { name, cardinality: 2, kind: "boolean" };
|
|
2706
|
+
case "enum":
|
|
2707
|
+
if (Array.isArray(obj.values)) {
|
|
2708
|
+
return { name, cardinality: obj.values.length, kind: "enum" };
|
|
2709
|
+
}
|
|
2710
|
+
return { name, cardinality: "unbounded", kind: "enum" };
|
|
2711
|
+
case "number":
|
|
2712
|
+
if (typeof obj.min === "number" && typeof obj.max === "number") {
|
|
2713
|
+
return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
|
|
2714
|
+
}
|
|
2715
|
+
return { name, cardinality: "unbounded", kind: "number" };
|
|
2716
|
+
case "array":
|
|
2717
|
+
return { name, cardinality: "unbounded", kind: "array" };
|
|
2718
|
+
case "string":
|
|
2719
|
+
return { name, cardinality: "unbounded", kind: "string" };
|
|
2720
|
+
default:
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
function legacyFieldCardinality(name, obj) {
|
|
2725
|
+
if ("values" in obj && Array.isArray(obj.values)) {
|
|
2726
|
+
const base = obj.values.length;
|
|
2727
|
+
const extra = obj.abstract === true ? 1 : 0;
|
|
2728
|
+
return {
|
|
2729
|
+
name,
|
|
2730
|
+
cardinality: base + extra,
|
|
2731
|
+
kind: obj.abstract ? "enum (abstract)" : "enum (values)"
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
if ("maxLength" in obj && !("type" in obj)) {
|
|
2735
|
+
return { name, cardinality: "unbounded", kind: "array" };
|
|
2736
|
+
}
|
|
2737
|
+
if ("min" in obj && "max" in obj && typeof obj.min === "number" && typeof obj.max === "number" && !("type" in obj)) {
|
|
2738
|
+
return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
|
|
2739
|
+
}
|
|
2740
|
+
return null;
|
|
2741
|
+
}
|
|
2742
|
+
function fieldCardinality(name, value) {
|
|
2743
|
+
if (Array.isArray(value)) {
|
|
2744
|
+
return { name, cardinality: value.length, kind: "enum (literal)" };
|
|
2745
|
+
}
|
|
2746
|
+
if (typeof value !== "object" || value === null) {
|
|
2747
|
+
return { name, cardinality: "unbounded", kind: "unknown" };
|
|
2748
|
+
}
|
|
2749
|
+
const obj = value;
|
|
2750
|
+
return typedFieldCardinality(name, obj) ?? legacyFieldCardinality(name, obj) ?? { name, cardinality: "unbounded", kind: "unknown" };
|
|
2751
|
+
}
|
|
2752
|
+
function permutations(n, k) {
|
|
2753
|
+
if (k > n)
|
|
2754
|
+
return 1;
|
|
2755
|
+
let result = 1;
|
|
2756
|
+
for (let i = 0;i < k; i++) {
|
|
2757
|
+
result *= n - i;
|
|
2758
|
+
}
|
|
2759
|
+
return result;
|
|
2760
|
+
}
|
|
2761
|
+
function getHandlerCount(config, analysis) {
|
|
2762
|
+
if (isLegacyConfig(config)) {
|
|
2763
|
+
const msgs = config.messages;
|
|
2764
|
+
if (msgs.include) {
|
|
2765
|
+
return msgs.include.length;
|
|
2766
|
+
}
|
|
2767
|
+
if (msgs.exclude) {
|
|
2768
|
+
return Math.max(0, analysis.handlers.length - msgs.exclude.length);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
return analysis.handlers.length;
|
|
2772
|
+
}
|
|
2773
|
+
function getMaxInFlight(config) {
|
|
2774
|
+
if (isLegacyConfig(config)) {
|
|
2775
|
+
return config.messages.maxInFlight ?? 1;
|
|
2776
|
+
}
|
|
2777
|
+
if (isAdapterConfig(config)) {
|
|
2778
|
+
return config.bounds?.maxInFlight ?? 1;
|
|
2779
|
+
}
|
|
2780
|
+
return 1;
|
|
2781
|
+
}
|
|
2782
|
+
function getMaxTabs(config) {
|
|
2783
|
+
if (isLegacyConfig(config)) {
|
|
2784
|
+
return config.messages.maxTabs ?? 1;
|
|
2785
|
+
}
|
|
2786
|
+
return 1;
|
|
2787
|
+
}
|
|
2788
|
+
function getFeasibility(states) {
|
|
2789
|
+
if (states < 1e5)
|
|
2790
|
+
return "trivial";
|
|
2791
|
+
if (states <= 1e6)
|
|
2792
|
+
return "feasible";
|
|
2793
|
+
if (states <= 1e7)
|
|
2794
|
+
return "slow";
|
|
2795
|
+
return "infeasible";
|
|
2796
|
+
}
|
|
2797
|
+
function feasibilityLabel(f) {
|
|
2798
|
+
switch (f) {
|
|
2799
|
+
case "trivial":
|
|
2800
|
+
return "trivial (should complete in seconds)";
|
|
2801
|
+
case "feasible":
|
|
2802
|
+
return "feasible (should complete in minutes)";
|
|
2803
|
+
case "slow":
|
|
2804
|
+
return "slow (may take 10–30 minutes)";
|
|
2805
|
+
case "infeasible":
|
|
2806
|
+
return "infeasible (likely won't terminate)";
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
function estimateStateSpace(config, analysis) {
|
|
2810
|
+
const state = config.state;
|
|
2811
|
+
const fields = [];
|
|
2812
|
+
const warnings = [];
|
|
2813
|
+
const suggestions = [];
|
|
2814
|
+
for (const [name, value] of Object.entries(state)) {
|
|
2815
|
+
fields.push(fieldCardinality(name, value));
|
|
2816
|
+
}
|
|
2817
|
+
const unboundedFields = fields.filter((f) => f.cardinality === "unbounded");
|
|
2818
|
+
const boundedFields = fields.filter((f) => f.cardinality !== "unbounded");
|
|
2819
|
+
const fieldProduct = boundedFields.length > 0 ? boundedFields.reduce((acc, f) => acc * f.cardinality, 1) : 1;
|
|
2820
|
+
if (unboundedFields.length > 0) {
|
|
2821
|
+
warnings.push(`${unboundedFields.length} field(s) have unbounded domains: ${unboundedFields.map((f) => f.name).join(", ")}`);
|
|
2822
|
+
suggestions.push(`Fields ${unboundedFields.map((f) => f.name).join(", ")} have unbounded domains — their actual impact depends on handler logic`);
|
|
2823
|
+
}
|
|
2824
|
+
const handlerCount = getHandlerCount(config, analysis);
|
|
2825
|
+
const maxInFlight = getMaxInFlight(config);
|
|
2826
|
+
const maxTabs = getMaxTabs(config);
|
|
2827
|
+
const contextCount = maxTabs + 1;
|
|
2828
|
+
const totalStateSpace = fieldProduct ** contextCount;
|
|
2829
|
+
const interleavingFactor = permutations(handlerCount, maxInFlight);
|
|
2830
|
+
const estimatedStates = totalStateSpace * interleavingFactor;
|
|
2831
|
+
const feasibility = getFeasibility(estimatedStates);
|
|
2832
|
+
if (handlerCount > 15) {
|
|
2833
|
+
suggestions.push("Consider splitting into subsystems");
|
|
2834
|
+
}
|
|
2835
|
+
for (const f of boundedFields) {
|
|
2836
|
+
if (f.cardinality > 50) {
|
|
2837
|
+
suggestions.push(`Consider reducing bounds for field "${f.name}" (${f.cardinality} values)`);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
if (maxInFlight > 2) {
|
|
2841
|
+
const reducedInterleaving = permutations(handlerCount, 2);
|
|
2842
|
+
const reduction = reducedInterleaving > 0 ? Math.round(interleavingFactor / reducedInterleaving) : 1;
|
|
2843
|
+
suggestions.push(`Reducing maxInFlight from ${maxInFlight} to 2 would reduce interleaving by ~${reduction}x`);
|
|
2844
|
+
}
|
|
2845
|
+
return {
|
|
2846
|
+
fields,
|
|
2847
|
+
fieldProduct,
|
|
2848
|
+
handlerCount,
|
|
2849
|
+
maxInFlight,
|
|
2850
|
+
contextCount,
|
|
2851
|
+
totalStateSpace,
|
|
2852
|
+
interleavingFactor,
|
|
2853
|
+
estimatedStates,
|
|
2854
|
+
feasibility,
|
|
2855
|
+
warnings,
|
|
2856
|
+
suggestions
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2369
2860
|
// tools/verify/src/codegen/config.ts
|
|
2370
2861
|
class ConfigGenerator {
|
|
2371
2862
|
lines = [];
|
|
@@ -2930,6 +3421,9 @@ class ConfigValidator {
|
|
|
2930
3421
|
if (config.tier2) {
|
|
2931
3422
|
this.validateTier2Optimizations(config.tier2);
|
|
2932
3423
|
}
|
|
3424
|
+
if (config.subsystems) {
|
|
3425
|
+
this.validateSubsystems(config.subsystems, config.state);
|
|
3426
|
+
}
|
|
2933
3427
|
}
|
|
2934
3428
|
findNullPlaceholders(obj, path2) {
|
|
2935
3429
|
if (obj === null || obj === undefined) {
|
|
@@ -3246,6 +3740,68 @@ class ConfigValidator {
|
|
|
3246
3740
|
}
|
|
3247
3741
|
}
|
|
3248
3742
|
}
|
|
3743
|
+
validateSubsystems(subsystems, stateConfig) {
|
|
3744
|
+
const stateFieldNames = Object.keys(stateConfig);
|
|
3745
|
+
const allAssignedHandlers = new Map;
|
|
3746
|
+
const allAssignedFields = new Map;
|
|
3747
|
+
for (const [subsystemName, subsystem] of Object.entries(subsystems)) {
|
|
3748
|
+
for (const field of subsystem.state) {
|
|
3749
|
+
if (!stateFieldNames.includes(field)) {
|
|
3750
|
+
this.issues.push({
|
|
3751
|
+
type: "invalid_value",
|
|
3752
|
+
severity: "error",
|
|
3753
|
+
field: `subsystems.${subsystemName}.state`,
|
|
3754
|
+
message: `State field "${field}" does not exist in top-level state config`,
|
|
3755
|
+
suggestion: `Available fields: ${stateFieldNames.join(", ")}`
|
|
3756
|
+
});
|
|
3757
|
+
}
|
|
3758
|
+
const existingOwner = allAssignedFields.get(field);
|
|
3759
|
+
if (existingOwner) {
|
|
3760
|
+
this.issues.push({
|
|
3761
|
+
type: "invalid_value",
|
|
3762
|
+
severity: "warning",
|
|
3763
|
+
field: `subsystems.${subsystemName}.state`,
|
|
3764
|
+
message: `State field "${field}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
|
|
3765
|
+
suggestion: "State fields should be partitioned across subsystems for non-interference"
|
|
3766
|
+
});
|
|
3767
|
+
} else {
|
|
3768
|
+
allAssignedFields.set(field, subsystemName);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
if (subsystem.handlers.length === 0) {
|
|
3772
|
+
this.issues.push({
|
|
3773
|
+
type: "invalid_value",
|
|
3774
|
+
severity: "warning",
|
|
3775
|
+
field: `subsystems.${subsystemName}.handlers`,
|
|
3776
|
+
message: `Subsystem "${subsystemName}" has no handlers`,
|
|
3777
|
+
suggestion: "Add at least one handler to the subsystem"
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
for (const handler of subsystem.handlers) {
|
|
3781
|
+
const existingOwner = allAssignedHandlers.get(handler);
|
|
3782
|
+
if (existingOwner) {
|
|
3783
|
+
this.issues.push({
|
|
3784
|
+
type: "invalid_value",
|
|
3785
|
+
severity: "error",
|
|
3786
|
+
field: `subsystems.${subsystemName}.handlers`,
|
|
3787
|
+
message: `Handler "${handler}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
|
|
3788
|
+
suggestion: "Each handler must belong to exactly one subsystem"
|
|
3789
|
+
});
|
|
3790
|
+
} else {
|
|
3791
|
+
allAssignedHandlers.set(handler, subsystemName);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
if (subsystem.state.length === 0) {
|
|
3795
|
+
this.issues.push({
|
|
3796
|
+
type: "invalid_value",
|
|
3797
|
+
severity: "warning",
|
|
3798
|
+
field: `subsystems.${subsystemName}.state`,
|
|
3799
|
+
message: `Subsystem "${subsystemName}" has no state fields`,
|
|
3800
|
+
suggestion: "Add at least one state field to the subsystem"
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3249
3805
|
}
|
|
3250
3806
|
function validateConfig(configPath) {
|
|
3251
3807
|
const validator = new ConfigValidator;
|
|
@@ -3664,7 +4220,8 @@ class HandlerExtractor {
|
|
|
3664
4220
|
packageRoot;
|
|
3665
4221
|
warnings;
|
|
3666
4222
|
currentFunctionParams = [];
|
|
3667
|
-
|
|
4223
|
+
contextOverrides;
|
|
4224
|
+
constructor(tsConfigPath, contextOverrides) {
|
|
3668
4225
|
this.project = new Project({
|
|
3669
4226
|
tsConfigFilePath: tsConfigPath
|
|
3670
4227
|
});
|
|
@@ -3672,6 +4229,7 @@ class HandlerExtractor {
|
|
|
3672
4229
|
this.relationshipExtractor = new RelationshipExtractor;
|
|
3673
4230
|
this.analyzedFiles = new Set;
|
|
3674
4231
|
this.warnings = [];
|
|
4232
|
+
this.contextOverrides = contextOverrides || new Map;
|
|
3675
4233
|
this.packageRoot = this.findPackageRoot(tsConfigPath);
|
|
3676
4234
|
}
|
|
3677
4235
|
warnUnsupportedPattern(pattern, location, suggestion) {
|
|
@@ -3717,13 +4275,14 @@ class HandlerExtractor {
|
|
|
3717
4275
|
const messageTypes = new Set;
|
|
3718
4276
|
const invalidMessageTypes = new Set;
|
|
3719
4277
|
const stateConstraints = [];
|
|
4278
|
+
const globalStateConstraints = [];
|
|
3720
4279
|
const verifiedStates = [];
|
|
3721
4280
|
this.warnings = [];
|
|
3722
4281
|
const allSourceFiles = this.project.getSourceFiles();
|
|
3723
4282
|
const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
|
|
3724
4283
|
this.debugLogSourceFiles(allSourceFiles, entryPoints);
|
|
3725
4284
|
for (const entryPoint of entryPoints) {
|
|
3726
|
-
this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
|
|
4285
|
+
this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
|
|
3727
4286
|
}
|
|
3728
4287
|
if (verifiedStates.length > 0) {
|
|
3729
4288
|
if (process.env["POLLY_DEBUG"]) {
|
|
@@ -3753,11 +4312,12 @@ class HandlerExtractor {
|
|
|
3753
4312
|
handlers,
|
|
3754
4313
|
messageTypes,
|
|
3755
4314
|
stateConstraints,
|
|
4315
|
+
globalStateConstraints,
|
|
3756
4316
|
verifiedStates,
|
|
3757
4317
|
warnings: this.warnings
|
|
3758
4318
|
};
|
|
3759
4319
|
}
|
|
3760
|
-
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
|
|
4320
|
+
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates) {
|
|
3761
4321
|
const filePath = sourceFile.getFilePath();
|
|
3762
4322
|
if (this.analyzedFiles.has(filePath)) {
|
|
3763
4323
|
return;
|
|
@@ -3771,6 +4331,8 @@ class HandlerExtractor {
|
|
|
3771
4331
|
this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
|
|
3772
4332
|
const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
|
|
3773
4333
|
stateConstraints.push(...fileConstraints);
|
|
4334
|
+
const fileGlobalConstraints = this.extractGlobalStateConstraintsFromFile(sourceFile);
|
|
4335
|
+
globalStateConstraints.push(...fileGlobalConstraints);
|
|
3774
4336
|
const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
|
|
3775
4337
|
verifiedStates.push(...fileVerifiedStates);
|
|
3776
4338
|
const importDeclarations = sourceFile.getImportDeclarations();
|
|
@@ -3784,7 +4346,7 @@ class HandlerExtractor {
|
|
|
3784
4346
|
}
|
|
3785
4347
|
continue;
|
|
3786
4348
|
}
|
|
3787
|
-
this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
|
|
4349
|
+
this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
|
|
3788
4350
|
} else if (process.env["POLLY_DEBUG"]) {
|
|
3789
4351
|
const specifier = importDecl.getModuleSpecifierValue();
|
|
3790
4352
|
if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
|
|
@@ -3885,8 +4447,20 @@ class HandlerExtractor {
|
|
|
3885
4447
|
handlers.push(handler);
|
|
3886
4448
|
}
|
|
3887
4449
|
}
|
|
4450
|
+
if (methodName === "ws") {
|
|
4451
|
+
this.extractElysiaWsHandlers(node, context, filePath, handlers);
|
|
4452
|
+
}
|
|
4453
|
+
if (this.isRestMethod(methodName) && this.isWebFrameworkFile(node.getSourceFile())) {
|
|
4454
|
+
const restHandler = this.extractRestHandler(node, methodName, context, filePath);
|
|
4455
|
+
if (restHandler) {
|
|
4456
|
+
handlers.push(restHandler);
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
3888
4459
|
}
|
|
3889
4460
|
}
|
|
4461
|
+
isRestMethod(name) {
|
|
4462
|
+
return ["get", "post", "put", "delete", "patch"].includes(name);
|
|
4463
|
+
}
|
|
3890
4464
|
isElseIfStatement(node) {
|
|
3891
4465
|
const parent = node.getParent();
|
|
3892
4466
|
return parent !== undefined && Node2.isIfStatement(parent);
|
|
@@ -3952,6 +4526,135 @@ class HandlerExtractor {
|
|
|
3952
4526
|
parameters
|
|
3953
4527
|
};
|
|
3954
4528
|
}
|
|
4529
|
+
extractElysiaWsHandlers(node, context, filePath, handlers) {
|
|
4530
|
+
const args = node.getArguments();
|
|
4531
|
+
if (args.length < 2)
|
|
4532
|
+
return;
|
|
4533
|
+
const routeArg = args[0];
|
|
4534
|
+
if (!routeArg || !Node2.isStringLiteral(routeArg))
|
|
4535
|
+
return;
|
|
4536
|
+
const routePath = routeArg.getLiteralValue();
|
|
4537
|
+
const configArg = args[1];
|
|
4538
|
+
if (!configArg || !Node2.isObjectLiteralExpression(configArg))
|
|
4539
|
+
return;
|
|
4540
|
+
const callbacks = ["message", "open", "close"];
|
|
4541
|
+
for (const cbName of callbacks) {
|
|
4542
|
+
const prop = configArg.getProperty(cbName);
|
|
4543
|
+
if (!prop)
|
|
4544
|
+
continue;
|
|
4545
|
+
let funcBody = null;
|
|
4546
|
+
if (Node2.isMethodDeclaration(prop)) {
|
|
4547
|
+
funcBody = prop;
|
|
4548
|
+
} else if (Node2.isPropertyAssignment(prop)) {
|
|
4549
|
+
const init = prop.getInitializer();
|
|
4550
|
+
if (init && (Node2.isArrowFunction(init) || Node2.isFunctionExpression(init))) {
|
|
4551
|
+
funcBody = init;
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
if (!funcBody)
|
|
4555
|
+
continue;
|
|
4556
|
+
if (cbName === "message") {
|
|
4557
|
+
const body = funcBody.getBody();
|
|
4558
|
+
if (!body)
|
|
4559
|
+
continue;
|
|
4560
|
+
const subHandlers = this.extractSubHandlersFromBody(body, context, filePath);
|
|
4561
|
+
if (subHandlers.length > 0) {
|
|
4562
|
+
handlers.push(...subHandlers);
|
|
4563
|
+
} else {
|
|
4564
|
+
handlers.push(this.buildWsHandler(`ws_message`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
|
|
4565
|
+
}
|
|
4566
|
+
} else {
|
|
4567
|
+
handlers.push(this.buildWsHandler(`ws_${cbName}`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
extractSubHandlersFromBody(body, context, filePath) {
|
|
4572
|
+
const subHandlers = [];
|
|
4573
|
+
body.forEachDescendant((child) => {
|
|
4574
|
+
if (Node2.isIfStatement(child) && !this.isElseIfStatement(child)) {
|
|
4575
|
+
const typeGuardHandlers = this.extractTypeGuardHandlers(child, context, filePath);
|
|
4576
|
+
subHandlers.push(...typeGuardHandlers);
|
|
4577
|
+
}
|
|
4578
|
+
if (Node2.isSwitchStatement(child)) {
|
|
4579
|
+
const switchHandlers = this.extractSwitchCaseHandlers(child, context, filePath);
|
|
4580
|
+
subHandlers.push(...switchHandlers);
|
|
4581
|
+
}
|
|
4582
|
+
});
|
|
4583
|
+
return subHandlers;
|
|
4584
|
+
}
|
|
4585
|
+
buildWsHandler(messageType, _routePath, context, filePath, funcBody, line) {
|
|
4586
|
+
const assignments = [];
|
|
4587
|
+
const preconditions = [];
|
|
4588
|
+
const postconditions = [];
|
|
4589
|
+
this.currentFunctionParams = this.extractParameterNames(funcBody);
|
|
4590
|
+
this.extractAssignments(funcBody, assignments);
|
|
4591
|
+
this.extractVerificationConditions(funcBody, preconditions, postconditions);
|
|
4592
|
+
this.currentFunctionParams = [];
|
|
4593
|
+
return {
|
|
4594
|
+
messageType,
|
|
4595
|
+
node: context,
|
|
4596
|
+
assignments,
|
|
4597
|
+
preconditions,
|
|
4598
|
+
postconditions,
|
|
4599
|
+
location: { file: filePath, line },
|
|
4600
|
+
origin: "event"
|
|
4601
|
+
};
|
|
4602
|
+
}
|
|
4603
|
+
extractRestHandler(node, methodName, context, filePath) {
|
|
4604
|
+
const args = node.getArguments();
|
|
4605
|
+
if (args.length < 2)
|
|
4606
|
+
return null;
|
|
4607
|
+
const routeArg = args[0];
|
|
4608
|
+
if (!routeArg || !Node2.isStringLiteral(routeArg))
|
|
4609
|
+
return null;
|
|
4610
|
+
const routePath = routeArg.getLiteralValue();
|
|
4611
|
+
const httpMethod = methodName.toUpperCase();
|
|
4612
|
+
const messageType = `${httpMethod} ${routePath}`;
|
|
4613
|
+
const handlerArg = args[1];
|
|
4614
|
+
const assignments = [];
|
|
4615
|
+
const preconditions = [];
|
|
4616
|
+
const postconditions = [];
|
|
4617
|
+
let actualHandler = null;
|
|
4618
|
+
if (Node2.isArrowFunction(handlerArg) || Node2.isFunctionExpression(handlerArg)) {
|
|
4619
|
+
actualHandler = handlerArg;
|
|
4620
|
+
} else if (Node2.isIdentifier(handlerArg)) {
|
|
4621
|
+
actualHandler = this.resolveFunctionReference(handlerArg);
|
|
4622
|
+
}
|
|
4623
|
+
let parameters;
|
|
4624
|
+
if (actualHandler) {
|
|
4625
|
+
this.currentFunctionParams = this.extractParameterNames(actualHandler);
|
|
4626
|
+
parameters = this.currentFunctionParams.length > 0 ? [...this.currentFunctionParams] : undefined;
|
|
4627
|
+
this.extractAssignments(actualHandler, assignments);
|
|
4628
|
+
this.extractVerificationConditions(actualHandler, preconditions, postconditions);
|
|
4629
|
+
this.currentFunctionParams = [];
|
|
4630
|
+
}
|
|
4631
|
+
return {
|
|
4632
|
+
messageType,
|
|
4633
|
+
node: context,
|
|
4634
|
+
assignments,
|
|
4635
|
+
preconditions,
|
|
4636
|
+
postconditions,
|
|
4637
|
+
location: {
|
|
4638
|
+
file: filePath,
|
|
4639
|
+
line: node.getStartLineNumber()
|
|
4640
|
+
},
|
|
4641
|
+
origin: "event",
|
|
4642
|
+
parameters,
|
|
4643
|
+
handlerKind: "rest",
|
|
4644
|
+
httpMethod,
|
|
4645
|
+
routePath
|
|
4646
|
+
};
|
|
4647
|
+
}
|
|
4648
|
+
isWebFrameworkFile(sourceFile) {
|
|
4649
|
+
const frameworks = ["elysia", "express", "hono", "fastify", "koa", "@elysiajs/eden"];
|
|
4650
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
4651
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
4652
|
+
if (frameworks.some((fw) => specifier === fw || specifier.startsWith(`${fw}/`))) {
|
|
4653
|
+
return true;
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
return false;
|
|
4657
|
+
}
|
|
3955
4658
|
extractAssignments(funcNode, assignments) {
|
|
3956
4659
|
funcNode.forEachDescendant((node) => {
|
|
3957
4660
|
if (Node2.isBinaryExpression(node)) {
|
|
@@ -4786,32 +5489,64 @@ class HandlerExtractor {
|
|
|
4786
5489
|
try {
|
|
4787
5490
|
const ifStmt = ifNode;
|
|
4788
5491
|
const condition = ifStmt.getExpression();
|
|
4789
|
-
if (
|
|
4790
|
-
|
|
5492
|
+
if (Node2.isCallExpression(condition)) {
|
|
5493
|
+
const funcExpr = condition.getExpression();
|
|
5494
|
+
const funcName = Node2.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
|
|
5495
|
+
this.debugLogProcessingFunction(funcName);
|
|
5496
|
+
const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
|
|
5497
|
+
if (!messageType) {
|
|
5498
|
+
this.debugLogUnresolvedMessageType(funcName);
|
|
5499
|
+
return null;
|
|
5500
|
+
}
|
|
5501
|
+
const line = ifStmt.getStartLineNumber();
|
|
5502
|
+
const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
|
|
5503
|
+
return {
|
|
5504
|
+
messageType,
|
|
5505
|
+
node: context,
|
|
5506
|
+
assignments: [],
|
|
5507
|
+
preconditions: [],
|
|
5508
|
+
postconditions: [],
|
|
5509
|
+
location: { file: filePath, line },
|
|
5510
|
+
relationships
|
|
5511
|
+
};
|
|
4791
5512
|
}
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
5513
|
+
if (Node2.isBinaryExpression(condition)) {
|
|
5514
|
+
const messageType = this.extractMessageTypeFromEqualityCheck(condition);
|
|
5515
|
+
if (messageType) {
|
|
5516
|
+
const line = ifStmt.getStartLineNumber();
|
|
5517
|
+
const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
|
|
5518
|
+
return {
|
|
5519
|
+
messageType,
|
|
5520
|
+
node: context,
|
|
5521
|
+
assignments: [],
|
|
5522
|
+
preconditions: [],
|
|
5523
|
+
postconditions: [],
|
|
5524
|
+
location: { file: filePath, line },
|
|
5525
|
+
relationships
|
|
5526
|
+
};
|
|
5527
|
+
}
|
|
4799
5528
|
}
|
|
4800
|
-
|
|
4801
|
-
const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
|
|
4802
|
-
return {
|
|
4803
|
-
messageType,
|
|
4804
|
-
node: context,
|
|
4805
|
-
assignments: [],
|
|
4806
|
-
preconditions: [],
|
|
4807
|
-
postconditions: [],
|
|
4808
|
-
location: { file: filePath, line },
|
|
4809
|
-
relationships
|
|
4810
|
-
};
|
|
5529
|
+
return null;
|
|
4811
5530
|
} catch (_error) {
|
|
4812
5531
|
return null;
|
|
4813
5532
|
}
|
|
4814
5533
|
}
|
|
5534
|
+
extractMessageTypeFromEqualityCheck(expr) {
|
|
5535
|
+
const operator = expr.getOperatorToken().getText();
|
|
5536
|
+
if (operator !== "===" && operator !== "==")
|
|
5537
|
+
return null;
|
|
5538
|
+
const left = expr.getLeft();
|
|
5539
|
+
const right = expr.getRight();
|
|
5540
|
+
const stringLiteral = Node2.isStringLiteral(right) ? right : Node2.isStringLiteral(left) ? left : null;
|
|
5541
|
+
const propAccess = Node2.isPropertyAccessExpression(left) ? left : Node2.isPropertyAccessExpression(right) ? right : null;
|
|
5542
|
+
if (!stringLiteral || !propAccess)
|
|
5543
|
+
return null;
|
|
5544
|
+
const propName = propAccess.getName();
|
|
5545
|
+
if (propName !== "type" && propName !== "kind" && propName !== "action") {
|
|
5546
|
+
return null;
|
|
5547
|
+
}
|
|
5548
|
+
return stringLiteral.getLiteralValue();
|
|
5549
|
+
}
|
|
4815
5550
|
debugLogProcessingFunction(funcName) {
|
|
4816
5551
|
if (process.env["POLLY_DEBUG"] && funcName) {
|
|
4817
5552
|
console.log(`[DEBUG] Processing if condition with function: ${funcName}`);
|
|
@@ -5021,6 +5756,10 @@ class HandlerExtractor {
|
|
|
5021
5756
|
return messageType;
|
|
5022
5757
|
}
|
|
5023
5758
|
inferContext(filePath) {
|
|
5759
|
+
for (const [pathPrefix, context] of this.contextOverrides.entries()) {
|
|
5760
|
+
if (filePath.startsWith(pathPrefix))
|
|
5761
|
+
return context;
|
|
5762
|
+
}
|
|
5024
5763
|
const path2 = filePath.toLowerCase();
|
|
5025
5764
|
return this.inferElectronContext(path2) || this.inferWorkerContext(path2) || this.inferServerAppContext(path2) || this.inferChromeExtensionContext(path2) || "unknown";
|
|
5026
5765
|
}
|
|
@@ -5119,6 +5858,81 @@ class HandlerExtractor {
|
|
|
5119
5858
|
});
|
|
5120
5859
|
return constraints;
|
|
5121
5860
|
}
|
|
5861
|
+
extractGlobalStateConstraintsFromFile(sourceFile) {
|
|
5862
|
+
const constraints = [];
|
|
5863
|
+
const filePath = sourceFile.getFilePath();
|
|
5864
|
+
sourceFile.forEachDescendant((node) => {
|
|
5865
|
+
const constraint = this.recognizeGlobalStateConstraint(node, filePath);
|
|
5866
|
+
if (constraint) {
|
|
5867
|
+
constraints.push(constraint);
|
|
5868
|
+
}
|
|
5869
|
+
});
|
|
5870
|
+
return constraints;
|
|
5871
|
+
}
|
|
5872
|
+
recognizeGlobalStateConstraint(node, filePath) {
|
|
5873
|
+
if (!Node2.isCallExpression(node)) {
|
|
5874
|
+
return null;
|
|
5875
|
+
}
|
|
5876
|
+
const expression = node.getExpression();
|
|
5877
|
+
if (!Node2.isIdentifier(expression)) {
|
|
5878
|
+
return null;
|
|
5879
|
+
}
|
|
5880
|
+
const functionName = expression.getText();
|
|
5881
|
+
if (functionName !== "stateConstraint") {
|
|
5882
|
+
return null;
|
|
5883
|
+
}
|
|
5884
|
+
const args = node.getArguments();
|
|
5885
|
+
if (args.length < 2) {
|
|
5886
|
+
return null;
|
|
5887
|
+
}
|
|
5888
|
+
const nameArg = args[0];
|
|
5889
|
+
if (!Node2.isStringLiteral(nameArg)) {
|
|
5890
|
+
return null;
|
|
5891
|
+
}
|
|
5892
|
+
const name = nameArg.getLiteralValue();
|
|
5893
|
+
const predicateArg = args[1];
|
|
5894
|
+
if (!Node2.isArrowFunction(predicateArg)) {
|
|
5895
|
+
return null;
|
|
5896
|
+
}
|
|
5897
|
+
const body = predicateArg.getBody();
|
|
5898
|
+
let expressionText;
|
|
5899
|
+
if (Node2.isBlock(body)) {
|
|
5900
|
+
const returnStatement = body.getStatements().find((s) => Node2.isReturnStatement(s));
|
|
5901
|
+
if (!returnStatement || !Node2.isReturnStatement(returnStatement)) {
|
|
5902
|
+
return null;
|
|
5903
|
+
}
|
|
5904
|
+
const returnExpr = returnStatement.getExpression();
|
|
5905
|
+
if (!returnExpr) {
|
|
5906
|
+
return null;
|
|
5907
|
+
}
|
|
5908
|
+
expressionText = returnExpr.getText();
|
|
5909
|
+
} else {
|
|
5910
|
+
expressionText = body.getText();
|
|
5911
|
+
}
|
|
5912
|
+
let message;
|
|
5913
|
+
if (args.length >= 3) {
|
|
5914
|
+
const optionsArg = args[2];
|
|
5915
|
+
if (Node2.isObjectLiteralExpression(optionsArg)) {
|
|
5916
|
+
for (const prop of optionsArg.getProperties()) {
|
|
5917
|
+
if (Node2.isPropertyAssignment(prop) && prop.getName() === "message") {
|
|
5918
|
+
const value = prop.getInitializer();
|
|
5919
|
+
if (value && Node2.isStringLiteral(value)) {
|
|
5920
|
+
message = value.getLiteralValue();
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
return {
|
|
5927
|
+
name,
|
|
5928
|
+
expression: expressionText,
|
|
5929
|
+
message,
|
|
5930
|
+
location: {
|
|
5931
|
+
file: filePath,
|
|
5932
|
+
line: node.getStartLineNumber()
|
|
5933
|
+
}
|
|
5934
|
+
};
|
|
5935
|
+
}
|
|
5122
5936
|
recognizeStateConstraint(node, filePath) {
|
|
5123
5937
|
if (!Node2.isCallExpression(node)) {
|
|
5124
5938
|
return [];
|
|
@@ -5433,7 +6247,8 @@ class TypeExtractor {
|
|
|
5433
6247
|
messageTypes: validMessageTypes,
|
|
5434
6248
|
fields,
|
|
5435
6249
|
handlers: completeHandlers,
|
|
5436
|
-
stateConstraints: handlerAnalysis.stateConstraints
|
|
6250
|
+
stateConstraints: handlerAnalysis.stateConstraints,
|
|
6251
|
+
globalStateConstraints: handlerAnalysis.globalStateConstraints
|
|
5437
6252
|
};
|
|
5438
6253
|
}
|
|
5439
6254
|
extractHandlerAnalysis() {
|
|
@@ -5864,6 +6679,10 @@ async function main() {
|
|
|
5864
6679
|
case "validate":
|
|
5865
6680
|
await validateCommand();
|
|
5866
6681
|
break;
|
|
6682
|
+
case "--estimate":
|
|
6683
|
+
case "estimate":
|
|
6684
|
+
await estimateCommand();
|
|
6685
|
+
break;
|
|
5867
6686
|
case "--help":
|
|
5868
6687
|
case "help":
|
|
5869
6688
|
showHelp();
|
|
@@ -5980,6 +6799,70 @@ async function validateCommand() {
|
|
|
5980
6799
|
`, COLORS.red));
|
|
5981
6800
|
process.exit(1);
|
|
5982
6801
|
}
|
|
6802
|
+
async function estimateCommand() {
|
|
6803
|
+
const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
|
|
6804
|
+
console.log(color(`
|
|
6805
|
+
\uD83D\uDCCA Estimating state space...
|
|
6806
|
+
`, COLORS.blue));
|
|
6807
|
+
const validation = validateConfig(configPath);
|
|
6808
|
+
if (!validation.valid) {
|
|
6809
|
+
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
6810
|
+
console.log(color(`❌ Configuration incomplete (${errors.length} error(s))
|
|
6811
|
+
`, COLORS.red));
|
|
6812
|
+
for (const error of errors.slice(0, 3)) {
|
|
6813
|
+
console.log(color(` • ${error.message}`, COLORS.red));
|
|
6814
|
+
}
|
|
6815
|
+
console.log(`
|
|
6816
|
+
Run 'polly verify --validate' to see all issues`);
|
|
6817
|
+
process.exit(1);
|
|
6818
|
+
}
|
|
6819
|
+
const config = await loadVerificationConfig(configPath);
|
|
6820
|
+
const analysis = await runCodebaseAnalysis();
|
|
6821
|
+
const typedConfig = config;
|
|
6822
|
+
const typedAnalysis = analysis;
|
|
6823
|
+
const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
|
|
6824
|
+
if (exprValidation.warnings.length > 0) {
|
|
6825
|
+
displayExpressionWarnings(exprValidation);
|
|
6826
|
+
}
|
|
6827
|
+
const estimate = estimateStateSpace(typedConfig, typedAnalysis);
|
|
6828
|
+
displayEstimate(estimate);
|
|
6829
|
+
}
|
|
6830
|
+
function displayEstimate(estimate) {
|
|
6831
|
+
console.log(color(`State space estimate:
|
|
6832
|
+
`, COLORS.blue));
|
|
6833
|
+
console.log(color(" Fields:", COLORS.blue));
|
|
6834
|
+
for (const field of estimate.fields) {
|
|
6835
|
+
const name = field.name.padEnd(28);
|
|
6836
|
+
const kind = field.kind.padEnd(18);
|
|
6837
|
+
const card = field.cardinality === "unbounded" ? color("(excluded — unbounded)", COLORS.yellow) : `${field.cardinality} values`;
|
|
6838
|
+
console.log(` ${name} ${kind} ${card}`);
|
|
6839
|
+
}
|
|
6840
|
+
console.log();
|
|
6841
|
+
console.log(` Field combinations: ${color(String(estimate.fieldProduct), COLORS.green)}`);
|
|
6842
|
+
console.log(` Handlers: ${estimate.handlerCount}`);
|
|
6843
|
+
console.log(` Max in-flight: ${estimate.maxInFlight}`);
|
|
6844
|
+
console.log(` Contexts: ${estimate.contextCount} (${estimate.contextCount - 1} tab${estimate.contextCount - 1 !== 1 ? "s" : ""} + background)`);
|
|
6845
|
+
console.log(` Interleaving factor: ${estimate.interleavingFactor}`);
|
|
6846
|
+
console.log();
|
|
6847
|
+
console.log(` Estimated states: ${color(`~${estimate.estimatedStates.toLocaleString()}`, COLORS.green)}`);
|
|
6848
|
+
const feasColor = estimate.feasibility === "trivial" || estimate.feasibility === "feasible" ? COLORS.green : estimate.feasibility === "slow" ? COLORS.yellow : COLORS.red;
|
|
6849
|
+
console.log();
|
|
6850
|
+
console.log(` Assessment: ${color(feasibilityLabel(estimate.feasibility), feasColor)}`);
|
|
6851
|
+
if (estimate.warnings.length > 0) {
|
|
6852
|
+
console.log();
|
|
6853
|
+
for (const w of estimate.warnings) {
|
|
6854
|
+
console.log(color(` ⚠️ ${w}`, COLORS.yellow));
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
if (estimate.suggestions.length > 0) {
|
|
6858
|
+
console.log();
|
|
6859
|
+
console.log(color(" Suggestions:", COLORS.blue));
|
|
6860
|
+
for (const s of estimate.suggestions) {
|
|
6861
|
+
console.log(color(` • ${s}`, COLORS.gray));
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
console.log();
|
|
6865
|
+
}
|
|
5983
6866
|
function displayValidationErrors(errors) {
|
|
5984
6867
|
if (errors.length === 0)
|
|
5985
6868
|
return;
|
|
@@ -6011,6 +6894,19 @@ function displayValidationWarnings(warnings) {
|
|
|
6011
6894
|
console.log();
|
|
6012
6895
|
}
|
|
6013
6896
|
}
|
|
6897
|
+
function displayExpressionWarnings(result) {
|
|
6898
|
+
if (result.warnings.length === 0)
|
|
6899
|
+
return;
|
|
6900
|
+
console.log(color(`⚠️ Found ${result.warnCount} expression warning(s):
|
|
6901
|
+
`, COLORS.yellow));
|
|
6902
|
+
for (const w of result.warnings) {
|
|
6903
|
+
console.log(color(` ⚠ ${w.message}`, COLORS.yellow));
|
|
6904
|
+
console.log(color(` ${w.expression}`, COLORS.gray));
|
|
6905
|
+
console.log(color(` at ${w.location.file}:${w.location.line}`, COLORS.gray));
|
|
6906
|
+
console.log(color(` → ${w.suggestion}`, COLORS.yellow));
|
|
6907
|
+
console.log();
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6014
6910
|
async function verifyCommand() {
|
|
6015
6911
|
const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
|
|
6016
6912
|
console.log(color(`
|
|
@@ -6073,15 +6969,29 @@ function getMaxDepth(config) {
|
|
|
6073
6969
|
async function runFullVerification(configPath) {
|
|
6074
6970
|
const config = await loadVerificationConfig(configPath);
|
|
6075
6971
|
const analysis = await runCodebaseAnalysis();
|
|
6972
|
+
const typedConfig = config;
|
|
6973
|
+
const typedAnalysis = analysis;
|
|
6974
|
+
const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
|
|
6975
|
+
if (exprValidation.warnings.length > 0) {
|
|
6976
|
+
displayExpressionWarnings(exprValidation);
|
|
6977
|
+
}
|
|
6978
|
+
if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
|
|
6979
|
+
await runSubsystemVerification(typedConfig, typedAnalysis);
|
|
6980
|
+
return;
|
|
6981
|
+
}
|
|
6982
|
+
await runMonolithicVerification(config, analysis);
|
|
6983
|
+
}
|
|
6984
|
+
async function runMonolithicVerification(config, analysis) {
|
|
6076
6985
|
const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
|
|
6077
6986
|
findAndCopyBaseSpec(specDir);
|
|
6078
6987
|
console.log(color("✓ Specification generated", COLORS.green));
|
|
6079
6988
|
console.log(color(` ${specPath}`, COLORS.gray));
|
|
6080
6989
|
console.log();
|
|
6081
6990
|
const docker = await setupDocker();
|
|
6082
|
-
const
|
|
6083
|
-
const
|
|
6084
|
-
const
|
|
6991
|
+
const typedConfig = config;
|
|
6992
|
+
const timeoutSeconds = getTimeout(typedConfig);
|
|
6993
|
+
const workers = getWorkers(typedConfig);
|
|
6994
|
+
const maxDepth = getMaxDepth(typedConfig);
|
|
6085
6995
|
console.log(color("⚙️ Running TLC model checker...", COLORS.blue));
|
|
6086
6996
|
if (timeoutSeconds === 0) {
|
|
6087
6997
|
console.log(color(" No timeout set - will run until completion", COLORS.gray));
|
|
@@ -6101,6 +7011,122 @@ async function runFullVerification(configPath) {
|
|
|
6101
7011
|
});
|
|
6102
7012
|
displayVerificationResults(result, specDir);
|
|
6103
7013
|
}
|
|
7014
|
+
async function runSubsystemVerification(config, analysis) {
|
|
7015
|
+
const subsystems = config.subsystems;
|
|
7016
|
+
const subsystemNames = Object.keys(subsystems);
|
|
7017
|
+
console.log(color(`\uD83D\uDCE6 Subsystem-scoped verification (${subsystemNames.length} subsystems)
|
|
7018
|
+
`, COLORS.blue));
|
|
7019
|
+
const { checkNonInterference: checkNonInterference2 } = await Promise.resolve().then(() => exports_non_interference);
|
|
7020
|
+
const interference = checkNonInterference2(subsystems, analysis.handlers);
|
|
7021
|
+
if (interference.valid) {
|
|
7022
|
+
console.log(color("✓ Non-interference: verified (no cross-subsystem state writes)", COLORS.green));
|
|
7023
|
+
console.log();
|
|
7024
|
+
} else {
|
|
7025
|
+
console.log(color(`⚠️ Non-interference violations detected:
|
|
7026
|
+
`, COLORS.yellow));
|
|
7027
|
+
for (const v of interference.violations) {
|
|
7028
|
+
console.log(color(` • Handler "${v.handler}" (${v.subsystem}) writes to "${v.writesTo}" owned by "${v.ownedBy}"`, COLORS.yellow));
|
|
7029
|
+
}
|
|
7030
|
+
console.log();
|
|
7031
|
+
console.log(color(" Compositional verification may not be sound. Consider restructuring subsystem boundaries.", COLORS.yellow));
|
|
7032
|
+
console.log();
|
|
7033
|
+
}
|
|
7034
|
+
const assignedHandlers = new Set(Object.values(subsystems).flatMap((s) => s.handlers));
|
|
7035
|
+
const unassigned = analysis.messageTypes.filter((mt) => !assignedHandlers.has(mt));
|
|
7036
|
+
if (unassigned.length > 0) {
|
|
7037
|
+
console.log(color(`⚠️ ${unassigned.length} handler(s) not assigned to any subsystem (will not be verified):`, COLORS.yellow));
|
|
7038
|
+
for (const h of unassigned.slice(0, 10)) {
|
|
7039
|
+
console.log(color(` • ${h}`, COLORS.yellow));
|
|
7040
|
+
}
|
|
7041
|
+
if (unassigned.length > 10) {
|
|
7042
|
+
console.log(color(` ... and ${unassigned.length - 10} more`, COLORS.yellow));
|
|
7043
|
+
}
|
|
7044
|
+
console.log();
|
|
7045
|
+
}
|
|
7046
|
+
const docker = await setupDocker();
|
|
7047
|
+
const timeoutSeconds = getTimeout(config);
|
|
7048
|
+
const workers = getWorkers(config);
|
|
7049
|
+
const maxDepth = getMaxDepth(config);
|
|
7050
|
+
const { generateSubsystemTLA: generateSubsystemTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
|
|
7051
|
+
const results = [];
|
|
7052
|
+
for (const name of subsystemNames) {
|
|
7053
|
+
const sub = subsystems[name];
|
|
7054
|
+
const startTime = Date.now();
|
|
7055
|
+
console.log(color(`⚙️ Verifying subsystem: ${name}...`, COLORS.blue));
|
|
7056
|
+
const { spec, cfg } = await generateSubsystemTLA2(name, sub, config, analysis);
|
|
7057
|
+
const specDir = path4.join(process.cwd(), "specs", "tla", "generated", name);
|
|
7058
|
+
if (!fs4.existsSync(specDir)) {
|
|
7059
|
+
fs4.mkdirSync(specDir, { recursive: true });
|
|
7060
|
+
}
|
|
7061
|
+
const specPath = path4.join(specDir, `UserApp_${name}.tla`);
|
|
7062
|
+
const cfgPath = path4.join(specDir, `UserApp_${name}.cfg`);
|
|
7063
|
+
fs4.writeFileSync(specPath, spec);
|
|
7064
|
+
fs4.writeFileSync(cfgPath, cfg);
|
|
7065
|
+
findAndCopyBaseSpec(specDir);
|
|
7066
|
+
const result = await docker.runTLC(specPath, {
|
|
7067
|
+
workers,
|
|
7068
|
+
timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
|
|
7069
|
+
maxDepth
|
|
7070
|
+
});
|
|
7071
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
7072
|
+
results.push({
|
|
7073
|
+
name,
|
|
7074
|
+
success: result.success,
|
|
7075
|
+
handlerCount: sub.handlers.length,
|
|
7076
|
+
stateCount: result.stats?.distinctStates ?? 0,
|
|
7077
|
+
elapsed,
|
|
7078
|
+
stats: result.stats,
|
|
7079
|
+
error: result.error
|
|
7080
|
+
});
|
|
7081
|
+
if (result.success) {
|
|
7082
|
+
console.log(color(` ✓ ${name} passed (${elapsed.toFixed(1)}s)`, COLORS.green));
|
|
7083
|
+
} else {
|
|
7084
|
+
console.log(color(` ✗ ${name} failed`, COLORS.red));
|
|
7085
|
+
if (result.violation) {
|
|
7086
|
+
console.log(color(` Invariant violated: ${result.violation.name}`, COLORS.red));
|
|
7087
|
+
} else if (result.error) {
|
|
7088
|
+
console.log(color(` Error: ${result.error}`, COLORS.red));
|
|
7089
|
+
}
|
|
7090
|
+
fs4.writeFileSync(path4.join(specDir, "tlc-output.log"), result.output);
|
|
7091
|
+
}
|
|
7092
|
+
}
|
|
7093
|
+
console.log();
|
|
7094
|
+
displayCompositionalReport(results, interference.valid);
|
|
7095
|
+
}
|
|
7096
|
+
function displayCompositionalReport(results, nonInterferenceValid) {
|
|
7097
|
+
console.log(color(`Subsystem verification results:
|
|
7098
|
+
`, COLORS.blue));
|
|
7099
|
+
for (const r of results) {
|
|
7100
|
+
const status = r.success ? color("✓", COLORS.green) : color("✗", COLORS.red);
|
|
7101
|
+
const name = r.name.padEnd(20);
|
|
7102
|
+
const handlers = `${r.handlerCount} handler${r.handlerCount !== 1 ? "s" : ""}`;
|
|
7103
|
+
const states = `${r.stateCount} states`;
|
|
7104
|
+
const time = `${r.elapsed.toFixed(1)}s`;
|
|
7105
|
+
console.log(` ${status} ${name} ${handlers.padEnd(14)} ${states.padEnd(14)} ${time}`);
|
|
7106
|
+
}
|
|
7107
|
+
console.log();
|
|
7108
|
+
const nonIntLabel = nonInterferenceValid ? color("✓ verified (no cross-subsystem state writes)", COLORS.green) : color("⚠ violations detected", COLORS.yellow);
|
|
7109
|
+
console.log(` Non-interference: ${nonIntLabel}`);
|
|
7110
|
+
console.log();
|
|
7111
|
+
const allPassed = results.every((r) => r.success);
|
|
7112
|
+
if (allPassed && nonInterferenceValid) {
|
|
7113
|
+
console.log(color("Compositional result: ✓ PASS", COLORS.green));
|
|
7114
|
+
console.log(color(" All subsystems verified independently. By non-interference,", COLORS.gray));
|
|
7115
|
+
console.log(color(" the full system satisfies all per-subsystem invariants.", COLORS.gray));
|
|
7116
|
+
} else if (allPassed && !nonInterferenceValid) {
|
|
7117
|
+
console.log(color("Compositional result: ⚠ PASS (with warnings)", COLORS.yellow));
|
|
7118
|
+
console.log(color(" All subsystems passed, but non-interference violations exist.", COLORS.gray));
|
|
7119
|
+
console.log(color(" Compositional soundness is not guaranteed.", COLORS.gray));
|
|
7120
|
+
} else {
|
|
7121
|
+
console.log(color("Compositional result: ✗ FAIL", COLORS.red));
|
|
7122
|
+
const failed = results.filter((r) => !r.success);
|
|
7123
|
+
for (const f of failed) {
|
|
7124
|
+
console.log(color(` Failed: ${f.name}`, COLORS.red));
|
|
7125
|
+
}
|
|
7126
|
+
process.exit(1);
|
|
7127
|
+
}
|
|
7128
|
+
console.log();
|
|
7129
|
+
}
|
|
6104
7130
|
async function loadVerificationConfig(configPath) {
|
|
6105
7131
|
const resolvedPath = path4.resolve(configPath);
|
|
6106
7132
|
const configModule = await import(`file://${resolvedPath}?t=${Date.now()}`);
|
|
@@ -6260,6 +7286,9 @@ ${color("Commands:", COLORS.blue)}
|
|
|
6260
7286
|
${color("bun verify --validate", COLORS.green)}
|
|
6261
7287
|
Validate existing configuration without running verification
|
|
6262
7288
|
|
|
7289
|
+
${color("bun verify --estimate", COLORS.green)}
|
|
7290
|
+
Estimate state space without running TLC
|
|
7291
|
+
|
|
6263
7292
|
${color("bun verify --help", COLORS.green)}
|
|
6264
7293
|
Show this help message
|
|
6265
7294
|
|
|
@@ -6314,4 +7343,4 @@ main().catch((error) => {
|
|
|
6314
7343
|
process.exit(1);
|
|
6315
7344
|
});
|
|
6316
7345
|
|
|
6317
|
-
//# debugId=
|
|
7346
|
+
//# debugId=7EE824400DEB8F7564756E2164756E21
|