# Z3-backed complexity analysis feasibility and SAST lane map > Date: 2026-06-10 > Scope: research/planning pass for adding complexity/resource analysis to Fold's source code-analysis lane while avoiding duplicated scanner and security/SAST work. ## Executive takeaways 1. **Feasible, but not as "ask Z3 for Big-O".** Z3 is useful for proving loop bounds, ranking functions, path feasibility, transition implications, and bounded cost obligations. It should sit under a small resource-bound model, not become the whole complexity analyzer. 2. **Start with security-relevant complexity, not general asymptotic inference.** The most Fold-aligned MVP is a `complexity`/`resource-bound` source detector that reports likely algorithmic-complexity DoS, nested input-dependent loops, unbounded loop/call costs, and complexity regressions under a conservative confidence model. 3. **Reuse the existing security walk.** Fold already has a composite source security walker and observer hook model. Complexity should either be another observer over that walk for statement/event counting or a sibling walk that consumes the same `SourcePathState`, range facts, `Octagon`, and loop facts. Do **not** create another ad hoc AST traversal with its own path/range model. 4. **Refactor detector registration first.** Adding another detector by extending the current boolean-heavy `SourceDetectorPlan` and large `scan_runner.cpp` if-chain will increase duplicated work. Move toward descriptor-driven detector registration before or during the complexity MVP. 5. **Library strategy: borrow ideas, not embed a big tool.** LLVM ScalarEvolution, Infer Cost, KoAT/AProVE/CoFloCo/LoAT, CiaoPP-style Horn/cost relations, CRAB/Clam, and Z3 muZ are the right references. Direct embedding is likely too heavy or mismatched for Fold's Clang-AST lane. ## Literature and tool evidence | Source/tool | What it does | Lesson for Fold | |---|---|---| | **Infer Cost** (`infer --cost`) | Computes asymptotic execution cost and supports complexity diffing/regression reporting. Docs state Java is primary; C/C++ support is limited. It reports degree increases such as `O(n)` -> `O(n^2)` through `reportdiff`. | Fold should separate **cost summaries** from **findings**, and support baseline/regression workflows rather than only per-scan warnings. | | **LLVM ScalarEvolution (SCEV)** | Models induction variables, add recurrences, exit/backedge counts, and trip counts in LLVM IR. | Best practical library idea for loop trip-count recognition. Since Fold is AST-first, use SCEV as design vocabulary or optional LLVM-lane later, not as the first dependency. | | **KoAT / AProVE** | Automatic complexity/termination for integer programs, including C integer programs via translation, with upper runtime and size bounds. | Mature approach: translate loops/procedures to integer transition systems, infer size bounds and runtime bounds modularly. Good long-term export/backend model. | | **CoFloCo / COSTA/PUBS family** | Infers symbolic upper/lower bounds from cost equations independent of source language. Often uses recurrence/cost-relation generation plus solving. | Fold can generate a small internal `CostRelation` IR and only later export to CoFloCo-like formats. | | **LoAT** | Lower-bound/nontermination analysis over transition systems and linear CHCs; parses CHC SMT-LIB. | Useful for proving worst-case lower-bound hazards (`at least n^2`) and nontermination candidates, but not MVP. | | **Difference-constraint resource bound analysis** | Literature shows scalable imperative-program bounds by modeling transitions like `x' <= y + c`, and mutually inferring transition and variable bounds. | Fold already has intervals/octagons; first MVP should target difference constraints and affine loop updates. | | **CHC/Horn resource verification / CiaoPP** | Translates resource usage specifications and inferred resource functions into Horn clauses, often with interval preconditions. Z3's muZ supports CHCs. | Good fit for future `--complexity-chc` experimental backend. Use CHCs for summaries/procedures once intra-procedural loop bounds work. | | **Ranking-function synthesis** | Linear ranking functions prove termination and bound simple loop progress using SMT obligations. | Fold can use Z3 to prove candidate ranking/variant functions for loops and turn them into bounded trip counts. | | **pymwp** | Static analyzer for C programs that certifies polynomial growth bounds, compositional and automated, but not a Z3-centric security scanner. | Useful alternate target: classify polynomial vs unknown/exponential-ish growth before trying exact Big-O. | Key URLs reviewed: - Infer Cost: https://fbinfer.com/docs/checker-cost/ - Infer repo/manpage: https://github.com/facebook/infer - LLVM SCEV header/docs: https://github.com/llvm/llvm-project/blob/main/llvm/include/llvm/Analysis/ScalarEvolution.h - SCEV explainer: https://www.npopov.com/2023/10/03/LLVM-Scalar-evolution.html - KoAT: https://koat.verify.rwth-aachen.de/ - AProVE KoAT C integer programs: https://aprove.informatik.rwth-aachen.de/interface/v-koat/c - CoFloCo: https://github.com/aeflores/CoFloCo - LoAT: https://loat-developers.github.io/LoAT/ - Z3 fixedpoints/muZ: https://microsoft.github.io/z3guide/docs/fixedpoints/intro/ - Difference-constraint resource bounds: https://link.springer.com/article/10.1007/s10817-016-9402-4 ## Feasibility assessment ### What Z3 can do well for Fold - Prove **path feasibility** for loop-entry/exit guards, reusing `SourcePathState`. - Prove candidate **linear ranking functions** and monotone progress facts. - Prove/refute simple **trip-count upper bounds** such as `0 <= i < n`, `i += c`, `n - i` decreases. - Prove **nested-loop multiplication conditions** when inner bounds depend on outer/input variables. - Check **threshold obligations**: `cost > limit`, `cost grows with tainted input`, `cost_after > cost_before` for baseline comparison. - Validate detector claims under current path/guarded facts, keeping `unknown` as precision loss. ### What Z3 should not be expected to do alone - Infer closed-form Big-O for arbitrary C++ control flow, heap containers, iterators, recursion, virtual calls, callbacks, or data-dependent loops. - Solve nonlinear recurrences robustly under scanner budgets. - Replace abstract interpretation/polyhedral/SCEV-style loop recognition. - Produce useful complexity reports without a separate cost model and summary format. ### Practical MVP confidence levels | Confidence | Meaning for complexity | |---|---| | `proved` | Bound/regression/hazard proved under Fold's bounded/inferred model and current path facts. | | `latent` | Complexity hazard depends on caller/input size precondition or tainted input range not discharged locally. | | `heuristic` | Structural pattern indicates risk, but loop bound/cost is partial or unsupported. | | `syntactic` | Pattern only, e.g. nested loops over same container with no bound proof. | A general complexity detector should use the same bounded-proof humility as security detectors: proved within model, not universal program complexity. ## Fold scan/SAST lane map ### Current source detector selection `SourceDetectorPlan` is a hand-maintained boolean struct: `include/fold/cli/detector_plan.hpp:14-44`. Profiles and aliases are duplicated manually in `src/cli/detector_plan.cpp:45-77` and `src/cli/detector_plan.cpp:85-117`. Implication: adding `complexity` by just appending more booleans will work but increase maintenance duplication. A detector descriptor table should own: ```text name, aliases, category, backend/lane, profile name, default group, factory/run mode, proof mode, summary-backed?, cross-tu?, report kind ``` ### Current source security scan path - `src/cli/scan_runner.cpp:1990-1992` runs composite security detectors when enabled. - Fallback individual detector paths duplicate boilerplate for each detector, e.g. div/null/int/oob at `src/cli/scan_runner.cpp:1995-2065`. - Composite registration happens in `src/cli/scan_runner.cpp:2352-2419`, building observers for div, null, overflow, truncation, sign conversion, OOB, taint, SQL injection, and double-free. - `src/cli/scan_runner.cpp:2423-2431` multiplexes observers through one `walkContextWithObserver(...)` call. - Results/profile attribution are split back out at `src/cli/scan_runner.cpp:2436-2455`. Implication: complexity analysis should not add another full TU walk if it only needs AST/control-flow facts already seen by `SecurityWalker`. Prefer a new observer or a shared `AnalysisWalker` facade. ### Current security walker/state path - `BatchSafetyObserver` queues hook calls and flushes under cooperative deadline: `src/detect/security_walker.cpp:1225-1269`. - `SecurityWalker` owns per-function CFG/path traversal and budget checks: `src/detect/security_walker.cpp:1496-1525`. - Per-function setup creates `SymbolicEnv`, Z3 solver, def-use, initial contracts, and then walks CFG/body: `src/detect/security_walker.cpp:2853-2888`. - Source path API exists: `include/fold/symbolic/source_path_state.hpp:19-47`. - Existing loop/invariant hooks are minimal but relevant: `include/fold/symbolic/loop_invariant.hpp:10-23`. - Existing octagon relational facts can support difference constraints: `include/fold/symbolic/octagon.hpp:15-32`. Implication: complexity should reuse this function setup and enrich it with a `LoopCostAnalyzer` rather than maintain separate path, contract, debug-assert, and def-use logic. ### Current reporting model `FindingKind` is security/quality oriented and has no complexity/resource kind today: `include/fold/report/finding.hpp:13-72`. Evidence already has backend/proof/path/precondition fields that complexity can reuse: `include/fold/report/finding.hpp:177-214`. Recommendation: add one or two finding kinds only after the design is stable: - `ResourceComplexity` / `AlgorithmicComplexity` for security-relevant input-driven cost hazards. - Optionally `ComplexityRegression` for baseline diff mode, if reports should show it as a finding rather than a profile artifact. For an MVP, a JSON sidecar (`complexity.summaries.json`) may be safer than immediately expanding SARIF rules. ## Where we may be doing double work 1. **Detector registration/orchestration duplication.** `SourceDetectorPlan` + profile list + alias enablement + scan-runner if-chains are all manually synchronized. Complexity will worsen this unless registration becomes descriptor-driven. 2. **Multiple path/range models.** SecurityWalker, individual detectors, dead/redundant/uninit walkers, intervals, octagon, guarded facts, and loop invariant hooks overlap. Complexity should consume a single `FunctionAnalysisFacts` product rather than clone range/path logic. 3. **Separate structural detectors vs observer detectors.** Some detectors run on the composite observer path; others run custom AST/CFG traversals. A complexity pass will need both structural loops and symbolic facts. This argues for a shared `SourceAnalysisContext` with path facts, loop facts, def-use, summaries, and budget. 4. **Profiles vs findings.** Fold already has rich solver/profile counters in `SolverStats`; complexity may be both a metric and a finding. Keep summary/profile output separate from security findings until the user asks for gating/security warnings. 5. **Source vs binary risk.** Do not route binary P-Code into source complexity IR. Binary can later have separate heuristic cost summaries, but source complexity should remain Clang/AST/CFG grounded. ## Recommended architecture ### New source-lane concepts ```cpp struct CostSymbol { std::string name; // e.g. n, size(v), len(s), argc ir::AccessPath subject; // canonical source identity where available }; struct LoopBound { enum class Kind { Constant, Linear, Polynomial, Unknown, UnboundedCandidate }; Kind kind; std::string expression; // textual symbolic bound: n, n*m, n^2 z3::expr guard; // path under which bound applies Confidence confidence; }; struct CostSummary { std::string function; std::string cost_expr; // O(1), O(n), O(n*m), unknown std::vector loops; EncodingPrecisionContext precision; }; ``` ### Analysis pipeline ```text Clang AST/CFG -> existing SecurityWalker/SourcePathState setup -> LoopCostCollector: loop headers, guards, induction vars, call costs -> Bound inference: 1. cheap syntactic affine recognizer 2. interval/octagon/difference-constraint facts 3. Z3 candidate ranking/bound proof 4. unknown/timeout precision tag -> CostSummary sidecar -> optional Finding when security-relevant or regression-threshold crossed ``` ### Detector shape Do **not** put complexity fully inside `SafetyObserver` if it needs loop headers before expression encoding. Instead: - Add a `SourceAnalysisObserver` or `FunctionAnalysisPass` interface. - Keep existing `SafetyObserver` as hazard-sink observer. - Let both share `SourceAnalysisContext`: ```cpp struct SourceAnalysisContext { clang::ASTContext& ast; const clang::FunctionDecl* function; SymbolicEnv& env; SourcePathState& state; const ir::DefUseChains& def_use; const symbolic::Octagon& octagon; SolverBudget budget; SolverStats& stats; }; ``` This is the refactor that prevents complexity from redoing contract setup, path pruning, range facts, and solver accounting. ## MVP roadmap ### Phase 0 — design and dedup before features - Add a detector descriptor table to replace parallel bool/profile/name wiring for source detectors. - Add `complexity` as a recognized detector flag but keep it hidden/experimental. - Add `CostSummary` sidecar schema behind `--profile` or `--complexity-output`. Validation: detector-plan unit tests should prove every descriptor has one profile/rule alias and every CLI alias maps exactly once. ### Phase 1 — cheap loop inventory and cost sidecar - Build `LoopCostCollector` over Clang AST/CFG. - Emit per-function loop count, nesting depth, tainted/input-like variables used in loop guards, and unknown-call-in-loop markers. - No findings yet unless `--complexity-report-heuristic` is set. Validation fixtures: - constant loop -> `O(1)` / bounded - `for i < n` -> linear symbolic bound - nested `for i `n^2` structural estimate - loop over `argv`/`read`/`size()` input -> latent security relevance ### Phase 2 — Z3-proved affine loop bounds - Recognize induction var init/update/guard. - Generate candidate bound obligations and prove with Z3 under `SourcePathState` path facts. - Reuse `Octagon` for difference constraints and `GuardedFactStore` for guarded range facts. Examples: - `for (int i = 0; i < n; ++i)` proves `<= max(n, 0)`. - `while (i < n) { i += 2; }` proves linear with step. - Early-return guards constrain latent preconditions, e.g. `if (n > 1000) return;` caps cost. ### Phase 3 — security/SAST findings Add `AlgorithmicComplexity` finding only for actionable hazards: - user-controlled/input-sized loop cost exceeds configured degree/threshold; - nested input-dependent loops in request/parse/crypto/string paths; - unbounded loop with tainted exit condition; - complexity regression vs baseline. Keep general summaries as sidecar/profile data, not warnings. ### Phase 4 — interprocedural cost summaries - Add function `CostSummary` to existing summary store or a sibling cache. - Compose call cost inside loops. - Treat unknown calls with a configurable cost policy (`O(1)`, unknown, summary-only). ### Phase 5 — optional external backend experiments - Export integer transition systems or cost relations for KoAT/CoFloCo/LoAT experiments. - Try Z3 muZ CHCs for loop/procedure summaries. - Consider optional LLVM/SCEV lane only if AST approach hits clear precision limits. ## Security/SAST mapping Complexity analysis should be in the **source SAST lane** initially: - It is source semantics sensitive: C++ containers, functions, input APIs, contracts. - It reuses Clang AST/CFG and existing symbolic facts. - It can produce security findings when attacker-controlled inputs drive high cost. It should **not** be in binary mode initially: - Binary loop bounds from P-Code need separate register/memory/CFG recovery. - Binary cost evidence would be heuristic and should stay separate if added later. It should **integrate with confirmation later**: - Fuzz/DAST can measure slow inputs/timeouts, but runtime timing is noisy. - Confirmation task schema may need `hazard=algorithmic-complexity`, `resource=time|allocations`, `expected_violation=cost threshold exceeded`. ## Recommended next tasks 1. Refactor source detector registration to descriptor-driven metadata. 2. Add `CostSummary` model and sidecar serialization tests. 3. Implement loop inventory collector with no findings. 4. Add affine Z3-bound proof for simple `for`/`while` loops. 5. Add security-relevance classification using taint/input-source and request/parse/string sink context. 6. Add benchmark fixtures and baseline/regression gate. ## Non-goals for the first implementation - Full arbitrary C++ Big-O inference. - Whole-program CHC engine. - Binary P-Code complexity proof. - Exact container/allocator modeling. - Replacing existing security detectors with complexity analysis.