From cf896582742ce5542da053161f36bd6b4ffe698d Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Wed, 7 Oct 2026 19:44:20 +0200 Subject: [PATCH 1/8] Add DFISolver: IFDS solving via DFS-interval reachability Add a generic IFDS solver based on the DFI paper (Hsu et al., CGO'23) with an interval reachability index (IntervalReachability.h) and CSR graphs (CsrGraph.h). Make Pearce's SCC algorithm reusable via visitSCCsFrom(). Co-Authored-By: Claude Opus 5.5 --- docs/DFISolver-Design.md | 463 ++++++++++++++++++ .../IfdsIde/Solver/DFIExplodedSupergraph.h | 215 ++++++++ .../DataFlow/IfdsIde/Solver/DFISolver.h | 389 +++++++++++++++ .../DataFlow/IfdsIde/Solver/DFISolverConfig.h | 29 ++ .../IfdsIde/Solver/DFISolverResults.h | 303 ++++++++++++ include/phasar/Utils/CsrGraph.h | 164 +++++++ include/phasar/Utils/IntervalReachability.h | 381 ++++++++++++++ include/phasar/Utils/SCCGeneric.h | 174 ++++--- include/phasar/Utils/TypedVector.h | 4 + lib/DataFlow/IfdsIde/IfdsIde.cppm | 8 + .../IfdsIde/Solver/DFIExplodedSupergraph.cpp | 204 ++++++++ lib/Utils/Utils.cppm | 10 + .../DataFlow/IfdsIde/CMakeLists.txt | 1 + .../DataFlow/IfdsIde/DFISolverTest.cpp | 260 ++++++++++ unittests/Utils/CMakeLists.txt | 1 + unittests/Utils/IntervalReachabilityTest.cpp | 322 ++++++++++++ 16 files changed, 2858 insertions(+), 70 deletions(-) create mode 100644 docs/DFISolver-Design.md create mode 100644 include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h create mode 100644 include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h create mode 100644 include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h create mode 100644 include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h create mode 100644 include/phasar/Utils/CsrGraph.h create mode 100644 include/phasar/Utils/IntervalReachability.h create mode 100644 lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp create mode 100644 unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp create mode 100644 unittests/Utils/IntervalReachabilityTest.cpp diff --git a/docs/DFISolver-Design.md b/docs/DFISolver-Design.md new file mode 100644 index 0000000000..2f0c17ab4b --- /dev/null +++ b/docs/DFISolver-Design.md @@ -0,0 +1,463 @@ +# DFISolver: Design + +Design for a generic `DFISolver` that solves any `psr::IFDSProblem` with the +DFS-interval reachability technique from Hsu, Hetzelt, Franz: "DFI: An +Interprocedural Value-Flow Analysis Framework that Scales to Large Codebases" +(CGO'23, arXiv:2209.02638). The MLIR part of the paper (`dfi.store`, +`dfi.call`) is out of scope; that is the job of the analysis problem (and +optionally a sparse ICFG). + +## 1. What we take from DFI, and what we change + +### 1.1 DFI in a nutshell + +1. Value-flow is graph reachability on a sparse graph whose edges come from + local transfer functions provided by the client. +2. A DFS from a set of client-chosen roots ("reversed roots") assigns each + vertex a DFS-tree interval ``. Tree reachability = interval + subsumption, O(1). +3. Non-tree edges are handled by attaching an *interval set* `Pi_v` to each + vertex: for a cross edge `s -> d`, `Pi_d` is merged into `s` and all its + DFS ancestors; for a back edge, all vertices of the SCC get the same set. + Adjacent intervals are coalesced. +4. Interprocedurally, a function's value-flow summary (arguments -> results) + is computed with the intra-procedural index and propagated into all callers + (Algorithm 1, worklist until fixpoint). Callers then contain summary edges. +5. Traversing in the reversed direction reduces the number of non-tree edges + for SSA def-use graphs (non-tree edges ~ in-degree in traversal direction). + +### 1.2 Adaptations for generic IFDS + +| DFI paper | DFISolver | +|---|---| +| Vertex = SSA value | Vertex = exploded-supergraph (ESG) node `` | +| Local transfer functions | `getNormal/Call/Ret/CallToRet/SummaryFlowFunction` | +| Graph built from the whole IR upfront | Graph is *discovered* on demand from the seeds (flow functions are forward-only and `d_t` is not enumerable) | +| Reversed roots chosen by client | `initialSeeds()` (+ unbalanced-return roots) | +| Timestamps `` with two ticks per vertex | Pre-order *rank* + subtree end `[Lo, Hi)`, one tick per *target* vertex (see 1.3) | +| Query `Pi_j "subsumes" Pi_i` (set vs. set) | Point query `rank(y) in Pi_x` (exact, see 1.3) | +| Cross/back edges merged into ancestors eagerly | Tarjan SCC fold: sets computed once per SCC at SCC completion | +| `S_f` summaries over argument/result indices | IFDS end summaries: pairs `(, )` | +| Reachable-function summary `Psi` for cross-function queries | One global index over the graph with call + summary edges (exact) | + +### 1.3 Two corrections/improvements of the index + +**Exact point query.** The paper declares `v_i ~> v_j` iff some interval of +`Pi_j` subsumes some interval of `Pi_i`. This yields false positives: if +`x -> z` and `y -> z` (cross edges into an already finished `z`), both sets +contain `I_z`, so the test claims `x ~> y`. We instead test whether the +*own rank* of the target lies in the source's set: + +``` +x ~> y <=> rank(y) in Pi_x (Pi_x = union of disjoint, sorted intervals) +``` + +Every interval in `Pi_x` is the DFS-subtree range of some vertex `w` with +`x ~> w`; every vertex in that range is reachable from `w`. Hence exact. +Cost: O(1) for the own range, otherwise O(log |Pi_x|) binary search (the paper +reports O(N^2) for its set test). + +**Projected ranks.** The rank counter is incremented only for *target* +vertices (a predicate). A subtree of the DFS still covers a contiguous rank +range of the targets it contains. With few targets (e.g. function exits when +computing summaries), interval sets collapse to 0-2 intervals almost always, +and subtrees without targets get an empty range and contribute nothing. With +"all vertices are targets", ranks are plain pre-order numbers. + +## 2. Graph model + +For a given IFDS problem, the solver materializes the realizable part of the +ESG: + +- **Vertices**: `` reachable from a seed via a realizable path. Exactly + the vertices whose facts IFDS reports, so `resultsAt(n)` = all vertices at + `n` (no index needed for the standard IFDS results). +- **Local edges** (intra-procedural, in function `f`): + - normal edges: ` -> `, `d' in normalFF(n, succ)(d)` + - call-to-return edges: ` -> ` + - summary-FF edges: ` -> ` from `getSummaryFlowFunction` + - summary edges: ` -> ` iff `d1 in callFF(d)`, + ` ~>_same-level `, `d3 in retFF(d2)` +- **Call edges**: ` -> `. Not used for summaries. +- **Return edges** are never stored; they are represented by summary edges. + +Let `G*` = vertices + local edges + call edges. Reachability in `G*` from `x` +is exactly realizable reachability along paths that may descend into callees +but never return beyond `x`'s frame (same-level + descending). This is the +semantics of the query API. Unbalanced returns (Sec. 4.6) add explicit +unbalanced-return edges. + +## 3. Algorithm overview + +``` +solve(): + init: compress seeds, compute call-graph SCCs (callees first) + Phase 1: repeat until no work: + F := pending function with the lowest CG-SCC id (callees first) + if F has unexplored vertices: explore(F) + elif F is dirty and its SCC + all lower SCCs are quiescent: summarize(F) + Phase 2: build result table (InstId -> facts) + optionally build global interval index over G* + release Phase 1 graph (unless KeepGraph) +``` + +Each ESG vertex is expanded (flow functions evaluated) **exactly once**. This +is the main difference to IFDS tabulation, which processes a vertex once per +reaching entry fact `d1` (path edges ``). + +### 3.1 explore(f) + +The local worklist of `f` holds unexpanded vertices. Expansion is batched per +instruction: pop all pending vertices, sort by `InstId` (radix sort on u64 +`Inst << 32 | Vtx`), and process each run so every flow-function object is +created once per `(n, succ)` per batch and applied to all facts. No +flow-function cache is needed. + +``` +expand(v = in f): + if isCallSite(n): + Callees := getCalleesOfCallAt(n) + for r in getReturnSitesOfCallAt(n): + for d' in callToRetFF(n, r, Callees)(d): addLocalEdge(v, ) + for g in Callees: + if SFF := getSummaryFlowFunction(n, g): + for r, d' in SFF(d) x ReturnSites: addLocalEdge(v, ) + continue + for d1 in callFF(n, g)(d), sp in getStartPointsOf(g): + E := getOrCreateVertex(, g) // new -> push to g's WL, mark entry + addCallEdge(v, E) // also registers v as caller of E + for X in SummaryExits[E]: applySummary(v, E, X) + else: + for succ in getSuccsOf(n): + for d' in normalFF(n, succ)(d): addLocalEdge(v, ) + (AutoAddZero: zero always maps to zero) + +applySummary(v = , E, X = ): + for r in getReturnSitesOfCallAt(c): + for d3 in retFF(c, fun(E), ep, r)(d2): // memoized per (c, X, r) + addSummaryEdge(v, ) // new target -> push to caller's WL + mark caller dirty +``` + +All edges created by `expand(v)` are produced in one go, so they are appended +contiguously to the edge array (append-only CSR: per vertex `EdgeBegin`, +`EdgeCount`). Summary edges arrive later and go to a per-function append-only +pair list. + +`advanceToNextUser(n, d)` (sparse IFDS, `has_advanceToNextUser_v`) is applied +when creating a target vertex, exactly like `IterativeIDESolver`. This is the +generic analog of DFI's sparse def-use graph. + +### 3.2 summarize(f) + +Computes all same-level summaries `(entry, exit)` of `f`: + +1. Build a transient local CSR over `f`'s vertices using local ids + (`LocalIdx[v]`, assigned at vertex creation): local edges + summary edges, + no call edges. Reused scratch buffers, O(|V_f| + |E_f|). +2. Run the interval-index kernel (Sec. 4) with + roots = entry vertices of `f` (+ seed / unbalanced roots in `f`), + targets = exit vertices of `f`. +3. For each entry `E`: enumerate exit ranks in `Pi(scc(E))`, map + `rank -> exit vertex`, diff against the sorted `SummaryExits[E]`. +4. For each new pair `(E, X)` and each caller vertex `v` of `E`: + `applySummary(v, E, X)`. Append `X` to `SummaryExits[E]`. + +The local index is discarded afterwards; only the summary pairs persist. + +### 3.3 Scheduling + +- Call-graph SCCs via `computeCGSCCs` (Tarjan; SCC id 0 = leaves). Priority = + SCC id, lowest first, i.e. callees before callers. +- `PendingSCCs: BitSet`; pick via find-first-set. Per SCC a small FIFO + of functions with work. +- A function is summarized only when its SCC and all lower SCCs have empty + local worklists. New entries only appear in callees (lower SCC ids or same + SCC), new summary edges only in callers (higher or same SCC), so callers are + typically summarized once per "wave". Recursion inside an SCC iterates to a + fixpoint. + +### 3.4 Correctness sketch + +- *Soundness w.r.t. IFDS*: every IFDS path edge ` -> ` with + `` realizable implies `` is materialized: induction over the + IFDS tabulation; the only non-local step is the return, which is covered by + `applySummary` once the callee's index contains `(E, X)`. At fixpoint every + function is summarized after its last change, so all summary pairs exist. +- *Precision*: vertices are only created from realizable vertices + (normal/c2r/call targets) or via `applySummary` from a realizable call vertex + and a same-level callee path. Flow functions are therefore never evaluated + on unrealizable contexts, so side effects in flow functions (e.g. leak + reporting) see the same `(n, d)` set as with IFDS. +- *Termination*: all sets grow monotonically over finite domains. + +## 4. Interval index kernel + +Generic, independent of IFDS; lives in `phasar/Utils/IntervalReachability.h`. +Input: any `is_const_graph` (typically `CsrGraph` from +`phasar/Utils/CsrGraph.h`), roots, target predicate. A single DFS computes +SCCs, ranks, and interval sets. It reuses Pearce's SCC algorithm from +`SCCGeneric.h` via `visitSCCsFrom()`, whose `OnDiscover` hook assigns ranks +and whose `OnSCC` hook computes the interval sets. + +### 4.1 Per-SCC label + +``` +Pi(S) = Extras(S) U [Lo(S), Hi(S)) +``` + +- `[Lo, Hi)`: rank range of the DFS subtree of the SCC root (all members are + in it). `Lo = RankCounter` at root discovery, `Hi = RankCounter` at SCC + completion. +- `Extras(S)`: sorted, coalesced, disjoint intervals, **all `< Lo(S)`** + (invariant). When the root finishes, every vertex reachable from it has + been visited, so no reachable target has rank `>= Hi`; anything inside + `[Lo, Hi)` is already covered. So extras only ever need clipping at `Lo`. + +### 4.2 SCC completion + +Tarjan completes SCCs in reverse topological order, so every successor SCC +`W != S` of a member is final when `S` completes. + +``` +onSccComplete(S, Members): + Lo, Hi := RankBegin[root], RankCounter + Cand := [] + for m in Members, w in succ(m): + W := SccOf[w] + if W == S or Seen[W] == S: continue // stamp array dedups + Seen[W] := S + if Hi(W) <= Lo and Lo(W) < Hi(W): Cand += [Lo(W), Hi(W)) // cross edge + Cand += clip(Extras(W), Lo) + Extras(S) := fastPath or coalesce(sort(Cand)) +``` + +**Fast path** (chains, loop bodies): exactly one distinct successor `W`, `W` is +inside the subtree, and `Extras(W).back().Hi <= Lo` -> reuse `ExtraSetId(W)` +without copying. Otherwise sort + coalesce (`[a, b) [b, c) -> [a, c)`), then +optionally hash-cons the resulting span in the interval pool. + +### 4.3 Query + +``` +reaches(x, y): S := SccOf[x], R := Rank[y] + return Lo(S) <= R < Hi(S) || binarySearch(Extras(S), R) +``` + +Enumeration of everything reachable from `x` = walk `Pi(S)`; with +"all vertices are targets" and vertices renumbered by rank, each interval is a +contiguous slice of the vertex arrays. + +### 4.4 Direction + +The ESG is explored forward (forced by `computeTargets`). The kernel can index +`G*` or its reverse (`IndexDirection::Forward | Backward`, reverse CSR is one +counting sort). Same answers, different `|Pi|` and different enumeration +locality: forward makes "what does `x` reach" contiguous, backward makes +"what reaches `y`" (provenance) contiguous. The paper's in-degree argument is +specific to SSA def-use graphs; for ESGs (merge points vs. branch/gen +fan-out) this must be measured. Default: Forward. + +## 5. Data layout + +All ids are `uint32_t` strong typedefs (`PHASAR_STRONG_TYPEDEF`), containers +are `TypedVector` / `BitSet`. `InstId` comes from +`IRDB.getInstructionId(n)` (no hashing), `FactId` from a `Compressor` +(zero fact = 0), `FunctionId` from `FunctionCompressor`. + +### 5.1 Phase 1 (mutable, append-only) + +| Data | Type | Bytes/vertex | Notes | +|---|---|---|---| +| `VertexOf` | flat hash map `u64(Inst, Fact) -> VertexId` | ~12-16 | the only hash probe per edge target | +| `VtxInst`, `VtxFact` | `TypedVector` x2 | 8 | SoA | +| `VtxEdgeBegin`, `VtxEdgeCount` | `TypedVector` x2 | 8 | into `Edges` | +| `LocalIdx` | `TypedVector` | 4 | index within `FunVertices[f]` | +| `Edges` | `std::vector` | ~4 x out-degree | normal + c2r + summary-FF | +| `FunVertices[f]` | `TypedVector>` | 4 | | +| `FunSummaryEdges[f]` | vector of `(VertexId, VertexId)` | | turned into transient CSR in `summarize` | +| `FunEntries[f]`, `FunExits[f]` | vectors of `VertexId` | | exits = vertices at `isExitInst` | +| `EntryIdx` | `DenseMap` (entries are rare) | | | +| `EntryCallers[E]`, `SummaryExits[E]` | `TypedVector>` | | `SummaryExits` kept sorted | +| `CallEdges` | vector of `(VertexId, VertexId)` | | for the global index only | +| `RetFFMemo` | map `(InstId c, VertexId X) -> span in target pool` | | avoids repeated `retFF` on the same exit fact | +| `FunWL[f]` | `std::vector` | | | +| `PendingSCCs`, `Dirty` | `BitSet` | | | + +Per vertex ~36-40 bytes + edges. For comparison, `IterativeIDESolver` stores +one jump-function entry per path edge `` (hash-table slot of +`combineIds(d1, d2)` per instruction + ValTab entry); path edges per vertex = +number of entry facts reaching it, which is what blows up on large programs. + +### 5.2 Kernel scratch (reused across all `summarize` calls) + +`Index[], Low[], RankBegin[], SccOf[]` (u32, sized to the largest local +graph), `OnStack` bit vector, Tarjan stack, DFS frame stack `{Vtx, EdgeCursor}`, +`Seen[]` stamps, candidate interval buffer. No allocation in steady state. + +### 5.3 Final results (immutable) + +| Data | Type | Bytes | +|---|---|---| +| `VtxInst`, `VtxFact` (renumbered by rank) | `u32` x2 | 8 / vertex | +| `InstOffsets` | CSR offsets | 4 / instruction | +| `VerticesAt` | ranks, sorted by `FactId` per instruction | 4 / vertex | +| `FactCompressor` | `d_t <-> FactId` | | +| *index:* `SccOf` | `u32` | 4 / vertex | +| *index:* `SccLo`, `SccHi`, `SccExtras` | `u32` x3 | 12 / SCC | +| *index:* `IntervalPool`, `SetOffsets` | CSR of `{u32 Lo, Hi}` | 8 / interval | + +Phase 1 structures (hash map, edges, summaries) are released after Phase 2 +unless `KeepGraph` is set. + +## 6. API + +```cpp +namespace psr { + +enum class DfiIndexDirection : uint8_t { Forward, Backward }; +enum class DfiQueryTargets : uint8_t { All, Interesting }; + +struct DFISolverConfig { + static constexpr bool AutoAddZero = true; + static constexpr bool BuildQueryIndex = true; + static constexpr DfiIndexDirection IndexDirection = DfiIndexDirection::Forward; + /// Interesting: rank only vertices at Problem.isInteresting(n) instructions + static constexpr DfiQueryTargets QueryTargets = DfiQueryTargets::All; + /// Keep the CSR of G* after solving (ESG export, witness paths) + static constexpr bool KeepGraph = false; + static constexpr bool HashConsIntervalSets = true; + static constexpr bool EnableStatistics = false; + + template using map_t = ...; +}; + +template +class DFISolver + : public IDESolverAPIMixin> { +public: + DFISolver(ProblemTy *Problem, const ICFGTy *ICF, + StaticSolverConfigTy Config = {}) noexcept; + + auto solve() &; + [[nodiscard]] auto solve() &&; + + /// Trivially-copyable view; usable as GenericSolverResults + [[nodiscard]] DFISolverResults getSolverResults() const noexcept; + [[nodiscard]] OwningDFISolverResults consumeSolverResults(); + + [[nodiscard]] DFIReachability getReachability() const noexcept + requires(StaticSolverConfigTy::BuildQueryIndex); + + [[nodiscard]] DFISolverStats getStats() const noexcept + requires(StaticSolverConfigTy::EnableStatistics); + +private: + // IDESolverAPIMixin hooks: one expansion batch or one summarize per step + void doInitialize(); + bool doNext(); + auto doFinalize() &; + auto doFinalize() &&; +}; + +template +[[nodiscard]] auto solveDFIProblem(ProblemTy &Problem, const ICFG auto &ICF); + +/// Context-sensitive reachability between materialized ESG nodes. +/// Semantics: realizable paths that may descend into callees but do not +/// return past the frame of the source (plus unbalanced returns from seeds). +template class DFIReachability { +public: + [[nodiscard]] std::optional find(ByConstRef Inst, + ByConstRef Fact) const; + [[nodiscard]] bool reaches(DfiVertexRef From, DfiVertexRef To) const noexcept; + [[nodiscard]] bool reaches(ByConstRef FromInst, ByConstRef FromFact, + ByConstRef ToInst, ByConstRef ToFact) const; + + /// Facts at At that are reachable from From + void forEachReachableAt(DfiVertexRef From, ByConstRef At, + std::invocable> auto Handler) const; + void forEachReachable(DfiVertexRef From, + std::invocable, ByConstRef> auto Handler) const; +}; + +} // namespace psr +``` + +`DFISolverResults` implements what `GenericSolverResults` dispatches to: +`resultAt`, `resultsAt(n, StripZero)`, `ifdsResultsAt`, `size`, +`containsNode`, `foreachResultEntry`. `resultsAt(n)` is a slice of +`VerticesAt`; `resultAt(n, d)` is a binary search in it. + +`forEachReachableAt(From, At)` intersects `Pi(From)` with the rank-sorted +vertices at `At` (merge of two sorted lists), the typical taint query +"which facts at sink `At` originate from source `From`". + +## 7. IFDS feature mapping + +| Feature | Handling | +|---|---| +| `AutoAddZero` | zero fact always propagated by the solver | +| `getSummaryFlowFunction` | local edges to return sites, callee not entered | +| Callee without body | only call-to-return flow | +| `followReturnsPastSeeds` | seeds (and derived unbalanced roots) are extra roots in `summarize`; exits reached from them return to *all* callers of `f` via `retFF`, creating unbalanced roots in the callers and unbalanced-return edges in `G*`; for functions without callers, `applyUnbalancedRetFlowFunctionSideEffects` if `UnbalancedRetSideEffectProvider` | +| `has_advanceToNextUser_v` | applied on target-vertex creation | +| `isInteresting(n)` | `QueryTargets::Interesting` restricts ranks to those vertices | +| `emitESG` / `recordEdges` | requires `KeepGraph`; dumps the CSR of `G*` | +| `computeValues` | not applicable (IFDS only, `l_t = BinaryDomain`) | +| `IDESolverAPIMixin` (`solveUntil`, cancellation) | `doNext` = one batch / one summarize | + +**Witness paths** (with `KeepGraph`): reconstruct a path `x ~> y` greedily by +following any successor `s` with `reaches(s, y)`; no backtracking needed, +O(path length x out-degree x log |Pi|). + +## 8. Complexity + +- Flow-function applications: once per vertex (IFDS: once per path edge). +- Return flow: once per `(call site, exit vertex, return site)` via memo. +- `summarize(f)`: O(|V_f| + |E_f| + merge cost) per call; number of calls per + function bounded by the number of change waves, small with callees-first + scheduling except in large recursive SCCs. +- Global index: O(|V| + |E| + sum |Pi|). Sets are tiny in practice (paper: + median 2, max 17), further reduced by coalescing ranks and the fast path. +- Worst case `sum |Pi| = O(|V|^2)`, same as any transitive-closure labeling. + `BuildQueryIndex = false` skips it entirely if only IFDS results are needed. + +## 9. Files and tests + +New files: + +- `include/phasar/Utils/CsrGraph.h`: `CsrGraph` + `GraphTraits`. +- `include/phasar/Utils/IntervalReachability.h`: kernel (Sec. 4), interval + pool, query. No IFDS dependency. Built on `visitSCCsFrom()` in + `SCCGeneric.h`. +- `include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h` +- `include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h` +- `include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h` (results view, + owning results, `DFIReachability`) +- `include/phasar/DataFlow/IfdsIde/Solver/DFISolverStats.h` + +Tests: + +- Kernel: random graphs (DAGs, cycles, dense SCCs, empty target sets, single + vertex) checked against brute-force BFS transitive closure, both + directions, all-targets and projected targets. +- Solver: differential tests against `IterativeIDESolver` with the existing + IFDS analyses (`IFDSTaintAnalysis`, `IFDSUninitializedVariables`, + `IFDSConstAnalysis`, `IFDSTypeAnalysis`, ...) on the existing + `test/llvm_test_code` programs: identical `ifdsResultsAt` everywhere, + identical leak sets. Recursion and `followReturnsPastSeeds` cases included. +- Reachability: for small programs, compare `reaches` against a reference that + re-runs `IterativeIDESolver` with `` as single seed. + +## 10. Open points + +- **Incremental local index**: `summarize` currently rebuilds the local index + of `f` from scratch. For large recursive SCCs an incremental variant (only + new roots / new edges) may pay off. Measure first. +- **Parallelism**: functions in different subtrees of the CG-SCC DAG with + pending work are independent in `explore`; `summarize` writes only into + callers. A level-synchronous parallel scheduler is possible later. +- **Index direction and target selection** need empirical evaluation on ESGs + (Sec. 4.4). diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h b/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h new file mode 100644 index 0000000000..2f79a1c8de --- /dev/null +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h @@ -0,0 +1,215 @@ +#pragma once + +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +#include "phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h" +#include "phasar/Utils/BitSet.h" +#include "phasar/Utils/CsrGraph.h" +#include "phasar/Utils/FunctionId.h" +#include "phasar/Utils/IntervalReachability.h" +#include "phasar/Utils/SCCGeneric.h" +#include "phasar/Utils/SCCId.h" +#include "phasar/Utils/StrongTypeDef.h" +#include "phasar/Utils/TypedVector.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include +#include +#include +#include + +/// A function entry reached by at least one call +PHASAR_STRONG_TYPEDEF(psr::dfi, uint32_t, EntryId); +/// Index of a vertex among the vertices of its function +PHASAR_STRONG_TYPEDEF(psr::dfi, uint32_t, LocalVertexId); + +namespace psr::detail { + +/// The exploded supergraph under construction by the DFISolver. +/// +/// Works on ids only; evaluating flow functions is up to the solver. Tracks +/// which vertices still need to be expanded and which functions need to be +/// summarized, and schedules this work callees first. +class DFIExplodedSupergraph { +public: + enum class ActionKind { Expand, Summarize }; + + struct Action { + FunctionId Fun{}; + ActionKind Kind{}; + }; + + /// \param CGSCCs The SCCs of the call-graph in reverse topological order, + /// as computed by computeCGSCCs() + explicit DFIExplodedSupergraph(SCCHolder CGSCCs); + + /// The next step towards the fixpoint, or std::nullopt if there is none. + /// Prefers callees over callers, and expansion over summarization. + [[nodiscard]] std::optional nextAction(); + + /// Returns the vertex . New vertices are scheduled for + /// expansion. + dfi::VertexId getOrCreateVertex(dfi::InstId Inst, dfi::FactId Fact, + FunctionId Fun, bool IsExit); + + /// Marks Vtx as seed. Returns false, if it was a seed already. + bool addSeed(dfi::VertexId Vtx); + + /// Marks the vertex Vtx, which must be at a start point, as function entry. + dfi::EntryId getOrCreateEntry(dfi::VertexId Vtx); + + /// Removes the vertices scheduled for expansion in Fun, sorted by + /// instruction. The result is valid until the next call of this function. + [[nodiscard]] llvm::ArrayRef takeWorklist(FunctionId Fun); + + /// Records the intra-procedural out-edges of Vtx: call beginEdges(Vtx), then + /// addEdge() for each target, then endEdges(Vtx). Vtx must not be expanded + /// before. Creating vertices in between is fine. + void beginEdges(dfi::VertexId Vtx) { + VtxEdgeBegin[Vtx] = uint32_t(Edges.size()); + } + void addEdge(dfi::VertexId Target) { Edges.push_back(Target); } + void endEdges(dfi::VertexId Vtx) { VtxEdgeEnd[Vtx] = uint32_t(Edges.size()); } + + /// Adds a summary edge from the call vertex CallVtx to the return-site + /// vertex RetVtx. Returns false, if the edge was already there. + bool addSummaryEdge(dfi::VertexId CallVtx, dfi::VertexId RetVtx); + + /// Adds a call edge from CallVtx to Entry. Returns the exits already known + /// to be reachable from Entry; the solver must apply them to CallVtx. The + /// result is invalidated by getOrCreateEntry(). + [[nodiscard]] llvm::ArrayRef addCallEdge(dfi::VertexId CallVtx, + dfi::EntryId Entry); + + /// Computes the same-level reachability from the entries of Fun to its exit + /// vertices and calls Handler(CallVtx, Entry, Exit) for each new + /// (Entry, Exit) pair and each call vertex CallVtx calling Entry. + void summarize( + FunctionId Fun, + std::invocable auto Handler) { + indexFunction(Fun); + + for (auto Entry : FunState[Fun].Entries) { + auto NewExits = updateSummaryExits(Fun, Entry); + // Handler may add vertices and summary edges, but no entries or callers + for (size_t I = 0; I != EntryCallers[Entry].size(); ++I) { + for (auto Exit : NewExits) { + std::invoke(Handler, EntryCallers[Entry][I], Entry, Exit); + } + } + } + } + + [[nodiscard]] size_t numVertices() const noexcept { return VtxInst.size(); } + [[nodiscard]] dfi::InstId instOf(dfi::VertexId Vtx) const noexcept { + return VtxInst[Vtx]; + } + [[nodiscard]] dfi::FactId factOf(dfi::VertexId Vtx) const noexcept { + return VtxFact[Vtx]; + } + [[nodiscard]] FunctionId funOf(dfi::VertexId Vtx) const noexcept { + return VtxFun[Vtx]; + } + [[nodiscard]] FunctionId funOf(dfi::EntryId Entry) const noexcept { + return VtxFun[EntryVertex[Entry]]; + } + + [[nodiscard]] llvm::ArrayRef seeds() const noexcept { + return SeedVertices; + } + + /// The graph of intra-procedural, summary, and call edges + [[nodiscard]] CsrGraph buildSupergraph() const; + + [[nodiscard]] TypedVector + takeVertexInsts() noexcept { + return std::move(VtxInst); + } + [[nodiscard]] TypedVector + takeVertexFacts() noexcept { + return std::move(VtxFact); + } + +private: + struct FunctionState { + TypedVector Vertices; + llvm::SmallVector Worklist; + /// Entries and seeds + llvm::SmallVector Roots; + llvm::SmallVector Entries; + llvm::SmallVector, 0> SummaryEdges; + /// Whether the graph changed since the last summarize() + bool Dirty = false; + }; + + void markDirty(FunctionId Fun); + bool addRoot(dfi::VertexId Vtx); + + /// Builds LocalIndex for the vertices of Fun, with exits as targets + void indexFunction(FunctionId Fun); + + /// Adds the exits reachable from Entry according to LocalIndex to its + /// summary. Returns the newly added ones. + [[nodiscard]] llvm::ArrayRef + updateSummaryExits(FunctionId Fun, dfi::EntryId Entry); + + [[nodiscard]] llvm::ArrayRef + edgesOf(dfi::VertexId Vtx) const noexcept { + auto Begin = VtxEdgeBegin[Vtx]; + return llvm::ArrayRef(Edges).slice(Begin, + VtxEdgeEnd[Vtx] - Begin); + } + + SCCHolder CGSCCs; + BitSet> PendingSCCs; + TypedVector FunState; + + // Per vertex + llvm::DenseMap, dfi::VertexId> VertexOf; + TypedVector VtxInst; + TypedVector VtxFact; + TypedVector VtxFun; + TypedVector VtxLocalId; + /// Intra-procedural out-edges, excluding summary edges: + /// Edges[VtxEdgeBegin[V] .. VtxEdgeEnd[V]) + TypedVector VtxEdgeBegin; + TypedVector VtxEdgeEnd; + BitSet IsExit; + BitSet IsRoot; + llvm::SmallVector Edges; + llvm::SmallVector SeedVertices; + + // Per entry + llvm::DenseMap EntryOf; + TypedVector EntryVertex; + TypedVector> EntryCallers; + /// Exit vertices reachable from the entry, sorted + TypedVector> SummaryExits; + + llvm::DenseSet> SummaryEdgeSet; + llvm::SmallVector, 0> CallEdges; + + // Scratch buffers + llvm::SmallVector Batch; + llvm::SmallVector, 0> + LocalEdges; + llvm::SmallVector LocalRoots; + llvm::SmallVector NewExits; + CsrGraph LocalGraph; + IntervalReachabilityBuilder LocalBuilder; + IntervalReachabilityIndex LocalIndex; +}; + +} // namespace psr::detail diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h new file mode 100644 index 0000000000..66461abbf9 --- /dev/null +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h @@ -0,0 +1,389 @@ +#pragma once + +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +#include "phasar/ControlFlow/CGSCCs.h" +#include "phasar/ControlFlow/ICFG.h" +#include "phasar/ControlFlow/SparseCFGProvider.h" +#include "phasar/DataFlow/IfdsIde/IFDSProblem.h" +#include "phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h" +#include "phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h" +#include "phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h" +#include "phasar/DataFlow/IfdsIde/Solver/IDESolverAPIMixin.h" +#include "phasar/Utils/ByRef.h" +#include "phasar/Utils/FunctionId.h" +#include "phasar/Utils/IntervalReachability.h" +#include "phasar/Utils/IotaIterator.h" +#include "phasar/Utils/Logger.h" +#include "phasar/Utils/Utilities.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include + +namespace psr { + +/// Solves an IFDS problem by materializing the realizable part of the exploded +/// supergraph and answering reachability with DFS-interval labels, following +/// "DFI: An Interprocedural Value-Flow Analysis Framework that Scales to Large +/// Codebases" by Hsu, Hetzelt, and Franz (CGO'23). See +/// docs/DFISolver-Design.md. +/// +/// Computes the same results as the IFDS solvers, while evaluating each flow +/// function only once per data-flow fact and instruction. Additionally, with +/// StaticSolverConfigTy::BuildQueryIndex, getReachability() answers +/// context-sensitive reachability queries between arbitrary +/// (instruction, fact) pairs that hold. +/// +/// Does not support IFDSIDESolverConfig::followReturnsPastSeeds(). +template +class DFISolver : public IDESolverAPIMixin< + DFISolver> { + friend IDESolverAPIMixin>; + +public: + using domain_t = typename ProblemTy::ProblemAnalysisDomain; + using d_t = typename domain_t::d_t; + using n_t = typename domain_t::n_t; + using f_t = typename domain_t::f_t; + using i_t = ICFGTy; + using config_t = StaticSolverConfigTy; + + DFISolver(ProblemTy *Problem, const ICFGTy *ICF, + StaticSolverConfigTy /*Config*/ = {}) + : Problem(&assertNotNull(Problem)), ICF(&assertNotNull(ICF)), + Results(std::make_unique>()) {} + + [[nodiscard]] DFISolverResults getSolverResults() const noexcept { + return DFISolverResults(Results.get()); + } + + [[nodiscard]] OwningDFISolverResults + consumeSolverResults() noexcept { + return OwningDFISolverResults(std::move(Results)); + } + + /// Only valid after solving + [[nodiscard]] DFIReachability getReachability() const noexcept + requires(StaticSolverConfigTy::BuildQueryIndex) + { + return DFIReachability(Results.get()); + } + + void dumpResults(llvm::raw_ostream &OS = llvm::outs()) const { + getSolverResults().dumpResults(OS); + } + +private: + using FlowFunctionPtrType = typename ProblemTy::FlowFunctionPtrType; + + struct CalleeInfo { + FunctionId Callee{}; + FlowFunctionPtrType FF{}; + /// Empty for summary flow functions + llvm::SmallVector StartPoints; + bool IsSummaryFF{}; + }; + + // --- IDESolverAPIMixin + + void doInitialize() { + if (getProblemSolverConfig(*Problem).followReturnsPastSeeds()) { + PHASAR_LOG_LEVEL(WARNING, "The DFISolver does not support " + "followReturnsPastSeeds; ignoring it"); + } + + for (const auto &Fun : ICF->getAllFunctions()) { + Funs.getOrInsert(Fun); + } + Graph.emplace(computeCGSCCs(*ICF, Funs)); + + auto ZeroId = Results->Facts.getOrInsert(Problem->getZeroValue()); + assert(ZeroId == dfi::FactId::Zero); + (void)ZeroId; + + auto Seeds = Problem->initialSeeds(); + for (const auto &[Inst, SeedFacts] : Seeds.getSeeds()) { + auto Fun = Funs.get(ICF->getFunctionOf(Inst)); + for (const auto &[Fact, Value] : SeedFacts) { + Graph->addSeed(getOrCreateVertex(Inst, Fact, Fun)); + } + } + } + + [[nodiscard]] bool doNext() { + auto Next = Graph->nextAction(); + if (!Next) { + return false; + } + + if (Next->Kind == detail::DFIExplodedSupergraph::ActionKind::Expand) { + expand(Next->Fun); + } else { + Graph->summarize( + Next->Fun, + [this](dfi::VertexId CallVtx, dfi::EntryId Entry, + dfi::VertexId Exit) { applySummary(CallVtx, Entry, Exit); }); + } + return true; + } + + auto doFinalize() & { + finalizeResults(); + return getSolverResults(); + } + + auto doFinalize() && { + finalizeResults(); + return consumeSolverResults(); + } + + // --- Flow functions + + dfi::VertexId getOrCreateVertex(ByConstRef Inst, ByConstRef Fact, + FunctionId Fun) { + return Graph->getOrCreateVertex(Results->Insts.getOrInsert(Inst), + Results->Facts.getOrInsert(Fact), Fun, + ICF->isExitInst(Inst)); + } + + /// Creates the vertex reached by an intra-procedural edge to Succ + dfi::VertexId getOrCreateSuccVertex(ByConstRef Succ, + ByConstRef Fact, FunctionId Fun) { + if constexpr (has_advanceToNextUser_v) { + return getOrCreateVertex(ICF->advanceToNextUser(Succ, Fact), Fact, Fun); + } else { + return getOrCreateVertex(Succ, Fact, Fun); + } + } + + [[nodiscard]] n_t instOf(dfi::VertexId Vtx) const { + return Results->Insts[Graph->instOf(Vtx)]; + } + [[nodiscard]] d_t factOf(dfi::VertexId Vtx) const { + return Results->Facts[Graph->factOf(Vtx)]; + } + + [[nodiscard]] auto computeTargets(const FlowFunctionPtrType &FF, + ByConstRef Fact) { + auto Ret = (*FF).computeTargets(Fact); + if constexpr (StaticSolverConfigTy::AutoAddZero) { + if (Problem->isZeroValue(Fact)) { + Ret.insert(Problem->getZeroValue()); + } + } + return Ret; + } + + /// Adds the summary edges from the call vertex CallVtx that go through the + /// same-level path from Entry to the exit vertex Exit + void applySummary(dfi::VertexId CallVtx, dfi::EntryId Entry, + dfi::VertexId Exit) { + n_t CallSite = instOf(CallVtx); + n_t ExitInst = instOf(Exit); + d_t ExitFact = factOf(Exit); + f_t Callee = Funs[Graph->funOf(Entry)]; + auto Caller = Graph->funOf(CallVtx); + + for (const auto &RetSite : ICF->getReturnSitesOfCallAt(CallSite)) { + auto FF = + Problem->getRetFlowFunction(CallSite, Callee, ExitInst, RetSite); + for (const auto &Fact : computeTargets(FF, ExitFact)) { + Graph->addSummaryEdge(CallVtx, + getOrCreateSuccVertex(RetSite, Fact, Caller)); + } + } + } + + /// Expands all pending vertices of Fun, batched per instruction, such that + /// each flow function is constructed once per batch. + void expand(FunctionId Fun) { + auto Batch = Graph->takeWorklist(Fun); + for (const auto *It = Batch.begin(), *End = Batch.end(); It != End;) { + auto Inst = Graph->instOf(*It); + const auto *RunEnd = std::find_if(It, End, [this, Inst](auto Vtx) { + return Graph->instOf(Vtx) != Inst; + }); + + n_t Stmt = Results->Insts[Inst]; + llvm::ArrayRef Run(It, RunEnd); + if (ICF->isCallSite(Stmt)) { + expandCall(Stmt, Run, Fun); + } else { + expandNormal(Stmt, Run, Fun); + } + It = RunEnd; + } + } + + void expandNormal(ByConstRef Stmt, llvm::ArrayRef Run, + FunctionId Fun) { + llvm::SmallVector, 2> NormalFFs; + for (const auto &Succ : ICF->getSuccsOf(Stmt)) { + NormalFFs.emplace_back(Succ, Problem->getNormalFlowFunction(Stmt, Succ)); + } + + for (auto Vtx : Run) { + d_t Fact = factOf(Vtx); + Graph->beginEdges(Vtx); + for (const auto &[Succ, FF] : NormalFFs) { + for (const auto &Target : computeTargets(FF, Fact)) { + Graph->addEdge(getOrCreateSuccVertex(Succ, Target, Fun)); + } + } + Graph->endEdges(Vtx); + } + } + + void expandCall(ByConstRef CallSite, llvm::ArrayRef Run, + FunctionId Fun) { + llvm::SmallVector Callees; + for (const auto &Callee : ICF->getCalleesOfCallAt(CallSite)) { + Callees.push_back(Callee); + } + + llvm::SmallVector RetSites; + llvm::SmallVector CallToRetFFs; + for (const auto &RetSite : ICF->getReturnSitesOfCallAt(CallSite)) { + RetSites.push_back(RetSite); + CallToRetFFs.push_back( + Problem->getCallToRetFlowFunction(CallSite, RetSite, Callees)); + } + + llvm::SmallVector CalleeInfos; + for (const auto &Callee : Callees) { + auto &Info = CalleeInfos.emplace_back(); + Info.Callee = Funs.get(Callee); + FlowFunctionPtrType SummaryFF = + Problem->getSummaryFlowFunction(CallSite, Callee); + if (SummaryFF) { + Info.FF = std::move(SummaryFF); + Info.IsSummaryFF = true; + continue; + } + + Info.FF = Problem->getCallFlowFunction(CallSite, Callee); + for (const auto &StartPoint : ICF->getStartPointsOf(Callee)) { + Info.StartPoints.push_back(StartPoint); + } + } + + for (auto Vtx : Run) { + d_t Fact = factOf(Vtx); + + Graph->beginEdges(Vtx); + for (const auto &[RetSite, FF] : llvm::zip(RetSites, CallToRetFFs)) { + for (const auto &Target : computeTargets(FF, Fact)) { + Graph->addEdge(getOrCreateSuccVertex(RetSite, Target, Fun)); + } + } + for (const auto &Info : CalleeInfos) { + if (!Info.IsSummaryFF) { + continue; + } + for (const auto &Target : computeTargets(Info.FF, Fact)) { + for (const auto &RetSite : RetSites) { + Graph->addEdge(getOrCreateSuccVertex(RetSite, Target, Fun)); + } + } + } + Graph->endEdges(Vtx); + + for (const auto &Info : CalleeInfos) { + if (Info.IsSummaryFF) { + continue; + } + for (const auto &CalleeFact : computeTargets(Info.FF, Fact)) { + for (const auto &StartPoint : Info.StartPoints) { + auto Entry = Graph->getOrCreateEntry( + getOrCreateVertex(StartPoint, CalleeFact, Info.Callee)); + auto KnownExits = Graph->addCallEdge(Vtx, Entry); + for (auto Exit : KnownExits) { + applySummary(Vtx, Entry, Exit); + } + } + } + } + } + } + + // --- Results + + void finalizeResults() { + auto &R = *Results; + if constexpr (StaticSolverConfigTy::BuildQueryIndex) { + R.Index.emplace(); + IntervalReachabilityBuilder().build( + Graph->buildSupergraph(), Graph->seeds(), *R.Index); + } + + R.VtxInst = Graph->takeVertexInsts(); + R.VtxFact = Graph->takeVertexFacts(); + auto NumVertices = R.VtxInst.size(); + + R.InstOffsets.assign(R.Insts.size() + 1, 0); + for (auto Inst : R.VtxInst) { + ++R.InstOffsets[Inst]; + } + std::partial_sum(R.InstOffsets.begin(), R.InstOffsets.end(), + R.InstOffsets.begin()); + R.VerticesAt.resize_for_overwrite(NumVertices); + for (auto Vtx : llvm::reverse(iota(NumVertices))) { + R.VerticesAt[--R.InstOffsets[R.VtxInst[Vtx]]] = Vtx; + } + for (auto Inst : iota(R.Insts.size())) { + llvm::ArrayRef Bounds(&R.InstOffsets[Inst], 2); + auto Vertices = llvm::MutableArrayRef(R.VerticesAt) + .slice(Bounds[0], Bounds[1] - Bounds[0]); + llvm::sort(Vertices, [&R](dfi::VertexId Lhs, dfi::VertexId Rhs) { + return R.VtxFact[Lhs] < R.VtxFact[Rhs]; + }); + } + + Graph.reset(); + Funs = FunctionCompressor(); + } + + ProblemTy *Problem{}; + const ICFGTy *ICF{}; + std::unique_ptr> Results; + + /// Only while solving + FunctionCompressor Funs; + std::optional Graph; +}; + +template +DFISolver(ProblemTy *, const ICFGTy *) + -> DFISolver; +template +DFISolver(ProblemTy *, const ICFGTy *, StaticSolverConfigTy) + -> DFISolver; + +/// Solves the given IFDS problem with the DFISolver and returns the owning +/// results +template +[[nodiscard]] auto solveDFIProblem(ProblemTy &Problem, const ICFG auto &ICF, + StaticSolverConfigTy Config = {}) { + return DFISolver(&Problem, &ICF, Config).solve(); +} + +} // namespace psr diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h new file mode 100644 index 0000000000..e8d858b8c7 --- /dev/null +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h @@ -0,0 +1,29 @@ +#pragma once + +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +namespace psr { + +/// Static configuration of the DFISolver. Derive from it to change options. +struct DFISolverConfig { + /// Propagate the zero fact along all flow functions, even if the analysis + /// problem does not do so explicitly + static constexpr bool AutoAddZero = true; + /// Build the reachability index required by DFISolver::getReachability() + static constexpr bool BuildQueryIndex = true; +}; + +/// DFISolverConfig without reachability index; use it if only the IFDS +/// results are of interest +struct DFISolverConfigNoIndex : DFISolverConfig { + static constexpr bool BuildQueryIndex = false; +}; + +} // namespace psr diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h new file mode 100644 index 0000000000..9afae26d83 --- /dev/null +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h @@ -0,0 +1,303 @@ +#pragma once + +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +#include "phasar/Domain/BinaryDomain.h" +#include "phasar/Utils/ByRef.h" +#include "phasar/Utils/Compressor.h" +#include "phasar/Utils/IntervalReachability.h" +#include "phasar/Utils/Printer.h" +#include "phasar/Utils/StrongTypeDef.h" +#include "phasar/Utils/TypedVector.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/raw_ostream.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/// A node of the exploded supergraph, materialized by the DFISolver +PHASAR_STRONG_TYPEDEF(psr::dfi, uint32_t, VertexId); +PHASAR_STRONG_TYPEDEF(psr::dfi, uint32_t, InstId); +/// The zero fact always has id 0 +PHASAR_STRONG_TYPEDEF(psr::dfi, uint32_t, FactId, Zero = 0); + +namespace psr { + +namespace detail { +template struct DFIResultsData { + Compressor Insts; + Compressor Facts; + + TypedVector VtxInst; + TypedVector VtxFact; + + /// The vertices at instruction I are + /// VerticesAt[InstOffsets[I] .. InstOffsets[I+1]), sorted by fact + TypedVector InstOffsets{0}; + llvm::SmallVector VerticesAt; + + std::optional> Index; +}; + +template class DFISolverResultsBase { +public: + using n_t = N; + using d_t = D; + using l_t = BinaryDomain; + + /// BinaryDomain::BOTTOM if Fact holds at Stmt, BinaryDomain::TOP otherwise + [[nodiscard]] l_t resultAt(ByConstRef Stmt, ByConstRef Fact) const { + return findVertex(Stmt, Fact) ? BinaryDomain::BOTTOM : BinaryDomain::TOP; + } + + [[nodiscard]] std::unordered_map + resultsAt(ByConstRef Stmt, bool StripZero = false) const { + std::unordered_map Ret; + forEachFactAt(Stmt, StripZero, [&Ret](ByConstRef Fact) { + Ret.try_emplace(Fact, BinaryDomain::BOTTOM); + }); + return Ret; + } + + [[nodiscard]] std::set ifdsResultsAt(ByConstRef Stmt) const { + std::set Ret; + forEachFactAt(Stmt, /*StripZero*/ false, + [&Ret](ByConstRef Fact) { Ret.insert(Fact); }); + return Ret; + } + + /// Calls Handler for each fact holding at Stmt + void forEachFactAt(ByConstRef Stmt, bool StripZero, + std::invocable> auto Handler) const { + const auto &Data = data(); + for (auto Vtx : verticesAt(Stmt)) { + auto Fact = Data.VtxFact[Vtx]; + if (StripZero && Fact == dfi::FactId::Zero) { + continue; + } + std::invoke(Handler, Data.Facts[Fact]); + } + } + + /// Number of instructions with results + [[nodiscard]] size_t size() const noexcept { return data().Insts.size(); } + + [[nodiscard]] bool containsNode(ByConstRef Stmt) const { + return data().Insts.getOrNull(Stmt).has_value(); + } + + void foreachResultEntry( + std::invocable> auto Handler) const { + const auto &Data = data(); + for (auto [Inst, Stmt] : Data.Insts.enumerate()) { + for (auto Vtx : verticesAt(Inst)) { + std::invoke(Handler, + std::make_tuple(Stmt, Data.Facts[Data.VtxFact[Vtx]], + BinaryDomain::BOTTOM)); + } + } + } + + void dumpResults(llvm::raw_ostream &OS = llvm::outs()) const { + const auto &Data = data(); + OS << "\n***************************************************************\n" + << "* Raw DFISolver results *\n" + << "***************************************************************\n"; + for (auto [Inst, Stmt] : Data.Insts.enumerate()) { + OS << "\nN: " << NToString(Stmt) << '\n'; + for (auto Vtx : verticesAt(Inst)) { + OS << "\tD: " << DToString(Data.Facts[Data.VtxFact[Vtx]]) << '\n'; + } + } + } + + /// The vertex for Fact at Stmt, if Fact holds there + [[nodiscard]] std::optional + findVertex(ByConstRef Stmt, ByConstRef Fact) const { + const auto &Data = data(); + auto FactId = Data.Facts.getOrNull(Fact); + if (!FactId) { + return std::nullopt; + } + auto Vertices = verticesAt(Stmt); + const auto *It = + std::lower_bound(Vertices.begin(), Vertices.end(), *FactId, + [&Data](dfi::VertexId Vtx, dfi::FactId F) { + return Data.VtxFact[Vtx] < F; + }); + if (It == Vertices.end() || Data.VtxFact[*It] != *FactId) { + return std::nullopt; + } + return *It; + } + + [[nodiscard]] llvm::ArrayRef + verticesAt(ByConstRef Stmt) const { + auto Inst = data().Insts.getOrNull(Stmt); + if (!Inst) { + return {}; + } + return verticesAt(*Inst); + } + + [[nodiscard]] llvm::ArrayRef + verticesAt(dfi::InstId Inst) const { + const auto &Data = data(); + llvm::ArrayRef Bounds(&Data.InstOffsets[Inst], 2); + return llvm::ArrayRef(Data.VerticesAt) + .slice(Bounds[0], Bounds[1] - Bounds[0]); + } + + [[nodiscard]] n_t instOf(dfi::VertexId Vtx) const { + return data().Insts[data().VtxInst[Vtx]]; + } + [[nodiscard]] d_t factOf(dfi::VertexId Vtx) const { + return data().Facts[data().VtxFact[Vtx]]; + } + +private: + [[nodiscard]] const DFIResultsData &data() const noexcept { + return static_cast(this)->dataImpl(); + } +}; +} // namespace detail + +/// Non-owning view into the results of a DFISolver. Conforms to the interface +/// expected by GenericSolverResults. +template +class DFISolverResults + : public detail::DFISolverResultsBase, N, D> { + friend detail::DFISolverResultsBase, N, D>; + +public: + explicit DFISolverResults(const detail::DFIResultsData *Data) noexcept + : Data(Data) { + assert(Data != nullptr); + } + +private: + [[nodiscard]] const detail::DFIResultsData &dataImpl() const noexcept { + return *Data; + } + + const detail::DFIResultsData *Data{}; +}; + +/// Owning variant of DFISolverResults +template +class OwningDFISolverResults + : public detail::DFISolverResultsBase, N, D> { + friend detail::DFISolverResultsBase, N, D>; + +public: + explicit OwningDFISolverResults( + std::unique_ptr> Data) noexcept + : Data(std::move(Data)) { + assert(this->Data != nullptr); + } + + [[nodiscard]] DFISolverResults get() const & noexcept { + return DFISolverResults(Data.get()); + } + DFISolverResults get() && = delete; + + [[nodiscard]] operator DFISolverResults() const & noexcept { + return get(); + } + operator DFISolverResults() && = delete; + +private: + [[nodiscard]] const detail::DFIResultsData &dataImpl() const noexcept { + return *Data; + } + + std::unique_ptr> Data; +}; + +/// Context-sensitive reachability between the exploded-supergraph nodes that +/// the DFISolver has materialized. +/// +/// From reaches To iff there is a realizable path from From to To that may +/// descend into callees, but does not return beyond the function of From. +/// Every node reaches itself. +template class DFIReachability { +public: + using n_t = N; + using d_t = D; + + explicit DFIReachability(const detail::DFIResultsData *Data) noexcept + : Results(Data), Index(&*Data->Index) {} + + /// The node , if it has been materialized, i.e., if Fact holds + /// at Stmt. + [[nodiscard]] std::optional find(ByConstRef Stmt, + ByConstRef Fact) const { + return Results.findVertex(Stmt, Fact); + } + + [[nodiscard]] bool reaches(dfi::VertexId From, + dfi::VertexId To) const noexcept { + return Index->reaches(From, To); + } + + [[nodiscard]] bool reaches(ByConstRef FromStmt, ByConstRef FromFact, + ByConstRef ToStmt, + ByConstRef ToFact) const { + auto From = find(FromStmt, FromFact); + auto To = find(ToStmt, ToFact); + return From && To && reaches(*From, *To); + } + + /// Calls Handler for each fact at At that is reachable from From + void forEachReachableAt(dfi::VertexId From, ByConstRef At, + std::invocable> auto Handler) const { + for (auto To : Results.verticesAt(At)) { + if (Index->reaches(From, To)) { + std::invoke(Handler, Results.factOf(To)); + } + } + } + + /// Calls Handler for each node reachable from From + void forEachReachable( + dfi::VertexId From, + std::invocable, ByConstRef> auto Handler) const { + Index->forEachReachableTarget(From, [&](dfi::VertexId To) { + std::invoke(Handler, Results.instOf(To), Results.factOf(To)); + }); + } + + [[nodiscard]] n_t instOf(dfi::VertexId Vtx) const { + return Results.instOf(Vtx); + } + [[nodiscard]] d_t factOf(dfi::VertexId Vtx) const { + return Results.factOf(Vtx); + } + + [[nodiscard]] const IntervalReachabilityIndex & + index() const noexcept { + return *Index; + } + +private: + DFISolverResults Results; + const IntervalReachabilityIndex *Index{}; +}; + +} // namespace psr diff --git a/include/phasar/Utils/CsrGraph.h b/include/phasar/Utils/CsrGraph.h new file mode 100644 index 0000000000..90734ba4f2 --- /dev/null +++ b/include/phasar/Utils/CsrGraph.h @@ -0,0 +1,164 @@ +#pragma once + +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +#include "phasar/Utils/EmptyBaseOptimizationUtils.h" +#include "phasar/Utils/GraphTraits.h" +#include "phasar/Utils/IotaIterator.h" +#include "phasar/Utils/RepeatIterator.h" +#include "phasar/Utils/TypeTraits.h" +#include "phasar/Utils/TypedVector.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include +#include +#include + +namespace psr { + +/// Immutable directed graph in compressed-sparse-row format: the successors of +/// vertex V are Targets[Offsets[V] .. Offsets[V+1]). +/// +/// Build it with fromEdges() or assignEdges(). assignEdges() reuses the +/// allocated storage, so a single CsrGraph object can serve as buffer for many +/// short-lived graphs. +template struct CsrGraph { + /// Builds the graph from (Source, Target) pairs. The successors of each + /// vertex keep the relative order of Edges. + [[nodiscard]] static CsrGraph + fromEdges(size_t NumVertices, llvm::ArrayRef> Edges) { + CsrGraph Ret; + Ret.assignEdges(NumVertices, Edges); + return Ret; + } + + /// Returns G with all edges inverted. The predecessors of each vertex are + /// sorted ascending. The roots of the result are the vertices without + /// successors in G. + [[nodiscard]] static CsrGraph reversed(const CsrGraph &G) { + CsrGraph Ret; + Ret.countEdges(G.numVertices(), G.Targets); + Ret.Targets.resize_for_overwrite(G.numEdges()); + for (auto From : llvm::reverse(iota(G.numVertices()))) { + for (auto To : G.succsOf(From)) { + Ret.Targets[--Ret.Offsets[To]] = From; + } + } + for (auto Vtx : iota(G.numVertices())) { + if (G.succsOf(Vtx).empty()) { + Ret.Roots.push_back(Vtx); + } + } + return Ret; + } + + /// Replaces the contents of this graph by NumVertices vertices and the + /// given (Source, Target) edges. Clears the roots. + void assignEdges(size_t NumVertices, + llvm::ArrayRef> Edges) { + countEdges(NumVertices, llvm::make_first_range(Edges)); + Targets.resize_for_overwrite(Edges.size()); + for (auto [From, To] : llvm::reverse(Edges)) { + assert(size_t(To) < NumVertices); + Targets[--Offsets[From]] = To; + } + Roots.clear(); + } + + [[nodiscard]] size_t numVertices() const noexcept { + assert(!Offsets.empty()); + return Offsets.size() - 1; + } + + [[nodiscard]] size_t numEdges() const noexcept { return Targets.size(); } + + [[nodiscard]] llvm::ArrayRef succsOf(VtxId Vtx) const noexcept { + assert(size_t(Vtx) < numVertices()); + llvm::ArrayRef Bounds(&Offsets[Vtx], 2); + return llvm::ArrayRef(Targets).slice(Bounds[0], + Bounds[1] - Bounds[0]); + } + + /// Sets Offsets[V] to the end of the successors of V. Inserting the edges + /// at --Offsets[From] in reverse order then leaves Offsets[V] at the begin. + void countEdges(size_t NumVertices, auto &&Sources) { + Offsets.assign(NumVertices + 1, 0); + for (auto From : Sources) { + ++Offsets[From]; + } + std::partial_sum(Offsets.begin(), Offsets.end(), Offsets.begin()); + } + + /// One entry per vertex plus a sentinel holding numEdges() + TypedVector Offsets{0}; + llvm::SmallVector Targets; + llvm::SmallVector Roots; +}; + +template struct GraphTraits> { + using graph_type = CsrGraph; + using value_type = EmptyType; + using vertex_t = VtxId; + using edge_t = VtxId; + + static constexpr auto Invalid = VtxId(UINT32_MAX); + + [[nodiscard]] static llvm::ArrayRef outEdges(const graph_type &G, + vertex_t Vtx) noexcept { + return G.succsOf(Vtx); + } + + [[nodiscard]] static size_t outDegree(const graph_type &G, + vertex_t Vtx) noexcept { + return G.succsOf(Vtx).size(); + } + + [[nodiscard]] static auto nodes(const graph_type &G) noexcept { + return repeat(EmptyType{}, G.numVertices()); + } + + [[nodiscard]] static llvm::ArrayRef + roots(const graph_type &G) noexcept { + return G.Roots; + } + + [[nodiscard]] static auto vertices(const graph_type &G) noexcept { + return iota(G.numVertices()); + } + + [[nodiscard]] static value_type node(const graph_type & /*G*/, + vertex_t /*Vtx*/) noexcept { + return {}; + } + + [[nodiscard]] static size_t size(const graph_type &G) noexcept { + return G.numVertices(); + } + + [[nodiscard]] static size_t + roots_size(const graph_type &G) noexcept { // NOLINT + return G.Roots.size(); + } + + [[nodiscard]] static constexpr vertex_t target(edge_t Edge) noexcept { + return Edge; + } + + [[nodiscard]] static constexpr edge_t withEdgeTarget(edge_t /*Edge*/, + vertex_t Tar) noexcept { + return Tar; + } +}; + +} // namespace psr diff --git a/include/phasar/Utils/IntervalReachability.h b/include/phasar/Utils/IntervalReachability.h new file mode 100644 index 0000000000..549613522a --- /dev/null +++ b/include/phasar/Utils/IntervalReachability.h @@ -0,0 +1,381 @@ +#pragma once + +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +#include "phasar/Utils/GraphTraits.h" +#include "phasar/Utils/SCCGeneric.h" +#include "phasar/Utils/SCCId.h" +#include "phasar/Utils/StrongTypeDef.h" +#include "phasar/Utils/TypeTraits.h" +#include "phasar/Utils/TypedVector.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" + +#include +#include +#include +#include +#include +#include +#include + +/// Position of a target vertex in an IntervalReachabilityIndex. +PHASAR_STRONG_TYPEDEF(psr, uint32_t, ReachRank, None = UINT32_MAX); +/// Id of a set of rank intervals in an IntervalReachabilityIndex. +PHASAR_STRONG_TYPEDEF(psr, uint32_t, IntervalSetId, Empty = 0); + +namespace psr { + +/// Half-open interval [Lo, Hi) of ranks. +struct RankInterval { + [[nodiscard]] constexpr bool empty() const noexcept { return Lo >= Hi; } + [[nodiscard]] constexpr size_t size() const noexcept { + return empty() ? 0 : to_underlying(Hi) - to_underlying(Lo); + } + [[nodiscard]] constexpr bool contains(ReachRank Rank) const noexcept { + return Lo <= Rank && Rank < Hi; + } + + [[nodiscard]] friend constexpr bool + operator==(RankInterval Lhs, RankInterval Rhs) noexcept = default; + + ReachRank Lo{}; + ReachRank Hi{}; +}; + +template class IntervalReachabilityBuilder; + +/// Reachability index over a directed graph: answers whether a vertex reaches +/// a target vertex in O(log k), where k is the number of rank intervals of the +/// source (typically 1-3). +/// +/// Built by IntervalReachabilityBuilder from a set of roots: only vertices +/// reachable from the roots are indexed. The targets are a subset of the +/// indexed vertices, selected at build time; each target has a unique rank in +/// [0, numTargets()). Every indexed vertex reaches itself. +/// +/// The targets reachable from a vertex form a sorted list of disjoint rank +/// intervals, see forEachReachableRange(). +template class IntervalReachabilityIndex { + friend class IntervalReachabilityBuilder; + +public: + using SCCIdTy = SCCId; + + static constexpr SCCIdTy NoSCC = SCCIdTy(UINT32_MAX); + + [[nodiscard]] size_t numVertices() const noexcept { return SCCOf.size(); } + [[nodiscard]] size_t numTargets() const noexcept { + return RankToVertex.size(); + } + [[nodiscard]] size_t numSCCs() const noexcept { return SCCRange.size(); } + + /// Whether Vtx was reachable from the roots at build time. + [[nodiscard]] bool isIndexed(VtxId Vtx) const noexcept { + return SCCOf[Vtx] != NoSCC; + } + + [[nodiscard]] bool isTarget(VtxId Vtx) const noexcept { + return RankOf[Vtx] != ReachRank::None; + } + + /// The rank of Vtx, or ReachRank::None if Vtx is not a target. + [[nodiscard]] ReachRank rankOf(VtxId Vtx) const noexcept { + return RankOf[Vtx]; + } + + [[nodiscard]] VtxId vertexOfRank(ReachRank Rank) const noexcept { + return RankToVertex[Rank]; + } + + /// The targets covered by the given rank interval. + [[nodiscard]] llvm::ArrayRef + targetsIn(RankInterval Interval) const noexcept { + if (Interval.empty()) { + return {}; + } + return {&RankToVertex[Interval.Lo], Interval.size()}; + } + + /// The strongly connected component of Vtx, or NoSCC if Vtx is not indexed. + /// SCC ids are in reverse topological order: an SCC only reaches SCCs with + /// smaller or equal id. + [[nodiscard]] SCCIdTy sccOf(VtxId Vtx) const noexcept { return SCCOf[Vtx]; } + + /// Whether From reaches the target To. False, if From is not indexed or To + /// is not a target. + [[nodiscard]] bool reaches(VtxId From, VtxId To) const noexcept { + auto SCC = SCCOf[From]; + auto Rank = RankOf[To]; + if (SCC == NoSCC || Rank == ReachRank::None) { + return false; + } + return sccReaches(SCC, Rank); + } + + /// Whether the given SCC reaches the target with the given rank. + [[nodiscard]] bool sccReaches(SCCIdTy SCC, ReachRank Rank) const noexcept { + if (SCCRange[SCC].contains(Rank)) { + return true; + } + + auto Extras = extrasOf(SCC); + const auto *It = std::upper_bound( + Extras.begin(), Extras.end(), Rank, + [](ReachRank R, RankInterval Interval) { return R < Interval.Lo; }); + return It != Extras.begin() && Rank < std::prev(It)->Hi; + } + + /// Calls Handler for each interval of target ranks reachable from From, in + /// ascending order. The intervals are disjoint and non-empty. + void forEachReachableRange(VtxId From, + std::invocable auto Handler) const { + auto SCC = SCCOf[From]; + if (SCC == NoSCC) { + return; + } + for (auto Interval : extrasOf(SCC)) { + std::invoke(Handler, Interval); + } + if (!SCCRange[SCC].empty()) { + std::invoke(Handler, SCCRange[SCC]); + } + } + + /// Calls Handler for each target reachable from From, in rank order. + void forEachReachableTarget(VtxId From, + std::invocable auto Handler) const { + forEachReachableRange(From, [&](RankInterval Interval) { + for (auto Vtx : targetsIn(Interval)) { + std::invoke(Handler, Vtx); + } + }); + } + + /// Number of rank intervals describing the targets reachable from SCC. + [[nodiscard]] size_t numIntervalsOf(SCCIdTy SCC) const noexcept { + return extrasOf(SCC).size() + size_t(!SCCRange[SCC].empty()); + } + + [[nodiscard]] size_t getApproxSizeInBytes() const noexcept { + return SCCOf.capacity() * sizeof(SCCIdTy) + + RankOf.capacity() * sizeof(ReachRank) + + RankToVertex.capacity() * sizeof(VtxId) + + SCCRange.capacity() * sizeof(RankInterval) + + SCCExtras.capacity() * sizeof(IntervalSetId) + + IntervalPool.capacity_in_bytes() + + SetOffsets.capacity() * sizeof(uint32_t); + } + +private: + [[nodiscard]] llvm::ArrayRef + extrasOf(SCCIdTy SCC) const noexcept { + llvm::ArrayRef Bounds(&SetOffsets[SCCExtras[SCC]], 2); + return llvm::ArrayRef(IntervalPool) + .slice(Bounds[0], Bounds[1] - Bounds[0]); + } + + void reset(size_t NumVertices) { + SCCOf.assign(NumVertices, NoSCC); + RankOf.assign(NumVertices, ReachRank::None); + RankToVertex.clear(); + SCCRange.clear(); + SCCExtras.clear(); + IntervalPool.clear(); + SetOffsets.clear(); + // IntervalSetId::Empty + SetOffsets.push_back(0); + SetOffsets.push_back(0); + } + + TypedVector SCCOf; + TypedVector RankOf; + TypedVector RankToVertex; + + /// Ranks of the targets in the DFS subtree of the SCC's root + TypedVector SCCRange; + /// Targets reachable via non-tree edges. Their intervals all lie below + /// SCCRange[SCC].Lo: targets with higher ranks are discovered after the SCC + /// is complete, so they are not reachable from it. + TypedVector SCCExtras; + + /// Interval sets in CSR format + llvm::SmallVector IntervalPool; + TypedVector SetOffsets; +}; + +/// Computes an IntervalReachabilityIndex during a single DFS (Pearce's SCC +/// algorithm, see visitSCCsFrom()). +/// +/// Owns scratch buffers that are reused across build() calls; keep the builder +/// alive when indexing many (small) graphs. +/// +/// Complexity: O(V + E + total size of the merged interval sets). +template class IntervalReachabilityBuilder { +public: + using IndexTy = IntervalReachabilityIndex; + using SCCIdTy = typename IndexTy::SCCIdTy; + + /// Indexes all vertices of Graph reachable from Roots, all of them as + /// targets. The ranks are then DFS pre-order numbers. + template + requires std::same_as::vertex_t, VtxId> + void build(const G &Graph, llvm::ArrayRef Roots, IndexTy &Out) { + build(Graph, Roots, [](VtxId /*Vtx*/) { return true; }, Out); + } + + /// Indexes all vertices of Graph reachable from Roots, only those + /// satisfying IsTarget as targets. + /// + /// Select few targets if only reachability to those is of interest: the + /// interval sets shrink accordingly. + template + requires std::same_as::vertex_t, VtxId> + void build(const G &Graph, llvm::ArrayRef Roots, + std::predicate auto IsTarget, IndexTy &Out) { + auto NumVertices = GraphTraits::size(Graph); + Out.reset(NumVertices); + SCCData.reset(NumVertices); + RankBegin.resize(NumVertices); + SeenBy.clear(); + + const auto OnDiscover = [&](VtxId Vtx) { + auto Rank = ReachRank(Out.RankToVertex.size()); + RankBegin[Vtx] = Rank; + if (std::invoke(IsTarget, Vtx)) { + Out.RankOf[Vtx] = Rank; + Out.RankToVertex.push_back(Vtx); + } + }; + const auto OnSCC = [&](SCCIdTy SCC, llvm::ArrayRef Members) { + completeSCC(Graph, SCC, Members, Out); + }; + + for (auto Root : Roots) { + if (!SCCData.isVisited(Root)) { + visitSCCsFrom(Graph, Root, SCCData, OnDiscover, OnSCC); + } + } + } + +private: + template + void completeSCC(const G &Graph, SCCIdTy SCC, llvm::ArrayRef Members, + IndexTy &Out) { + assert(size_t(SCC) == Out.SCCRange.size()); + + // Members.back() is the root of the SCC in the DFS tree. Its subtree is + // completely discovered. + RankInterval Range{RankBegin[Members.back()], + ReachRank(Out.RankToVertex.size())}; + + for (auto Member : Members) { + Out.SCCOf[Member] = SCC; + } + Out.SCCRange.push_back(Range); + SeenBy.push_back(SCC); + + // All successor SCCs are complete, as SCCs complete in reverse + // topological order + SuccSCCs.clear(); + for (auto Member : Members) { + for (const auto &Edge : GraphTraits::outEdges(Graph, Member)) { + auto SuccSCC = Out.SCCOf[GraphTraits::target(Edge)]; + if (SuccSCC == SCC || SeenBy[SuccSCC] == SCC) { + continue; + } + SeenBy[SuccSCC] = SCC; + SuccSCCs.push_back(SuccSCC); + } + } + + Out.SCCExtras.push_back(mergeExtras(Range.Lo, Out)); + } + + /// Computes the extras-set of an SCC whose range starts at Lo from the + /// interval sets of its successor SCCs. + /// + /// The range of a successor SCC lies either inside the own range (DFS-tree + /// descendant) or entirely below Lo (cross edge). Successor extras at or + /// above Lo are covered by the own range, so they are clipped at Lo. + [[nodiscard]] IntervalSetId mergeExtras(ReachRank Lo, IndexTy &Out) { + const auto IsCrossRange = [Lo](RankInterval Range) { + return !Range.empty() && Range.Hi <= Lo; + }; + + // Fast path for chains and loop bodies: a single contributing descendant, + // whose extras need no clipping + size_t NumContributing = 0; + std::optional Reusable; + for (auto SuccSCC : SuccSCCs) { + bool IsCross = IsCrossRange(Out.SCCRange[SuccSCC]); + auto Extras = Out.extrasOf(SuccSCC); + if (!IsCross && Extras.empty()) { + continue; + } + + ++NumContributing; + Reusable = std::nullopt; + if (!IsCross && Extras.back().Hi <= Lo) { + Reusable = Out.SCCExtras[SuccSCC]; + } + } + if (NumContributing == 0) { + return IntervalSetId::Empty; + } + if (NumContributing == 1 && Reusable) { + return *Reusable; + } + + Candidates.clear(); + for (auto SuccSCC : SuccSCCs) { + auto Range = Out.SCCRange[SuccSCC]; + if (IsCrossRange(Range)) { + Candidates.push_back(Range); + } + for (auto Interval : Out.extrasOf(SuccSCC)) { + if (Interval.Lo >= Lo) { + break; + } + Candidates.push_back({Interval.Lo, std::min(Interval.Hi, Lo)}); + } + } + + std::sort( + Candidates.begin(), Candidates.end(), + [](RankInterval Lhs, RankInterval Rhs) { return Lhs.Lo < Rhs.Lo; }); + + auto Ret = IntervalSetId(Out.SetOffsets.size() - 1); + for (auto Interval : Candidates) { + // Coalesce overlapping and adjacent intervals + if (Out.IntervalPool.size() != Out.SetOffsets.asRef().back() && + Interval.Lo <= Out.IntervalPool.back().Hi) { + Out.IntervalPool.back().Hi = + std::max(Out.IntervalPool.back().Hi, Interval.Hi); + continue; + } + Out.IntervalPool.push_back(Interval); + } + Out.SetOffsets.push_back(uint32_t(Out.IntervalPool.size())); + return Ret; + } + + PearceSCCData SCCData; + TypedVector RankBegin; + + /// The last SCC that has seen this SCC as successor + TypedVector SeenBy; + + llvm::SmallVector SuccSCCs; + llvm::SmallVector Candidates; +}; + +} // namespace psr diff --git a/include/phasar/Utils/SCCGeneric.h b/include/phasar/Utils/SCCGeneric.h index 391cce0af1..2aa8de80e4 100644 --- a/include/phasar/Utils/SCCGeneric.h +++ b/include/phasar/Utils/SCCGeneric.h @@ -25,6 +25,7 @@ #include "llvm/Support/raw_ostream.h" #include +#include #include namespace psr { @@ -238,36 +239,77 @@ computeSCCOrder(const SCCHolder &SCCs, return Ret; } -namespace detail { -/// Data for Pearce's Algorithm. -template struct Pearce4Data { +/// State of Pearce's SCC algorithm. Can be reused across multiple calls to +/// visitSCCsFrom() on the same graph and, after reset(), on other graphs to +/// avoid repeated allocations. +template struct PearceSCCData { TypedVector RIndex; // only per-vertex array BitSet Root; // root[v] in Algorithm 4 uint32_t Index = 1; // DFS counter - uint32_t C; // SCC id counter + uint32_t C{}; // SCC id counter llvm::SmallVector Stack; - explicit Pearce4Data(size_t Num) - : RIndex(Num, 0), Root(Num), C(Num ? Num - 1 : 0) {} + PearceSCCData() noexcept = default; + explicit PearceSCCData(size_t Num) { reset(Num); } + + /// Prepares the state for a fresh search on a graph with Num vertices. + void reset(size_t Num) { + RIndex.assign(Num, 0); + Root.clear(); + Root.reserve(Num); + Index = 1; + C = Num ? uint32_t(Num - 1) : 0; + Stack.clear(); + } + + [[nodiscard]] bool isVisited(GraphNodeId Vtx) const noexcept { + return RIndex[Vtx] != 0; + } }; +namespace detail { + +/// Completes the SCC rooted at Root. The members are the stack elements with +/// an rindex not smaller than the root's. +template +void pearce4FinishSCC(PearceSCCData &Data, GraphNodeId Root, + OnSCCFn &OnSCC) { + auto RootIndex = Data.RIndex[Root]; + size_t NumMembers = 1; + while (NumMembers <= Data.Stack.size() && + RootIndex <= Data.RIndex[Data.Stack[Data.Stack.size() - NumMembers]]) { + ++NumMembers; + } + Data.Stack.push_back(Root); + + auto Members = llvm::ArrayRef(Data.Stack).take_back(NumMembers); + auto NewSCC = SCCId(uint32_t(Data.RIndex.size() - 1 - Data.C)); + std::invoke(OnSCC, NewSCC, Members); + + for (auto W : Members) { + Data.RIndex[W] = Data.C; + } + Data.Index -= uint32_t(NumMembers); + Data.C--; + Data.Stack.truncate(Data.Stack.size() - NumMembers); +} + // Recursive variant of Pearce's algorithm (based on Algo 3 in the paper) -template +template static void pearce4VisitRec(const G &Graph, typename GraphTraits::vertex_t V, - Pearce4Data::vertex_t> &Data, - SCCHolder::vertex_t> &Holder) { + PearceSCCData::vertex_t> &Data, + OnDiscoverFn &OnDiscover, OnSCCFn &OnSCC) { using GTraits = psr::GraphTraits; - using Vertex = typename GTraits::vertex_t; - using SCCId = psr::SCCId; bool Root = true; Data.RIndex[V] = Data.Index++; + std::invoke(OnDiscover, V); for (const auto &Edge : GTraits::outEdges(Graph, V)) { auto W = GTraits::target(Edge); if (Data.RIndex[W] == 0) { - pearce4VisitRec(Graph, W, Data, Holder); + pearce4VisitRec(Graph, W, Data, OnDiscover, OnSCC); } if (Data.RIndex[W] < Data.RIndex[V]) { Data.RIndex[V] = Data.RIndex[W]; @@ -276,37 +318,20 @@ pearce4VisitRec(const G &Graph, typename GraphTraits::vertex_t V, } if (Root) { - Data.Index--; - auto NewSCC = SCCId(Holder.NodesInSCC.size()); - auto &Nodes = Holder.NodesInSCC.emplace_back(); - - while (!Data.Stack.empty() && - Data.RIndex[V] <= Data.RIndex[Data.Stack.back()]) { - auto W = Data.Stack.pop_back_val(); - Data.RIndex[W] = Data.C; - Data.Index--; - - Holder.SCCOfNode[W] = NewSCC; - Nodes.push_back(W); - } - Nodes.push_back(V); - Holder.SCCOfNode[V] = NewSCC; - Data.RIndex[V] = Data.C; - Data.C--; + pearce4FinishSCC(Data, V, OnSCC); } else { Data.Stack.push_back(V); } } // Iterative variant of Pearce's algorithm (adapted from on Algo 4 in the paper) -template +template static void pearce4VisitIt(const G &Graph, typename GraphTraits::vertex_t Start, - Pearce4Data::vertex_t> &Data, - SCCHolder::vertex_t> &Holder) { + PearceSCCData::vertex_t> &Data, + OnDiscoverFn &OnDiscover, OnSCCFn &OnSCC) { using GTraits = psr::GraphTraits; using Vertex = typename GTraits::vertex_t; - using SCCId = psr::SCCId; using OutEdgeRange = decltype(GTraits::outEdges(Graph, std::declval())); @@ -338,6 +363,7 @@ pearce4VisitIt(const G &Graph, typename GraphTraits::vertex_t Start, Data.RIndex[W] = Data.Index++; Data.Root.insert(W); + std::invoke(OnDiscover, W); auto &&OutEdges = GTraits::outEdges(Graph, W); Frame = &CallStack.emplace_back( @@ -366,24 +392,7 @@ pearce4VisitIt(const G &Graph, typename GraphTraits::vertex_t Start, // finish visiting V and backtrack to the parent if (Data.Root.contains(V)) { - // Found a SCC - - Data.Index--; - auto NewSCC = SCCId(Holder.NodesInSCC.size()); - auto &Nodes = Holder.NodesInSCC.emplace_back(); - while (!Data.Stack.empty() && - Data.RIndex[V] <= Data.RIndex[Data.Stack.back()]) { - auto W = Data.Stack.pop_back_val(); - Data.RIndex[W] = Data.C; - Data.Index--; - - Holder.SCCOfNode[W] = NewSCC; - Nodes.push_back(W); - } - Nodes.push_back(V); - Holder.SCCOfNode[V] = NewSCC; - Data.RIndex[V] = Data.C; - Data.C--; + pearce4FinishSCC(Data, V, OnSCC); } else { Data.Stack.push_back(V); } @@ -394,6 +403,7 @@ pearce4VisitIt(const G &Graph, typename GraphTraits::vertex_t Start, // Initialize the callstack by pushing the initial frame Data.RIndex[Start] = Data.Index++; Data.Root.insert(Start); + std::invoke(OnDiscover, Start); { auto &&OutEdges = GTraits::outEdges(Graph, Start); static_assert( @@ -426,6 +436,35 @@ pearce4VisitIt(const G &Graph, typename GraphTraits::vertex_t Start, } // namespace detail +/// Runs Pearce's SCC algorithm from Start, visiting all vertices reachable +/// from Start that have not yet been visited with the same Data. Call it with +/// multiple start vertices to cover more of the graph. +/// +/// \param OnDiscover Called with each vertex when it is visited first, i.e., +/// in DFS pre-order. +/// \param OnSCC Called with each SCC when it is complete, in reverse +/// topological order. SCC ids are sequential, starting at 0 after +/// Data.reset(). Members.back() is the first discovered member, i.e., the root +/// of the SCC in the DFS tree. At this point, all SCCs reachable from this SCC +/// are complete and the DFS subtree of the root is fully discovered. +template +void visitSCCsFrom( + const G &Graph, typename GraphTraits::vertex_t Start, + PearceSCCData::vertex_t> &Data, + std::invocable::vertex_t> auto OnDiscover, + std::invocable::vertex_t>, + llvm::ArrayRef::vertex_t>> auto + OnSCC, + std::bool_constant /*Iterative*/ = {}) { + assert(!Data.isVisited(Start)); + if constexpr (Iterative) { + detail::pearce4VisitIt(Graph, Start, Data, OnDiscover, OnSCC); + } else { + detail::pearce4VisitRec(Graph, Start, Data, OnDiscover, OnSCC); + } + assert(Data.Stack.empty() && "The start vertex must be an SCC root"); +} + /// Compute SCCs adapted from the paper "A Space-Efficient Algorithm for Finding /// Strongly Connected Components", Pearce 2015, DOI: /// @@ -448,27 +487,22 @@ computeSCCs(const G &Graph, std::bool_constant /*Iterative*/ = {}) { Ret.SCCOfNode.resize(N); - detail::Pearce4Data Data(N); + PearceSCCData Data(N); - // for all v ∈ V do if rindex[v]==0 then visit(v) - for (auto V : GTraits::vertices(Graph)) { - if (Data.RIndex[V] == 0) { - if constexpr (Iterative) { - detail::pearce4VisitIt(Graph, V, Data, Ret); - } else { - detail::pearce4VisitRec(Graph, V, Data, Ret); - } + const auto OnSCC = [&Ret](SCCId SCC, llvm::ArrayRef Members) { + assert(size_t(SCC) == Ret.NodesInSCC.size()); + Ret.NodesInSCC.emplace_back(Members.begin(), Members.end()); + for (auto Member : Members) { + Ret.SCCOfNode[Member] = SCC; + } + }; - if (!Data.Stack.empty()) { - auto NewSCC = SCCId(Ret.NodesInSCC.size()); - auto &Nodes = Ret.NodesInSCC.emplace_back(); - Nodes.reserve(Data.Stack.size()); - for (auto Vtx : Data.Stack) { - Nodes.push_back(Vtx); - Ret.SCCOfNode[Vtx] = NewSCC; - } - Data.Stack.clear(); - } + // for all v in V do if rindex[v]==0 then visit(v) + for (auto V : GTraits::vertices(Graph)) { + if (!Data.isVisited(V)) { + visitSCCsFrom( + Graph, V, Data, [](Vertex /*Vtx*/) {}, OnSCC, + std::bool_constant{}); } } diff --git a/include/phasar/Utils/TypedVector.h b/include/phasar/Utils/TypedVector.h index 9447f07c1c..0c018b6956 100644 --- a/include/phasar/Utils/TypedVector.h +++ b/include/phasar/Utils/TypedVector.h @@ -54,6 +54,10 @@ class TypedVector { void resize(size_t Sz, ByConstRef Val) { Vec.resize(Sz, Val); } + void assign(size_t Sz, ByConstRef Val) { Vec.assign(Sz, Val); } + + void clear() noexcept { Vec.clear(); } + [[nodiscard]] bool empty() const noexcept { return Vec.empty(); } [[nodiscard]] bool any() const noexcept { return !Vec.empty(); } [[nodiscard]] size_t size() const noexcept { return Vec.size(); } diff --git a/lib/DataFlow/IfdsIde/IfdsIde.cppm b/lib/DataFlow/IfdsIde/IfdsIde.cppm index 4a443803af..61cce44f0c 100644 --- a/lib/DataFlow/IfdsIde/IfdsIde.cppm +++ b/lib/DataFlow/IfdsIde/IfdsIde.cppm @@ -11,6 +11,7 @@ module; #include "phasar/DataFlow/IfdsIde/IFDSTabulationProblem.h" #include "phasar/DataFlow/IfdsIde/IfdsIdeDomain.h" #include "phasar/DataFlow/IfdsIde/Solver/Compressor.h" +#include "phasar/DataFlow/IfdsIde/Solver/DFISolver.h" #include "phasar/DataFlow/IfdsIde/Solver/EdgeFunctionCache.h" #include "phasar/DataFlow/IfdsIde/Solver/FlowEdgeFunctionCacheNG.h" #include "phasar/DataFlow/IfdsIde/Solver/FlowFunctionCache.h" @@ -47,6 +48,11 @@ using psr::checkSREquality; using psr::Compressor; using psr::defaultJoinOrNull; using psr::DefaultMapKeyCompressor; +using psr::DFIReachability; +using psr::DFISolver; +using psr::DFISolverConfig; +using psr::DFISolverConfigNoIndex; +using psr::DFISolverResults; using psr::EdgeFunctionCache; using psr::EdgeFunctionCacheStats; using psr::EdgeFunctionComposer; @@ -80,7 +86,9 @@ using psr::LLVMMapKeyCompressor; using psr::MapKeyCompressorCombinator; using psr::NodeCompressorTraits; using psr::NoneCompressor; +using psr::OwningDFISolverResults; using psr::OwningSolverResults; +using psr::solveDFIProblem; using psr::SolverConfigOptions; using psr::SolverResults; using psr::ValCompressorTraits; diff --git a/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp b/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp new file mode 100644 index 0000000000..426e1e3fe4 --- /dev/null +++ b/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp @@ -0,0 +1,204 @@ +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +#include "phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h" + +#include "phasar/Utils/IotaIterator.h" + +#include "llvm/ADT/STLExtras.h" + +#include + +using psr::detail::DFIExplodedSupergraph; + +DFIExplodedSupergraph::DFIExplodedSupergraph( + psr::SCCHolder CGSCCs) + : CGSCCs(std::move(CGSCCs)) { + FunState.resize(this->CGSCCs.SCCOfNode.size()); + PendingSCCs.reserve(this->CGSCCs.size()); +} + +auto DFIExplodedSupergraph::nextAction() -> std::optional { + while (auto SCC = PendingSCCs.findFirst()) { + const auto &FunsInSCC = CGSCCs.NodesInSCC[*SCC]; + for (auto Fun : FunsInSCC) { + if (!FunState[Fun].Worklist.empty()) { + return Action{Fun, ActionKind::Expand}; + } + } + for (auto Fun : FunsInSCC) { + if (FunState[Fun].Dirty) { + return Action{Fun, ActionKind::Summarize}; + } + } + PendingSCCs.erase(*SCC); + } + return std::nullopt; +} + +psr::dfi::VertexId DFIExplodedSupergraph::getOrCreateVertex(dfi::InstId Inst, + dfi::FactId Fact, + FunctionId Fun, + bool IsExitVtx) { + auto [It, Inserted] = + VertexOf.try_emplace({Inst, Fact}, dfi::VertexId(VtxInst.size())); + auto Vtx = It->second; + if (!Inserted) { + return Vtx; + } + + auto &FS = FunState[Fun]; + VtxInst.push_back(Inst); + VtxFact.push_back(Fact); + VtxFun.push_back(Fun); + VtxLocalId.push_back(dfi::LocalVertexId(FS.Vertices.size())); + VtxEdgeBegin.push_back(0); + VtxEdgeEnd.push_back(0); + FS.Vertices.push_back(Vtx); + if (IsExitVtx) { + IsExit.insert(Vtx); + } + + FS.Worklist.push_back(Vtx); + markDirty(Fun); + return Vtx; +} + +bool DFIExplodedSupergraph::addSeed(dfi::VertexId Vtx) { + if (!addRoot(Vtx)) { + return false; + } + SeedVertices.push_back(Vtx); + return true; +} + +psr::dfi::EntryId DFIExplodedSupergraph::getOrCreateEntry(dfi::VertexId Vtx) { + auto [It, Inserted] = + EntryOf.try_emplace(Vtx, dfi::EntryId(EntryVertex.size())); + auto Entry = It->second; + if (Inserted) { + EntryVertex.push_back(Vtx); + EntryCallers.emplace_back(); + SummaryExits.emplace_back(); + FunState[VtxFun[Vtx]].Entries.push_back(Entry); + addRoot(Vtx); + } + return Entry; +} + +llvm::ArrayRef +DFIExplodedSupergraph::takeWorklist(FunctionId Fun) { + Batch.clear(); + std::swap(Batch, FunState[Fun].Worklist); + llvm::sort(Batch, [this](dfi::VertexId Lhs, dfi::VertexId Rhs) { + return VtxInst[Lhs] < VtxInst[Rhs]; + }); + return Batch; +} + +bool DFIExplodedSupergraph::addSummaryEdge(dfi::VertexId CallVtx, + dfi::VertexId RetVtx) { + if (!SummaryEdgeSet.insert({CallVtx, RetVtx}).second) { + return false; + } + auto Caller = VtxFun[CallVtx]; + FunState[Caller].SummaryEdges.emplace_back(CallVtx, RetVtx); + markDirty(Caller); + return true; +} + +llvm::ArrayRef +DFIExplodedSupergraph::addCallEdge(dfi::VertexId CallVtx, dfi::EntryId Entry) { + EntryCallers[Entry].push_back(CallVtx); + CallEdges.emplace_back(CallVtx, EntryVertex[Entry]); + return SummaryExits[Entry]; +} + +psr::CsrGraph +DFIExplodedSupergraph::buildSupergraph() const { + assert(VtxInst.size() == VtxFun.size() && + "Must not build the supergraph after takeVertexInsts()"); + llvm::SmallVector, 0> GraphEdges; + GraphEdges.reserve(Edges.size() + SummaryEdgeSet.size() + CallEdges.size()); + for (auto From : iota(numVertices())) { + for (auto To : edgesOf(From)) { + GraphEdges.emplace_back(From, To); + } + } + for (const auto &FS : FunState) { + GraphEdges.append(FS.SummaryEdges.begin(), FS.SummaryEdges.end()); + } + GraphEdges.append(CallEdges.begin(), CallEdges.end()); + return CsrGraph::fromEdges(numVertices(), GraphEdges); +} + +void DFIExplodedSupergraph::markDirty(FunctionId Fun) { + FunState[Fun].Dirty = true; + PendingSCCs.insert(CGSCCs.SCCOfNode[Fun]); +} + +bool DFIExplodedSupergraph::addRoot(dfi::VertexId Vtx) { + if (!IsRoot.tryInsert(Vtx)) { + return false; + } + auto Fun = VtxFun[Vtx]; + FunState[Fun].Roots.push_back(Vtx); + markDirty(Fun); + return true; +} + +void DFIExplodedSupergraph::indexFunction(FunctionId Fun) { + auto &FS = FunState[Fun]; + FS.Dirty = false; + + LocalEdges.clear(); + for (auto Vtx : FS.Vertices) { + auto From = VtxLocalId[Vtx]; + for (auto To : edgesOf(Vtx)) { + LocalEdges.emplace_back(From, VtxLocalId[To]); + } + } + for (auto [From, To] : FS.SummaryEdges) { + LocalEdges.emplace_back(VtxLocalId[From], VtxLocalId[To]); + } + LocalGraph.assignEdges(FS.Vertices.size(), LocalEdges); + + LocalRoots.clear(); + for (auto Root : FS.Roots) { + LocalRoots.push_back(VtxLocalId[Root]); + } + + LocalBuilder.build( + LocalGraph, LocalRoots, + [this, &FS](dfi::LocalVertexId Local) { + return IsExit.contains(FS.Vertices[Local]); + }, + LocalIndex); +} + +llvm::ArrayRef +DFIExplodedSupergraph::updateSummaryExits(FunctionId Fun, dfi::EntryId Entry) { + const auto &FS = FunState[Fun]; + auto &Exits = SummaryExits[Entry]; + + NewExits.clear(); + LocalIndex.forEachReachableTarget( + VtxLocalId[EntryVertex[Entry]], [&](dfi::LocalVertexId Local) { + auto Exit = FS.Vertices[Local]; + if (!std::binary_search(Exits.begin(), Exits.end(), Exit)) { + NewExits.push_back(Exit); + } + }); + + if (!NewExits.empty()) { + Exits.append(NewExits.begin(), NewExits.end()); + llvm::sort(Exits); + } + return NewExits; +} diff --git a/lib/Utils/Utils.cppm b/lib/Utils/Utils.cppm index e395a4d3c6..45d9304e0e 100644 --- a/lib/Utils/Utils.cppm +++ b/lib/Utils/Utils.cppm @@ -11,6 +11,7 @@ module; #include "phasar/Utils/ByRef.h" #include "phasar/Utils/ChronoUtils.h" #include "phasar/Utils/Compressor.h" +#include "phasar/Utils/CsrGraph.h" #include "phasar/Utils/DFAMinimizer.h" #include "phasar/Utils/DOTGraph.h" #include "phasar/Utils/DebugOutput.h" @@ -24,6 +25,7 @@ module; #include "phasar/Utils/GraphTraits.h" #include "phasar/Utils/IO.h" #include "phasar/Utils/InitPhasar.h" +#include "phasar/Utils/IntervalReachability.h" #include "phasar/Utils/IotaIterator.h" #include "phasar/Utils/JoinLattice.h" #include "phasar/Utils/LibCSummary.h" @@ -100,6 +102,7 @@ using psr::computeSCCOrder; using psr::computeSCCs; using psr::computeSCCsAndTopologicalOrder; using psr::createTimeStamp; +using psr::CsrGraph; using psr::DefaultConstruct; using psr::DefaultNodeTransform; using psr::DenseSet; @@ -134,6 +137,9 @@ using psr::IdentityFn; using psr::IgnoreArgs; using psr::InitPhasar; using psr::intersectWith; +using psr::IntervalReachabilityBuilder; +using psr::IntervalReachabilityIndex; +using psr::IntervalSetId; using psr::iota; using psr::IotaIterator; using psr::is_const_graph; @@ -177,7 +183,10 @@ using psr::Overloaded; using psr::PAMM; using psr::PAMM_SEVERITY_LEVEL; using psr::parseSeverityLevel; +using psr::PearceSCCData; using psr::printGraph; +using psr::RankInterval; +using psr::ReachRank; using psr::readFile; using psr::readFileOrErr; using psr::readFileOrNull; @@ -215,6 +224,7 @@ using psr::UnorderedTable1d; using psr::unsetFlag; using psr::unwrapNullable; using psr::variant_idx; +using psr::visitSCCsFrom; using psr::writeTextFile; } // namespace psr diff --git a/unittests/PhasarLLVM/DataFlow/IfdsIde/CMakeLists.txt b/unittests/PhasarLLVM/DataFlow/IfdsIde/CMakeLists.txt index f9b8ad4e24..29f9656f3e 100644 --- a/unittests/PhasarLLVM/DataFlow/IfdsIde/CMakeLists.txt +++ b/unittests/PhasarLLVM/DataFlow/IfdsIde/CMakeLists.txt @@ -7,6 +7,7 @@ set(IfdsIdeSources InteractiveIDESolverTest.cpp SparseIDESolverTest.cpp IterativeIDESolverTest.cpp + DFISolverTest.cpp CFLFieldSensTest.cpp WPDSSolverTest.cpp ) diff --git a/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp b/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp new file mode 100644 index 0000000000..6d7e0bbde5 --- /dev/null +++ b/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp @@ -0,0 +1,260 @@ +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +#include "phasar/DataFlow/IfdsIde/Solver/DFISolver.h" + +#include "phasar/DataFlow/IfdsIde/Solver/GenericSolverResults.h" +#include "phasar/DataFlow/IfdsIde/Solver/IterativeIDESolver.h" +#include "phasar/DataFlow/IfdsIde/Solver/StaticIDESolverConfig.h" +#include "phasar/Domain/BinaryDomain.h" +#include "phasar/PhasarLLVM/ControlFlow/LLVMBasedICFG.h" +#include "phasar/PhasarLLVM/DB/LLVMProjectIRDB.h" +#include "phasar/PhasarLLVM/DataFlow/IfdsIde/Problems/IDELinearConstantAnalysis.h" +#include "phasar/PhasarLLVM/DataFlow/IfdsIde/Problems/IFDSTaintAnalysis.h" +#include "phasar/PhasarLLVM/HelperAnalyses.h" +#include "phasar/PhasarLLVM/Pointer/LLVMAliasSet.h" +#include "phasar/PhasarLLVM/SimpleAnalysisConstructor.h" +#include "phasar/PhasarLLVM/TaintConfig/LLVMTaintConfig.h" +#include "phasar/PhasarLLVM/TypeHierarchy/DIBasedTypeHierarchy.h" +#include "phasar/PhasarLLVM/Utils/LLVMShorthands.h" +#include "phasar/Utils/DebugOutput.h" + +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/IR/Instruction.h" +#include "llvm/IR/Value.h" + +#include "TaintTest.h" +#include "TestConfig.h" +#include "gtest/gtest.h" + +#include +#include +#include + +namespace { + +using n_t = const llvm::Instruction *; +using d_t = const llvm::Value *; + +void compareResults(const psr::LLVMProjectIRDB &IRDB, const auto &Expected, + const auto &Actual) { + for (const auto *Inst : IRDB.getAllInstructions()) { + auto ExpectedFacts = Expected.ifdsResultsAt(Inst); + auto ActualFacts = Actual.ifdsResultsAt(Inst); + EXPECT_EQ(ExpectedFacts, ActualFacts) + << "Results differ at " << psr::llvmIRToString(Inst) + << "\n Expected: " << psr::PrettyPrinter{ExpectedFacts} + << "\n Got: " << psr::PrettyPrinter{ActualFacts}; + } +} + +/// All materialized facts must be reachable from a seed +template +void checkReachableFromSeeds(const psr::LLVMProjectIRDB &IRDB, + ProblemTy &Problem, + const psr::DFISolverResults &Results, + const psr::DFIReachability &Reach) { + llvm::SmallVector SeedVertices; + for (const auto &[Inst, Facts] : Problem.initialSeeds().getSeeds()) { + for (const auto &[Fact, Value] : Facts) { + auto Vtx = Reach.find(Inst, Fact); + ASSERT_TRUE(Vtx.has_value()) << "Seed not materialized"; + SeedVertices.push_back(*Vtx); + } + } + + for (const auto *Inst : IRDB.getAllInstructions()) { + for (auto Vtx : Results.verticesAt(Inst)) { + EXPECT_TRUE(llvm::any_of( + SeedVertices, + [&](psr::dfi::VertexId Seed) { return Reach.reaches(Seed, Vtx); })) + << "Fact " << psr::llvmIRToString(Reach.factOf(Vtx)) << " at " + << psr::llvmIRToString(Inst) << " not reachable from any seed"; + EXPECT_TRUE(Reach.reaches(Vtx, Vtx)); + } + } +} + +class DFISolverLCATest : public ::testing::TestWithParam { +protected: + static constexpr auto PathToLlFiles = + PHASAR_BUILD_SUBFOLDER("linear_constant/"); +}; + +TEST_P(DFISolverLCATest, SameResultsAsIterativeIFDSSolver) { + psr::LLVMProjectIRDB IRDB(PathToLlFiles + GetParam()); + psr::DIBasedTypeHierarchy TH(IRDB); + psr::LLVMAliasSet PT(&IRDB); + psr::LLVMBasedICFG ICFG(&IRDB, psr::CallGraphAnalysisType::OTF, {"main"}, &TH, + &PT, psr::Soundness::Soundy, + /*IncludeGlobals*/ true); + psr::IDELinearConstantAnalysis Problem(&IRDB, &ICFG, {"main"}); + + psr::IterativeIDESolver IterSolver(&Problem, &ICFG, psr::IFDSSolverConfig{}); + IterSolver.solve(); + + psr::DFISolver Solver(&Problem, &ICFG); + auto Results = Solver.solve(); + + compareResults(IRDB, IterSolver.getSolverResults(), Results); + checkReachableFromSeeds(IRDB, Problem, Results, Solver.getReachability()); + + [[maybe_unused]] psr::GenericSolverResults + Generic = Results; +} + +TEST_P(DFISolverLCATest, OwningResultsWithoutIndex) { + psr::LLVMProjectIRDB IRDB(PathToLlFiles + GetParam()); + psr::DIBasedTypeHierarchy TH(IRDB); + psr::LLVMAliasSet PT(&IRDB); + psr::LLVMBasedICFG ICFG(&IRDB, psr::CallGraphAnalysisType::OTF, {"main"}, &TH, + &PT, psr::Soundness::Soundy, + /*IncludeGlobals*/ true); + psr::IDELinearConstantAnalysis Problem(&IRDB, &ICFG, {"main"}); + + psr::IterativeIDESolver IterSolver(&Problem, &ICFG, psr::IFDSSolverConfig{}); + IterSolver.solve(); + + auto Results = + psr::solveDFIProblem(Problem, ICFG, psr::DFISolverConfigNoIndex{}); + + compareResults(IRDB, IterSolver.getSolverResults(), Results); +} + +constexpr std::string_view LCATestFiles[] = { + "basic_01_cpp_dbg.ll", + "basic_02_cpp_dbg.ll", + "basic_03_cpp_dbg.ll", + "basic_04_cpp_dbg.ll", + "basic_05_cpp_dbg.ll", + "basic_06_cpp_dbg.ll", + "basic_07_cpp_dbg.ll", + "basic_08_cpp_dbg.ll", + "basic_09_cpp_dbg.ll", + "basic_10_cpp_dbg.ll", + "basic_11_cpp_dbg.ll", + "basic_12_cpp_dbg.ll", + + "branch_01_cpp_dbg.ll", + "branch_02_cpp_dbg.ll", + "branch_03_cpp_dbg.ll", + "branch_04_cpp_dbg.ll", + "branch_05_cpp_dbg.ll", + "branch_06_cpp_dbg.ll", + "branch_07_cpp_dbg.ll", + + "while_01_cpp_dbg.ll", + "while_02_cpp_dbg.ll", + "while_03_cpp_dbg.ll", + "while_04_cpp_dbg.ll", + "while_05_cpp_dbg.ll", + "for_01_cpp_dbg.ll", + + "call_01_cpp_dbg.ll", + "call_02_cpp_dbg.ll", + "call_03_cpp_dbg.ll", + "call_04_cpp_dbg.ll", + "call_05_cpp_dbg.ll", + "call_06_cpp_dbg.ll", + "call_07_cpp_dbg.ll", + "call_08_cpp_dbg.ll", + "call_09_cpp_dbg.ll", + "call_10_cpp_dbg.ll", + "call_11_cpp_dbg.ll", + + "recursion_01_cpp_dbg.ll", + "recursion_02_cpp_dbg.ll", + "recursion_03_cpp_dbg.ll", + + "global_01_cpp_dbg.ll", + "global_02_cpp_dbg.ll", + "global_03_cpp_dbg.ll", + "global_04_cpp_dbg.ll", + "global_05_cpp_dbg.ll", + "global_06_cpp_dbg.ll", + "global_07_cpp_dbg.ll", + "global_08_cpp_dbg.ll", + "global_09_cpp_dbg.ll", + "global_10_cpp_dbg.ll", + "global_11_cpp_dbg.ll", + "global_12_cpp_dbg.ll", + "global_13_cpp_dbg.ll", + "global_14_cpp_dbg.ll", + "global_15_cpp_dbg.ll", + "global_16_cpp_dbg.ll", + + "overflow_add_cpp_dbg.ll", + "overflow_sub_cpp_dbg.ll", + "overflow_mul_cpp_dbg.ll", + "overflow_div_min_by_neg_one_cpp_dbg.ll", + + "ub_division_by_zero_cpp_dbg.ll", + "ub_modulo_by_zero_cpp_dbg.ll", + "external_fun_cpp.ll", +}; + +INSTANTIATE_TEST_SUITE_P(DFISolverLCATest, DFISolverLCATest, + ::testing::ValuesIn(LCATestFiles)); + +class DFISolverTaintTest : public ::testing::TestWithParam { +protected: + static constexpr auto PathToLlFiles = + PHASAR_BUILD_SUBFOLDER("taint_analysis/dummy_source_sink/"); + static inline const std::vector EntryPoints = {"main"}; +}; + +/// Leaks are reported as side effects of the flow functions +TEST_P(DFISolverTaintTest, SameLeaksAsIterativeIDESolver) { + psr::HelperAnalyses HA(PathToLlFiles + GetParam(), EntryPoints); + auto Config = psr::unittest::getDefaultConfig(); + + auto ExpectedProblem = psr::createAnalysisProblem( + HA, &Config, EntryPoints); + psr::IterativeIDESolver ExpectedSolver(&ExpectedProblem, &HA.getICFG(), + psr::IFDSSolverConfig{}); + auto ExpectedResults = ExpectedSolver.solve(); + + auto Problem = psr::createAnalysisProblem( + HA, &Config, EntryPoints); + psr::DFISolver Solver(&Problem, &HA.getICFG()); + auto Results = Solver.solve(); + + EXPECT_EQ(ExpectedProblem.Leaks, Problem.Leaks) + << "Leaks differ:\n Expected: " + << psr::PrettyPrinter{ExpectedProblem.Leaks} + << "\n Got: " << psr::PrettyPrinter{Problem.Leaks}; + + compareResults(HA.getProjectIRDB(), ExpectedResults, Results); + checkReachableFromSeeds(HA.getProjectIRDB(), Problem, Results, + Solver.getReachability()); +} + +constexpr std::string_view TaintTestFiles[] = { + "taint_01_cpp_dbg.ll", "taint_01_cpp_m2r_dbg.ll", + "taint_02_cpp_dbg.ll", "taint_03_cpp_dbg.ll", + "taint_04_cpp_dbg.ll", "taint_05_cpp_dbg.ll", + "taint_06_cpp_m2r_dbg.ll", "sret_c_dbg.ll", + "taint_exception_01_cpp_dbg.ll", "taint_exception_01_cpp_m2r_dbg.ll", + "taint_exception_02_cpp_dbg.ll", "taint_exception_03_cpp_dbg.ll", + "taint_exception_04_cpp_dbg.ll", "taint_exception_05_cpp_dbg.ll", + "taint_exception_06_cpp_dbg.ll", "taint_exception_07_cpp_dbg.ll", + "taint_exception_08_cpp_dbg.ll", "taint_exception_09_cpp_dbg.ll", + "taint_exception_10_cpp_dbg.ll", "taint_lib_sum_01_cpp_dbg.ll", +}; + +INSTANTIATE_TEST_SUITE_P(DFISolverTaintTest, DFISolverTaintTest, + ::testing::ValuesIn(TaintTestFiles)); + +} // namespace + +int main(int Argc, char **Argv) { + ::testing::InitGoogleTest(&Argc, Argv); + return RUN_ALL_TESTS(); +} diff --git a/unittests/Utils/CMakeLists.txt b/unittests/Utils/CMakeLists.txt index aca8f680fc..257a8ab745 100644 --- a/unittests/Utils/CMakeLists.txt +++ b/unittests/Utils/CMakeLists.txt @@ -12,6 +12,7 @@ set(UtilsSources OnTheFlyAnalysisPrinterTest.cpp SourceMgrPrinterTest.cpp SCCGenericTest.cpp + IntervalReachabilityTest.cpp ) if(PHASAR_ENABLE_DYNAMIC_LOG) diff --git a/unittests/Utils/IntervalReachabilityTest.cpp b/unittests/Utils/IntervalReachabilityTest.cpp new file mode 100644 index 0000000000..ce8d82daee --- /dev/null +++ b/unittests/Utils/IntervalReachabilityTest.cpp @@ -0,0 +1,322 @@ +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +#include "phasar/Utils/IntervalReachability.h" + +#include "phasar/Utils/BitSet.h" +#include "phasar/Utils/CsrGraph.h" +#include "phasar/Utils/GraphTraits.h" +#include "phasar/Utils/IotaIterator.h" + +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/SmallVector.h" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +namespace { + +enum class VtxId : uint32_t {}; + +using Graph = psr::CsrGraph; +using Edge = std::pair; +using Index = psr::IntervalReachabilityIndex; +using Builder = psr::IntervalReachabilityBuilder; +using VtxSet = psr::BitSet; + +static_assert(psr::is_const_graph); + +constexpr VtxId operator""_v(unsigned long long Id) { return VtxId(Id); } + +Graph makeGraph(size_t NumVertices, llvm::ArrayRef Edges) { + return Graph::fromEdges(NumVertices, Edges); +} + +/// Vertices reachable from Root, including Root itself +VtxSet reachableFrom(const Graph &G, VtxId Root) { + VtxSet Seen(G.numVertices()); + llvm::SmallVector WL{Root}; + Seen.insert(Root); + while (!WL.empty()) { + auto Vtx = WL.pop_back_val(); + for (auto Succ : G.succsOf(Vtx)) { + if (Seen.tryInsert(Succ)) { + WL.push_back(Succ); + } + } + } + return Seen; +} + +/// Checks the index against a brute-force transitive closure +void checkIndex(const Graph &G, llvm::ArrayRef Roots, + const VtxSet &IsTarget, const Index &Idx) { + ASSERT_EQ(Idx.numVertices(), G.numVertices()); + + VtxSet Indexed(G.numVertices()); + for (auto Root : Roots) { + Indexed |= reachableFrom(G, Root); + } + + size_t NumTargets = 0; + for (auto Vtx : psr::iota(G.numVertices())) { + EXPECT_EQ(Idx.isIndexed(Vtx), Indexed.contains(Vtx)) + << "Vertex " << uint32_t(Vtx); + bool ExpectTarget = Indexed.contains(Vtx) && IsTarget.contains(Vtx); + EXPECT_EQ(Idx.isTarget(Vtx), ExpectTarget) << "Vertex " << uint32_t(Vtx); + if (ExpectTarget) { + ++NumTargets; + EXPECT_EQ(Idx.vertexOfRank(Idx.rankOf(Vtx)), Vtx); + } + } + EXPECT_EQ(Idx.numTargets(), NumTargets); + + for (auto From : Indexed) { + auto Reachable = reachableFrom(G, From); + + VtxSet ExpectedTargets(G.numVertices()); + for (auto To : psr::iota(G.numVertices())) { + bool Expected = Reachable.contains(To) && IsTarget.contains(To); + if (Expected) { + ExpectedTargets.insert(To); + } + EXPECT_EQ(Idx.reaches(From, To), Expected) + << "From " << uint32_t(From) << " To " << uint32_t(To); + } + + std::optional PrevHi; + Idx.forEachReachableRange(From, [&](psr::RankInterval Interval) { + EXPECT_FALSE(Interval.empty()); + if (PrevHi) { + EXPECT_LE(uint32_t(*PrevHi), uint32_t(Interval.Lo)) + << "Intervals not sorted or not disjoint"; + } + PrevHi = Interval.Hi; + }); + + VtxSet Enumerated(G.numVertices()); + Idx.forEachReachableTarget(From, [&](VtxId To) { + EXPECT_TRUE(Enumerated.tryInsert(To)) << "Duplicate target"; + }); + EXPECT_TRUE(Enumerated == ExpectedTargets) << "From " << uint32_t(From); + + // SCC ids are in reverse topological order + for (auto Succ : G.succsOf(From)) { + EXPECT_LE(+Idx.sccOf(Succ), +Idx.sccOf(From)); + } + } +} + +void checkIndex(const Graph &G, llvm::ArrayRef Roots, const Index &Idx) { + checkIndex(G, Roots, VtxSet(G.numVertices(), true), Idx); +} + +TEST(CsrGraphTest, FromEdgesKeepsEdgeOrder) { + auto G = makeGraph(4, {{2_v, 1_v}, {0_v, 3_v}, {2_v, 0_v}, {0_v, 1_v}}); + + ASSERT_EQ(G.numVertices(), 4U); + ASSERT_EQ(G.numEdges(), 4U); + EXPECT_EQ(G.succsOf(0_v), llvm::ArrayRef({3_v, 1_v})); + EXPECT_TRUE(G.succsOf(1_v).empty()); + EXPECT_EQ(G.succsOf(2_v), llvm::ArrayRef({1_v, 0_v})); + EXPECT_TRUE(G.succsOf(3_v).empty()); +} + +TEST(CsrGraphTest, Reversed) { + auto G = makeGraph(4, {{2_v, 1_v}, {0_v, 3_v}, {2_v, 0_v}, {0_v, 1_v}}); + auto Rev = Graph::reversed(G); + + ASSERT_EQ(Rev.numVertices(), 4U); + ASSERT_EQ(Rev.numEdges(), 4U); + EXPECT_EQ(Rev.succsOf(0_v), llvm::ArrayRef({2_v})); + EXPECT_EQ(Rev.succsOf(1_v), llvm::ArrayRef({0_v, 2_v})); + EXPECT_TRUE(Rev.succsOf(2_v).empty()); + EXPECT_EQ(Rev.succsOf(3_v), llvm::ArrayRef({0_v})); + EXPECT_EQ(Rev.Roots, (llvm::SmallVector{1_v, 3_v})); +} + +TEST(CsrGraphTest, AssignEdgesReusesGraph) { + auto G = makeGraph(3, {{0_v, 1_v}, {1_v, 2_v}}); + G.assignEdges(2, {{1_v, 0_v}}); + + ASSERT_EQ(G.numVertices(), 2U); + ASSERT_EQ(G.numEdges(), 1U); + EXPECT_TRUE(G.succsOf(0_v).empty()); + EXPECT_EQ(G.succsOf(1_v), llvm::ArrayRef({0_v})); +} + +TEST(IntervalReachabilityTest, Chain) { + auto G = makeGraph(4, {{0_v, 1_v}, {1_v, 2_v}, {2_v, 3_v}}); + Index Idx; + Builder().build(G, {0_v}, Idx); + + checkIndex(G, {0_v}, Idx); + EXPECT_EQ(Idx.numSCCs(), 4U); + for (auto Vtx : psr::iota(4)) { + EXPECT_EQ(Idx.numIntervalsOf(Idx.sccOf(Vtx)), 1U); + } +} + +TEST(IntervalReachabilityTest, SharedSuccessorIsNoFalsePositive) { + // 1 and 2 both reach 3 via a tree- resp. cross-edge, but not each other + auto G = makeGraph(4, {{0_v, 1_v}, {0_v, 2_v}, {1_v, 3_v}, {2_v, 3_v}}); + Index Idx; + Builder().build(G, {0_v}, Idx); + + checkIndex(G, {0_v}, Idx); + EXPECT_FALSE(Idx.reaches(1_v, 2_v)); + EXPECT_FALSE(Idx.reaches(2_v, 1_v)); + EXPECT_TRUE(Idx.reaches(2_v, 3_v)); +} + +TEST(IntervalReachabilityTest, Cycle) { + auto G = makeGraph(5, {{0_v, 1_v}, {1_v, 2_v}, {2_v, 0_v}, {2_v, 3_v}}); + Index Idx; + Builder().build(G, {1_v}, Idx); + + checkIndex(G, {1_v}, Idx); + EXPECT_EQ(Idx.sccOf(0_v), Idx.sccOf(1_v)); + EXPECT_EQ(Idx.sccOf(1_v), Idx.sccOf(2_v)); + EXPECT_NE(Idx.sccOf(2_v), Idx.sccOf(3_v)); + EXPECT_FALSE(Idx.isIndexed(4_v)); + EXPECT_EQ(Idx.sccOf(4_v), Index::NoSCC); +} + +TEST(IntervalReachabilityTest, SelfLoopAndMultiEdges) { + auto G = makeGraph(3, {{0_v, 0_v}, {0_v, 1_v}, {0_v, 1_v}, {1_v, 2_v}}); + Index Idx; + Builder().build(G, {0_v}, Idx); + + checkIndex(G, {0_v}, Idx); +} + +TEST(IntervalReachabilityTest, CrossEdgesIntoEarlierSubtrees) { + // DFS from 0 visits 1 -> 2 first; 3 -> 2 and 4 -> 1 are cross edges + auto G = makeGraph(6, {{0_v, 1_v}, + {1_v, 2_v}, + {0_v, 3_v}, + {3_v, 2_v}, + {3_v, 4_v}, + {4_v, 1_v}, + {4_v, 5_v}}); + Index Idx; + Builder().build(G, {0_v}, Idx); + + checkIndex(G, {0_v}, Idx); + EXPECT_TRUE(Idx.reaches(3_v, 1_v)); + EXPECT_FALSE(Idx.reaches(1_v, 3_v)); +} + +TEST(IntervalReachabilityTest, MultipleRoots) { + auto G = makeGraph(6, {{0_v, 1_v}, {2_v, 1_v}, {2_v, 3_v}, {4_v, 5_v}}); + Index Idx; + Builder().build(G, {0_v, 2_v}, Idx); + + checkIndex(G, {0_v, 2_v}, Idx); + EXPECT_FALSE(Idx.isIndexed(4_v)); + EXPECT_FALSE(Idx.isIndexed(5_v)); +} + +TEST(IntervalReachabilityTest, ProjectedTargets) { + auto G = makeGraph( + 6, + {{0_v, 1_v}, {1_v, 2_v}, {0_v, 3_v}, {3_v, 4_v}, {4_v, 2_v}, {3_v, 5_v}}); + VtxSet IsTarget(6); + IsTarget.insert(2_v); + IsTarget.insert(5_v); + + Index Idx; + Builder().build( + G, {0_v}, [&](VtxId Vtx) { return IsTarget.contains(Vtx); }, Idx); + + checkIndex(G, {0_v}, IsTarget, Idx); + EXPECT_EQ(Idx.numTargets(), 2U); + EXPECT_EQ(Idx.numIntervalsOf(Idx.sccOf(0_v)), 1U); + EXPECT_EQ(Idx.numIntervalsOf(Idx.sccOf(1_v)), 1U); +} + +TEST(IntervalReachabilityTest, EmptyGraph) { + Graph G; + Index Idx; + Builder().build(G, {}, Idx); + + EXPECT_EQ(Idx.numVertices(), 0U); + EXPECT_EQ(Idx.numTargets(), 0U); + EXPECT_EQ(Idx.numSCCs(), 0U); +} + +Graph makeRandomGraph(std::mt19937 &Rng, size_t NumVertices, size_t NumEdges) { + std::uniform_int_distribution VtxDist(0, NumVertices - 1); + llvm::SmallVector Edges; + Edges.reserve(NumEdges); + for (size_t I = 0; I != NumEdges; ++I) { + Edges.emplace_back(VtxId(VtxDist(Rng)), VtxId(VtxDist(Rng))); + } + return makeGraph(NumVertices, Edges); +} + +TEST(IntervalReachabilityTest, RandomGraphs) { + std::mt19937 Rng(42); // NOLINT(cert-msc32-c,cert-msc51-cpp) + std::bernoulli_distribution IsTargetDist(0.3); + + // Reuse builder and index across graphs to check that no state leaks + Builder B; + Index Idx; + + for (size_t NumVertices : {1, 2, 5, 16, 50}) { + for (double EdgeFactor : {0.5, 1.0, 1.5, 3.0}) { + for (int Iteration = 0; Iteration != 8; ++Iteration) { + auto G = makeRandomGraph(Rng, NumVertices, + size_t(double(NumVertices) * EdgeFactor)); + + llvm::SmallVector Roots{VtxId(0)}; + if (NumVertices > 2) { + Roots.push_back(VtxId(NumVertices / 2)); + } + + SCOPED_TRACE("NumVertices=" + std::to_string(NumVertices) + + ", EdgeFactor=" + std::to_string(EdgeFactor) + + ", Iteration=" + std::to_string(Iteration)); + + B.build(G, Roots, Idx); + checkIndex(G, Roots, Idx); + + VtxSet IsTarget(NumVertices); + for (auto Vtx : psr::iota(NumVertices)) { + if (IsTargetDist(Rng)) { + IsTarget.insert(Vtx); + } + } + B.build( + G, Roots, [&](VtxId Vtx) { return IsTarget.contains(Vtx); }, Idx); + checkIndex(G, Roots, IsTarget, Idx); + + auto Rev = Graph::reversed(G); + B.build(Rev, Roots, Idx); + checkIndex(Rev, Roots, Idx); + + if (::testing::Test::HasFailure()) { + return; + } + } + } + } +} + +} // namespace + +int main(int Argc, char **Argv) { + ::testing::InitGoogleTest(&Argc, Argv); + return RUN_ALL_TESTS(); +} From 78ba58a8957597b47b8881a9f33adc4efabd60a3 Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Wed, 7 Oct 2026 19:56:17 +0200 Subject: [PATCH 2/8] Support followReturnsPastSeeds in DFISolver Co-Authored-By: Claude Opus 5.5 --- docs/DFISolver-Design.md | 2 +- .../IfdsIde/Solver/DFIExplodedSupergraph.h | 58 +++++++++- .../DataFlow/IfdsIde/Solver/DFISolver.h | 46 ++++++-- .../IfdsIde/Solver/DFIExplodedSupergraph.cpp | 106 +++++++++++++++--- .../DataFlow/IfdsIde/DFISolverTest.cpp | 69 +++++++++++- 5 files changed, 249 insertions(+), 32 deletions(-) diff --git a/docs/DFISolver-Design.md b/docs/DFISolver-Design.md index 2f0c17ab4b..36a5e85d28 100644 --- a/docs/DFISolver-Design.md +++ b/docs/DFISolver-Design.md @@ -401,7 +401,7 @@ vertices at `At` (merge of two sorted lists), the typical taint query | `AutoAddZero` | zero fact always propagated by the solver | | `getSummaryFlowFunction` | local edges to return sites, callee not entered | | Callee without body | only call-to-return flow | -| `followReturnsPastSeeds` | seeds (and derived unbalanced roots) are extra roots in `summarize`; exits reached from them return to *all* callers of `f` via `retFF`, creating unbalanced roots in the callers and unbalanced-return edges in `G*`; for functions without callers, `applyUnbalancedRetFlowFunctionSideEffects` if `UnbalancedRetSideEffectProvider` | +| `followReturnsPastSeeds` | as in the IDESolver: exits reached from a seed without callers, or from an unbalanced root while the zero entry of `f` has no callers, return to *all* call-graph callers of `f` via `retFF`. This creates unbalanced roots in the callers and unbalanced-return edges in `G*`. Unbalanced roots count as reached from the zero entry, i.e. its summaries include their exits. For functions without callers, `applyUnbalancedRetFlowFunctionSideEffects` if `UnbalancedRetSideEffectProvider` | | `has_advanceToNextUser_v` | applied on target-vertex creation | | `isInteresting(n)` | `QueryTargets::Interesting` restricts ranks to those vertices | | `emitESG` / `recordEdges` | requires `KeepGraph`; dumps the CSR of `G*` | diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h b/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h index 2f79a1c8de..bd1ae059b0 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h @@ -53,7 +53,11 @@ class DFIExplodedSupergraph { /// \param CGSCCs The SCCs of the call-graph in reverse topological order, /// as computed by computeCGSCCs() - explicit DFIExplodedSupergraph(SCCHolder CGSCCs); + /// \param FollowReturnsPastSeeds Whether summarize() reports exits that are + /// reachable without a calling context, see + /// IFDSIDESolverConfig::followReturnsPastSeeds() + explicit DFIExplodedSupergraph(SCCHolder CGSCCs, + bool FollowReturnsPastSeeds); /// The next step towards the fixpoint, or std::nullopt if there is none. /// Prefers callees over callers, and expansion over summarization. @@ -93,12 +97,23 @@ class DFIExplodedSupergraph { [[nodiscard]] llvm::ArrayRef addCallEdge(dfi::VertexId CallVtx, dfi::EntryId Entry); + /// Records the unbalanced return from the exit vertex Exit to RetVtx, which + /// is reachable without a calling context. RetVtx then behaves as if it was + /// reached from the zero fact at the start of its function. + void addUnbalancedReturn(dfi::VertexId Exit, dfi::VertexId RetVtx); + /// Computes the same-level reachability from the entries of Fun to its exit /// vertices and calls Handler(CallVtx, Entry, Exit) for each new /// (Entry, Exit) pair and each call vertex CallVtx calling Entry. + /// + /// With FollowReturnsPastSeeds, also calls UnbalancedHandler(Exit) for each + /// new exit vertex that is reachable without calling context, i.e., from a + /// seed without callers or from an unbalanced return. The solver must + /// return it to all callers of Fun. void summarize( FunctionId Fun, - std::invocable auto Handler) { + std::invocable auto Handler, + std::invocable auto UnbalancedHandler) { indexFunction(Fun); for (auto Entry : FunState[Fun].Entries) { @@ -110,6 +125,14 @@ class DFIExplodedSupergraph { } } } + + if (FollowReturnsPastSeeds) { + // UnbalancedHandler may add vertices, roots and unbalanced returns, but + // no entries or callers + for (auto Exit : updateUnbalancedExits(Fun)) { + std::invoke(UnbalancedHandler, Exit); + } + } } [[nodiscard]] size_t numVertices() const noexcept { return VtxInst.size(); } @@ -130,7 +153,7 @@ class DFIExplodedSupergraph { return SeedVertices; } - /// The graph of intra-procedural, summary, and call edges + /// The graph of intra-procedural, summary, call, and unbalanced-return edges [[nodiscard]] CsrGraph buildSupergraph() const; [[nodiscard]] TypedVector @@ -146,10 +169,15 @@ class DFIExplodedSupergraph { struct FunctionState { TypedVector Vertices; llvm::SmallVector Worklist; - /// Entries and seeds + /// Entries, seeds, and unbalanced-return targets llvm::SmallVector Roots; llvm::SmallVector Entries; + llvm::SmallVector Seeds; + /// Targets of unbalanced returns into this function + llvm::SmallVector UnbalancedRoots; llvm::SmallVector, 0> SummaryEdges; + /// Exit vertices already reported as unbalanced, sorted + llvm::SmallVector UnbalancedExits; /// Whether the graph changed since the last summarize() bool Dirty = false; }; @@ -157,14 +185,32 @@ class DFIExplodedSupergraph { void markDirty(FunctionId Fun); bool addRoot(dfi::VertexId Vtx); + [[nodiscard]] bool hasCallers(dfi::VertexId Vtx) const; + /// Whether a call reaches the zero fact at a start point of Fun + [[nodiscard]] bool isZeroEntryCalled(FunctionId Fun) const; + /// Builds LocalIndex for the vertices of Fun, with exits as targets void indexFunction(FunctionId Fun); + /// Appends the exits reachable from Src according to LocalIndex to NewExits, + /// unless they are in Known + void collectNewExits(FunctionId Fun, dfi::VertexId Src, + llvm::ArrayRef Known); + + /// Sorts NewExits, removes duplicates, and merges them into Known + void mergeNewExits(llvm::SmallVectorImpl &Known); + /// Adds the exits reachable from Entry according to LocalIndex to its - /// summary. Returns the newly added ones. + /// summary. Returns the newly added ones. As in the IDESolver, the zero + /// entry also reaches whatever the unbalanced-return targets reach. [[nodiscard]] llvm::ArrayRef updateSummaryExits(FunctionId Fun, dfi::EntryId Entry); + /// Adds the exits reachable without calling context according to LocalIndex + /// to the unbalanced exits of Fun. Returns the newly added ones. + [[nodiscard]] llvm::ArrayRef + updateUnbalancedExits(FunctionId Fun); + [[nodiscard]] llvm::ArrayRef edgesOf(dfi::VertexId Vtx) const noexcept { auto Begin = VtxEdgeBegin[Vtx]; @@ -200,6 +246,8 @@ class DFIExplodedSupergraph { llvm::DenseSet> SummaryEdgeSet; llvm::SmallVector, 0> CallEdges; + llvm::DenseSet> UnbalancedReturnEdges; + bool FollowReturnsPastSeeds{}; // Scratch buffers llvm::SmallVector Batch; diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h index 66461abbf9..ab69623dd6 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h @@ -21,7 +21,6 @@ #include "phasar/Utils/FunctionId.h" #include "phasar/Utils/IntervalReachability.h" #include "phasar/Utils/IotaIterator.h" -#include "phasar/Utils/Logger.h" #include "phasar/Utils/Utilities.h" #include "llvm/ADT/ArrayRef.h" @@ -49,8 +48,6 @@ namespace psr { /// StaticSolverConfigTy::BuildQueryIndex, getReachability() answers /// context-sensitive reachability queries between arbitrary /// (instruction, fact) pairs that hold. -/// -/// Does not support IFDSIDESolverConfig::followReturnsPastSeeds(). template @@ -105,15 +102,11 @@ class DFISolver : public IDESolverAPIMixin< // --- IDESolverAPIMixin void doInitialize() { - if (getProblemSolverConfig(*Problem).followReturnsPastSeeds()) { - PHASAR_LOG_LEVEL(WARNING, "The DFISolver does not support " - "followReturnsPastSeeds; ignoring it"); - } - for (const auto &Fun : ICF->getAllFunctions()) { Funs.getOrInsert(Fun); } - Graph.emplace(computeCGSCCs(*ICF, Funs)); + Graph.emplace(computeCGSCCs(*ICF, Funs), + getProblemSolverConfig(*Problem).followReturnsPastSeeds()); auto ZeroId = Results->Facts.getOrInsert(Problem->getZeroValue()); assert(ZeroId == dfi::FactId::Zero); @@ -137,10 +130,12 @@ class DFISolver : public IDESolverAPIMixin< if (Next->Kind == detail::DFIExplodedSupergraph::ActionKind::Expand) { expand(Next->Fun); } else { + auto Fun = Next->Fun; Graph->summarize( - Next->Fun, + Fun, [this](dfi::VertexId CallVtx, dfi::EntryId Entry, - dfi::VertexId Exit) { applySummary(CallVtx, Entry, Exit); }); + dfi::VertexId Exit) { applySummary(CallVtx, Entry, Exit); }, + [this, Fun](dfi::VertexId Exit) { returnUnbalanced(Fun, Exit); }); } return true; } @@ -212,6 +207,35 @@ class DFISolver : public IDESolverAPIMixin< } } + /// Returns the exit vertex Exit of Fun, which is reachable without calling + /// context, to all callers of Fun in the call-graph + void returnUnbalanced(FunctionId Fun, dfi::VertexId Exit) { + n_t ExitInst = instOf(Exit); + d_t ExitFact = factOf(Exit); + f_t Callee = Funs[Fun]; + + bool HasCallers = false; + for (const auto &CallSite : ICF->getCallersOf(Callee)) { + HasCallers = true; + auto Caller = Funs.get(ICF->getFunctionOf(CallSite)); + for (const auto &RetSite : ICF->getReturnSitesOfCallAt(CallSite)) { + auto FF = + Problem->getRetFlowFunction(CallSite, Callee, ExitInst, RetSite); + for (const auto &Fact : computeTargets(FF, ExitFact)) { + Graph->addUnbalancedReturn( + Exit, getOrCreateSuccVertex(RetSite, Fact, Caller)); + } + } + } + + if constexpr (UnbalancedRetSideEffectProvider) { + if (!HasCallers) { + Problem->applyUnbalancedRetFlowFunctionSideEffects(Callee, ExitInst, + ExitFact); + } + } + } + /// Expands all pending vertices of Fun, batched per instruction, such that /// each flow function is constructed once per batch. void expand(FunctionId Fun) { diff --git a/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp b/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp index 426e1e3fe4..362b976bee 100644 --- a/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp +++ b/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp @@ -18,8 +18,9 @@ using psr::detail::DFIExplodedSupergraph; DFIExplodedSupergraph::DFIExplodedSupergraph( - psr::SCCHolder CGSCCs) - : CGSCCs(std::move(CGSCCs)) { + psr::SCCHolder CGSCCs, bool FollowReturnsPastSeeds) + : CGSCCs(std::move(CGSCCs)), + FollowReturnsPastSeeds(FollowReturnsPastSeeds) { FunState.resize(this->CGSCCs.SCCOfNode.size()); PendingSCCs.reserve(this->CGSCCs.size()); } @@ -75,6 +76,7 @@ bool DFIExplodedSupergraph::addSeed(dfi::VertexId Vtx) { return false; } SeedVertices.push_back(Vtx); + FunState[VtxFun[Vtx]].Seeds.push_back(Vtx); return true; } @@ -120,6 +122,22 @@ DFIExplodedSupergraph::addCallEdge(dfi::VertexId CallVtx, dfi::EntryId Entry) { return SummaryExits[Entry]; } +void DFIExplodedSupergraph::addUnbalancedReturn(dfi::VertexId Exit, + dfi::VertexId RetVtx) { + if (!UnbalancedReturnEdges.insert({Exit, RetVtx}).second) { + return; + } + + auto Fun = VtxFun[RetVtx]; + if (addRoot(RetVtx)) { + FunState[Fun].UnbalancedRoots.push_back(RetVtx); + } else if (!llvm::is_contained(FunState[Fun].UnbalancedRoots, RetVtx)) { + // An entry or seed; still, it may now reach exits via the zero entry + FunState[Fun].UnbalancedRoots.push_back(RetVtx); + markDirty(Fun); + } +} + psr::CsrGraph DFIExplodedSupergraph::buildSupergraph() const { assert(VtxInst.size() == VtxFun.size() && @@ -135,6 +153,7 @@ DFIExplodedSupergraph::buildSupergraph() const { GraphEdges.append(FS.SummaryEdges.begin(), FS.SummaryEdges.end()); } GraphEdges.append(CallEdges.begin(), CallEdges.end()); + GraphEdges.append(UnbalancedReturnEdges.begin(), UnbalancedReturnEdges.end()); return CsrGraph::fromEdges(numVertices(), GraphEdges); } @@ -153,6 +172,18 @@ bool DFIExplodedSupergraph::addRoot(dfi::VertexId Vtx) { return true; } +bool DFIExplodedSupergraph::hasCallers(dfi::VertexId Vtx) const { + auto It = EntryOf.find(Vtx); + return It != EntryOf.end() && !EntryCallers[It->second].empty(); +} + +bool DFIExplodedSupergraph::isZeroEntryCalled(FunctionId Fun) const { + return llvm::any_of(FunState[Fun].Entries, [this](dfi::EntryId Entry) { + return VtxFact[EntryVertex[Entry]] == dfi::FactId::Zero && + !EntryCallers[Entry].empty(); + }); +} + void DFIExplodedSupergraph::indexFunction(FunctionId Fun) { auto &FS = FunState[Fun]; FS.Dirty = false; @@ -182,23 +213,70 @@ void DFIExplodedSupergraph::indexFunction(FunctionId Fun) { LocalIndex); } +void DFIExplodedSupergraph::collectNewExits( + FunctionId Fun, dfi::VertexId Src, llvm::ArrayRef Known) { + const auto &FS = FunState[Fun]; + auto LocalSrc = VtxLocalId[Src]; + if (size_t(LocalSrc) >= LocalIndex.numVertices()) { + // Created after indexFunction(); covered by the next summarize() + return; + } + + LocalIndex.forEachReachableTarget(LocalSrc, [&](dfi::LocalVertexId Local) { + auto Exit = FS.Vertices[Local]; + if (!std::binary_search(Known.begin(), Known.end(), Exit)) { + NewExits.push_back(Exit); + } + }); +} + +void DFIExplodedSupergraph::mergeNewExits( + llvm::SmallVectorImpl &Known) { + if (NewExits.empty()) { + return; + } + llvm::sort(NewExits); + NewExits.erase(std::unique(NewExits.begin(), NewExits.end()), NewExits.end()); + Known.append(NewExits.begin(), NewExits.end()); + llvm::sort(Known); +} + llvm::ArrayRef DFIExplodedSupergraph::updateSummaryExits(FunctionId Fun, dfi::EntryId Entry) { - const auto &FS = FunState[Fun]; auto &Exits = SummaryExits[Entry]; + auto EntryVtx = EntryVertex[Entry]; NewExits.clear(); - LocalIndex.forEachReachableTarget( - VtxLocalId[EntryVertex[Entry]], [&](dfi::LocalVertexId Local) { - auto Exit = FS.Vertices[Local]; - if (!std::binary_search(Exits.begin(), Exits.end(), Exit)) { - NewExits.push_back(Exit); - } - }); - - if (!NewExits.empty()) { - Exits.append(NewExits.begin(), NewExits.end()); - llvm::sort(Exits); + collectNewExits(Fun, EntryVtx, Exits); + if (VtxFact[EntryVtx] == dfi::FactId::Zero) { + for (auto Root : FunState[Fun].UnbalancedRoots) { + collectNewExits(Fun, Root, Exits); + } } + + mergeNewExits(Exits); + return NewExits; +} + +llvm::ArrayRef +DFIExplodedSupergraph::updateUnbalancedExits(FunctionId Fun) { + auto &FS = FunState[Fun]; + + NewExits.clear(); + for (auto Seed : FS.Seeds) { + if (!hasCallers(Seed)) { + collectNewExits(Fun, Seed, FS.UnbalancedExits); + } + } + // As in the IDESolver, facts from unbalanced returns behave as if they were + // reached from the zero entry, so they only return past this function if + // the zero entry has no callers. + if (!isZeroEntryCalled(Fun)) { + for (auto Root : FS.UnbalancedRoots) { + collectNewExits(Fun, Root, FS.UnbalancedExits); + } + } + + mergeNewExits(FS.UnbalancedExits); return NewExits; } diff --git a/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp b/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp index 6d7e0bbde5..44412ce687 100644 --- a/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp +++ b/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp @@ -10,6 +10,7 @@ #include "phasar/DataFlow/IfdsIde/Solver/DFISolver.h" #include "phasar/DataFlow/IfdsIde/Solver/GenericSolverResults.h" +#include "phasar/DataFlow/IfdsIde/Solver/IFDSSolver.h" #include "phasar/DataFlow/IfdsIde/Solver/IterativeIDESolver.h" #include "phasar/DataFlow/IfdsIde/Solver/StaticIDESolverConfig.h" #include "phasar/Domain/BinaryDomain.h" @@ -17,6 +18,7 @@ #include "phasar/PhasarLLVM/DB/LLVMProjectIRDB.h" #include "phasar/PhasarLLVM/DataFlow/IfdsIde/Problems/IDELinearConstantAnalysis.h" #include "phasar/PhasarLLVM/DataFlow/IfdsIde/Problems/IFDSTaintAnalysis.h" +#include "phasar/PhasarLLVM/DataFlow/IfdsIde/Problems/IFDSUninitializedVariables.h" #include "phasar/PhasarLLVM/HelperAnalyses.h" #include "phasar/PhasarLLVM/Pointer/LLVMAliasSet.h" #include "phasar/PhasarLLVM/SimpleAnalysisConstructor.h" @@ -44,10 +46,12 @@ using n_t = const llvm::Instruction *; using d_t = const llvm::Value *; void compareResults(const psr::LLVMProjectIRDB &IRDB, const auto &Expected, - const auto &Actual) { + const auto &Actual, d_t IgnoredFact = nullptr) { for (const auto *Inst : IRDB.getAllInstructions()) { auto ExpectedFacts = Expected.ifdsResultsAt(Inst); auto ActualFacts = Actual.ifdsResultsAt(Inst); + ExpectedFacts.erase(IgnoredFact); + ActualFacts.erase(IgnoredFact); EXPECT_EQ(ExpectedFacts, ActualFacts) << "Results differ at " << psr::llvmIRToString(Inst) << "\n Expected: " << psr::PrettyPrinter{ExpectedFacts} @@ -252,6 +256,69 @@ constexpr std::string_view TaintTestFiles[] = { INSTANTIATE_TEST_SUITE_P(DFISolverTaintTest, DFISolverTaintTest, ::testing::ValuesIn(TaintTestFiles)); +struct UnbalancedTestCase { + std::string_view File; + std::string_view EntryPoint; +}; + +class DFISolverUnbalancedTest + : public ::testing::TestWithParam { +protected: + static constexpr auto PathToLlFiles = + PHASAR_BUILD_SUBFOLDER("uninitialized_variables/"); + static inline const std::vector ICFGEntryPoints = {"main"}; +}; + +/// The IterativeIDESolver does not support followReturnsPastSeeds, so compare +/// against the IFDSSolver +TEST_P(DFISolverUnbalancedTest, SameResultsAsIFDSSolver) { + const auto &[File, EntryPoint] = GetParam(); + // The ICFG starts at main, such that the seeded function has callers + psr::HelperAnalyses HA(PathToLlFiles + File, ICFGEntryPoints); + const auto &IRDB = HA.getProjectIRDB(); + std::vector ProblemEntryPoints = {std::string(EntryPoint)}; + + auto ExpectedProblem = + psr::createAnalysisProblem( + HA, ProblemEntryPoints); + ExpectedProblem.getIFDSIDESolverConfig().setFollowReturnsPastSeeds(true); + psr::IFDSSolver ExpectedSolver(&ExpectedProblem, &HA.getICFG()); + auto ExpectedResults = ExpectedSolver.solve(); + + auto Problem = psr::createAnalysisProblem( + HA, ProblemEntryPoints); + Problem.getIFDSIDESolverConfig().setFollowReturnsPastSeeds(true); + psr::DFISolver Solver(&Problem, &HA.getICFG()); + auto Results = Solver.solve(); + + // The IDESolver drops the zero fact at call sites and exits that are only + // reachable via unbalanced returns, as its value computation (phase II) + // starts at the start points only + compareResults(IRDB, ExpectedResults, Results, Problem.getZeroValue()); + checkReachableFromSeeds(IRDB, Problem, Results, Solver.getReachability()); + EXPECT_EQ(ExpectedProblem.getAllUndefUses(), Problem.getAllUndefUses()); + + // Unbalanced returns into main, if any, also happen in the DFISolver + const auto *Main = IRDB.getFunctionDefinition("main"); + ASSERT_NE(Main, nullptr); + const auto HasResultsInMain = [&](const auto &SR) { + return llvm::any_of(IRDB.getAllInstructionsOf(Main), [&](const auto *Inst) { + return !SR.ifdsResultsAt(Inst).empty(); + }); + }; + EXPECT_EQ(HasResultsInMain(ExpectedResults), HasResultsInMain(Results)); +} + +constexpr UnbalancedTestCase UnbalancedTestCases[] = { + {"multiple_calls_cpp_dbg.ll", "_Z8functionv"}, + {"recursion_cpp_dbg.ll", "_Z3fooRii"}, + {"callsite_cpp_dbg.ll", "_Z3foov"}, + {"return_uninit_cpp_dbg.ll", "_Z3foov"}, +}; + +INSTANTIATE_TEST_SUITE_P(DFISolverUnbalancedTest, DFISolverUnbalancedTest, + ::testing::ValuesIn(UnbalancedTestCases)); + } // namespace int main(int Argc, char **Argv) { From 1564a884ec65d0838ed84ebb23292cf7ef11637d Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Wed, 7 Oct 2026 20:14:10 +0200 Subject: [PATCH 3/8] Add dfi-taint to phasar-cli; pass CLI SolverConfig to ifds-taint Co-Authored-By: Claude Opus 5.5 --- .../DataFlow/IfdsIde/Solver/DFISolver.h | 26 ++++++++++++++++--- .../DataFlow/IfdsIde/Solver/IDESolver.h | 24 +++++++++++++++++ .../DataFlow/IfdsIde/Solver/IFDSSolver.h | 13 ++++++++++ .../PhasarLLVM/Utils/DataFlowAnalysisType.def | 1 + .../Controller/AnalysisController.cpp | 3 +++ .../Controller/AnalysisControllerInternal.h | 1 + .../AnalysisControllerXDFITaint.cpp | 26 +++++++++++++++++++ .../AnalysisControllerXIFDSTaint.cpp | 6 +++-- 8 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 tools/phasar-cli/Controller/AnalysisControllerXDFITaint.cpp diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h index ab69623dd6..9eb4d09f14 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h @@ -12,6 +12,7 @@ #include "phasar/ControlFlow/CGSCCs.h" #include "phasar/ControlFlow/ICFG.h" #include "phasar/ControlFlow/SparseCFGProvider.h" +#include "phasar/DataFlow/IfdsIde/IFDSIDESolverConfig.h" #include "phasar/DataFlow/IfdsIde/IFDSProblem.h" #include "phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h" #include "phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h" @@ -64,9 +65,18 @@ class DFISolver : public IDESolverAPIMixin< using config_t = StaticSolverConfigTy; DFISolver(ProblemTy *Problem, const ICFGTy *ICF, - StaticSolverConfigTy /*Config*/ = {}) + StaticSolverConfigTy Config = {}) + : DFISolver(Problem, ICF, Config, + getProblemSolverConfig(assertNotNull(Problem))) {} + + /// Uses SolverConfig instead of the problem's solver config. Only + /// followReturnsPastSeeds is respected; AutoAddZero is taken from + /// StaticSolverConfigTy. + DFISolver(ProblemTy *Problem, const ICFGTy *ICF, + StaticSolverConfigTy /*Config*/, IFDSIDESolverConfig SolverConfig) : Problem(&assertNotNull(Problem)), ICF(&assertNotNull(ICF)), - Results(std::make_unique>()) {} + Results(std::make_unique>()), + SolverConfig(SolverConfig) {} [[nodiscard]] DFISolverResults getSolverResults() const noexcept { return DFISolverResults(Results.get()); @@ -88,6 +98,11 @@ class DFISolver : public IDESolverAPIMixin< getSolverResults().dumpResults(OS); } + /// Only valid after solving + void emitTextReport(llvm::raw_ostream &OS = llvm::outs()) { + Problem->emitTextReport(getSolverResults(), OS); + } + private: using FlowFunctionPtrType = typename ProblemTy::FlowFunctionPtrType; @@ -106,7 +121,7 @@ class DFISolver : public IDESolverAPIMixin< Funs.getOrInsert(Fun); } Graph.emplace(computeCGSCCs(*ICF, Funs), - getProblemSolverConfig(*Problem).followReturnsPastSeeds()); + SolverConfig.followReturnsPastSeeds()); auto ZeroId = Results->Facts.getOrInsert(Problem->getZeroValue()); assert(ZeroId == dfi::FactId::Zero); @@ -388,6 +403,7 @@ class DFISolver : public IDESolverAPIMixin< ProblemTy *Problem{}; const ICFGTy *ICF{}; std::unique_ptr> Results; + IFDSIDESolverConfig SolverConfig; /// Only while solving FunctionCompressor Funs; @@ -400,6 +416,10 @@ DFISolver(ProblemTy *, const ICFGTy *) template DFISolver(ProblemTy *, const ICFGTy *, StaticSolverConfigTy) -> DFISolver; +template +DFISolver(ProblemTy *, const ICFGTy *, StaticSolverConfigTy, + IFDSIDESolverConfig) + -> DFISolver; /// Solves the given IFDS problem with the DFISolver and returns the owning /// results diff --git a/include/phasar/DataFlow/IfdsIde/Solver/IDESolver.h b/include/phasar/DataFlow/IfdsIde/Solver/IDESolver.h index 560b42befe..4ed7a908a6 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/IDESolver.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/IDESolver.h @@ -106,6 +106,13 @@ class IDESolverProblemWrapperStorage { : ProblemOwner( std::make_unique>(Problem)) {} + template + IDESolverProblemWrapperStorage(ProblemTy *Problem, + IFDSIDESolverConfig SolverConfig) + : IDESolverProblemWrapperStorage(Problem) { + ProblemOwner->setIFDSIDESolverConfig(SolverConfig); + } + std::unique_ptr> ProblemOwner{}; }; @@ -168,6 +175,23 @@ class IDESolver AllTop(this->ProblemOwner->allTopFunction()), Seeds(Problem->initialSeeds()) {} + /// Uses SolverConfig instead of the problem's solver config + template + requires(!std::derived_from< + ProblemTy, + IDETabulationProblem>) + IDESolver(ProblemTy *Problem, const i_t *ICF, + IFDSIDESolverConfig SolverConfig) + : detail::IDESolverProblemWrapperStorage( + Problem, SolverConfig), + IDEProblem(*this->ProblemOwner), ZeroValue(Problem->getZeroValue()), + ICF(&assertNotNull(ICF)), + SolverConfig(this->ProblemOwner->getIFDSIDESolverConfig()), + CachedFlowEdgeFunctions(this->ProblemOwner.get()), + AllTop(this->ProblemOwner->allTopFunction()), + Seeds(Problem->initialSeeds()) {} + IDESolver(const IDESolver &) = delete; IDESolver &operator=(const IDESolver &) = delete; IDESolver(IDESolver &&) = delete; diff --git a/include/phasar/DataFlow/IfdsIde/Solver/IFDSSolver.h b/include/phasar/DataFlow/IfdsIde/Solver/IFDSSolver.h index 3b9462f5fb..2988b61def 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/IFDSSolver.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/IFDSSolver.h @@ -70,6 +70,15 @@ class IFDSSolver : public IDESolver, IFDSSolver(ProblemTy *IFDSProblem, const ICFGTy *ICF) : Base(IFDSProblem, ICF) {} + /// Uses SolverConfig instead of the problem's solver config + template + requires(std::same_as< + WithBinaryValueDomain, + WithBinaryValueDomain>) + IFDSSolver(ProblemTy *IFDSProblem, const ICFGTy *ICF, + IFDSIDESolverConfig SolverConfig) + : Base(IFDSProblem, ICF, SolverConfig) {} + ~IFDSSolver() override = default; /// Returns the data-flow results at the given statement. @@ -129,6 +138,10 @@ template IFDSSolver(Problem *, const ICF *) -> IFDSSolver; +template +IFDSSolver(Problem *, const ICF *, IFDSIDESolverConfig) + -> IFDSSolver; template using IFDSSolver_P diff --git a/include/phasar/PhasarLLVM/Utils/DataFlowAnalysisType.def b/include/phasar/PhasarLLVM/Utils/DataFlowAnalysisType.def index 8c981903a8..41836543ed 100644 --- a/include/phasar/PhasarLLVM/Utils/DataFlowAnalysisType.def +++ b/include/phasar/PhasarLLVM/Utils/DataFlowAnalysisType.def @@ -17,6 +17,7 @@ DATA_FLOW_ANALYSIS_TYPES(IFDSTaintAnalysis, "ifds-taint", "Simple, alias-aware t DATA_FLOW_ANALYSIS_TYPES(IFDSCFLEnvTaintAnalysis, "ifds-fieldsens-taint", "Same base analysis as ifds-taint, but uses CFL-Environment-Transformers to achieve field sensitivity. Use with --analysis-config") DATA_FLOW_ANALYSIS_TYPES(MonoIFDSTaintAnalysis, "monoifds-taint", "Simple, alias-aware taint-analysis as POC for the MonoIFDS solver. Use with --analysis-config") DATA_FLOW_ANALYSIS_TYPES(SparseIFDSTaintAnalysis, "sparse-ifds-taint", "Simple, alias-aware taint-analysis utilizing SparseIFDS. Use with --analysis-config") +DATA_FLOW_ANALYSIS_TYPES(DFITaintAnalysis, "dfi-taint", "Same analysis as ifds-taint, but solved with the DFISolver. Use with --analysis-config") DATA_FLOW_ANALYSIS_TYPES(IDEExtendedTaintAnalysis, "ide-xtaint", "More advanced alias-aware taint analysis that provides limited field-sensitivity. Use with --analysis-config") DATA_FLOW_ANALYSIS_TYPES(IFDSTypeAnalysis, "ifds-type", "Simple type analysis") DATA_FLOW_ANALYSIS_TYPES(IDECSTDIOTypeStateAnalysis, "ide-stdio-ts", "Find invalid usages of the libc file-io") diff --git a/tools/phasar-cli/Controller/AnalysisController.cpp b/tools/phasar-cli/Controller/AnalysisController.cpp index 7e3e6a224d..10a61b1751 100644 --- a/tools/phasar-cli/Controller/AnalysisController.cpp +++ b/tools/phasar-cli/Controller/AnalysisController.cpp @@ -139,6 +139,9 @@ static void executeWholeProgram(AnalysisController &Data) { case DataFlowAnalysisType::SparseIFDSTaintAnalysis: executeSparseIFDSTaint(Data); continue; + case DataFlowAnalysisType::DFITaintAnalysis: + executeDFITaint(Data); + continue; case DataFlowAnalysisType::IDEExtendedTaintAnalysis: executeIDEXTaint(Data); continue; diff --git a/tools/phasar-cli/Controller/AnalysisControllerInternal.h b/tools/phasar-cli/Controller/AnalysisControllerInternal.h index c9f06c2f05..f5760735b3 100644 --- a/tools/phasar-cli/Controller/AnalysisControllerInternal.h +++ b/tools/phasar-cli/Controller/AnalysisControllerInternal.h @@ -48,6 +48,7 @@ executeIntraMonoFullConstant(AnalysisController &Data); LLVM_LIBRARY_VISIBILITY void executeIntraMonoSolverTest(AnalysisController &Data); LLVM_LIBRARY_VISIBILITY void executeSparseIFDSTaint(AnalysisController &Data); +LLVM_LIBRARY_VISIBILITY void executeDFITaint(AnalysisController &Data); LLVM_LIBRARY_VISIBILITY void executeInterMonoSolverTest(AnalysisController &Data); LLVM_LIBRARY_VISIBILITY void executeInterMonoTaint(AnalysisController &Data); diff --git a/tools/phasar-cli/Controller/AnalysisControllerXDFITaint.cpp b/tools/phasar-cli/Controller/AnalysisControllerXDFITaint.cpp new file mode 100644 index 0000000000..06b5ab46ae --- /dev/null +++ b/tools/phasar-cli/Controller/AnalysisControllerXDFITaint.cpp @@ -0,0 +1,26 @@ +/****************************************************************************** + * Copyright (c) 2026 Fabian Schiebel. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of LICENSE.txt. + * + * Contributors: + * Fabian Schiebel and others + *****************************************************************************/ + +#include "phasar/DataFlow/IfdsIde/Solver/DFISolver.h" +#include "phasar/PhasarLLVM/DataFlow/IfdsIde/Problems/IFDSTaintAnalysis.h" + +#include "AnalysisControllerInternalIDE.h" + +using namespace psr; + +void controller::executeDFITaint(AnalysisController &Data) { + auto Config = makeTaintConfig(Data); + // Note: Don't blindly generate argc and argv. Use a proper taint config + // instead + auto Problem = createAnalysisProblem( + Data.HA, &Config, Data.getEntryPoints(), false); + DFISolver Solver(&Problem, &Data.HA.getICFG(), DFISolverConfigNoIndex{}, + Data.SolverConfig); + executeIfdsIdeAnalysisImpl(Solver, Data); +} diff --git a/tools/phasar-cli/Controller/AnalysisControllerXIFDSTaint.cpp b/tools/phasar-cli/Controller/AnalysisControllerXIFDSTaint.cpp index 66898a4530..b1e68161d7 100644 --- a/tools/phasar-cli/Controller/AnalysisControllerXIFDSTaint.cpp +++ b/tools/phasar-cli/Controller/AnalysisControllerXIFDSTaint.cpp @@ -17,6 +17,8 @@ void controller::executeIFDSTaint(AnalysisController &Data) { auto Config = makeTaintConfig(Data); // Note: Don't blindly generate argc and argv. Use a proper taint config // instead - executeIFDSAnalysis(Data, &Config, Data.getEntryPoints(), - false); + auto Problem = createAnalysisProblem( + Data.HA, &Config, Data.getEntryPoints(), false); + IFDSSolver Solver(&Problem, &Data.HA.getICFG(), Data.SolverConfig); + executeIfdsIdeAnalysisImpl(Solver, Data); } From 1bd5405d41d0131b65a97f8261e36ba50239a7d4 Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Fri, 9 Oct 2026 19:12:46 +0200 Subject: [PATCH 4/8] Add backward index direction to DFISolver Add DFIIndexDirection config option and DFIReachability enumeration of ancestors/descendants; test result and reachability APIs. Co-Authored-By: Claude Opus 5.5 --- docs/DFISolver-Design.md | 33 ++- .../DataFlow/IfdsIde/Solver/DFISolver.h | 21 +- .../DataFlow/IfdsIde/Solver/DFISolverConfig.h | 19 ++ .../IfdsIde/Solver/DFISolverResults.h | 71 +++++- lib/DataFlow/IfdsIde/IfdsIde.cppm | 2 + .../DataFlow/IfdsIde/DFISolverTest.cpp | 204 ++++++++++++++++-- 6 files changed, 308 insertions(+), 42 deletions(-) diff --git a/docs/DFISolver-Design.md b/docs/DFISolver-Design.md index 36a5e85d28..0b7c9b7237 100644 --- a/docs/DFISolver-Design.md +++ b/docs/DFISolver-Design.md @@ -251,13 +251,19 @@ contiguous slice of the vertex arrays. ### 4.4 Direction The ESG is explored forward (forced by `computeTargets`). The kernel can index -`G*` or its reverse (`IndexDirection::Forward | Backward`, reverse CSR is one -counting sort). Same answers, different `|Pi|` and different enumeration +`G*` or its reverse (`DFIIndexDirection::Forward | Backward`, reverse CSR is +one counting sort). Same answers, different `|Pi|` and different enumeration locality: forward makes "what does `x` reach" contiguous, backward makes -"what reaches `y`" (provenance) contiguous. The paper's in-degree argument is +"what reaches `y`" (provenance) contiguous. The respective other enumeration +scans all vertices with point queries. The paper's in-degree argument is specific to SSA def-use graphs; for ESGs (merge points vs. branch/gen fan-out) this must be measured. Default: Forward. +Backward roots: the sinks of `G*`, followed by all vertices. Vertices on +cycles of `G*` without a path to a sink are not reachable from any sink in the +reversed graph; the builder skips already visited roots, so the trailing +vertices only start new DFS trees for those. + ## 5. Data layout All ids are `uint32_t` strong typedefs (`PHASAR_STRONG_TYPEDEF`), containers @@ -315,13 +321,13 @@ unless `KeepGraph` is set. ```cpp namespace psr { -enum class DfiIndexDirection : uint8_t { Forward, Backward }; +enum class DFIIndexDirection : uint8_t { Forward, Backward }; enum class DfiQueryTargets : uint8_t { All, Interesting }; struct DFISolverConfig { static constexpr bool AutoAddZero = true; static constexpr bool BuildQueryIndex = true; - static constexpr DfiIndexDirection IndexDirection = DfiIndexDirection::Forward; + static constexpr DFIIndexDirection IndexDirection = DFIIndexDirection::Forward; /// Interesting: rank only vertices at Problem.isInteresting(n) instructions static constexpr DfiQueryTargets QueryTargets = DfiQueryTargets::All; /// Keep the CSR of G* after solving (ESG export, witness paths) @@ -376,10 +382,17 @@ public: ByConstRef ToInst, ByConstRef ToFact) const; /// Facts at At that are reachable from From - void forEachReachableAt(DfiVertexRef From, ByConstRef At, - std::invocable> auto Handler) const; - void forEachReachable(DfiVertexRef From, - std::invocable, ByConstRef> auto Handler) const; + void forEachDescendantFactAt(DfiVertexRef From, ByConstRef At, + std::invocable> auto Handler) const; + /// Facts at At that reach To + void forEachAncestorFactAt(DfiVertexRef To, ByConstRef At, + std::invocable> auto Handler) const; + /// Contiguous for a forward index, full scan for a backward index + void forEachDescendant(DfiVertexRef From, + std::invocable, ByConstRef> auto Handler) const; + /// Contiguous for a backward index, full scan for a forward index + void forEachAncestor(DfiVertexRef To, + std::invocable, ByConstRef> auto Handler) const; }; } // namespace psr @@ -390,7 +403,7 @@ public: `containsNode`, `foreachResultEntry`. `resultsAt(n)` is a slice of `VerticesAt`; `resultAt(n, d)` is a binary search in it. -`forEachReachableAt(From, At)` intersects `Pi(From)` with the rank-sorted +`forEachDescendantFactAt(From, At)` intersects `Pi(From)` with the rank-sorted vertices at `At` (merge of two sorted lists), the typical taint query "which facts at sink `At` originate from source `From`". diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h index 9eb4d09f14..1028f948c1 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h @@ -19,6 +19,7 @@ #include "phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h" #include "phasar/DataFlow/IfdsIde/Solver/IDESolverAPIMixin.h" #include "phasar/Utils/ByRef.h" +#include "phasar/Utils/CsrGraph.h" #include "phasar/Utils/FunctionId.h" #include "phasar/Utils/IntervalReachability.h" #include "phasar/Utils/IotaIterator.h" @@ -365,12 +366,28 @@ class DFISolver : public IDESolverAPIMixin< // --- Results + /// Indexes the reversed supergraph. Its roots are the sinks of the + /// supergraph; all other vertices follow, as vertices on cycles without a + /// path to a sink are not reachable from any sink. + void buildBackwardIndex(IntervalReachabilityIndex &Out) { + auto Reversed = CsrGraph::reversed(Graph->buildSupergraph()); + auto Roots = std::move(Reversed.Roots); + llvm::append_range(Roots, iota(Reversed.numVertices())); + IntervalReachabilityBuilder().build(Reversed, Roots, Out); + } + void finalizeResults() { auto &R = *Results; if constexpr (StaticSolverConfigTy::BuildQueryIndex) { R.Index.emplace(); - IntervalReachabilityBuilder().build( - Graph->buildSupergraph(), Graph->seeds(), *R.Index); + R.IndexDirection = StaticSolverConfigTy::IndexDirection; + if constexpr (StaticSolverConfigTy::IndexDirection == + DFIIndexDirection::Backward) { + buildBackwardIndex(*R.Index); + } else { + IntervalReachabilityBuilder().build( + Graph->buildSupergraph(), Graph->seeds(), *R.Index); + } } R.VtxInst = Graph->takeVertexInsts(); diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h index e8d858b8c7..882ea24899 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h @@ -9,8 +9,18 @@ * Fabian Schiebel and others *****************************************************************************/ +#include + namespace psr { +/// The direction in which the DFISolver's reachability index is built. Both +/// directions answer the same queries; they differ in which enumeration is +/// cheap and in the index size. +enum class DFIIndexDirection : uint8_t { + Forward, + Backward, +}; + /// Static configuration of the DFISolver. Derive from it to change options. struct DFISolverConfig { /// Propagate the zero fact along all flow functions, even if the analysis @@ -18,6 +28,8 @@ struct DFISolverConfig { static constexpr bool AutoAddZero = true; /// Build the reachability index required by DFISolver::getReachability() static constexpr bool BuildQueryIndex = true; + static constexpr DFIIndexDirection IndexDirection = + DFIIndexDirection::Forward; }; /// DFISolverConfig without reachability index; use it if only the IFDS @@ -26,4 +38,11 @@ struct DFISolverConfigNoIndex : DFISolverConfig { static constexpr bool BuildQueryIndex = false; }; +/// DFISolverConfig with a backward reachability index; use it if mostly the +/// origins of facts are queried +struct DFISolverConfigBackward : DFISolverConfig { + static constexpr DFIIndexDirection IndexDirection = + DFIIndexDirection::Backward; +}; + } // namespace psr diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h index 9afae26d83..2c1931c828 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h @@ -9,10 +9,12 @@ * Fabian Schiebel and others *****************************************************************************/ +#include "phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h" #include "phasar/Domain/BinaryDomain.h" #include "phasar/Utils/ByRef.h" #include "phasar/Utils/Compressor.h" #include "phasar/Utils/IntervalReachability.h" +#include "phasar/Utils/IotaIterator.h" #include "phasar/Utils/Printer.h" #include "phasar/Utils/StrongTypeDef.h" #include "phasar/Utils/TypedVector.h" @@ -53,6 +55,7 @@ template struct DFIResultsData { llvm::SmallVector VerticesAt; std::optional> Index; + DFIIndexDirection IndexDirection = DFIIndexDirection::Forward; }; template class DFISolverResultsBase { @@ -235,14 +238,20 @@ class OwningDFISolverResults /// /// From reaches To iff there is a realizable path from From to To that may /// descend into callees, but does not return beyond the function of From. -/// Every node reaches itself. +/// Every node reaches itself. The nodes reachable from a node are its +/// descendants, the nodes reaching it are its ancestors; both include the node +/// itself. +/// +/// Point queries cost O(log k) in both index directions. Enumerating the +/// descendants (forward index) or ancestors (backward index) is linear in the +/// output; the other enumeration scans all nodes. template class DFIReachability { public: using n_t = N; using d_t = D; explicit DFIReachability(const detail::DFIResultsData *Data) noexcept - : Results(Data), Index(&*Data->Index) {} + : Results(Data), Index(&*Data->Index), Direction(Data->IndexDirection) {} /// The node , if it has been materialized, i.e., if Fact holds /// at Stmt. @@ -253,6 +262,9 @@ template class DFIReachability { [[nodiscard]] bool reaches(dfi::VertexId From, dfi::VertexId To) const noexcept { + if (Direction == DFIIndexDirection::Backward) { + return Index->reaches(To, From); + } return Index->reaches(From, To); } @@ -265,22 +277,39 @@ template class DFIReachability { } /// Calls Handler for each fact at At that is reachable from From - void forEachReachableAt(dfi::VertexId From, ByConstRef At, + void + forEachDescendantFactAt(dfi::VertexId From, ByConstRef At, std::invocable> auto Handler) const { for (auto To : Results.verticesAt(At)) { - if (Index->reaches(From, To)) { + if (reaches(From, To)) { std::invoke(Handler, Results.factOf(To)); } } } + /// Calls Handler for each fact at At that reaches To + void + forEachAncestorFactAt(dfi::VertexId To, ByConstRef At, + std::invocable> auto Handler) const { + for (auto From : Results.verticesAt(At)) { + if (reaches(From, To)) { + std::invoke(Handler, Results.factOf(From)); + } + } + } + /// Calls Handler for each node reachable from From - void forEachReachable( + void forEachDescendant( dfi::VertexId From, std::invocable, ByConstRef> auto Handler) const { - Index->forEachReachableTarget(From, [&](dfi::VertexId To) { - std::invoke(Handler, Results.instOf(To), Results.factOf(To)); - }); + forEachRelated(From, DFIIndexDirection::Forward, Handler); + } + + /// Calls Handler for each node that reaches To + void forEachAncestor( + dfi::VertexId To, + std::invocable, ByConstRef> auto Handler) const { + forEachRelated(To, DFIIndexDirection::Backward, Handler); } [[nodiscard]] n_t instOf(dfi::VertexId Vtx) const { @@ -295,9 +324,35 @@ template class DFIReachability { return *Index; } + [[nodiscard]] DFIIndexDirection indexDirection() const noexcept { + return Direction; + } + private: + /// Calls Handler for each node related to Vtx in direction Dir + void forEachRelated( + dfi::VertexId Vtx, DFIIndexDirection Dir, + std::invocable, ByConstRef> auto &Handler) const { + const auto Report = [&](dfi::VertexId Related) { + std::invoke(Handler, Results.instOf(Related), Results.factOf(Related)); + }; + + if (Dir == Direction) { + Index->forEachReachableTarget(Vtx, Report); + return; + } + + // The index points the other way, so the related nodes are not contiguous + for (auto Other : iota(Index->numVertices())) { + if (Index->reaches(Other, Vtx)) { + Report(Other); + } + } + } + DFISolverResults Results; const IntervalReachabilityIndex *Index{}; + DFIIndexDirection Direction{}; }; } // namespace psr diff --git a/lib/DataFlow/IfdsIde/IfdsIde.cppm b/lib/DataFlow/IfdsIde/IfdsIde.cppm index 61cce44f0c..12913ca972 100644 --- a/lib/DataFlow/IfdsIde/IfdsIde.cppm +++ b/lib/DataFlow/IfdsIde/IfdsIde.cppm @@ -48,9 +48,11 @@ using psr::checkSREquality; using psr::Compressor; using psr::defaultJoinOrNull; using psr::DefaultMapKeyCompressor; +using psr::DFIIndexDirection; using psr::DFIReachability; using psr::DFISolver; using psr::DFISolverConfig; +using psr::DFISolverConfigBackward; using psr::DFISolverConfigNoIndex; using psr::DFISolverResults; using psr::EdgeFunctionCache; diff --git a/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp b/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp index 44412ce687..9aebc03b67 100644 --- a/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp +++ b/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp @@ -29,6 +29,7 @@ #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" +#include "llvm/IR/InstrTypes.h" #include "llvm/IR/Instruction.h" #include "llvm/IR/Value.h" @@ -36,8 +37,11 @@ #include "TestConfig.h" #include "gtest/gtest.h" +#include #include #include +#include +#include #include namespace { @@ -212,32 +216,43 @@ class DFISolverTaintTest : public ::testing::TestWithParam { static constexpr auto PathToLlFiles = PHASAR_BUILD_SUBFOLDER("taint_analysis/dummy_source_sink/"); static inline const std::vector EntryPoints = {"main"}; + + /// Leaks are reported as side effects of the flow functions + template + static void checkSameLeaksAsIterativeIDESolver(const llvm::Twine &File) { + psr::HelperAnalyses HA(File, EntryPoints); + auto Config = psr::unittest::getDefaultConfig(); + + auto ExpectedProblem = psr::createAnalysisProblem( + HA, &Config, EntryPoints); + psr::IterativeIDESolver ExpectedSolver(&ExpectedProblem, &HA.getICFG(), + psr::IFDSSolverConfig{}); + auto ExpectedResults = ExpectedSolver.solve(); + + auto Problem = psr::createAnalysisProblem( + HA, &Config, EntryPoints); + psr::DFISolver Solver(&Problem, &HA.getICFG(), DFIConfigTy{}); + auto Results = Solver.solve(); + + EXPECT_EQ(ExpectedProblem.Leaks, Problem.Leaks) + << "Leaks differ:\n Expected: " + << psr::PrettyPrinter{ExpectedProblem.Leaks} + << "\n Got: " << psr::PrettyPrinter{Problem.Leaks}; + + compareResults(HA.getProjectIRDB(), ExpectedResults, Results); + checkReachableFromSeeds(HA.getProjectIRDB(), Problem, Results, + Solver.getReachability()); + } }; -/// Leaks are reported as side effects of the flow functions TEST_P(DFISolverTaintTest, SameLeaksAsIterativeIDESolver) { - psr::HelperAnalyses HA(PathToLlFiles + GetParam(), EntryPoints); - auto Config = psr::unittest::getDefaultConfig(); - - auto ExpectedProblem = psr::createAnalysisProblem( - HA, &Config, EntryPoints); - psr::IterativeIDESolver ExpectedSolver(&ExpectedProblem, &HA.getICFG(), - psr::IFDSSolverConfig{}); - auto ExpectedResults = ExpectedSolver.solve(); - - auto Problem = psr::createAnalysisProblem( - HA, &Config, EntryPoints); - psr::DFISolver Solver(&Problem, &HA.getICFG()); - auto Results = Solver.solve(); - - EXPECT_EQ(ExpectedProblem.Leaks, Problem.Leaks) - << "Leaks differ:\n Expected: " - << psr::PrettyPrinter{ExpectedProblem.Leaks} - << "\n Got: " << psr::PrettyPrinter{Problem.Leaks}; + checkSameLeaksAsIterativeIDESolver(PathToLlFiles + + GetParam()); +} - compareResults(HA.getProjectIRDB(), ExpectedResults, Results); - checkReachableFromSeeds(HA.getProjectIRDB(), Problem, Results, - Solver.getReachability()); +TEST_P(DFISolverTaintTest, SameLeaksAsIterativeIDESolverBackwardIndex) { + checkSameLeaksAsIterativeIDESolver( + PathToLlFiles + GetParam()); } constexpr std::string_view TaintTestFiles[] = { @@ -319,6 +334,151 @@ constexpr UnbalancedTestCase UnbalancedTestCases[] = { INSTANTIATE_TEST_SUITE_P(DFISolverUnbalancedTest, DFISolverUnbalancedTest, ::testing::ValuesIn(UnbalancedTestCases)); +using NodeSet = std::set>; + +NodeSet allNodes(const psr::DFISolverResults &Results) { + NodeSet Ret; + Results.foreachResultEntry([&Ret](const auto &Entry) { + Ret.emplace(std::get<0>(Entry), std::get<1>(Entry)); + }); + return Ret; +} + +NodeSet descendants(const psr::DFIReachability &Reach, + psr::dfi::VertexId From) { + NodeSet Ret; + Reach.forEachDescendant( + From, [&Ret](n_t Inst, d_t Fact) { Ret.emplace(Inst, Fact); }); + return Ret; +} + +NodeSet ancestors(const psr::DFIReachability &Reach, + psr::dfi::VertexId To) { + NodeSet Ret; + Reach.forEachAncestor( + To, [&Ret](n_t Inst, d_t Fact) { Ret.emplace(Inst, Fact); }); + return Ret; +} + +/// int a = source(); sink(a); +class DFISolverAPITest : public ::testing::Test { +protected: + static constexpr auto PathToLlFiles = + PHASAR_BUILD_SUBFOLDER("taint_analysis/dummy_source_sink/"); + static inline const std::vector EntryPoints = {"main"}; + + DFISolverAPITest() + : HA(PathToLlFiles + "taint_01_cpp_dbg.ll", EntryPoints), + Problem(psr::createAnalysisProblem( + HA, &Config, EntryPoints)) {} + + [[nodiscard]] const llvm::CallBase *callTo(llvm::StringRef Callee) { + for (const auto *Inst : HA.getProjectIRDB().getAllInstructions()) { + const auto *Call = llvm::dyn_cast(Inst); + if (Call && Call->getCalledFunction() && + Call->getCalledFunction()->getName() == Callee) { + return Call; + } + } + return nullptr; + } + + psr::HelperAnalyses HA; + psr::LLVMTaintConfig Config = psr::unittest::getDefaultConfig(); + psr::IFDSTaintAnalysis Problem; +}; + +TEST_F(DFISolverAPITest, ResultsAgreeWithIfdsResults) { + auto Results = psr::solveDFIProblem(Problem, HA.getICFG()); + const auto *Zero = Problem.getZeroValue(); + + NodeSet ExpectedNodes; + size_t NumInstsWithResults = 0; + for (const auto *Inst : HA.getProjectIRDB().getAllInstructions()) { + auto Facts = Results.ifdsResultsAt(Inst); + EXPECT_EQ(!Facts.empty(), Results.containsNode(Inst)); + NumInstsWithResults += !Facts.empty(); + + auto WithZero = Results.resultsAt(Inst); + auto WithoutZero = Results.resultsAt(Inst, /*StripZero*/ true); + EXPECT_EQ(WithZero.size(), Facts.size()); + EXPECT_EQ(WithoutZero.size(), Facts.size() - Facts.count(Zero)); + EXPECT_FALSE(WithoutZero.contains(Zero)); + + for (const auto *Fact : Facts) { + ExpectedNodes.emplace(Inst, Fact); + EXPECT_TRUE(WithZero.contains(Fact)); + EXPECT_EQ(Results.resultAt(Inst, Fact), psr::BinaryDomain::BOTTOM); + } + } + + EXPECT_EQ(Results.size(), NumInstsWithResults); + EXPECT_EQ(allNodes(Results), ExpectedNodes); + Results.foreachResultEntry([](const auto &Entry) { + EXPECT_EQ(std::get<2>(Entry), psr::BinaryDomain::BOTTOM); + }); + + const auto *SourceCall = callTo("_Z6sourcev"); + ASSERT_NE(SourceCall, nullptr); + const auto *MainEntry = &SourceCall->getFunction()->getEntryBlock().front(); + EXPECT_EQ(Results.resultAt(MainEntry, SourceCall), psr::BinaryDomain::TOP); +} + +template +class DFIReachabilityTest : public DFISolverAPITest {}; + +using DFIIndexConfigs = + ::testing::Types; +TYPED_TEST_SUITE(DFIReachabilityTest, DFIIndexConfigs); + +TYPED_TEST(DFIReachabilityTest, TaintFlowsFromSourceToSink) { + psr::DFISolver Solver(&this->Problem, &this->HA.getICFG(), TypeParam{}); + auto Results = Solver.solve(); + auto Reach = Solver.getReachability(); + EXPECT_EQ(Reach.indexDirection(), TypeParam::IndexDirection); + + const auto *SourceCall = this->callTo("_Z6sourcev"); + const auto *SinkCall = this->callTo("_Z4sinki"); + ASSERT_NE(SourceCall, nullptr); + ASSERT_NE(SinkCall, nullptr); + const auto *AfterSource = SourceCall->getNextNode(); + const auto *Leaked = SinkCall->getArgOperand(0); + const auto *Zero = this->Problem.getZeroValue(); + + auto Source = Reach.find(AfterSource, SourceCall); + auto Sink = Reach.find(SinkCall, Leaked); + ASSERT_TRUE(Source.has_value()); + ASSERT_TRUE(Sink.has_value()); + + EXPECT_TRUE(Reach.reaches(AfterSource, SourceCall, SinkCall, Leaked)); + EXPECT_FALSE(Reach.reaches(SinkCall, Leaked, AfterSource, SourceCall)); + + std::set DescendantsAtSink; + Reach.forEachDescendantFactAt( + *Source, SinkCall, [&](d_t Fact) { DescendantsAtSink.insert(Fact); }); + EXPECT_TRUE(DescendantsAtSink.contains(Leaked)); + EXPECT_FALSE(DescendantsAtSink.contains(Zero)); + + std::set AncestorsAtSource; + Reach.forEachAncestorFactAt( + *Sink, AfterSource, [&](d_t Fact) { AncestorsAtSource.insert(Fact); }); + EXPECT_TRUE(AncestorsAtSource.contains(SourceCall)); + EXPECT_FALSE(AncestorsAtSource.contains(Zero)); + + NodeSet ExpectedDescendants; + NodeSet ExpectedAncestors; + for (const auto &[Inst, Fact] : allNodes(Results)) { + if (Reach.reaches(AfterSource, SourceCall, Inst, Fact)) { + ExpectedDescendants.emplace(Inst, Fact); + } + if (Reach.reaches(Inst, Fact, SinkCall, Leaked)) { + ExpectedAncestors.emplace(Inst, Fact); + } + } + EXPECT_EQ(descendants(Reach, *Source), ExpectedDescendants); + EXPECT_EQ(ancestors(Reach, *Sink), ExpectedAncestors); +} + } // namespace int main(int Argc, char **Argv) { From 983869a1fdde14e82f8e4e111a027c9c3b1ee9b8 Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Fri, 9 Oct 2026 20:34:59 +0200 Subject: [PATCH 5/8] Small perf improvement in IterativeIDESolver by caching summary-ffs as well --- .../DataFlow/IfdsIde/GenericFlowFunction.h | 5 ---- .../IfdsIde/Solver/FlowFunctionCache.h | 23 +++++++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/include/phasar/DataFlow/IfdsIde/GenericFlowFunction.h b/include/phasar/DataFlow/IfdsIde/GenericFlowFunction.h index a7c9a99124..021d10f2f4 100644 --- a/include/phasar/DataFlow/IfdsIde/GenericFlowFunction.h +++ b/include/phasar/DataFlow/IfdsIde/GenericFlowFunction.h @@ -93,11 +93,6 @@ class GenericFlowFunction { [[nodiscard]] bool operator==(std::nullptr_t) const noexcept { return FF == nullptr; } - [[nodiscard]] bool - operator!=(GenericFlowFunctionView Other) const noexcept { - return !(*this == Other); - } - [[nodiscard]] bool operator!=(std::nullptr_t) const noexcept { return FF; } private: FlowFunctionPtrType FF; diff --git a/include/phasar/DataFlow/IfdsIde/Solver/FlowFunctionCache.h b/include/phasar/DataFlow/IfdsIde/Solver/FlowFunctionCache.h index 17c121deec..0b0d8a2819 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/FlowFunctionCache.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/FlowFunctionCache.h @@ -263,17 +263,25 @@ class FlowFunctionCache decltype(auto) getSummaryFlowFunction(ProblemTy &Problem, ByConstRef CallSite, ByConstRef Callee, - uint64_t /*CSCalleeId*/) { + uint64_t CSCalleeId) { + // Note: summary-ff can ignore AutoAddZero because it is guaranteed that the + // ctr-ff will propagate 0 if constexpr (needs_cache_v) { - return GenericFlowFunction( - Problem.getSummaryFlowFunction(CallSite, Callee)); + // Most calls have no summary, i.e., a nullptr flow function. So, cannot + // just check for null as empty-marker + auto [It, Inserted] = SummaryFFCache.insert(CSCalleeId, summary_ff_t{}); + if (Inserted) { + It->second = Problem.getSummaryFlowFunction(CallSite, Callee); + } + + if constexpr (std::is_same_v>) { + return GenericFlowFunctionView(It->second); + } else { + return GenericFlowFunctionView(getPointerFrom(It->second)); + } } else { return Problem.getSummaryFlowFunction(CallSite, Callee); } - - /// XXX: The old FECache doesn't cache summary FFs, so refrain from doing - /// this here as well. Enable it, once the user-problems reliably return - /// std::nullptr_t such that caching can be disabled automatically } void clear() noexcept { @@ -289,6 +297,7 @@ class FlowFunctionCache detail::IntraFlowFunctionsMixin::reserveIntraFFs( NumInsts, NumCalls); CallFFCache.reserve(NumCalls); + SummaryFFCache.reserve(NumCalls); RetFFCache.reserve(NumFuns); SimpleRetFFCache.reserve(NumFuns); } From 8cdefc510be3dd4ca8b60ded052d596f4153707b Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Sat, 10 Oct 2026 14:11:56 +0200 Subject: [PATCH 6/8] minor --- .../IfdsIde/Solver/DFIExplodedSupergraph.h | 8 +-- .../DataFlow/IfdsIde/Solver/DFISolver.h | 34 ++++++------ .../IfdsIde/Solver/DFISolverResults.h | 12 ++--- include/phasar/Utils/CsrGraph.h | 45 ++++++++++++---- include/phasar/Utils/IntervalReachability.h | 52 ++++++++++++------- include/phasar/Utils/SCCId.h | 10 ++++ include/phasar/Utils/StrongTypeDef.h | 5 ++ .../IfdsIde/Solver/DFIExplodedSupergraph.cpp | 17 +++--- unittests/Utils/IntervalReachabilityTest.cpp | 32 +++++------- 9 files changed, 129 insertions(+), 86 deletions(-) diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h b/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h index bd1ae059b0..a10c451760 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h @@ -82,6 +82,8 @@ class DFIExplodedSupergraph { /// addEdge() for each target, then endEdges(Vtx). Vtx must not be expanded /// before. Creating vertices in between is fine. void beginEdges(dfi::VertexId Vtx) { + assert(VtxEdgeBegin[Vtx] == UINT32_MAX && + "beginEdges() called before for Vtx!"); VtxEdgeBegin[Vtx] = uint32_t(Edges.size()); } void addEdge(dfi::VertexId Target) { Edges.push_back(Target); } @@ -213,9 +215,9 @@ class DFIExplodedSupergraph { [[nodiscard]] llvm::ArrayRef edgesOf(dfi::VertexId Vtx) const noexcept { - auto Begin = VtxEdgeBegin[Vtx]; - return llvm::ArrayRef(Edges).slice(Begin, - VtxEdgeEnd[Vtx] - Begin); + auto Start = VtxEdgeBegin[Vtx]; + auto End = VtxEdgeEnd[Vtx]; + return llvm::ArrayRef(Edges).slice(Start, End - Start); } SCCHolder CGSCCs; diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h index 1028f948c1..ead185f616 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h @@ -23,6 +23,7 @@ #include "phasar/Utils/FunctionId.h" #include "phasar/Utils/IntervalReachability.h" #include "phasar/Utils/IotaIterator.h" +#include "phasar/Utils/Lazy.h" #include "phasar/Utils/Utilities.h" #include "llvm/ADT/ArrayRef.h" @@ -31,7 +32,6 @@ #include "llvm/Support/raw_ostream.h" #include -#include #include #include #include @@ -311,9 +311,8 @@ class DFISolver : public IDESolverAPIMixin< for (const auto &Callee : Callees) { auto &Info = CalleeInfos.emplace_back(); Info.Callee = Funs.get(Callee); - FlowFunctionPtrType SummaryFF = - Problem->getSummaryFlowFunction(CallSite, Callee); - if (SummaryFF) { + + if (auto SummaryFF = Problem->getSummaryFlowFunction(CallSite, Callee)) { Info.FF = std::move(SummaryFF); Info.IsSummaryFF = true; continue; @@ -369,25 +368,26 @@ class DFISolver : public IDESolverAPIMixin< /// Indexes the reversed supergraph. Its roots are the sinks of the /// supergraph; all other vertices follow, as vertices on cycles without a /// path to a sink are not reachable from any sink. - void buildBackwardIndex(IntervalReachabilityIndex &Out) { + [[nodiscard]] IntervalReachabilityIndex buildBackwardIndex() { auto Reversed = CsrGraph::reversed(Graph->buildSupergraph()); - auto Roots = std::move(Reversed.Roots); + auto Roots = std::move(Reversed).roots(); llvm::append_range(Roots, iota(Reversed.numVertices())); - IntervalReachabilityBuilder().build(Reversed, Roots, Out); + return IntervalReachabilityBuilder().build(Reversed, Roots); } void finalizeResults() { auto &R = *Results; if constexpr (StaticSolverConfigTy::BuildQueryIndex) { - R.Index.emplace(); R.IndexDirection = StaticSolverConfigTy::IndexDirection; - if constexpr (StaticSolverConfigTy::IndexDirection == - DFIIndexDirection::Backward) { - buildBackwardIndex(*R.Index); - } else { - IntervalReachabilityBuilder().build( - Graph->buildSupergraph(), Graph->seeds(), *R.Index); - } + R.Index.emplace(lazy{[&] { + if constexpr (StaticSolverConfigTy::IndexDirection == + DFIIndexDirection::Backward) { + return buildBackwardIndex(); + } else { + return IntervalReachabilityBuilder().build( + Graph->buildSupergraph(), Graph->seeds()); + } + }}); } R.VtxInst = Graph->takeVertexInsts(); @@ -405,9 +405,7 @@ class DFISolver : public IDESolverAPIMixin< R.VerticesAt[--R.InstOffsets[R.VtxInst[Vtx]]] = Vtx; } for (auto Inst : iota(R.Insts.size())) { - llvm::ArrayRef Bounds(&R.InstOffsets[Inst], 2); - auto Vertices = llvm::MutableArrayRef(R.VerticesAt) - .slice(Bounds[0], Bounds[1] - Bounds[0]); + auto Vertices = csrSuccsMut(R.InstOffsets, R.VerticesAt, Inst); llvm::sort(Vertices, [&R](dfi::VertexId Lhs, dfi::VertexId Rhs) { return R.VtxFact[Lhs] < R.VtxFact[Rhs]; }); diff --git a/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h index 2c1931c828..63a59080f3 100644 --- a/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h @@ -13,8 +13,10 @@ #include "phasar/Domain/BinaryDomain.h" #include "phasar/Utils/ByRef.h" #include "phasar/Utils/Compressor.h" +#include "phasar/Utils/CsrGraph.h" #include "phasar/Utils/IntervalReachability.h" #include "phasar/Utils/IotaIterator.h" +#include "phasar/Utils/Macros.h" #include "phasar/Utils/Printer.h" #include "phasar/Utils/StrongTypeDef.h" #include "phasar/Utils/TypedVector.h" @@ -161,10 +163,7 @@ template class DFISolverResultsBase { [[nodiscard]] llvm::ArrayRef verticesAt(dfi::InstId Inst) const { - const auto &Data = data(); - llvm::ArrayRef Bounds(&Data.InstOffsets[Inst], 2); - return llvm::ArrayRef(Data.VerticesAt) - .slice(Bounds[0], Bounds[1] - Bounds[0]); + return csrSuccs(data().InstOffsets, data().VerticesAt, Inst); } [[nodiscard]] n_t instOf(dfi::VertexId Vtx) const { @@ -184,12 +183,13 @@ template class DFISolverResultsBase { /// Non-owning view into the results of a DFISolver. Conforms to the interface /// expected by GenericSolverResults. template -class DFISolverResults +class [[gsl::Pointer]] DFISolverResults : public detail::DFISolverResultsBase, N, D> { friend detail::DFISolverResultsBase, N, D>; public: - explicit DFISolverResults(const detail::DFIResultsData *Data) noexcept + explicit DFISolverResults(const detail::DFIResultsData *Data + PSR_LIFETIME_CAPTURE_BY(this)) noexcept : Data(Data) { assert(Data != nullptr); } diff --git a/include/phasar/Utils/CsrGraph.h b/include/phasar/Utils/CsrGraph.h index 90734ba4f2..8c0de209c0 100644 --- a/include/phasar/Utils/CsrGraph.h +++ b/include/phasar/Utils/CsrGraph.h @@ -13,6 +13,7 @@ #include "phasar/Utils/GraphTraits.h" #include "phasar/Utils/IotaIterator.h" #include "phasar/Utils/RepeatIterator.h" +#include "phasar/Utils/StrongTypeDef.h" #include "phasar/Utils/TypeTraits.h" #include "phasar/Utils/TypedVector.h" @@ -27,19 +28,39 @@ namespace psr { +template +[[nodiscard]] inline llvm::ArrayRef +csrSuccs(const TypedVector &Offsets, + const llvm::SmallVectorImpl &Targets, IndexId Vtx) { + auto Start = Offsets[Vtx]; + auto End = Offsets[nextId(Vtx)]; + return llvm::ArrayRef(Targets).slice(Start, End - Start); +} + +template +[[nodiscard]] inline llvm::MutableArrayRef +csrSuccsMut(const TypedVector &Offsets, + llvm::SmallVectorImpl &Targets, IndexId Vtx) { + auto Start = Offsets[Vtx]; + auto End = Offsets[nextId(Vtx)]; + return llvm::MutableArrayRef(Targets).slice(Start, End - Start); +} + /// Immutable directed graph in compressed-sparse-row format: the successors of /// vertex V are Targets[Offsets[V] .. Offsets[V+1]). /// /// Build it with fromEdges() or assignEdges(). assignEdges() reuses the /// allocated storage, so a single CsrGraph object can serve as buffer for many /// short-lived graphs. -template struct CsrGraph { +template class CsrGraph { +public: /// Builds the graph from (Source, Target) pairs. The successors of each /// vertex keep the relative order of Edges. [[nodiscard]] static CsrGraph - fromEdges(size_t NumVertices, llvm::ArrayRef> Edges) { + fromEdges(size_t NumVertices, llvm::ArrayRef> Edges, + llvm::ArrayRef RootVtxs = {}) { CsrGraph Ret; - Ret.assignEdges(NumVertices, Edges); + Ret.assignEdges(NumVertices, Edges, RootVtxs); return Ret; } @@ -66,14 +87,15 @@ template struct CsrGraph { /// Replaces the contents of this graph by NumVertices vertices and the /// given (Source, Target) edges. Clears the roots. void assignEdges(size_t NumVertices, - llvm::ArrayRef> Edges) { + llvm::ArrayRef> Edges, + llvm::ArrayRef RootVtxs = {}) { countEdges(NumVertices, llvm::make_first_range(Edges)); Targets.resize_for_overwrite(Edges.size()); for (auto [From, To] : llvm::reverse(Edges)) { assert(size_t(To) < NumVertices); Targets[--Offsets[From]] = To; } - Roots.clear(); + Roots.assign(RootVtxs.begin(), RootVtxs.end()); } [[nodiscard]] size_t numVertices() const noexcept { @@ -84,12 +106,13 @@ template struct CsrGraph { [[nodiscard]] size_t numEdges() const noexcept { return Targets.size(); } [[nodiscard]] llvm::ArrayRef succsOf(VtxId Vtx) const noexcept { - assert(size_t(Vtx) < numVertices()); - llvm::ArrayRef Bounds(&Offsets[Vtx], 2); - return llvm::ArrayRef(Targets).slice(Bounds[0], - Bounds[1] - Bounds[0]); + return csrSuccs(Offsets, Targets, Vtx); } + [[nodiscard]] llvm::ArrayRef roots() const & noexcept { return Roots; } + [[nodiscard]] auto roots() && noexcept { return Roots; } + +private: /// Sets Offsets[V] to the end of the successors of V. Inserting the edges /// at --Offsets[From] in reverse order then leaves Offsets[V] at the begin. void countEdges(size_t NumVertices, auto &&Sources) { @@ -130,7 +153,7 @@ template struct GraphTraits> { [[nodiscard]] static llvm::ArrayRef roots(const graph_type &G) noexcept { - return G.Roots; + return G.roots(); } [[nodiscard]] static auto vertices(const graph_type &G) noexcept { @@ -148,7 +171,7 @@ template struct GraphTraits> { [[nodiscard]] static size_t roots_size(const graph_type &G) noexcept { // NOLINT - return G.Roots.size(); + return G.roots().size(); } [[nodiscard]] static constexpr vertex_t target(edge_t Edge) noexcept { diff --git a/include/phasar/Utils/IntervalReachability.h b/include/phasar/Utils/IntervalReachability.h index 549613522a..08f9edc50b 100644 --- a/include/phasar/Utils/IntervalReachability.h +++ b/include/phasar/Utils/IntervalReachability.h @@ -9,12 +9,14 @@ * Fabian Schiebel and others *****************************************************************************/ +#include "phasar/Utils/CsrGraph.h" #include "phasar/Utils/GraphTraits.h" #include "phasar/Utils/SCCGeneric.h" #include "phasar/Utils/SCCId.h" #include "phasar/Utils/StrongTypeDef.h" #include "phasar/Utils/TypeTraits.h" #include "phasar/Utils/TypedVector.h" +#include "phasar/Utils/Utilities.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/SmallVector.h" @@ -55,15 +57,9 @@ template class IntervalReachabilityBuilder; /// Reachability index over a directed graph: answers whether a vertex reaches /// a target vertex in O(log k), where k is the number of rank intervals of the -/// source (typically 1-3). +/// source (usually small). /// -/// Built by IntervalReachabilityBuilder from a set of roots: only vertices -/// reachable from the roots are indexed. The targets are a subset of the -/// indexed vertices, selected at build time; each target has a unique rank in -/// [0, numTargets()). Every indexed vertex reaches itself. -/// -/// The targets reachable from a vertex form a sorted list of disjoint rank -/// intervals, see forEachReachableRange(). +/// Built by IntervalReachabilityBuilder template class IntervalReachabilityIndex { friend class IntervalReachabilityBuilder; @@ -83,6 +79,7 @@ template class IntervalReachabilityIndex { return SCCOf[Vtx] != NoSCC; } + // A sink node [[nodiscard]] bool isTarget(VtxId Vtx) const noexcept { return RankOf[Vtx] != ReachRank::None; } @@ -178,9 +175,7 @@ template class IntervalReachabilityIndex { private: [[nodiscard]] llvm::ArrayRef extrasOf(SCCIdTy SCC) const noexcept { - llvm::ArrayRef Bounds(&SetOffsets[SCCExtras[SCC]], 2); - return llvm::ArrayRef(IntervalPool) - .slice(Bounds[0], Bounds[1] - Bounds[0]); + return csrSuccs(SetOffsets, IntervalPool, SCCExtras[SCC]); } void reset(size_t NumVertices) { @@ -226,10 +221,16 @@ template class IntervalReachabilityBuilder { /// Indexes all vertices of Graph reachable from Roots, all of them as /// targets. The ranks are then DFS pre-order numbers. + template + requires std::same_as::vertex_t, VtxId> + [[nodiscard]] IndexTy build(const G &Graph, llvm::ArrayRef Roots) { + return build(Graph, Roots, /*IsTarget=*/psr::TrueFn{}); + } + template requires std::same_as::vertex_t, VtxId> void build(const G &Graph, llvm::ArrayRef Roots, IndexTy &Out) { - build(Graph, Roots, [](VtxId /*Vtx*/) { return true; }, Out); + build(Graph, Roots, Out, /*IsTarget=*/psr::TrueFn{}); } /// Indexes all vertices of Graph reachable from Roots, only those @@ -239,8 +240,8 @@ template class IntervalReachabilityBuilder { /// interval sets shrink accordingly. template requires std::same_as::vertex_t, VtxId> - void build(const G &Graph, llvm::ArrayRef Roots, - std::predicate auto IsTarget, IndexTy &Out) { + void build(const G &Graph, llvm::ArrayRef Roots, IndexTy &Out, + std::predicate auto IsTarget) { auto NumVertices = GraphTraits::size(Graph); Out.reset(NumVertices); SCCData.reset(NumVertices); @@ -266,6 +267,17 @@ template class IntervalReachabilityBuilder { } } + /// Indexes all vertices of Graph reachable from Roots, only those + /// satisfying IsTarget as targets. + template + requires std::same_as::vertex_t, VtxId> + [[nodiscard]] IndexTy build(const G &Graph, llvm::ArrayRef Roots, + std::predicate auto IsTarget) { + IndexTy Out{}; + build(Graph, Roots, Out, copyOrRef(IsTarget)); + return Out; + } + private: template void completeSCC(const G &Graph, SCCIdTy SCC, llvm::ArrayRef Members, @@ -274,8 +286,10 @@ template class IntervalReachabilityBuilder { // Members.back() is the root of the SCC in the DFS tree. Its subtree is // completely discovered. - RankInterval Range{RankBegin[Members.back()], - ReachRank(Out.RankToVertex.size())}; + auto Range = RankInterval{ + .Lo = RankBegin[Members.back()], + .Hi = ReachRank(Out.RankToVertex.size()), + }; for (auto Member : Members) { Out.SCCOf[Member] = SCC; @@ -349,9 +363,9 @@ template class IntervalReachabilityBuilder { } } - std::sort( - Candidates.begin(), Candidates.end(), - [](RankInterval Lhs, RankInterval Rhs) { return Lhs.Lo < Rhs.Lo; }); + llvm::sort(Candidates, [](RankInterval Lhs, RankInterval Rhs) { + return Lhs.Lo < Rhs.Lo; + }); auto Ret = IntervalSetId(Out.SetOffsets.size() - 1); for (auto Interval : Candidates) { diff --git a/include/phasar/Utils/SCCId.h b/include/phasar/Utils/SCCId.h index 0cf4aeb0c0..8188a5ca15 100644 --- a/include/phasar/Utils/SCCId.h +++ b/include/phasar/Utils/SCCId.h @@ -58,7 +58,17 @@ template struct SCCId : detail::SCCIdBase { using detail::SCCIdBase::SCCIdBase; }; +// Overloads with the generic to_underlying from StrongTypeDef.h +template +[[nodiscard]] constexpr auto +// NOLINTNEXTLINE(readability-identifier-naming) +to_underlying(SCCId Scc) noexcept { + return Scc.Value; +} + static_assert(IdType>); +static_assert( + std::same_as{}))>); } // namespace psr diff --git a/include/phasar/Utils/StrongTypeDef.h b/include/phasar/Utils/StrongTypeDef.h index 94a3fe1bf5..ac41d093a0 100644 --- a/include/phasar/Utils/StrongTypeDef.h +++ b/include/phasar/Utils/StrongTypeDef.h @@ -121,4 +121,9 @@ template { return Val; } + +template [[nodiscard]] constexpr IdT nextId(IdT Id) noexcept { + return IdT(to_underlying(Id) + 1); +} + } // namespace psr diff --git a/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp b/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp index 362b976bee..0742480e5c 100644 --- a/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp +++ b/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp @@ -59,7 +59,7 @@ psr::dfi::VertexId DFIExplodedSupergraph::getOrCreateVertex(dfi::InstId Inst, VtxFact.push_back(Fact); VtxFun.push_back(Fun); VtxLocalId.push_back(dfi::LocalVertexId(FS.Vertices.size())); - VtxEdgeBegin.push_back(0); + VtxEdgeBegin.push_back(UINT32_MAX); VtxEdgeEnd.push_back(0); FS.Vertices.push_back(Vtx); if (IsExitVtx) { @@ -150,9 +150,9 @@ DFIExplodedSupergraph::buildSupergraph() const { } } for (const auto &FS : FunState) { - GraphEdges.append(FS.SummaryEdges.begin(), FS.SummaryEdges.end()); + GraphEdges.append(FS.SummaryEdges); } - GraphEdges.append(CallEdges.begin(), CallEdges.end()); + GraphEdges.append(CallEdges); GraphEdges.append(UnbalancedReturnEdges.begin(), UnbalancedReturnEdges.end()); return CsrGraph::fromEdges(numVertices(), GraphEdges); } @@ -205,12 +205,11 @@ void DFIExplodedSupergraph::indexFunction(FunctionId Fun) { LocalRoots.push_back(VtxLocalId[Root]); } - LocalBuilder.build( - LocalGraph, LocalRoots, - [this, &FS](dfi::LocalVertexId Local) { - return IsExit.contains(FS.Vertices[Local]); - }, - LocalIndex); + // Use out-variable to enable memory-reuse + LocalBuilder.build(LocalGraph, LocalRoots, LocalIndex, + [this, &FS](dfi::LocalVertexId Local) { + return IsExit.contains(FS.Vertices[Local]); + }); } void DFIExplodedSupergraph::collectNewExits( diff --git a/unittests/Utils/IntervalReachabilityTest.cpp b/unittests/Utils/IntervalReachabilityTest.cpp index ce8d82daee..2f45daf9e1 100644 --- a/unittests/Utils/IntervalReachabilityTest.cpp +++ b/unittests/Utils/IntervalReachabilityTest.cpp @@ -143,7 +143,7 @@ TEST(CsrGraphTest, Reversed) { EXPECT_EQ(Rev.succsOf(1_v), llvm::ArrayRef({0_v, 2_v})); EXPECT_TRUE(Rev.succsOf(2_v).empty()); EXPECT_EQ(Rev.succsOf(3_v), llvm::ArrayRef({0_v})); - EXPECT_EQ(Rev.Roots, (llvm::SmallVector{1_v, 3_v})); + EXPECT_EQ(Rev.roots(), (llvm::ArrayRef{1_v, 3_v})); } TEST(CsrGraphTest, AssignEdgesReusesGraph) { @@ -158,8 +158,7 @@ TEST(CsrGraphTest, AssignEdgesReusesGraph) { TEST(IntervalReachabilityTest, Chain) { auto G = makeGraph(4, {{0_v, 1_v}, {1_v, 2_v}, {2_v, 3_v}}); - Index Idx; - Builder().build(G, {0_v}, Idx); + Index Idx = Builder().build(G, {0_v}); checkIndex(G, {0_v}, Idx); EXPECT_EQ(Idx.numSCCs(), 4U); @@ -171,8 +170,7 @@ TEST(IntervalReachabilityTest, Chain) { TEST(IntervalReachabilityTest, SharedSuccessorIsNoFalsePositive) { // 1 and 2 both reach 3 via a tree- resp. cross-edge, but not each other auto G = makeGraph(4, {{0_v, 1_v}, {0_v, 2_v}, {1_v, 3_v}, {2_v, 3_v}}); - Index Idx; - Builder().build(G, {0_v}, Idx); + Index Idx = Builder().build(G, {0_v}); checkIndex(G, {0_v}, Idx); EXPECT_FALSE(Idx.reaches(1_v, 2_v)); @@ -182,8 +180,7 @@ TEST(IntervalReachabilityTest, SharedSuccessorIsNoFalsePositive) { TEST(IntervalReachabilityTest, Cycle) { auto G = makeGraph(5, {{0_v, 1_v}, {1_v, 2_v}, {2_v, 0_v}, {2_v, 3_v}}); - Index Idx; - Builder().build(G, {1_v}, Idx); + Index Idx = Builder().build(G, {1_v}); checkIndex(G, {1_v}, Idx); EXPECT_EQ(Idx.sccOf(0_v), Idx.sccOf(1_v)); @@ -195,8 +192,7 @@ TEST(IntervalReachabilityTest, Cycle) { TEST(IntervalReachabilityTest, SelfLoopAndMultiEdges) { auto G = makeGraph(3, {{0_v, 0_v}, {0_v, 1_v}, {0_v, 1_v}, {1_v, 2_v}}); - Index Idx; - Builder().build(G, {0_v}, Idx); + Index Idx = Builder().build(G, {0_v}); checkIndex(G, {0_v}, Idx); } @@ -210,8 +206,7 @@ TEST(IntervalReachabilityTest, CrossEdgesIntoEarlierSubtrees) { {3_v, 4_v}, {4_v, 1_v}, {4_v, 5_v}}); - Index Idx; - Builder().build(G, {0_v}, Idx); + Index Idx = Builder().build(G, {0_v}); checkIndex(G, {0_v}, Idx); EXPECT_TRUE(Idx.reaches(3_v, 1_v)); @@ -220,8 +215,7 @@ TEST(IntervalReachabilityTest, CrossEdgesIntoEarlierSubtrees) { TEST(IntervalReachabilityTest, MultipleRoots) { auto G = makeGraph(6, {{0_v, 1_v}, {2_v, 1_v}, {2_v, 3_v}, {4_v, 5_v}}); - Index Idx; - Builder().build(G, {0_v, 2_v}, Idx); + Index Idx = Builder().build(G, {0_v, 2_v}); checkIndex(G, {0_v, 2_v}, Idx); EXPECT_FALSE(Idx.isIndexed(4_v)); @@ -236,9 +230,8 @@ TEST(IntervalReachabilityTest, ProjectedTargets) { IsTarget.insert(2_v); IsTarget.insert(5_v); - Index Idx; - Builder().build( - G, {0_v}, [&](VtxId Vtx) { return IsTarget.contains(Vtx); }, Idx); + Index Idx = Builder().build( + G, {0_v}, [&](VtxId Vtx) { return IsTarget.contains(Vtx); }); checkIndex(G, {0_v}, IsTarget, Idx); EXPECT_EQ(Idx.numTargets(), 2U); @@ -248,8 +241,7 @@ TEST(IntervalReachabilityTest, ProjectedTargets) { TEST(IntervalReachabilityTest, EmptyGraph) { Graph G; - Index Idx; - Builder().build(G, {}, Idx); + Index Idx = Builder().build(G, {}); EXPECT_EQ(Idx.numVertices(), 0U); EXPECT_EQ(Idx.numTargets(), 0U); @@ -298,8 +290,8 @@ TEST(IntervalReachabilityTest, RandomGraphs) { IsTarget.insert(Vtx); } } - B.build( - G, Roots, [&](VtxId Vtx) { return IsTarget.contains(Vtx); }, Idx); + B.build(G, Roots, Idx, + [&](VtxId Vtx) { return IsTarget.contains(Vtx); }); checkIndex(G, Roots, IsTarget, Idx); auto Rev = Graph::reversed(G); From 74d127aa71ae661c70bd509a75b2a1101fc2b4e9 Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Sat, 10 Oct 2026 14:20:50 +0200 Subject: [PATCH 7/8] Remove the agent's design doc --- docs/DFISolver-Design.md | 476 --------------------------------------- 1 file changed, 476 deletions(-) delete mode 100644 docs/DFISolver-Design.md diff --git a/docs/DFISolver-Design.md b/docs/DFISolver-Design.md deleted file mode 100644 index 0b7c9b7237..0000000000 --- a/docs/DFISolver-Design.md +++ /dev/null @@ -1,476 +0,0 @@ -# DFISolver: Design - -Design for a generic `DFISolver` that solves any `psr::IFDSProblem` with the -DFS-interval reachability technique from Hsu, Hetzelt, Franz: "DFI: An -Interprocedural Value-Flow Analysis Framework that Scales to Large Codebases" -(CGO'23, arXiv:2209.02638). The MLIR part of the paper (`dfi.store`, -`dfi.call`) is out of scope; that is the job of the analysis problem (and -optionally a sparse ICFG). - -## 1. What we take from DFI, and what we change - -### 1.1 DFI in a nutshell - -1. Value-flow is graph reachability on a sparse graph whose edges come from - local transfer functions provided by the client. -2. A DFS from a set of client-chosen roots ("reversed roots") assigns each - vertex a DFS-tree interval ``. Tree reachability = interval - subsumption, O(1). -3. Non-tree edges are handled by attaching an *interval set* `Pi_v` to each - vertex: for a cross edge `s -> d`, `Pi_d` is merged into `s` and all its - DFS ancestors; for a back edge, all vertices of the SCC get the same set. - Adjacent intervals are coalesced. -4. Interprocedurally, a function's value-flow summary (arguments -> results) - is computed with the intra-procedural index and propagated into all callers - (Algorithm 1, worklist until fixpoint). Callers then contain summary edges. -5. Traversing in the reversed direction reduces the number of non-tree edges - for SSA def-use graphs (non-tree edges ~ in-degree in traversal direction). - -### 1.2 Adaptations for generic IFDS - -| DFI paper | DFISolver | -|---|---| -| Vertex = SSA value | Vertex = exploded-supergraph (ESG) node `` | -| Local transfer functions | `getNormal/Call/Ret/CallToRet/SummaryFlowFunction` | -| Graph built from the whole IR upfront | Graph is *discovered* on demand from the seeds (flow functions are forward-only and `d_t` is not enumerable) | -| Reversed roots chosen by client | `initialSeeds()` (+ unbalanced-return roots) | -| Timestamps `` with two ticks per vertex | Pre-order *rank* + subtree end `[Lo, Hi)`, one tick per *target* vertex (see 1.3) | -| Query `Pi_j "subsumes" Pi_i` (set vs. set) | Point query `rank(y) in Pi_x` (exact, see 1.3) | -| Cross/back edges merged into ancestors eagerly | Tarjan SCC fold: sets computed once per SCC at SCC completion | -| `S_f` summaries over argument/result indices | IFDS end summaries: pairs `(, )` | -| Reachable-function summary `Psi` for cross-function queries | One global index over the graph with call + summary edges (exact) | - -### 1.3 Two corrections/improvements of the index - -**Exact point query.** The paper declares `v_i ~> v_j` iff some interval of -`Pi_j` subsumes some interval of `Pi_i`. This yields false positives: if -`x -> z` and `y -> z` (cross edges into an already finished `z`), both sets -contain `I_z`, so the test claims `x ~> y`. We instead test whether the -*own rank* of the target lies in the source's set: - -``` -x ~> y <=> rank(y) in Pi_x (Pi_x = union of disjoint, sorted intervals) -``` - -Every interval in `Pi_x` is the DFS-subtree range of some vertex `w` with -`x ~> w`; every vertex in that range is reachable from `w`. Hence exact. -Cost: O(1) for the own range, otherwise O(log |Pi_x|) binary search (the paper -reports O(N^2) for its set test). - -**Projected ranks.** The rank counter is incremented only for *target* -vertices (a predicate). A subtree of the DFS still covers a contiguous rank -range of the targets it contains. With few targets (e.g. function exits when -computing summaries), interval sets collapse to 0-2 intervals almost always, -and subtrees without targets get an empty range and contribute nothing. With -"all vertices are targets", ranks are plain pre-order numbers. - -## 2. Graph model - -For a given IFDS problem, the solver materializes the realizable part of the -ESG: - -- **Vertices**: `` reachable from a seed via a realizable path. Exactly - the vertices whose facts IFDS reports, so `resultsAt(n)` = all vertices at - `n` (no index needed for the standard IFDS results). -- **Local edges** (intra-procedural, in function `f`): - - normal edges: ` -> `, `d' in normalFF(n, succ)(d)` - - call-to-return edges: ` -> ` - - summary-FF edges: ` -> ` from `getSummaryFlowFunction` - - summary edges: ` -> ` iff `d1 in callFF(d)`, - ` ~>_same-level `, `d3 in retFF(d2)` -- **Call edges**: ` -> `. Not used for summaries. -- **Return edges** are never stored; they are represented by summary edges. - -Let `G*` = vertices + local edges + call edges. Reachability in `G*` from `x` -is exactly realizable reachability along paths that may descend into callees -but never return beyond `x`'s frame (same-level + descending). This is the -semantics of the query API. Unbalanced returns (Sec. 4.6) add explicit -unbalanced-return edges. - -## 3. Algorithm overview - -``` -solve(): - init: compress seeds, compute call-graph SCCs (callees first) - Phase 1: repeat until no work: - F := pending function with the lowest CG-SCC id (callees first) - if F has unexplored vertices: explore(F) - elif F is dirty and its SCC + all lower SCCs are quiescent: summarize(F) - Phase 2: build result table (InstId -> facts) - optionally build global interval index over G* - release Phase 1 graph (unless KeepGraph) -``` - -Each ESG vertex is expanded (flow functions evaluated) **exactly once**. This -is the main difference to IFDS tabulation, which processes a vertex once per -reaching entry fact `d1` (path edges ``). - -### 3.1 explore(f) - -The local worklist of `f` holds unexpanded vertices. Expansion is batched per -instruction: pop all pending vertices, sort by `InstId` (radix sort on u64 -`Inst << 32 | Vtx`), and process each run so every flow-function object is -created once per `(n, succ)` per batch and applied to all facts. No -flow-function cache is needed. - -``` -expand(v = in f): - if isCallSite(n): - Callees := getCalleesOfCallAt(n) - for r in getReturnSitesOfCallAt(n): - for d' in callToRetFF(n, r, Callees)(d): addLocalEdge(v, ) - for g in Callees: - if SFF := getSummaryFlowFunction(n, g): - for r, d' in SFF(d) x ReturnSites: addLocalEdge(v, ) - continue - for d1 in callFF(n, g)(d), sp in getStartPointsOf(g): - E := getOrCreateVertex(, g) // new -> push to g's WL, mark entry - addCallEdge(v, E) // also registers v as caller of E - for X in SummaryExits[E]: applySummary(v, E, X) - else: - for succ in getSuccsOf(n): - for d' in normalFF(n, succ)(d): addLocalEdge(v, ) - (AutoAddZero: zero always maps to zero) - -applySummary(v = , E, X = ): - for r in getReturnSitesOfCallAt(c): - for d3 in retFF(c, fun(E), ep, r)(d2): // memoized per (c, X, r) - addSummaryEdge(v, ) // new target -> push to caller's WL - mark caller dirty -``` - -All edges created by `expand(v)` are produced in one go, so they are appended -contiguously to the edge array (append-only CSR: per vertex `EdgeBegin`, -`EdgeCount`). Summary edges arrive later and go to a per-function append-only -pair list. - -`advanceToNextUser(n, d)` (sparse IFDS, `has_advanceToNextUser_v`) is applied -when creating a target vertex, exactly like `IterativeIDESolver`. This is the -generic analog of DFI's sparse def-use graph. - -### 3.2 summarize(f) - -Computes all same-level summaries `(entry, exit)` of `f`: - -1. Build a transient local CSR over `f`'s vertices using local ids - (`LocalIdx[v]`, assigned at vertex creation): local edges + summary edges, - no call edges. Reused scratch buffers, O(|V_f| + |E_f|). -2. Run the interval-index kernel (Sec. 4) with - roots = entry vertices of `f` (+ seed / unbalanced roots in `f`), - targets = exit vertices of `f`. -3. For each entry `E`: enumerate exit ranks in `Pi(scc(E))`, map - `rank -> exit vertex`, diff against the sorted `SummaryExits[E]`. -4. For each new pair `(E, X)` and each caller vertex `v` of `E`: - `applySummary(v, E, X)`. Append `X` to `SummaryExits[E]`. - -The local index is discarded afterwards; only the summary pairs persist. - -### 3.3 Scheduling - -- Call-graph SCCs via `computeCGSCCs` (Tarjan; SCC id 0 = leaves). Priority = - SCC id, lowest first, i.e. callees before callers. -- `PendingSCCs: BitSet`; pick via find-first-set. Per SCC a small FIFO - of functions with work. -- A function is summarized only when its SCC and all lower SCCs have empty - local worklists. New entries only appear in callees (lower SCC ids or same - SCC), new summary edges only in callers (higher or same SCC), so callers are - typically summarized once per "wave". Recursion inside an SCC iterates to a - fixpoint. - -### 3.4 Correctness sketch - -- *Soundness w.r.t. IFDS*: every IFDS path edge ` -> ` with - `` realizable implies `` is materialized: induction over the - IFDS tabulation; the only non-local step is the return, which is covered by - `applySummary` once the callee's index contains `(E, X)`. At fixpoint every - function is summarized after its last change, so all summary pairs exist. -- *Precision*: vertices are only created from realizable vertices - (normal/c2r/call targets) or via `applySummary` from a realizable call vertex - and a same-level callee path. Flow functions are therefore never evaluated - on unrealizable contexts, so side effects in flow functions (e.g. leak - reporting) see the same `(n, d)` set as with IFDS. -- *Termination*: all sets grow monotonically over finite domains. - -## 4. Interval index kernel - -Generic, independent of IFDS; lives in `phasar/Utils/IntervalReachability.h`. -Input: any `is_const_graph` (typically `CsrGraph` from -`phasar/Utils/CsrGraph.h`), roots, target predicate. A single DFS computes -SCCs, ranks, and interval sets. It reuses Pearce's SCC algorithm from -`SCCGeneric.h` via `visitSCCsFrom()`, whose `OnDiscover` hook assigns ranks -and whose `OnSCC` hook computes the interval sets. - -### 4.1 Per-SCC label - -``` -Pi(S) = Extras(S) U [Lo(S), Hi(S)) -``` - -- `[Lo, Hi)`: rank range of the DFS subtree of the SCC root (all members are - in it). `Lo = RankCounter` at root discovery, `Hi = RankCounter` at SCC - completion. -- `Extras(S)`: sorted, coalesced, disjoint intervals, **all `< Lo(S)`** - (invariant). When the root finishes, every vertex reachable from it has - been visited, so no reachable target has rank `>= Hi`; anything inside - `[Lo, Hi)` is already covered. So extras only ever need clipping at `Lo`. - -### 4.2 SCC completion - -Tarjan completes SCCs in reverse topological order, so every successor SCC -`W != S` of a member is final when `S` completes. - -``` -onSccComplete(S, Members): - Lo, Hi := RankBegin[root], RankCounter - Cand := [] - for m in Members, w in succ(m): - W := SccOf[w] - if W == S or Seen[W] == S: continue // stamp array dedups - Seen[W] := S - if Hi(W) <= Lo and Lo(W) < Hi(W): Cand += [Lo(W), Hi(W)) // cross edge - Cand += clip(Extras(W), Lo) - Extras(S) := fastPath or coalesce(sort(Cand)) -``` - -**Fast path** (chains, loop bodies): exactly one distinct successor `W`, `W` is -inside the subtree, and `Extras(W).back().Hi <= Lo` -> reuse `ExtraSetId(W)` -without copying. Otherwise sort + coalesce (`[a, b) [b, c) -> [a, c)`), then -optionally hash-cons the resulting span in the interval pool. - -### 4.3 Query - -``` -reaches(x, y): S := SccOf[x], R := Rank[y] - return Lo(S) <= R < Hi(S) || binarySearch(Extras(S), R) -``` - -Enumeration of everything reachable from `x` = walk `Pi(S)`; with -"all vertices are targets" and vertices renumbered by rank, each interval is a -contiguous slice of the vertex arrays. - -### 4.4 Direction - -The ESG is explored forward (forced by `computeTargets`). The kernel can index -`G*` or its reverse (`DFIIndexDirection::Forward | Backward`, reverse CSR is -one counting sort). Same answers, different `|Pi|` and different enumeration -locality: forward makes "what does `x` reach" contiguous, backward makes -"what reaches `y`" (provenance) contiguous. The respective other enumeration -scans all vertices with point queries. The paper's in-degree argument is -specific to SSA def-use graphs; for ESGs (merge points vs. branch/gen -fan-out) this must be measured. Default: Forward. - -Backward roots: the sinks of `G*`, followed by all vertices. Vertices on -cycles of `G*` without a path to a sink are not reachable from any sink in the -reversed graph; the builder skips already visited roots, so the trailing -vertices only start new DFS trees for those. - -## 5. Data layout - -All ids are `uint32_t` strong typedefs (`PHASAR_STRONG_TYPEDEF`), containers -are `TypedVector` / `BitSet`. `InstId` comes from -`IRDB.getInstructionId(n)` (no hashing), `FactId` from a `Compressor` -(zero fact = 0), `FunctionId` from `FunctionCompressor`. - -### 5.1 Phase 1 (mutable, append-only) - -| Data | Type | Bytes/vertex | Notes | -|---|---|---|---| -| `VertexOf` | flat hash map `u64(Inst, Fact) -> VertexId` | ~12-16 | the only hash probe per edge target | -| `VtxInst`, `VtxFact` | `TypedVector` x2 | 8 | SoA | -| `VtxEdgeBegin`, `VtxEdgeCount` | `TypedVector` x2 | 8 | into `Edges` | -| `LocalIdx` | `TypedVector` | 4 | index within `FunVertices[f]` | -| `Edges` | `std::vector` | ~4 x out-degree | normal + c2r + summary-FF | -| `FunVertices[f]` | `TypedVector>` | 4 | | -| `FunSummaryEdges[f]` | vector of `(VertexId, VertexId)` | | turned into transient CSR in `summarize` | -| `FunEntries[f]`, `FunExits[f]` | vectors of `VertexId` | | exits = vertices at `isExitInst` | -| `EntryIdx` | `DenseMap` (entries are rare) | | | -| `EntryCallers[E]`, `SummaryExits[E]` | `TypedVector>` | | `SummaryExits` kept sorted | -| `CallEdges` | vector of `(VertexId, VertexId)` | | for the global index only | -| `RetFFMemo` | map `(InstId c, VertexId X) -> span in target pool` | | avoids repeated `retFF` on the same exit fact | -| `FunWL[f]` | `std::vector` | | | -| `PendingSCCs`, `Dirty` | `BitSet` | | | - -Per vertex ~36-40 bytes + edges. For comparison, `IterativeIDESolver` stores -one jump-function entry per path edge `` (hash-table slot of -`combineIds(d1, d2)` per instruction + ValTab entry); path edges per vertex = -number of entry facts reaching it, which is what blows up on large programs. - -### 5.2 Kernel scratch (reused across all `summarize` calls) - -`Index[], Low[], RankBegin[], SccOf[]` (u32, sized to the largest local -graph), `OnStack` bit vector, Tarjan stack, DFS frame stack `{Vtx, EdgeCursor}`, -`Seen[]` stamps, candidate interval buffer. No allocation in steady state. - -### 5.3 Final results (immutable) - -| Data | Type | Bytes | -|---|---|---| -| `VtxInst`, `VtxFact` (renumbered by rank) | `u32` x2 | 8 / vertex | -| `InstOffsets` | CSR offsets | 4 / instruction | -| `VerticesAt` | ranks, sorted by `FactId` per instruction | 4 / vertex | -| `FactCompressor` | `d_t <-> FactId` | | -| *index:* `SccOf` | `u32` | 4 / vertex | -| *index:* `SccLo`, `SccHi`, `SccExtras` | `u32` x3 | 12 / SCC | -| *index:* `IntervalPool`, `SetOffsets` | CSR of `{u32 Lo, Hi}` | 8 / interval | - -Phase 1 structures (hash map, edges, summaries) are released after Phase 2 -unless `KeepGraph` is set. - -## 6. API - -```cpp -namespace psr { - -enum class DFIIndexDirection : uint8_t { Forward, Backward }; -enum class DfiQueryTargets : uint8_t { All, Interesting }; - -struct DFISolverConfig { - static constexpr bool AutoAddZero = true; - static constexpr bool BuildQueryIndex = true; - static constexpr DFIIndexDirection IndexDirection = DFIIndexDirection::Forward; - /// Interesting: rank only vertices at Problem.isInteresting(n) instructions - static constexpr DfiQueryTargets QueryTargets = DfiQueryTargets::All; - /// Keep the CSR of G* after solving (ESG export, witness paths) - static constexpr bool KeepGraph = false; - static constexpr bool HashConsIntervalSets = true; - static constexpr bool EnableStatistics = false; - - template using map_t = ...; -}; - -template -class DFISolver - : public IDESolverAPIMixin> { -public: - DFISolver(ProblemTy *Problem, const ICFGTy *ICF, - StaticSolverConfigTy Config = {}) noexcept; - - auto solve() &; - [[nodiscard]] auto solve() &&; - - /// Trivially-copyable view; usable as GenericSolverResults - [[nodiscard]] DFISolverResults getSolverResults() const noexcept; - [[nodiscard]] OwningDFISolverResults consumeSolverResults(); - - [[nodiscard]] DFIReachability getReachability() const noexcept - requires(StaticSolverConfigTy::BuildQueryIndex); - - [[nodiscard]] DFISolverStats getStats() const noexcept - requires(StaticSolverConfigTy::EnableStatistics); - -private: - // IDESolverAPIMixin hooks: one expansion batch or one summarize per step - void doInitialize(); - bool doNext(); - auto doFinalize() &; - auto doFinalize() &&; -}; - -template -[[nodiscard]] auto solveDFIProblem(ProblemTy &Problem, const ICFG auto &ICF); - -/// Context-sensitive reachability between materialized ESG nodes. -/// Semantics: realizable paths that may descend into callees but do not -/// return past the frame of the source (plus unbalanced returns from seeds). -template class DFIReachability { -public: - [[nodiscard]] std::optional find(ByConstRef Inst, - ByConstRef Fact) const; - [[nodiscard]] bool reaches(DfiVertexRef From, DfiVertexRef To) const noexcept; - [[nodiscard]] bool reaches(ByConstRef FromInst, ByConstRef FromFact, - ByConstRef ToInst, ByConstRef ToFact) const; - - /// Facts at At that are reachable from From - void forEachDescendantFactAt(DfiVertexRef From, ByConstRef At, - std::invocable> auto Handler) const; - /// Facts at At that reach To - void forEachAncestorFactAt(DfiVertexRef To, ByConstRef At, - std::invocable> auto Handler) const; - /// Contiguous for a forward index, full scan for a backward index - void forEachDescendant(DfiVertexRef From, - std::invocable, ByConstRef> auto Handler) const; - /// Contiguous for a backward index, full scan for a forward index - void forEachAncestor(DfiVertexRef To, - std::invocable, ByConstRef> auto Handler) const; -}; - -} // namespace psr -``` - -`DFISolverResults` implements what `GenericSolverResults` dispatches to: -`resultAt`, `resultsAt(n, StripZero)`, `ifdsResultsAt`, `size`, -`containsNode`, `foreachResultEntry`. `resultsAt(n)` is a slice of -`VerticesAt`; `resultAt(n, d)` is a binary search in it. - -`forEachDescendantFactAt(From, At)` intersects `Pi(From)` with the rank-sorted -vertices at `At` (merge of two sorted lists), the typical taint query -"which facts at sink `At` originate from source `From`". - -## 7. IFDS feature mapping - -| Feature | Handling | -|---|---| -| `AutoAddZero` | zero fact always propagated by the solver | -| `getSummaryFlowFunction` | local edges to return sites, callee not entered | -| Callee without body | only call-to-return flow | -| `followReturnsPastSeeds` | as in the IDESolver: exits reached from a seed without callers, or from an unbalanced root while the zero entry of `f` has no callers, return to *all* call-graph callers of `f` via `retFF`. This creates unbalanced roots in the callers and unbalanced-return edges in `G*`. Unbalanced roots count as reached from the zero entry, i.e. its summaries include their exits. For functions without callers, `applyUnbalancedRetFlowFunctionSideEffects` if `UnbalancedRetSideEffectProvider` | -| `has_advanceToNextUser_v` | applied on target-vertex creation | -| `isInteresting(n)` | `QueryTargets::Interesting` restricts ranks to those vertices | -| `emitESG` / `recordEdges` | requires `KeepGraph`; dumps the CSR of `G*` | -| `computeValues` | not applicable (IFDS only, `l_t = BinaryDomain`) | -| `IDESolverAPIMixin` (`solveUntil`, cancellation) | `doNext` = one batch / one summarize | - -**Witness paths** (with `KeepGraph`): reconstruct a path `x ~> y` greedily by -following any successor `s` with `reaches(s, y)`; no backtracking needed, -O(path length x out-degree x log |Pi|). - -## 8. Complexity - -- Flow-function applications: once per vertex (IFDS: once per path edge). -- Return flow: once per `(call site, exit vertex, return site)` via memo. -- `summarize(f)`: O(|V_f| + |E_f| + merge cost) per call; number of calls per - function bounded by the number of change waves, small with callees-first - scheduling except in large recursive SCCs. -- Global index: O(|V| + |E| + sum |Pi|). Sets are tiny in practice (paper: - median 2, max 17), further reduced by coalescing ranks and the fast path. -- Worst case `sum |Pi| = O(|V|^2)`, same as any transitive-closure labeling. - `BuildQueryIndex = false` skips it entirely if only IFDS results are needed. - -## 9. Files and tests - -New files: - -- `include/phasar/Utils/CsrGraph.h`: `CsrGraph` + `GraphTraits`. -- `include/phasar/Utils/IntervalReachability.h`: kernel (Sec. 4), interval - pool, query. No IFDS dependency. Built on `visitSCCsFrom()` in - `SCCGeneric.h`. -- `include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h` -- `include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h` -- `include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h` (results view, - owning results, `DFIReachability`) -- `include/phasar/DataFlow/IfdsIde/Solver/DFISolverStats.h` - -Tests: - -- Kernel: random graphs (DAGs, cycles, dense SCCs, empty target sets, single - vertex) checked against brute-force BFS transitive closure, both - directions, all-targets and projected targets. -- Solver: differential tests against `IterativeIDESolver` with the existing - IFDS analyses (`IFDSTaintAnalysis`, `IFDSUninitializedVariables`, - `IFDSConstAnalysis`, `IFDSTypeAnalysis`, ...) on the existing - `test/llvm_test_code` programs: identical `ifdsResultsAt` everywhere, - identical leak sets. Recursion and `followReturnsPastSeeds` cases included. -- Reachability: for small programs, compare `reaches` against a reference that - re-runs `IterativeIDESolver` with `` as single seed. - -## 10. Open points - -- **Incremental local index**: `summarize` currently rebuilds the local index - of `f` from scratch. For large recursive SCCs an incremental variant (only - new roots / new edges) may pay off. Measure first. -- **Parallelism**: functions in different subtrees of the CG-SCC DAG with - pending work are independent in `explore`; `summarize` writes only into - callers. A level-synchronous parallel scheduler is possible later. -- **Index direction and target selection** need empirical evaluation on ESGs - (Sec. 4.4). From b467dd0e1ee760bf6b8cd6a3e92be9155e9802d8 Mon Sep 17 00:00:00 2001 From: Fabian Schiebel Date: Sun, 11 Oct 2026 14:28:11 +0200 Subject: [PATCH 8/8] Update readme + minor --- CMakeLists.txt | 2 +- README.md | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 74e89fdef9..efe360619a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,7 +271,7 @@ add_nlohmann_json() add_json_schema_validator() # TBB -find_package(TBB) # needed by GCC +find_package(TBB QUIET) # needed by GCC # Coverage if (CODE_COVERAGE) diff --git a/README.md b/README.md index 2616604a26..ea2b1673f0 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,8 @@ You can find available literature on PhASAR [here](https://github.com/secure-sof ### Key Features - **IFDS/IDE solver**: Interprocedural data-flow solvers based on the IFDS/IDE algorithm -- **WPDS solver (experimental)**: Data-flow solver based on weighted pushdown systems. Can solve any IFDS/IDE problem - **MonoIFDS solver**: High-performance data-flow solver for *bottom-up* IFDS analyses -- **Sparse analysis**: SparseIFDS/SparseIDE/SparseWPDS for improved performance +- **Sparse analysis**: SparseIFDS/SparseIDE for improved performance - **Call-graph construction**: Several algorithms (CHA, RTA, VTA, alias-based) - **Type-hierarchy construction**: Extract high-level C++ type information from LLVM IR - **Points-to/alias infrastructure**: High-performance alias analyses for LLVM IR. Integration with state-of-the-art alias/points-to information from SVF supported @@ -29,14 +28,16 @@ You can find available literature on PhASAR [here](https://github.com/secure-sof - **Taint analysis**: Infrastructure for taint-configuration & IFDS/IDE-based taint analysis - **Modern C++20 API**: Modular, easy-to use interfaces, also for non C++ experts +#### Experimental Features + +- **WPDS solver**: Data-flow solver based on weighted pushdown systems. Can solve any IFDS/IDE problem; supports sparsification +- **DFI solver**: Data-flow solver based on DFS-interval reachability. Can solve any IFDS problem; supports sparsification + ### How do I get started with PhASAR? -We have some documentation on PhASAR in our [***Wiki***](https://github.com/secure-software-engineering/phasar/wiki). You probably would like to read -this README first. +We have some documentation on PhASAR in our [***Wiki***](https://github.com/secure-software-engineering/phasar/wiki). You probably would like to read this README first. -Please also have a look at PhASAR's project directory, in particular the -[examples](./examples/) directory and the custom tool -`tools/example-tool/myphasartool.cpp`. +Please also have a look at PhASAR's project directory, in particular the [examples](./examples/) directory and the custom tool `tools/example-tool/myphasartool.cpp`. You can find PhASAR's API reference [here](https://secure-software-engineering.github.io/phasar/). @@ -55,7 +56,7 @@ We actively test PhASAR with LLVM-16 and LLVM-22.1, so if something does not wor Specify the `PHASAR_LLVM_VERSION` cmake variable to change the LLVM version to use. > [!NOTE] -> Since v18, LLVM uses minor versions, so use `-DPHASAR_LLVM_VERSION=17` for LLVM-17 but `-DPHASAR_LLVM_VERSION=22.1` for LLVM-22 +> Since v18, LLVM uses minor versions, so use `-DPHASAR_LLVM_VERSION=17` for LLVM-17 but `-DPHASAR_LLVM_VERSION=18.1` for LLVM-18 ## Breaking Changes