@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
|
@@ -61,4 +61,4 @@ type UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationC
|
|
|
61
61
|
* ```
|
|
62
62
|
*/
|
|
63
63
|
export declare function defineVerification<T extends UnifiedVerificationConfig>(config: T): T;
|
|
64
|
-
export { $constraints, ensures, hasLength, inRange, oneOf, requires } from "./primitives/index.js";
|
|
64
|
+
export { $constraints, ensures, hasLength, inRange, oneOf, requires, stateConstraint, } from "./primitives/index.js";
|
|
@@ -57,6 +57,7 @@ function $constraints(stateField, constraints, options) {
|
|
|
57
57
|
}).catch(() => {});
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
function stateConstraint(name, predicate, options) {}
|
|
60
61
|
|
|
61
62
|
// tools/verify/src/config.ts
|
|
62
63
|
function defineVerification(config) {
|
|
@@ -80,6 +81,7 @@ function defineVerification(config) {
|
|
|
80
81
|
return config;
|
|
81
82
|
}
|
|
82
83
|
export {
|
|
84
|
+
stateConstraint,
|
|
83
85
|
requires,
|
|
84
86
|
oneOf,
|
|
85
87
|
inRange,
|
|
@@ -89,4 +91,4 @@ export {
|
|
|
89
91
|
$constraints
|
|
90
92
|
};
|
|
91
93
|
|
|
92
|
-
//# debugId=
|
|
94
|
+
//# debugId=465F57F5AB1A6BBD64756E2164756E21
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../tools/verify/src/primitives/index.ts", "../tools/verify/src/config.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// Verification primitives for formal verification\n// These are runtime no-ops but extracted during verification\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification and optional runtime checking.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers during verification.\n * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.\n *\n * @example\n * // Verification only (TLA+ generation)\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * });\n *\n * @example\n * // Runtime enforcement (function predicates)\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: {\n * requires: (state) => state.loggedIn === true,\n * message: \"Must be logged in to logout\"\n * },\n * }, { runtime: true });\n */\nexport function $constraints(\n stateField: string,\n constraints: Record<\n string,\n {\n requires?: string | ((state: unknown) => boolean);\n ensures?: string | ((state: unknown) => boolean);\n message?: string;\n }\n >,\n options?: { runtime?: boolean }\n): void {\n // Register constraints for runtime checking if enabled\n if (options?.runtime) {\n // Import dynamically to avoid circular dependencies\n // This is safe because it only happens at runtime, not during static analysis\n // @ts-expect-error - Dynamic import path resolves correctly at runtime\n import(\"../../../src/shared/lib/constraints.js\")\n .then(({ registerConstraints }) => {\n registerConstraints(stateField, constraints);\n })\n .catch(() => {\n // Silently ignore - constraints module may not be available during static analysis\n });\n }\n\n // For verification: Still a no-op at runtime\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n};\n",
|
|
6
|
-
"// ═══════════════════════════════════════════════════════════════\n// Configuration Helper for @fairfox/polly/verify\n// ═══════════════════════════════════════════════════════════════\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// ─────────────────────────────────────────────────────────────────\n// Configuration Types (inlined to avoid heavy dependencies)\n// ─────────────────────────────────────────────────────────────────\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n };\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export verification primitives for user code\nexport {
|
|
5
|
+
"// Verification primitives for formal verification\n// These are runtime no-ops but extracted during verification\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification and optional runtime checking.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers during verification.\n * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.\n *\n * @example\n * // Verification only (TLA+ generation)\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * });\n *\n * @example\n * // Runtime enforcement (function predicates)\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: {\n * requires: (state) => state.loggedIn === true,\n * message: \"Must be logged in to logout\"\n * },\n * }, { runtime: true });\n */\nexport function $constraints(\n stateField: string,\n constraints: Record<\n string,\n {\n requires?: string | ((state: unknown) => boolean);\n ensures?: string | ((state: unknown) => boolean);\n message?: string;\n }\n >,\n options?: { runtime?: boolean }\n): void {\n // Register constraints for runtime checking if enabled\n if (options?.runtime) {\n // Import dynamically to avoid circular dependencies\n // This is safe because it only happens at runtime, not during static analysis\n // @ts-expect-error - Dynamic import path resolves correctly at runtime\n import(\"../../../src/shared/lib/constraints.js\")\n .then(({ registerConstraints }) => {\n registerConstraints(stateField, constraints);\n })\n .catch(() => {\n // Silently ignore - constraints module may not be available during static analysis\n });\n }\n\n // For verification: Still a no-op at runtime\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n/**\n * Declare a global state constraint that prunes structurally impossible states.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLC CONSTRAINT clause, discarding states\n * that violate the predicate from the exploration queue entirely.\n *\n * Unlike `invariant()` (which checks but still explores), `stateConstraint()`\n * prevents the model checker from ever reaching the pruned states.\n *\n * @example\n * stateConstraint(\"LeaderRequiresConnection\", () =>\n * !connectionState.value.isLeader || connectionState.value.status === \"connected\"\n * )\n */\nexport function stateConstraint(\n name: string,\n predicate: () => boolean,\n options?: { message?: string }\n): void {\n void name;\n void predicate;\n void options;\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n stateConstraint,\n};\n",
|
|
6
|
+
"// ═══════════════════════════════════════════════════════════════\n// Configuration Helper for @fairfox/polly/verify\n// ═══════════════════════════════════════════════════════════════\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// ─────────────────────────────────────────────────────────────────\n// Configuration Types (inlined to avoid heavy dependencies)\n// ─────────────────────────────────────────────────────────────────\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n };\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export verification primitives for user code\nexport {\n $constraints,\n ensures,\n hasLength,\n inRange,\n oneOf,\n requires,\n stateConstraint,\n} from \"./primitives/index.js\";\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,SAAS,QAAQ,CAAC,WAAoB,SAAwB;AAmB9D,SAAS,OAAO,CAAC,WAAoB,SAAwB;AA8B7D,SAAS,OAAO,CAAC,OAAe,KAAa,KAAsB;AAAA,EACxE,OAAO,SAAS,OAAO,SAAS;AAAA;AAS3B,SAAS,KAAQ,CAAC,OAAU,SAAuB;AAAA,EACxD,OAAO,QAAQ,SAAS,KAAK;AAAA;AASxB,SAAS,SAAS,CAAC,OAAkB,YAAqD;AAAA,EAC/F,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,OAAO;AAAA;AA4BF,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;;;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,SAAS,QAAQ,CAAC,WAAoB,SAAwB;AAmB9D,SAAS,OAAO,CAAC,WAAoB,SAAwB;AA8B7D,SAAS,OAAO,CAAC,OAAe,KAAa,KAAsB;AAAA,EACxE,OAAO,SAAS,OAAO,SAAS;AAAA;AAS3B,SAAS,KAAQ,CAAC,OAAU,SAAuB;AAAA,EACxD,OAAO,QAAQ,SAAS,KAAK;AAAA;AASxB,SAAS,SAAS,CAAC,OAAkB,YAAqD;AAAA,EAC/F,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,OAAO;AAAA;AA4BF,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;AAqBK,SAAS,eAAe,CAC7B,MACA,WACA,SACM;;;AC1ED,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
|
|
9
|
+
"debugId": "465F57F5AB1A6BBD64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -92,6 +92,24 @@ export declare function $constraints(stateField: string, constraints: Record<str
|
|
|
92
92
|
}>, options?: {
|
|
93
93
|
runtime?: boolean;
|
|
94
94
|
}): void;
|
|
95
|
+
/**
|
|
96
|
+
* Declare a global state constraint that prunes structurally impossible states.
|
|
97
|
+
*
|
|
98
|
+
* In production: No-op (compiled away)
|
|
99
|
+
* In verification: Translated to TLC CONSTRAINT clause, discarding states
|
|
100
|
+
* that violate the predicate from the exploration queue entirely.
|
|
101
|
+
*
|
|
102
|
+
* Unlike `invariant()` (which checks but still explores), `stateConstraint()`
|
|
103
|
+
* prevents the model checker from ever reaching the pruned states.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* stateConstraint("LeaderRequiresConnection", () =>
|
|
107
|
+
* !connectionState.value.isLeader || connectionState.value.status === "connected"
|
|
108
|
+
* )
|
|
109
|
+
*/
|
|
110
|
+
export declare function stateConstraint(name: string, predicate: () => boolean, options?: {
|
|
111
|
+
message?: string;
|
|
112
|
+
}): void;
|
|
95
113
|
export declare const verify: {
|
|
96
114
|
requires: typeof requires;
|
|
97
115
|
ensures: typeof ensures;
|
|
@@ -100,4 +118,5 @@ export declare const verify: {
|
|
|
100
118
|
oneOf: typeof oneOf;
|
|
101
119
|
hasLength: typeof hasLength;
|
|
102
120
|
$constraints: typeof $constraints;
|
|
121
|
+
stateConstraint: typeof stateConstraint;
|
|
103
122
|
};
|
|
@@ -1638,13 +1638,14 @@ class HandlerExtractor {
|
|
|
1638
1638
|
const messageTypes = new Set;
|
|
1639
1639
|
const invalidMessageTypes = new Set;
|
|
1640
1640
|
const stateConstraints = [];
|
|
1641
|
+
const globalStateConstraints = [];
|
|
1641
1642
|
const verifiedStates = [];
|
|
1642
1643
|
this.warnings = [];
|
|
1643
1644
|
const allSourceFiles = this.project.getSourceFiles();
|
|
1644
1645
|
const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
|
|
1645
1646
|
this.debugLogSourceFiles(allSourceFiles, entryPoints);
|
|
1646
1647
|
for (const entryPoint of entryPoints) {
|
|
1647
|
-
this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
|
|
1648
|
+
this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
|
|
1648
1649
|
}
|
|
1649
1650
|
if (verifiedStates.length > 0) {
|
|
1650
1651
|
if (process.env["POLLY_DEBUG"]) {
|
|
@@ -1674,11 +1675,12 @@ class HandlerExtractor {
|
|
|
1674
1675
|
handlers,
|
|
1675
1676
|
messageTypes,
|
|
1676
1677
|
stateConstraints,
|
|
1678
|
+
globalStateConstraints,
|
|
1677
1679
|
verifiedStates,
|
|
1678
1680
|
warnings: this.warnings
|
|
1679
1681
|
};
|
|
1680
1682
|
}
|
|
1681
|
-
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
|
|
1683
|
+
analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates) {
|
|
1682
1684
|
const filePath = sourceFile.getFilePath();
|
|
1683
1685
|
if (this.analyzedFiles.has(filePath)) {
|
|
1684
1686
|
return;
|
|
@@ -1692,6 +1694,8 @@ class HandlerExtractor {
|
|
|
1692
1694
|
this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
|
|
1693
1695
|
const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
|
|
1694
1696
|
stateConstraints.push(...fileConstraints);
|
|
1697
|
+
const fileGlobalConstraints = this.extractGlobalStateConstraintsFromFile(sourceFile);
|
|
1698
|
+
globalStateConstraints.push(...fileGlobalConstraints);
|
|
1695
1699
|
const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
|
|
1696
1700
|
verifiedStates.push(...fileVerifiedStates);
|
|
1697
1701
|
const importDeclarations = sourceFile.getImportDeclarations();
|
|
@@ -1705,7 +1709,7 @@ class HandlerExtractor {
|
|
|
1705
1709
|
}
|
|
1706
1710
|
continue;
|
|
1707
1711
|
}
|
|
1708
|
-
this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
|
|
1712
|
+
this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
|
|
1709
1713
|
} else if (process.env["POLLY_DEBUG"]) {
|
|
1710
1714
|
const specifier = importDecl.getModuleSpecifierValue();
|
|
1711
1715
|
if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
|
|
@@ -3040,6 +3044,81 @@ class HandlerExtractor {
|
|
|
3040
3044
|
});
|
|
3041
3045
|
return constraints;
|
|
3042
3046
|
}
|
|
3047
|
+
extractGlobalStateConstraintsFromFile(sourceFile) {
|
|
3048
|
+
const constraints = [];
|
|
3049
|
+
const filePath = sourceFile.getFilePath();
|
|
3050
|
+
sourceFile.forEachDescendant((node) => {
|
|
3051
|
+
const constraint = this.recognizeGlobalStateConstraint(node, filePath);
|
|
3052
|
+
if (constraint) {
|
|
3053
|
+
constraints.push(constraint);
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
return constraints;
|
|
3057
|
+
}
|
|
3058
|
+
recognizeGlobalStateConstraint(node, filePath) {
|
|
3059
|
+
if (!Node4.isCallExpression(node)) {
|
|
3060
|
+
return null;
|
|
3061
|
+
}
|
|
3062
|
+
const expression = node.getExpression();
|
|
3063
|
+
if (!Node4.isIdentifier(expression)) {
|
|
3064
|
+
return null;
|
|
3065
|
+
}
|
|
3066
|
+
const functionName = expression.getText();
|
|
3067
|
+
if (functionName !== "stateConstraint") {
|
|
3068
|
+
return null;
|
|
3069
|
+
}
|
|
3070
|
+
const args = node.getArguments();
|
|
3071
|
+
if (args.length < 2) {
|
|
3072
|
+
return null;
|
|
3073
|
+
}
|
|
3074
|
+
const nameArg = args[0];
|
|
3075
|
+
if (!Node4.isStringLiteral(nameArg)) {
|
|
3076
|
+
return null;
|
|
3077
|
+
}
|
|
3078
|
+
const name = nameArg.getLiteralValue();
|
|
3079
|
+
const predicateArg = args[1];
|
|
3080
|
+
if (!Node4.isArrowFunction(predicateArg)) {
|
|
3081
|
+
return null;
|
|
3082
|
+
}
|
|
3083
|
+
const body = predicateArg.getBody();
|
|
3084
|
+
let expressionText;
|
|
3085
|
+
if (Node4.isBlock(body)) {
|
|
3086
|
+
const returnStatement = body.getStatements().find((s) => Node4.isReturnStatement(s));
|
|
3087
|
+
if (!returnStatement || !Node4.isReturnStatement(returnStatement)) {
|
|
3088
|
+
return null;
|
|
3089
|
+
}
|
|
3090
|
+
const returnExpr = returnStatement.getExpression();
|
|
3091
|
+
if (!returnExpr) {
|
|
3092
|
+
return null;
|
|
3093
|
+
}
|
|
3094
|
+
expressionText = returnExpr.getText();
|
|
3095
|
+
} else {
|
|
3096
|
+
expressionText = body.getText();
|
|
3097
|
+
}
|
|
3098
|
+
let message;
|
|
3099
|
+
if (args.length >= 3) {
|
|
3100
|
+
const optionsArg = args[2];
|
|
3101
|
+
if (Node4.isObjectLiteralExpression(optionsArg)) {
|
|
3102
|
+
for (const prop of optionsArg.getProperties()) {
|
|
3103
|
+
if (Node4.isPropertyAssignment(prop) && prop.getName() === "message") {
|
|
3104
|
+
const value = prop.getInitializer();
|
|
3105
|
+
if (value && Node4.isStringLiteral(value)) {
|
|
3106
|
+
message = value.getLiteralValue();
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
return {
|
|
3113
|
+
name,
|
|
3114
|
+
expression: expressionText,
|
|
3115
|
+
message,
|
|
3116
|
+
location: {
|
|
3117
|
+
file: filePath,
|
|
3118
|
+
line: node.getStartLineNumber()
|
|
3119
|
+
}
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3043
3122
|
recognizeStateConstraint(node, filePath) {
|
|
3044
3123
|
if (!Node4.isCallExpression(node)) {
|
|
3045
3124
|
return [];
|
|
@@ -5639,4 +5718,4 @@ main().catch((_error) => {
|
|
|
5639
5718
|
process.exit(1);
|
|
5640
5719
|
});
|
|
5641
5720
|
|
|
5642
|
-
//# debugId=
|
|
5721
|
+
//# debugId=6E19C4E7D7F05D9564756E2164756E21
|