@fairfox/polly 0.16.0 → 0.17.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 +583 -8
- package/dist/tools/verify/src/cli.js.map +10 -7
- package/dist/tools/verify/src/config.d.ts +1 -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 +83 -4
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +1 -1
|
@@ -1832,7 +1832,7 @@ var init_tla = __esm(() => {
|
|
|
1832
1832
|
this.addTemporalConstraints(config.tier2.temporalConstraints);
|
|
1833
1833
|
}
|
|
1834
1834
|
this.line("\\* State constraint to bound state space");
|
|
1835
|
-
this.addStateConstraint(config);
|
|
1835
|
+
this.addStateConstraint(config, _analysis);
|
|
1836
1836
|
this.line("=============================================================================");
|
|
1837
1837
|
}
|
|
1838
1838
|
addTemporalConstraints(constraints) {
|
|
@@ -1862,10 +1862,11 @@ var init_tla = __esm(() => {
|
|
|
1862
1862
|
});
|
|
1863
1863
|
}
|
|
1864
1864
|
}
|
|
1865
|
-
addStateConstraint(config) {
|
|
1865
|
+
addStateConstraint(config, analysis) {
|
|
1866
1866
|
const hasPerMessageBounds = config.messages.perMessageBounds && Object.keys(config.messages.perMessageBounds).length > 0;
|
|
1867
1867
|
const hasBoundedExploration = config.tier2?.boundedExploration?.maxDepth !== undefined;
|
|
1868
|
-
const
|
|
1868
|
+
const hasUserConstraints = (analysis.globalStateConstraints?.length ?? 0) > 0;
|
|
1869
|
+
const needsConjunction = hasPerMessageBounds || hasBoundedExploration || hasUserConstraints;
|
|
1869
1870
|
this.line("StateConstraint ==");
|
|
1870
1871
|
this.indent++;
|
|
1871
1872
|
if (needsConjunction) {
|
|
@@ -1879,6 +1880,13 @@ var init_tla = __esm(() => {
|
|
|
1879
1880
|
if (hasBoundedExploration && config.tier2?.boundedExploration?.maxDepth) {
|
|
1880
1881
|
this.line(`/\\ TLCGet("level") <= ${config.tier2.boundedExploration.maxDepth} \\* Tier 2: Bounded exploration`);
|
|
1881
1882
|
}
|
|
1883
|
+
if (hasUserConstraints && analysis.globalStateConstraints) {
|
|
1884
|
+
for (const constraint of analysis.globalStateConstraints) {
|
|
1885
|
+
const tlaExpr = this.tsExpressionToTLA(constraint.expression, false);
|
|
1886
|
+
this.line(`\\* ${constraint.name}${constraint.message ? `: ${constraint.message}` : ""}`);
|
|
1887
|
+
this.line(`/\\ \\A ctx \\in Contexts : (${tlaExpr})`);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1882
1890
|
} else {
|
|
1883
1891
|
this.line("Len(messages) <= MaxMessages");
|
|
1884
1892
|
}
|
|
@@ -1887,6 +1895,9 @@ var init_tla = __esm(() => {
|
|
|
1887
1895
|
if (hasBoundedExploration && config.tier2?.boundedExploration?.maxDepth) {
|
|
1888
1896
|
console.log(`[INFO] [TLAGenerator] Tier 2: Bounded exploration with maxDepth = ${config.tier2.boundedExploration.maxDepth}`);
|
|
1889
1897
|
}
|
|
1898
|
+
if (hasUserConstraints && analysis.globalStateConstraints) {
|
|
1899
|
+
console.log(`[INFO] [TLAGenerator] ${analysis.globalStateConstraints.length} user-defined state constraint(s) added to CONSTRAINT clause`);
|
|
1900
|
+
}
|
|
1890
1901
|
}
|
|
1891
1902
|
addDeliveredTracking() {
|
|
1892
1903
|
this.line("");
|
|
@@ -2366,6 +2377,400 @@ var init_docker = () => {};
|
|
|
2366
2377
|
import * as fs4 from "node:fs";
|
|
2367
2378
|
import * as path4 from "node:path";
|
|
2368
2379
|
|
|
2380
|
+
// tools/verify/src/analysis/expression-validator.ts
|
|
2381
|
+
var SIGNAL_VALUE_FIELD = /([a-zA-Z_]\w*)\.value\.([a-zA-Z_][\w.]*)/g;
|
|
2382
|
+
var SIGNAL_VALUE_BARE = /([a-zA-Z_]\w*)\.value\b(?!\.)/g;
|
|
2383
|
+
var STATE_DOT_FIELD = /state\.([a-zA-Z_][\w.]*)/g;
|
|
2384
|
+
var PAYLOAD_REF = /payload\.\w+/;
|
|
2385
|
+
function extractFieldRefs(expression) {
|
|
2386
|
+
const refs = [];
|
|
2387
|
+
for (const m of expression.matchAll(SIGNAL_VALUE_FIELD)) {
|
|
2388
|
+
const prefix = m[1];
|
|
2389
|
+
const suffix = m[2];
|
|
2390
|
+
refs.push(`${prefix}.${suffix}`);
|
|
2391
|
+
}
|
|
2392
|
+
for (const m of expression.matchAll(SIGNAL_VALUE_BARE)) {
|
|
2393
|
+
refs.push(m[1]);
|
|
2394
|
+
}
|
|
2395
|
+
for (const m of expression.matchAll(STATE_DOT_FIELD)) {
|
|
2396
|
+
refs.push(m[1]);
|
|
2397
|
+
}
|
|
2398
|
+
return [...new Set(refs)];
|
|
2399
|
+
}
|
|
2400
|
+
function fieldInConfig(fieldRef, configKeys, stateConfig) {
|
|
2401
|
+
if (configKeys.has(fieldRef))
|
|
2402
|
+
return true;
|
|
2403
|
+
const underscored = fieldRef.replace(/\./g, "_");
|
|
2404
|
+
if (configKeys.has(underscored))
|
|
2405
|
+
return true;
|
|
2406
|
+
const dotted = fieldRef.replace(/_/g, ".");
|
|
2407
|
+
if (configKeys.has(dotted))
|
|
2408
|
+
return true;
|
|
2409
|
+
if (stateConfig && fieldRef.endsWith(".length")) {
|
|
2410
|
+
const parent = fieldRef.slice(0, -".length".length);
|
|
2411
|
+
const parentEntry = resolveConfigEntry(parent, stateConfig);
|
|
2412
|
+
if (parentEntry !== undefined && isArrayLike(parentEntry))
|
|
2413
|
+
return true;
|
|
2414
|
+
}
|
|
2415
|
+
return false;
|
|
2416
|
+
}
|
|
2417
|
+
function resolveConfigEntry(fieldRef, stateConfig) {
|
|
2418
|
+
if (fieldRef in stateConfig)
|
|
2419
|
+
return stateConfig[fieldRef];
|
|
2420
|
+
const underscored = fieldRef.replace(/\./g, "_");
|
|
2421
|
+
if (underscored in stateConfig)
|
|
2422
|
+
return stateConfig[underscored];
|
|
2423
|
+
const dotted = fieldRef.replace(/_/g, ".");
|
|
2424
|
+
if (dotted in stateConfig)
|
|
2425
|
+
return stateConfig[dotted];
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
function isArrayLike(configEntry) {
|
|
2429
|
+
if (configEntry && typeof configEntry === "object") {
|
|
2430
|
+
const obj = configEntry;
|
|
2431
|
+
if (obj.type === "array")
|
|
2432
|
+
return true;
|
|
2433
|
+
if ("maxLength" in obj)
|
|
2434
|
+
return true;
|
|
2435
|
+
if ("maxSize" in obj)
|
|
2436
|
+
return true;
|
|
2437
|
+
}
|
|
2438
|
+
return false;
|
|
2439
|
+
}
|
|
2440
|
+
function isNullable(configEntry) {
|
|
2441
|
+
if (Array.isArray(configEntry)) {
|
|
2442
|
+
return configEntry.includes(null);
|
|
2443
|
+
}
|
|
2444
|
+
if (configEntry && typeof configEntry === "object") {
|
|
2445
|
+
const obj = configEntry;
|
|
2446
|
+
if (obj.abstract === true)
|
|
2447
|
+
return true;
|
|
2448
|
+
if (Array.isArray(obj.values)) {
|
|
2449
|
+
return obj.values.includes(null);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return false;
|
|
2453
|
+
}
|
|
2454
|
+
var UNSUPPORTED_METHODS = /\.(some|every|find|filter)\s*\(/;
|
|
2455
|
+
var OPTIONAL_CHAIN = /\?\./;
|
|
2456
|
+
var NULL_COMPARISON = /(===?\s*null|!==?\s*null|null\s*===?|null\s*!==?)/;
|
|
2457
|
+
function checkUnmodeledFields(expression, configKeys, stateConfig, messageType, conditionType, location) {
|
|
2458
|
+
const warnings = [];
|
|
2459
|
+
const refs = extractFieldRefs(expression);
|
|
2460
|
+
for (const ref of refs) {
|
|
2461
|
+
if (!fieldInConfig(ref, configKeys, stateConfig)) {
|
|
2462
|
+
warnings.push({
|
|
2463
|
+
kind: "unmodeled_field",
|
|
2464
|
+
message: `${conditionType}() in ${messageType} references unmodeled field '${ref}'`,
|
|
2465
|
+
messageType,
|
|
2466
|
+
conditionType,
|
|
2467
|
+
expression,
|
|
2468
|
+
location,
|
|
2469
|
+
suggestion: `Add '${ref}' to state config or remove from ${conditionType}()`
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
return warnings;
|
|
2474
|
+
}
|
|
2475
|
+
function checkUnsupportedMethods(expression, configKeys, stateConfig, messageType, conditionType, location) {
|
|
2476
|
+
const match = expression.match(UNSUPPORTED_METHODS);
|
|
2477
|
+
if (!match)
|
|
2478
|
+
return [];
|
|
2479
|
+
const refs = extractFieldRefs(expression);
|
|
2480
|
+
const allUnmodeled = refs.length > 0 && refs.every((r) => !fieldInConfig(r, configKeys, stateConfig));
|
|
2481
|
+
if (allUnmodeled)
|
|
2482
|
+
return [];
|
|
2483
|
+
return [
|
|
2484
|
+
{
|
|
2485
|
+
kind: "unsupported_method",
|
|
2486
|
+
message: `${conditionType}() in ${messageType} uses .${match[1]}() which cannot be translated to TLA+`,
|
|
2487
|
+
messageType,
|
|
2488
|
+
conditionType,
|
|
2489
|
+
expression,
|
|
2490
|
+
location,
|
|
2491
|
+
suggestion: `Rewrite without .${match[1]}() or model the check as a boolean state field`
|
|
2492
|
+
}
|
|
2493
|
+
];
|
|
2494
|
+
}
|
|
2495
|
+
function checkOptionalChaining(expression, messageType, conditionType, location) {
|
|
2496
|
+
if (!OPTIONAL_CHAIN.test(expression))
|
|
2497
|
+
return [];
|
|
2498
|
+
return [
|
|
2499
|
+
{
|
|
2500
|
+
kind: "optional_chaining",
|
|
2501
|
+
message: `${conditionType}() in ${messageType} uses optional chaining (?.)`,
|
|
2502
|
+
messageType,
|
|
2503
|
+
conditionType,
|
|
2504
|
+
expression,
|
|
2505
|
+
location,
|
|
2506
|
+
suggestion: "Optional chaining translation is fragile — consider explicit null checks or restructuring"
|
|
2507
|
+
}
|
|
2508
|
+
];
|
|
2509
|
+
}
|
|
2510
|
+
function checkNullComparisons(expression, stateConfig, configKeys, messageType, conditionType, location) {
|
|
2511
|
+
if (!NULL_COMPARISON.test(expression))
|
|
2512
|
+
return [];
|
|
2513
|
+
const warnings = [];
|
|
2514
|
+
const refs = extractFieldRefs(expression);
|
|
2515
|
+
for (const ref of refs) {
|
|
2516
|
+
if (!fieldInConfig(ref, configKeys, stateConfig))
|
|
2517
|
+
continue;
|
|
2518
|
+
const entry = resolveConfigEntry(ref, stateConfig);
|
|
2519
|
+
if (entry !== undefined && !isNullable(entry)) {
|
|
2520
|
+
warnings.push({
|
|
2521
|
+
kind: "null_comparison",
|
|
2522
|
+
message: `${conditionType}() in ${messageType} compares '${ref}' to null but field is not nullable`,
|
|
2523
|
+
messageType,
|
|
2524
|
+
conditionType,
|
|
2525
|
+
expression,
|
|
2526
|
+
location,
|
|
2527
|
+
suggestion: `Add null to '${ref}' values in state config, or remove the null check`
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
return warnings;
|
|
2532
|
+
}
|
|
2533
|
+
var WEAK_NEGATION = /([a-zA-Z_][\w.]*(?:\.value\.[\w.]+|\.value\b|))?\s*!==?\s*("[^"]*"|'[^']*'|\d+|true|false|null)/;
|
|
2534
|
+
function checkWeakPostconditions(expression, handler, messageType, conditionType, location) {
|
|
2535
|
+
if (conditionType !== "ensures")
|
|
2536
|
+
return [];
|
|
2537
|
+
const match = expression.match(WEAK_NEGATION);
|
|
2538
|
+
if (!match)
|
|
2539
|
+
return [];
|
|
2540
|
+
const refs = extractFieldRefs(expression);
|
|
2541
|
+
if (refs.length === 0)
|
|
2542
|
+
return [];
|
|
2543
|
+
for (const ref of refs) {
|
|
2544
|
+
const underscoredRef = ref.replace(/\./g, "_");
|
|
2545
|
+
const hasAssignment = handler.assignments.some((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
|
|
2546
|
+
if (hasAssignment) {
|
|
2547
|
+
const assignment = handler.assignments.find((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
|
|
2548
|
+
const assignedValue = assignment ? JSON.stringify(assignment.value) : "<value>";
|
|
2549
|
+
return [
|
|
2550
|
+
{
|
|
2551
|
+
kind: "weak_postcondition",
|
|
2552
|
+
message: `ensures() in ${messageType} uses !== for '${ref}' which has a concrete assignment`,
|
|
2553
|
+
messageType,
|
|
2554
|
+
conditionType,
|
|
2555
|
+
expression,
|
|
2556
|
+
location,
|
|
2557
|
+
suggestion: `Consider ensures(${ref} === ${assignedValue}) for a stronger postcondition`
|
|
2558
|
+
}
|
|
2559
|
+
];
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return [];
|
|
2563
|
+
}
|
|
2564
|
+
function validateExpressions(handlers, stateConfig) {
|
|
2565
|
+
const warnings = [];
|
|
2566
|
+
let validCount = 0;
|
|
2567
|
+
const configKeys = new Set(Object.keys(stateConfig));
|
|
2568
|
+
for (const handler of handlers) {
|
|
2569
|
+
const conditions = [
|
|
2570
|
+
...handler.preconditions.map((c) => ({
|
|
2571
|
+
cond: c,
|
|
2572
|
+
type: "requires"
|
|
2573
|
+
})),
|
|
2574
|
+
...handler.postconditions.map((c) => ({
|
|
2575
|
+
cond: c,
|
|
2576
|
+
type: "ensures"
|
|
2577
|
+
}))
|
|
2578
|
+
];
|
|
2579
|
+
for (const { cond, type } of conditions) {
|
|
2580
|
+
const refs = extractFieldRefs(cond.expression);
|
|
2581
|
+
const isPayloadOnly = refs.length === 0 && PAYLOAD_REF.test(cond.expression);
|
|
2582
|
+
const loc = {
|
|
2583
|
+
file: handler.location.file,
|
|
2584
|
+
line: cond.location.line,
|
|
2585
|
+
column: cond.location.column
|
|
2586
|
+
};
|
|
2587
|
+
const condWarnings = [];
|
|
2588
|
+
if (!isPayloadOnly) {
|
|
2589
|
+
condWarnings.push(...checkUnmodeledFields(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
|
|
2590
|
+
}
|
|
2591
|
+
condWarnings.push(...checkUnsupportedMethods(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
|
|
2592
|
+
condWarnings.push(...checkOptionalChaining(cond.expression, handler.messageType, type, loc));
|
|
2593
|
+
condWarnings.push(...checkNullComparisons(cond.expression, stateConfig, configKeys, handler.messageType, type, loc));
|
|
2594
|
+
condWarnings.push(...checkWeakPostconditions(cond.expression, handler, handler.messageType, type, loc));
|
|
2595
|
+
if (condWarnings.length === 0) {
|
|
2596
|
+
validCount++;
|
|
2597
|
+
} else {
|
|
2598
|
+
warnings.push(...condWarnings);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
return { warnings, validCount, warnCount: warnings.length };
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// tools/verify/src/config/types.ts
|
|
2606
|
+
function isAdapterConfig(config) {
|
|
2607
|
+
return "adapter" in config;
|
|
2608
|
+
}
|
|
2609
|
+
function isLegacyConfig(config) {
|
|
2610
|
+
return "messages" in config && !("adapter" in config);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// tools/verify/src/analysis/state-space-estimator.ts
|
|
2614
|
+
function typedFieldCardinality(name, obj) {
|
|
2615
|
+
if (!("type" in obj))
|
|
2616
|
+
return null;
|
|
2617
|
+
switch (obj.type) {
|
|
2618
|
+
case "boolean":
|
|
2619
|
+
return { name, cardinality: 2, kind: "boolean" };
|
|
2620
|
+
case "enum":
|
|
2621
|
+
if (Array.isArray(obj.values)) {
|
|
2622
|
+
return { name, cardinality: obj.values.length, kind: "enum" };
|
|
2623
|
+
}
|
|
2624
|
+
return { name, cardinality: "unbounded", kind: "enum" };
|
|
2625
|
+
case "number":
|
|
2626
|
+
if (typeof obj.min === "number" && typeof obj.max === "number") {
|
|
2627
|
+
return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
|
|
2628
|
+
}
|
|
2629
|
+
return { name, cardinality: "unbounded", kind: "number" };
|
|
2630
|
+
case "array":
|
|
2631
|
+
return { name, cardinality: "unbounded", kind: "array" };
|
|
2632
|
+
case "string":
|
|
2633
|
+
return { name, cardinality: "unbounded", kind: "string" };
|
|
2634
|
+
default:
|
|
2635
|
+
return null;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
function legacyFieldCardinality(name, obj) {
|
|
2639
|
+
if ("values" in obj && Array.isArray(obj.values)) {
|
|
2640
|
+
const base = obj.values.length;
|
|
2641
|
+
const extra = obj.abstract === true ? 1 : 0;
|
|
2642
|
+
return {
|
|
2643
|
+
name,
|
|
2644
|
+
cardinality: base + extra,
|
|
2645
|
+
kind: obj.abstract ? "enum (abstract)" : "enum (values)"
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
if ("maxLength" in obj && !("type" in obj)) {
|
|
2649
|
+
return { name, cardinality: "unbounded", kind: "array" };
|
|
2650
|
+
}
|
|
2651
|
+
if ("min" in obj && "max" in obj && typeof obj.min === "number" && typeof obj.max === "number" && !("type" in obj)) {
|
|
2652
|
+
return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
|
|
2653
|
+
}
|
|
2654
|
+
return null;
|
|
2655
|
+
}
|
|
2656
|
+
function fieldCardinality(name, value) {
|
|
2657
|
+
if (Array.isArray(value)) {
|
|
2658
|
+
return { name, cardinality: value.length, kind: "enum (literal)" };
|
|
2659
|
+
}
|
|
2660
|
+
if (typeof value !== "object" || value === null) {
|
|
2661
|
+
return { name, cardinality: "unbounded", kind: "unknown" };
|
|
2662
|
+
}
|
|
2663
|
+
const obj = value;
|
|
2664
|
+
return typedFieldCardinality(name, obj) ?? legacyFieldCardinality(name, obj) ?? { name, cardinality: "unbounded", kind: "unknown" };
|
|
2665
|
+
}
|
|
2666
|
+
function permutations(n, k) {
|
|
2667
|
+
if (k > n)
|
|
2668
|
+
return 1;
|
|
2669
|
+
let result = 1;
|
|
2670
|
+
for (let i = 0;i < k; i++) {
|
|
2671
|
+
result *= n - i;
|
|
2672
|
+
}
|
|
2673
|
+
return result;
|
|
2674
|
+
}
|
|
2675
|
+
function getHandlerCount(config, analysis) {
|
|
2676
|
+
if (isLegacyConfig(config)) {
|
|
2677
|
+
const msgs = config.messages;
|
|
2678
|
+
if (msgs.include) {
|
|
2679
|
+
return msgs.include.length;
|
|
2680
|
+
}
|
|
2681
|
+
if (msgs.exclude) {
|
|
2682
|
+
return Math.max(0, analysis.handlers.length - msgs.exclude.length);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
return analysis.handlers.length;
|
|
2686
|
+
}
|
|
2687
|
+
function getMaxInFlight(config) {
|
|
2688
|
+
if (isLegacyConfig(config)) {
|
|
2689
|
+
return config.messages.maxInFlight ?? 1;
|
|
2690
|
+
}
|
|
2691
|
+
if (isAdapterConfig(config)) {
|
|
2692
|
+
return config.bounds?.maxInFlight ?? 1;
|
|
2693
|
+
}
|
|
2694
|
+
return 1;
|
|
2695
|
+
}
|
|
2696
|
+
function getMaxTabs(config) {
|
|
2697
|
+
if (isLegacyConfig(config)) {
|
|
2698
|
+
return config.messages.maxTabs ?? 1;
|
|
2699
|
+
}
|
|
2700
|
+
return 1;
|
|
2701
|
+
}
|
|
2702
|
+
function getFeasibility(states) {
|
|
2703
|
+
if (states < 1e5)
|
|
2704
|
+
return "trivial";
|
|
2705
|
+
if (states <= 1e6)
|
|
2706
|
+
return "feasible";
|
|
2707
|
+
if (states <= 1e7)
|
|
2708
|
+
return "slow";
|
|
2709
|
+
return "infeasible";
|
|
2710
|
+
}
|
|
2711
|
+
function feasibilityLabel(f) {
|
|
2712
|
+
switch (f) {
|
|
2713
|
+
case "trivial":
|
|
2714
|
+
return "trivial (should complete in seconds)";
|
|
2715
|
+
case "feasible":
|
|
2716
|
+
return "feasible (should complete in minutes)";
|
|
2717
|
+
case "slow":
|
|
2718
|
+
return "slow (may take 10–30 minutes)";
|
|
2719
|
+
case "infeasible":
|
|
2720
|
+
return "infeasible (likely won't terminate)";
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
function estimateStateSpace(config, analysis) {
|
|
2724
|
+
const state = config.state;
|
|
2725
|
+
const fields = [];
|
|
2726
|
+
const warnings = [];
|
|
2727
|
+
const suggestions = [];
|
|
2728
|
+
for (const [name, value] of Object.entries(state)) {
|
|
2729
|
+
fields.push(fieldCardinality(name, value));
|
|
2730
|
+
}
|
|
2731
|
+
const unboundedFields = fields.filter((f) => f.cardinality === "unbounded");
|
|
2732
|
+
const boundedFields = fields.filter((f) => f.cardinality !== "unbounded");
|
|
2733
|
+
const fieldProduct = boundedFields.length > 0 ? boundedFields.reduce((acc, f) => acc * f.cardinality, 1) : 1;
|
|
2734
|
+
if (unboundedFields.length > 0) {
|
|
2735
|
+
warnings.push(`${unboundedFields.length} field(s) have unbounded domains: ${unboundedFields.map((f) => f.name).join(", ")}`);
|
|
2736
|
+
suggestions.push(`Fields ${unboundedFields.map((f) => f.name).join(", ")} have unbounded domains — their actual impact depends on handler logic`);
|
|
2737
|
+
}
|
|
2738
|
+
const handlerCount = getHandlerCount(config, analysis);
|
|
2739
|
+
const maxInFlight = getMaxInFlight(config);
|
|
2740
|
+
const maxTabs = getMaxTabs(config);
|
|
2741
|
+
const contextCount = maxTabs + 1;
|
|
2742
|
+
const totalStateSpace = fieldProduct ** contextCount;
|
|
2743
|
+
const interleavingFactor = permutations(handlerCount, maxInFlight);
|
|
2744
|
+
const estimatedStates = totalStateSpace * interleavingFactor;
|
|
2745
|
+
const feasibility = getFeasibility(estimatedStates);
|
|
2746
|
+
if (handlerCount > 15) {
|
|
2747
|
+
suggestions.push("Consider splitting into subsystems");
|
|
2748
|
+
}
|
|
2749
|
+
for (const f of boundedFields) {
|
|
2750
|
+
if (f.cardinality > 50) {
|
|
2751
|
+
suggestions.push(`Consider reducing bounds for field "${f.name}" (${f.cardinality} values)`);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
if (maxInFlight > 2) {
|
|
2755
|
+
const reducedInterleaving = permutations(handlerCount, 2);
|
|
2756
|
+
const reduction = reducedInterleaving > 0 ? Math.round(interleavingFactor / reducedInterleaving) : 1;
|
|
2757
|
+
suggestions.push(`Reducing maxInFlight from ${maxInFlight} to 2 would reduce interleaving by ~${reduction}x`);
|
|
2758
|
+
}
|
|
2759
|
+
return {
|
|
2760
|
+
fields,
|
|
2761
|
+
fieldProduct,
|
|
2762
|
+
handlerCount,
|
|
2763
|
+
maxInFlight,
|
|
2764
|
+
contextCount,
|
|
2765
|
+
totalStateSpace,
|
|
2766
|
+
interleavingFactor,
|
|
2767
|
+
estimatedStates,
|
|
2768
|
+
feasibility,
|
|
2769
|
+
warnings,
|
|
2770
|
+
suggestions
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2369
2774
|
// tools/verify/src/codegen/config.ts
|
|
2370
2775
|
class ConfigGenerator {
|
|
2371
2776
|
lines = [];
|
|
@@ -3717,13 +4122,14 @@ class HandlerExtractor {
|
|
|
3717
4122
|
const messageTypes = new Set;
|
|
3718
4123
|
const invalidMessageTypes = new Set;
|
|
3719
4124
|
const stateConstraints = [];
|
|
4125
|
+
const globalStateConstraints = [];
|
|
3720
4126
|
const verifiedStates = [];
|
|
3721
4127
|
this.warnings = [];
|
|
3722
4128
|
const allSourceFiles = this.project.getSourceFiles();
|
|
3723
4129
|
const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
|
|
3724
4130
|
this.debugLogSourceFiles(allSourceFiles, entryPoints);
|
|
3725
4131
|
for (const entryPoint of entryPoints) {
|
|
3726
|
-
this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
|
|
4132
|
+
this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
|
|
3727
4133
|
}
|
|
3728
4134
|
if (verifiedStates.length > 0) {
|
|
3729
4135
|
if (process.env["POLLY_DEBUG"]) {
|
|
@@ -3753,11 +4159,12 @@ class HandlerExtractor {
|
|
|
3753
4159
|
handlers,
|
|
3754
4160
|
messageTypes,
|
|
3755
4161
|
stateConstraints,
|
|
4162
|
+
globalStateConstraints,
|
|
3756
4163
|
verifiedStates,
|
|
3757
4164
|
warnings: this.warnings
|
|
3758
4165
|
};
|
|
3759
4166
|
}
|
|
3760
|
-
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
|
|
4167
|
+
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates) {
|
|
3761
4168
|
const filePath = sourceFile.getFilePath();
|
|
3762
4169
|
if (this.analyzedFiles.has(filePath)) {
|
|
3763
4170
|
return;
|
|
@@ -3771,6 +4178,8 @@ class HandlerExtractor {
|
|
|
3771
4178
|
this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
|
|
3772
4179
|
const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
|
|
3773
4180
|
stateConstraints.push(...fileConstraints);
|
|
4181
|
+
const fileGlobalConstraints = this.extractGlobalStateConstraintsFromFile(sourceFile);
|
|
4182
|
+
globalStateConstraints.push(...fileGlobalConstraints);
|
|
3774
4183
|
const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
|
|
3775
4184
|
verifiedStates.push(...fileVerifiedStates);
|
|
3776
4185
|
const importDeclarations = sourceFile.getImportDeclarations();
|
|
@@ -3784,7 +4193,7 @@ class HandlerExtractor {
|
|
|
3784
4193
|
}
|
|
3785
4194
|
continue;
|
|
3786
4195
|
}
|
|
3787
|
-
this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
|
|
4196
|
+
this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
|
|
3788
4197
|
} else if (process.env["POLLY_DEBUG"]) {
|
|
3789
4198
|
const specifier = importDecl.getModuleSpecifierValue();
|
|
3790
4199
|
if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
|
|
@@ -5119,6 +5528,81 @@ class HandlerExtractor {
|
|
|
5119
5528
|
});
|
|
5120
5529
|
return constraints;
|
|
5121
5530
|
}
|
|
5531
|
+
extractGlobalStateConstraintsFromFile(sourceFile) {
|
|
5532
|
+
const constraints = [];
|
|
5533
|
+
const filePath = sourceFile.getFilePath();
|
|
5534
|
+
sourceFile.forEachDescendant((node) => {
|
|
5535
|
+
const constraint = this.recognizeGlobalStateConstraint(node, filePath);
|
|
5536
|
+
if (constraint) {
|
|
5537
|
+
constraints.push(constraint);
|
|
5538
|
+
}
|
|
5539
|
+
});
|
|
5540
|
+
return constraints;
|
|
5541
|
+
}
|
|
5542
|
+
recognizeGlobalStateConstraint(node, filePath) {
|
|
5543
|
+
if (!Node2.isCallExpression(node)) {
|
|
5544
|
+
return null;
|
|
5545
|
+
}
|
|
5546
|
+
const expression = node.getExpression();
|
|
5547
|
+
if (!Node2.isIdentifier(expression)) {
|
|
5548
|
+
return null;
|
|
5549
|
+
}
|
|
5550
|
+
const functionName = expression.getText();
|
|
5551
|
+
if (functionName !== "stateConstraint") {
|
|
5552
|
+
return null;
|
|
5553
|
+
}
|
|
5554
|
+
const args = node.getArguments();
|
|
5555
|
+
if (args.length < 2) {
|
|
5556
|
+
return null;
|
|
5557
|
+
}
|
|
5558
|
+
const nameArg = args[0];
|
|
5559
|
+
if (!Node2.isStringLiteral(nameArg)) {
|
|
5560
|
+
return null;
|
|
5561
|
+
}
|
|
5562
|
+
const name = nameArg.getLiteralValue();
|
|
5563
|
+
const predicateArg = args[1];
|
|
5564
|
+
if (!Node2.isArrowFunction(predicateArg)) {
|
|
5565
|
+
return null;
|
|
5566
|
+
}
|
|
5567
|
+
const body = predicateArg.getBody();
|
|
5568
|
+
let expressionText;
|
|
5569
|
+
if (Node2.isBlock(body)) {
|
|
5570
|
+
const returnStatement = body.getStatements().find((s) => Node2.isReturnStatement(s));
|
|
5571
|
+
if (!returnStatement || !Node2.isReturnStatement(returnStatement)) {
|
|
5572
|
+
return null;
|
|
5573
|
+
}
|
|
5574
|
+
const returnExpr = returnStatement.getExpression();
|
|
5575
|
+
if (!returnExpr) {
|
|
5576
|
+
return null;
|
|
5577
|
+
}
|
|
5578
|
+
expressionText = returnExpr.getText();
|
|
5579
|
+
} else {
|
|
5580
|
+
expressionText = body.getText();
|
|
5581
|
+
}
|
|
5582
|
+
let message;
|
|
5583
|
+
if (args.length >= 3) {
|
|
5584
|
+
const optionsArg = args[2];
|
|
5585
|
+
if (Node2.isObjectLiteralExpression(optionsArg)) {
|
|
5586
|
+
for (const prop of optionsArg.getProperties()) {
|
|
5587
|
+
if (Node2.isPropertyAssignment(prop) && prop.getName() === "message") {
|
|
5588
|
+
const value = prop.getInitializer();
|
|
5589
|
+
if (value && Node2.isStringLiteral(value)) {
|
|
5590
|
+
message = value.getLiteralValue();
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
return {
|
|
5597
|
+
name,
|
|
5598
|
+
expression: expressionText,
|
|
5599
|
+
message,
|
|
5600
|
+
location: {
|
|
5601
|
+
file: filePath,
|
|
5602
|
+
line: node.getStartLineNumber()
|
|
5603
|
+
}
|
|
5604
|
+
};
|
|
5605
|
+
}
|
|
5122
5606
|
recognizeStateConstraint(node, filePath) {
|
|
5123
5607
|
if (!Node2.isCallExpression(node)) {
|
|
5124
5608
|
return [];
|
|
@@ -5433,7 +5917,8 @@ class TypeExtractor {
|
|
|
5433
5917
|
messageTypes: validMessageTypes,
|
|
5434
5918
|
fields,
|
|
5435
5919
|
handlers: completeHandlers,
|
|
5436
|
-
stateConstraints: handlerAnalysis.stateConstraints
|
|
5920
|
+
stateConstraints: handlerAnalysis.stateConstraints,
|
|
5921
|
+
globalStateConstraints: handlerAnalysis.globalStateConstraints
|
|
5437
5922
|
};
|
|
5438
5923
|
}
|
|
5439
5924
|
extractHandlerAnalysis() {
|
|
@@ -5864,6 +6349,10 @@ async function main() {
|
|
|
5864
6349
|
case "validate":
|
|
5865
6350
|
await validateCommand();
|
|
5866
6351
|
break;
|
|
6352
|
+
case "--estimate":
|
|
6353
|
+
case "estimate":
|
|
6354
|
+
await estimateCommand();
|
|
6355
|
+
break;
|
|
5867
6356
|
case "--help":
|
|
5868
6357
|
case "help":
|
|
5869
6358
|
showHelp();
|
|
@@ -5980,6 +6469,70 @@ async function validateCommand() {
|
|
|
5980
6469
|
`, COLORS.red));
|
|
5981
6470
|
process.exit(1);
|
|
5982
6471
|
}
|
|
6472
|
+
async function estimateCommand() {
|
|
6473
|
+
const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
|
|
6474
|
+
console.log(color(`
|
|
6475
|
+
\uD83D\uDCCA Estimating state space...
|
|
6476
|
+
`, COLORS.blue));
|
|
6477
|
+
const validation = validateConfig(configPath);
|
|
6478
|
+
if (!validation.valid) {
|
|
6479
|
+
const errors = validation.issues.filter((i) => i.severity === "error");
|
|
6480
|
+
console.log(color(`❌ Configuration incomplete (${errors.length} error(s))
|
|
6481
|
+
`, COLORS.red));
|
|
6482
|
+
for (const error of errors.slice(0, 3)) {
|
|
6483
|
+
console.log(color(` • ${error.message}`, COLORS.red));
|
|
6484
|
+
}
|
|
6485
|
+
console.log(`
|
|
6486
|
+
Run 'polly verify --validate' to see all issues`);
|
|
6487
|
+
process.exit(1);
|
|
6488
|
+
}
|
|
6489
|
+
const config = await loadVerificationConfig(configPath);
|
|
6490
|
+
const analysis = await runCodebaseAnalysis();
|
|
6491
|
+
const typedConfig = config;
|
|
6492
|
+
const typedAnalysis = analysis;
|
|
6493
|
+
const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
|
|
6494
|
+
if (exprValidation.warnings.length > 0) {
|
|
6495
|
+
displayExpressionWarnings(exprValidation);
|
|
6496
|
+
}
|
|
6497
|
+
const estimate = estimateStateSpace(typedConfig, typedAnalysis);
|
|
6498
|
+
displayEstimate(estimate);
|
|
6499
|
+
}
|
|
6500
|
+
function displayEstimate(estimate) {
|
|
6501
|
+
console.log(color(`State space estimate:
|
|
6502
|
+
`, COLORS.blue));
|
|
6503
|
+
console.log(color(" Fields:", COLORS.blue));
|
|
6504
|
+
for (const field of estimate.fields) {
|
|
6505
|
+
const name = field.name.padEnd(28);
|
|
6506
|
+
const kind = field.kind.padEnd(18);
|
|
6507
|
+
const card = field.cardinality === "unbounded" ? color("(excluded — unbounded)", COLORS.yellow) : `${field.cardinality} values`;
|
|
6508
|
+
console.log(` ${name} ${kind} ${card}`);
|
|
6509
|
+
}
|
|
6510
|
+
console.log();
|
|
6511
|
+
console.log(` Field combinations: ${color(String(estimate.fieldProduct), COLORS.green)}`);
|
|
6512
|
+
console.log(` Handlers: ${estimate.handlerCount}`);
|
|
6513
|
+
console.log(` Max in-flight: ${estimate.maxInFlight}`);
|
|
6514
|
+
console.log(` Contexts: ${estimate.contextCount} (${estimate.contextCount - 1} tab${estimate.contextCount - 1 !== 1 ? "s" : ""} + background)`);
|
|
6515
|
+
console.log(` Interleaving factor: ${estimate.interleavingFactor}`);
|
|
6516
|
+
console.log();
|
|
6517
|
+
console.log(` Estimated states: ${color(`~${estimate.estimatedStates.toLocaleString()}`, COLORS.green)}`);
|
|
6518
|
+
const feasColor = estimate.feasibility === "trivial" || estimate.feasibility === "feasible" ? COLORS.green : estimate.feasibility === "slow" ? COLORS.yellow : COLORS.red;
|
|
6519
|
+
console.log();
|
|
6520
|
+
console.log(` Assessment: ${color(feasibilityLabel(estimate.feasibility), feasColor)}`);
|
|
6521
|
+
if (estimate.warnings.length > 0) {
|
|
6522
|
+
console.log();
|
|
6523
|
+
for (const w of estimate.warnings) {
|
|
6524
|
+
console.log(color(` ⚠️ ${w}`, COLORS.yellow));
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
if (estimate.suggestions.length > 0) {
|
|
6528
|
+
console.log();
|
|
6529
|
+
console.log(color(" Suggestions:", COLORS.blue));
|
|
6530
|
+
for (const s of estimate.suggestions) {
|
|
6531
|
+
console.log(color(` • ${s}`, COLORS.gray));
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6534
|
+
console.log();
|
|
6535
|
+
}
|
|
5983
6536
|
function displayValidationErrors(errors) {
|
|
5984
6537
|
if (errors.length === 0)
|
|
5985
6538
|
return;
|
|
@@ -6011,6 +6564,19 @@ function displayValidationWarnings(warnings) {
|
|
|
6011
6564
|
console.log();
|
|
6012
6565
|
}
|
|
6013
6566
|
}
|
|
6567
|
+
function displayExpressionWarnings(result) {
|
|
6568
|
+
if (result.warnings.length === 0)
|
|
6569
|
+
return;
|
|
6570
|
+
console.log(color(`⚠️ Found ${result.warnCount} expression warning(s):
|
|
6571
|
+
`, COLORS.yellow));
|
|
6572
|
+
for (const w of result.warnings) {
|
|
6573
|
+
console.log(color(` ⚠ ${w.message}`, COLORS.yellow));
|
|
6574
|
+
console.log(color(` ${w.expression}`, COLORS.gray));
|
|
6575
|
+
console.log(color(` at ${w.location.file}:${w.location.line}`, COLORS.gray));
|
|
6576
|
+
console.log(color(` → ${w.suggestion}`, COLORS.yellow));
|
|
6577
|
+
console.log();
|
|
6578
|
+
}
|
|
6579
|
+
}
|
|
6014
6580
|
async function verifyCommand() {
|
|
6015
6581
|
const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
|
|
6016
6582
|
console.log(color(`
|
|
@@ -6073,6 +6639,12 @@ function getMaxDepth(config) {
|
|
|
6073
6639
|
async function runFullVerification(configPath) {
|
|
6074
6640
|
const config = await loadVerificationConfig(configPath);
|
|
6075
6641
|
const analysis = await runCodebaseAnalysis();
|
|
6642
|
+
const typedConfig = config;
|
|
6643
|
+
const typedAnalysis = analysis;
|
|
6644
|
+
const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
|
|
6645
|
+
if (exprValidation.warnings.length > 0) {
|
|
6646
|
+
displayExpressionWarnings(exprValidation);
|
|
6647
|
+
}
|
|
6076
6648
|
const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
|
|
6077
6649
|
findAndCopyBaseSpec(specDir);
|
|
6078
6650
|
console.log(color("✓ Specification generated", COLORS.green));
|
|
@@ -6260,6 +6832,9 @@ ${color("Commands:", COLORS.blue)}
|
|
|
6260
6832
|
${color("bun verify --validate", COLORS.green)}
|
|
6261
6833
|
Validate existing configuration without running verification
|
|
6262
6834
|
|
|
6835
|
+
${color("bun verify --estimate", COLORS.green)}
|
|
6836
|
+
Estimate state space without running TLC
|
|
6837
|
+
|
|
6263
6838
|
${color("bun verify --help", COLORS.green)}
|
|
6264
6839
|
Show this help message
|
|
6265
6840
|
|
|
@@ -6314,4 +6889,4 @@ main().catch((error) => {
|
|
|
6314
6889
|
process.exit(1);
|
|
6315
6890
|
});
|
|
6316
6891
|
|
|
6317
|
-
//# debugId=
|
|
6892
|
+
//# debugId=35FC1B345D5CADC664756E2164756E21
|