@fairfox/polly 0.89.1 → 0.90.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 +342 -84
- package/dist/tools/verify/src/cli.js.map +11 -9
- package/dist/tools/verify/src/config.d.ts +7 -0
- package/dist/tools/verify/src/config.js.map +2 -2
- package/dist/tools/visualize/src/cli.js +85 -13
- package/dist/tools/visualize/src/cli.js.map +5 -4
- package/package.json +1 -1
|
@@ -64,6 +64,13 @@ interface LegacyVerificationConfig {
|
|
|
64
64
|
verification?: {
|
|
65
65
|
timeout?: number;
|
|
66
66
|
workers?: number;
|
|
67
|
+
/**
|
|
68
|
+
* polly#181: docker-style container memory ceiling for the TLC run, e.g.
|
|
69
|
+
* "8g". The JVM heap is sized under it, so a model too large for the
|
|
70
|
+
* ceiling ends with a TLC out-of-memory message instead of a silent kill.
|
|
71
|
+
* Unset uses a fixed default (4g), never unbounded.
|
|
72
|
+
*/
|
|
73
|
+
memory?: string;
|
|
67
74
|
};
|
|
68
75
|
/**
|
|
69
76
|
* polly#171: check that nothing sent stays pending for ever.
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"sources": ["../tools/verify/src/primitives/index.ts", "../tools/verify/src/config.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
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/**\n * Cross-peer universal quantifier for use inside `requires` and\n * `ensures` predicates. The runtime is a no-op that simply runs the\n * predicate against a single dummy context (so the assertion never\n * triggers at runtime); the verifier recognizes the wrapper in\n * predicate source and emits a `\\A peer \\in Contexts \\ {ctx} : (...)`\n * clause that asks TLC to check the inner predicate against every\n * other context.\n *\n * @example\n * ensures(\n * forAllPeers(peer => peer.todos.value.length === todos.value.length),\n * \"every peer agrees on todo count\"\n * );\n */\nexport function forAllPeers<TPeer>(predicate: (peer: TPeer) => boolean): boolean {\n void predicate;\n return true;\n}\n\n/**\n * Cross-peer existential quantifier for use inside `requires` and\n * `ensures` predicates. Runtime no-op; the verifier emits a\n * `\\E peer \\in Contexts \\ {ctx} : (...)` clause.\n *\n * @example\n * ensures(\n * somePeer(peer => peer.user.value.loggedIn === true),\n * \"at least one peer has a logged-in user\"\n * );\n */\nexport function somePeer<TPeer>(predicate: (peer: TPeer) => boolean): boolean {\n void predicate;\n return true;\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 forAllPeers,\n somePeer,\n};\n",
|
|
6
|
-
"// Configuration Helper for @fairfox/polly/verify\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// Configuration Types (inlined to avoid heavy dependencies)\n\n// polly#160: a directional capability whose grant requires a precondition.\n// Desugars to a TLA+ safety invariant `(enabledBy) => (requires)`. enabledBy /\n// requires are TS boolean expressions in the requires()/ensures() dialect\n// referencing state via `state.` / signal `.value` form (e.g. \"state.authReady\").\n// Inlined here (like SubsystemConfig) so this authoring entry point stays\n// dependency-light; the canonical type is CapabilityConfig in config/types.ts.\ninterface CapabilityConfig {\n name: string;\n enabledBy: string;\n requires: string;\n message?: string;\n}\n\n// Subsystem configuration for compositional verification\ninterface SubsystemConfig {\n state: string[]; // Field names from parent state config\n handlers: string[]; // Message type names\n // Per-subsystem message bounds; override messages.maxInFlight and merge\n // into messages.perMessageBounds for this subsystem only.\n bounds?: {\n maxInFlight?: number;\n perMessageBounds?: Record<string, number>;\n };\n /** polly#171: check message-resolution liveness for this subsystem only. */\n liveness?: boolean;\n}\n\n// Point a subsystem's reachability witnesses at a hand-written TLA+ spec instead\n// of the generated UserApp_<subsystem>. Inlined here (like SubsystemConfig) to\n// keep this authoring entry point dependency-light; canonical type is\n// CustomTLAPath in config/types.ts.\ninterface CustomTLAPath {\n tla: string; // path (relative to cwd) to the hand-written .tla the witness EXTENDS\n cfg: string; // path (relative to cwd) to its .cfg\n module?: string; // module to EXTEND; defaults to the MODULE name parsed from `tla`\n fields?: Record<string, string>; // BDD field name → this spec's TLA+ variable name\n}\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n /**\n * polly#117: optional mesh-document declarations. When present, each\n * key names a `$meshState` document and its value declares the\n * field-level state schema for that document. The verifier emits a\n * separate slot in `contextStates[ctx].mesh[<docId>]` for these\n * fields and adds a `PropagateMeshOp` action that allows the doc's\n * value on one context to flow to another — modelling Automerge\n * sync between peers. Mesh references inside `forAllPeers` quantifiers\n * route through this slot so cross-peer convergence claims are\n * actually checked.\n *\n * @example\n * ```ts\n * defineVerification({\n * state: { localCounter: { type: \"number\", min: 0, max: 3 } },\n * mesh: {\n * todos: {\n * entries: { type: \"enum\", values: [\"empty\", \"one\", \"many\"] },\n * },\n * },\n * messages: { maxInFlight: 2 },\n * });\n * ```\n */\n mesh?: Record<string, 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 /**\n * polly#171: check that nothing sent stays pending for ever.\n *\n * Off by default because it is expensive: it adds a temporal property and the\n * routing fairness it needs, and TLC must then explore behaviours rather than\n * only reachable states. Measured on an 11-subsystem consumer model, the full\n * verify gate went from ~110s to ~512s.\n *\n * Every property the generator emits without this is a SAFETY property, and a\n * safety property is true of a system that does nothing at all — so a spec in\n * which routing has stopped verifies identically to one in which it works.\n * Turn this on for the subsystems whose progress actually matters.\n *\n * LIMITATION, and it is Lamport's own warning (Specifying Systems 14.3.5):\n * generated specs also declare a CONSTRAINT to bound the state space, and TLC\n * prints \"Declaring state or action constraints during liveness checking is\n * dangerous\" when both are present. A constraint truncates behaviours at its\n * boundary, so a liveness violation reachable only beyond that boundary is not\n * found. Treat a green liveness run as \"no wedge within the bounds\", not as a\n * proof of progress — the same reading every bounded model-checking result\n * deserves.\n */\n liveness?: boolean;\n\n // Subsystem-scoped verification (compositional)\n subsystems?: Record<string, SubsystemConfig>;\n\n // Bind a subsystem's witnesses to a hand-written .tla/.cfg (read only by the\n // --witness pass; a custom subsystem is skipped during generated-spec verify).\n customTLAPaths?: Record<string, CustomTLAPath>;\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 // polly#160: directional capability invariants + write-coupling lint groups\n capabilities?: CapabilityConfig[];\n coupledFields?: string[][];\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 // polly#160: directional capability invariants + write-coupling lint groups\n capabilities?: CapabilityConfig[];\n coupledFields?: string[][];\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 forAllPeers,\n hasLength,\n inRange,\n oneOf,\n requires,\n somePeer,\n stateConstraint,\n} from \"./primitives/index.js\";\n"
|
|
6
|
+
"// Configuration Helper for @fairfox/polly/verify\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// Configuration Types (inlined to avoid heavy dependencies)\n\n// polly#160: a directional capability whose grant requires a precondition.\n// Desugars to a TLA+ safety invariant `(enabledBy) => (requires)`. enabledBy /\n// requires are TS boolean expressions in the requires()/ensures() dialect\n// referencing state via `state.` / signal `.value` form (e.g. \"state.authReady\").\n// Inlined here (like SubsystemConfig) so this authoring entry point stays\n// dependency-light; the canonical type is CapabilityConfig in config/types.ts.\ninterface CapabilityConfig {\n name: string;\n enabledBy: string;\n requires: string;\n message?: string;\n}\n\n// Subsystem configuration for compositional verification\ninterface SubsystemConfig {\n state: string[]; // Field names from parent state config\n handlers: string[]; // Message type names\n // Per-subsystem message bounds; override messages.maxInFlight and merge\n // into messages.perMessageBounds for this subsystem only.\n bounds?: {\n maxInFlight?: number;\n perMessageBounds?: Record<string, number>;\n };\n /** polly#171: check message-resolution liveness for this subsystem only. */\n liveness?: boolean;\n}\n\n// Point a subsystem's reachability witnesses at a hand-written TLA+ spec instead\n// of the generated UserApp_<subsystem>. Inlined here (like SubsystemConfig) to\n// keep this authoring entry point dependency-light; canonical type is\n// CustomTLAPath in config/types.ts.\ninterface CustomTLAPath {\n tla: string; // path (relative to cwd) to the hand-written .tla the witness EXTENDS\n cfg: string; // path (relative to cwd) to its .cfg\n module?: string; // module to EXTEND; defaults to the MODULE name parsed from `tla`\n fields?: Record<string, string>; // BDD field name → this spec's TLA+ variable name\n}\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n /**\n * polly#117: optional mesh-document declarations. When present, each\n * key names a `$meshState` document and its value declares the\n * field-level state schema for that document. The verifier emits a\n * separate slot in `contextStates[ctx].mesh[<docId>]` for these\n * fields and adds a `PropagateMeshOp` action that allows the doc's\n * value on one context to flow to another — modelling Automerge\n * sync between peers. Mesh references inside `forAllPeers` quantifiers\n * route through this slot so cross-peer convergence claims are\n * actually checked.\n *\n * @example\n * ```ts\n * defineVerification({\n * state: { localCounter: { type: \"number\", min: 0, max: 3 } },\n * mesh: {\n * todos: {\n * entries: { type: \"enum\", values: [\"empty\", \"one\", \"many\"] },\n * },\n * },\n * messages: { maxInFlight: 2 },\n * });\n * ```\n */\n mesh?: Record<string, 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 * polly#181: docker-style container memory ceiling for the TLC run, e.g.\n * \"8g\". The JVM heap is sized under it, so a model too large for the\n * ceiling ends with a TLC out-of-memory message instead of a silent kill.\n * Unset uses a fixed default (4g), never unbounded.\n */\n memory?: string;\n };\n\n /**\n * polly#171: check that nothing sent stays pending for ever.\n *\n * Off by default because it is expensive: it adds a temporal property and the\n * routing fairness it needs, and TLC must then explore behaviours rather than\n * only reachable states. Measured on an 11-subsystem consumer model, the full\n * verify gate went from ~110s to ~512s.\n *\n * Every property the generator emits without this is a SAFETY property, and a\n * safety property is true of a system that does nothing at all — so a spec in\n * which routing has stopped verifies identically to one in which it works.\n * Turn this on for the subsystems whose progress actually matters.\n *\n * LIMITATION, and it is Lamport's own warning (Specifying Systems 14.3.5):\n * generated specs also declare a CONSTRAINT to bound the state space, and TLC\n * prints \"Declaring state or action constraints during liveness checking is\n * dangerous\" when both are present. A constraint truncates behaviours at its\n * boundary, so a liveness violation reachable only beyond that boundary is not\n * found. Treat a green liveness run as \"no wedge within the bounds\", not as a\n * proof of progress — the same reading every bounded model-checking result\n * deserves.\n */\n liveness?: boolean;\n\n // Subsystem-scoped verification (compositional)\n subsystems?: Record<string, SubsystemConfig>;\n\n // Bind a subsystem's witnesses to a hand-written .tla/.cfg (read only by the\n // --witness pass; a custom subsystem is skipped during generated-spec verify).\n customTLAPaths?: Record<string, CustomTLAPath>;\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 // polly#160: directional capability invariants + write-coupling lint groups\n capabilities?: CapabilityConfig[];\n coupledFields?: string[][];\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 // polly#160: directional capability invariants + write-coupling lint groups\n capabilities?: CapabilityConfig[];\n coupledFields?: string[][];\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 forAllPeers,\n hasLength,\n inRange,\n oneOf,\n requires,\n somePeer,\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;AAqBK,SAAS,eAAe,CAC7B,MACA,WACA,SACM;AAqBD,SAAS,WAAkB,CAAC,WAA8C;AAAA,EAE/E,OAAO;AAAA;AAcF,SAAS,QAAe,CAAC,WAA8C;AAAA,EAE5E,OAAO;AAAA;;;
|
|
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;AAqBD,SAAS,WAAkB,CAAC,WAA8C;AAAA,EAE/E,OAAO;AAAA;AAcF,SAAS,QAAe,CAAC,WAA8C;AAAA,EAE5E,OAAO;AAAA;;;ACRF,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
9
|
"debugId": "EB2A6F997258A60464756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -669,6 +669,39 @@ function sanitizeKind(kind) {
|
|
|
669
669
|
}
|
|
670
670
|
var CONTAINER_NAME_PREFIX = "polly-tla-", counter = 0;
|
|
671
671
|
|
|
672
|
+
// tools/verify/src/runner/memory.ts
|
|
673
|
+
function parseMemoryBytes(spec) {
|
|
674
|
+
const match = /^(\d+)\s*([bkmg])?$/i.exec(spec.trim());
|
|
675
|
+
if (!match?.[1])
|
|
676
|
+
return null;
|
|
677
|
+
const value = Number.parseInt(match[1], 10);
|
|
678
|
+
if (value <= 0)
|
|
679
|
+
return null;
|
|
680
|
+
const unit = (match[2] ?? "b").toLowerCase();
|
|
681
|
+
const multiplier = UNIT_BYTES[unit];
|
|
682
|
+
if (multiplier === undefined)
|
|
683
|
+
return null;
|
|
684
|
+
return value * multiplier;
|
|
685
|
+
}
|
|
686
|
+
function jvmHeapArg(memoryBytes) {
|
|
687
|
+
const megabytes = Math.max(64, Math.floor(memoryBytes * JVM_HEAP_FRACTION / MEGABYTE));
|
|
688
|
+
return `${megabytes}m`;
|
|
689
|
+
}
|
|
690
|
+
function javaToolOptions(memoryBytes) {
|
|
691
|
+
return `-Xmx${jvmHeapArg(memoryBytes)} -XX:+UseParallelGC`;
|
|
692
|
+
}
|
|
693
|
+
var DEFAULT_TLC_MEMORY = "4g", MIN_TLC_MEMORY = "512m", JVM_HEAP_FRACTION = 0.75, MEGABYTE, UNIT_BYTES, MIN_TLC_MEMORY_BYTES;
|
|
694
|
+
var init_memory = __esm(() => {
|
|
695
|
+
MEGABYTE = 1024 ** 2;
|
|
696
|
+
UNIT_BYTES = {
|
|
697
|
+
b: 1,
|
|
698
|
+
k: 1024,
|
|
699
|
+
m: MEGABYTE,
|
|
700
|
+
g: 1024 ** 3
|
|
701
|
+
};
|
|
702
|
+
MIN_TLC_MEMORY_BYTES = parseMemoryBytes(MIN_TLC_MEMORY) ?? 512 * 1024 ** 2;
|
|
703
|
+
});
|
|
704
|
+
|
|
672
705
|
// tools/verify/src/runner/docker.ts
|
|
673
706
|
import { spawn, spawnSync } from "node:child_process";
|
|
674
707
|
import * as fs6 from "node:fs";
|
|
@@ -759,11 +792,20 @@ class DockerRunner {
|
|
|
759
792
|
}
|
|
760
793
|
const containerName = mintContainerName("tlc");
|
|
761
794
|
sweepOrphanedContainersOnce();
|
|
795
|
+
const memory = options?.memory ?? DEFAULT_TLC_MEMORY;
|
|
796
|
+
const memoryBytes = parseMemoryBytes(memory);
|
|
797
|
+
if (memoryBytes === null) {
|
|
798
|
+
throw new Error(`Invalid verification.memory: "${memory}". Use a docker-style size such as "512m" or "8g".`);
|
|
799
|
+
}
|
|
762
800
|
const args = [
|
|
763
801
|
"run",
|
|
764
802
|
"--rm",
|
|
765
803
|
"--name",
|
|
766
804
|
containerName,
|
|
805
|
+
"--memory",
|
|
806
|
+
memory,
|
|
807
|
+
"-e",
|
|
808
|
+
`JAVA_TOOL_OPTIONS=${javaToolOptions(memoryBytes)}`,
|
|
767
809
|
"-v",
|
|
768
810
|
`${specDir}:/work`,
|
|
769
811
|
this.IMAGE_NAME,
|
|
@@ -782,9 +824,9 @@ class DockerRunner {
|
|
|
782
824
|
timeout: options?.timeout,
|
|
783
825
|
containerName
|
|
784
826
|
});
|
|
785
|
-
return this.parseTLCOutput(result);
|
|
827
|
+
return this.parseTLCOutput(result, memory);
|
|
786
828
|
}
|
|
787
|
-
parseTLCOutput(result) {
|
|
829
|
+
parseTLCOutput(result, memory) {
|
|
788
830
|
const output = result.stdout + result.stderr;
|
|
789
831
|
const violationMatch = output.match(/Error: Invariant (.*?) is violated/);
|
|
790
832
|
if (violationMatch) {
|
|
@@ -801,18 +843,13 @@ class DockerRunner {
|
|
|
801
843
|
if (result.exitCode !== 0 || output.includes("Error:")) {
|
|
802
844
|
return {
|
|
803
845
|
success: false,
|
|
804
|
-
error: this.extractError(output),
|
|
846
|
+
error: this.extractError(output, result.exitCode, memory),
|
|
805
847
|
output
|
|
806
848
|
};
|
|
807
849
|
}
|
|
808
|
-
const statesMatch = output.match(/(\d+) states generated/);
|
|
809
|
-
const distinctMatch = output.match(/(\d+) distinct states/);
|
|
810
850
|
return {
|
|
811
851
|
success: true,
|
|
812
|
-
|
|
813
|
-
statesGenerated: statesMatch?.[1] ? Number.parseInt(statesMatch[1], 10) : 0,
|
|
814
|
-
distinctStates: distinctMatch?.[1] ? Number.parseInt(distinctMatch[1], 10) : 0
|
|
815
|
-
},
|
|
852
|
+
...parseTLCStats(output) ?? {},
|
|
816
853
|
output
|
|
817
854
|
};
|
|
818
855
|
}
|
|
@@ -834,7 +871,14 @@ class DockerRunner {
|
|
|
834
871
|
}
|
|
835
872
|
return trace;
|
|
836
873
|
}
|
|
837
|
-
extractError(output) {
|
|
874
|
+
extractError(output, exitCode, memory) {
|
|
875
|
+
const cap = memory ? ` (--memory ${memory})` : "";
|
|
876
|
+
if (/java\.lang\.OutOfMemoryError/.test(output)) {
|
|
877
|
+
return `TLC ran out of heap${cap}. The model is too large for this ceiling — raise verification.memory, lower messages.maxInFlight, or split the subsystem.`;
|
|
878
|
+
}
|
|
879
|
+
if (exitCode === 137) {
|
|
880
|
+
return `TLC was killed for exceeding the container memory limit${cap}. Raise verification.memory, lower messages.maxInFlight, or split the subsystem.`;
|
|
881
|
+
}
|
|
838
882
|
if (/StatePool\w*\.run/.test(output) && /states\/.*No such file or directory/.test(output)) {
|
|
839
883
|
return "TLC state-pool IO failure (states/.../<n>: No such file or directory). This is a Docker filesystem flake, not a spec error — re-run the verification. See issue #152.";
|
|
840
884
|
}
|
|
@@ -931,8 +975,36 @@ class DockerRunner {
|
|
|
931
975
|
});
|
|
932
976
|
}
|
|
933
977
|
}
|
|
934
|
-
|
|
935
|
-
|
|
978
|
+
function parseTLCStats(output) {
|
|
979
|
+
const summary = TLC_SUMMARY.exec(output);
|
|
980
|
+
if (!summary?.[1] || !summary[2] || !summary[3])
|
|
981
|
+
return;
|
|
982
|
+
const statesGenerated = Number.parseInt(summary[1], 10);
|
|
983
|
+
const distinctStates = Number.parseInt(summary[2], 10);
|
|
984
|
+
const statesLeftOnQueue = Number.parseInt(summary[3], 10);
|
|
985
|
+
const initial = TLC_INITIAL_STATES.exec(output)?.[1];
|
|
986
|
+
const initialStates = initial === undefined ? undefined : Number.parseInt(initial, 10);
|
|
987
|
+
const depth = TLC_SEARCH_DEPTH.exec(output)?.[1];
|
|
988
|
+
const searchDepth = depth === undefined ? undefined : Number.parseInt(depth, 10);
|
|
989
|
+
return {
|
|
990
|
+
stats: {
|
|
991
|
+
statesGenerated,
|
|
992
|
+
distinctStates,
|
|
993
|
+
statesLeftOnQueue,
|
|
994
|
+
...initialStates === undefined ? {} : { initialStates },
|
|
995
|
+
...searchDepth === undefined ? {} : { searchDepth },
|
|
996
|
+
exhaustive: statesLeftOnQueue === 0,
|
|
997
|
+
noActionEnabled: initialStates !== undefined && distinctStates === initialStates
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
var __dirname = "/Users/AJT/projects/polly/packages/polly/tools/verify/src/runner", swept = false, TLC_SUMMARY, TLC_INITIAL_STATES, TLC_SEARCH_DEPTH;
|
|
1002
|
+
var init_docker = __esm(() => {
|
|
1003
|
+
init_memory();
|
|
1004
|
+
TLC_SUMMARY = /^(\d+) states generated, (\d+) distinct states found, (\d+) states left on queue/m;
|
|
1005
|
+
TLC_INITIAL_STATES = /Finished computing initial states: (\d+) distinct states generated/;
|
|
1006
|
+
TLC_SEARCH_DEPTH = /The depth of the complete state graph search is (\d+)/;
|
|
1007
|
+
});
|
|
936
1008
|
|
|
937
1009
|
// tools/visualize/src/cli.ts
|
|
938
1010
|
import * as fs8 from "node:fs";
|
|
@@ -7365,4 +7437,4 @@ main().catch((_error) => {
|
|
|
7365
7437
|
process.exit(1);
|
|
7366
7438
|
});
|
|
7367
7439
|
|
|
7368
|
-
//# debugId=
|
|
7440
|
+
//# debugId=F0D46FA5D13A2E3264756E2164756E21
|