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 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/DFIExplodedSupergraph.h b/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h new file mode 100644 index 0000000000..a10c451760 --- /dev/null +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.h @@ -0,0 +1,265 @@ +#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() + /// \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. + [[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) { + 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); } + 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); + + /// 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 UnbalancedHandler) { + 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); + } + } + } + + 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(); } + [[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, call, and unbalanced-return 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, 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; + }; + + 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. 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 Start = VtxEdgeBegin[Vtx]; + auto End = VtxEdgeEnd[Vtx]; + return llvm::ArrayRef(Edges).slice(Start, End - Start); + } + + 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; + llvm::DenseSet> UnbalancedReturnEdges; + bool FollowReturnsPastSeeds{}; + + // 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..ead185f616 --- /dev/null +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolver.h @@ -0,0 +1,448 @@ +#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/IFDSIDESolverConfig.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/CsrGraph.h" +#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" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/Support/raw_ostream.h" + +#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. +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 = {}) + : 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>()), + SolverConfig(SolverConfig) {} + + [[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); + } + + /// Only valid after solving + void emitTextReport(llvm::raw_ostream &OS = llvm::outs()) { + Problem->emitTextReport(getSolverResults(), 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() { + for (const auto &Fun : ICF->getAllFunctions()) { + Funs.getOrInsert(Fun); + } + Graph.emplace(computeCGSCCs(*ICF, Funs), + SolverConfig.followReturnsPastSeeds()); + + 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 { + auto Fun = Next->Fun; + Graph->summarize( + Fun, + [this](dfi::VertexId CallVtx, dfi::EntryId Entry, + dfi::VertexId Exit) { applySummary(CallVtx, Entry, Exit); }, + [this, Fun](dfi::VertexId Exit) { returnUnbalanced(Fun, 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)); + } + } + } + + /// 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) { + 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); + + if (auto SummaryFF = Problem->getSummaryFlowFunction(CallSite, Callee)) { + 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 + + /// 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. + [[nodiscard]] IntervalReachabilityIndex buildBackwardIndex() { + auto Reversed = CsrGraph::reversed(Graph->buildSupergraph()); + auto Roots = std::move(Reversed).roots(); + llvm::append_range(Roots, iota(Reversed.numVertices())); + return IntervalReachabilityBuilder().build(Reversed, Roots); + } + + void finalizeResults() { + auto &R = *Results; + if constexpr (StaticSolverConfigTy::BuildQueryIndex) { + R.IndexDirection = StaticSolverConfigTy::IndexDirection; + R.Index.emplace(lazy{[&] { + if constexpr (StaticSolverConfigTy::IndexDirection == + DFIIndexDirection::Backward) { + return buildBackwardIndex(); + } else { + return IntervalReachabilityBuilder().build( + Graph->buildSupergraph(), Graph->seeds()); + } + }}); + } + + 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())) { + 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]; + }); + } + + Graph.reset(); + Funs = FunctionCompressor(); + } + + ProblemTy *Problem{}; + const ICFGTy *ICF{}; + std::unique_ptr> Results; + IFDSIDESolverConfig SolverConfig; + + /// Only while solving + FunctionCompressor Funs; + std::optional Graph; +}; + +template +DFISolver(ProblemTy *, const ICFGTy *) + -> DFISolver; +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 +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..882ea24899 --- /dev/null +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverConfig.h @@ -0,0 +1,48 @@ +#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 + +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 + /// problem does not do so explicitly + 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 +/// results are of interest +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 new file mode 100644 index 0000000000..63a59080f3 --- /dev/null +++ b/include/phasar/DataFlow/IfdsIde/Solver/DFISolverResults.h @@ -0,0 +1,358 @@ +#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/DFISolverConfig.h" +#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" + +#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; + DFIIndexDirection IndexDirection = DFIIndexDirection::Forward; +}; + +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 { + return csrSuccs(data().InstOffsets, data().VerticesAt, Inst); + } + + [[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 [[gsl::Pointer]] DFISolverResults + : public detail::DFISolverResultsBase, N, D> { + friend detail::DFISolverResultsBase, N, D>; + +public: + explicit DFISolverResults(const detail::DFIResultsData *Data + PSR_LIFETIME_CAPTURE_BY(this)) 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. 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), Direction(Data->IndexDirection) {} + + /// 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 { + if (Direction == DFIIndexDirection::Backward) { + return Index->reaches(To, From); + } + 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 + forEachDescendantFactAt(dfi::VertexId From, ByConstRef At, + std::invocable> auto Handler) const { + for (auto To : Results.verticesAt(At)) { + 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 forEachDescendant( + dfi::VertexId From, + std::invocable, ByConstRef> auto Handler) const { + 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 { + return Results.instOf(Vtx); + } + [[nodiscard]] d_t factOf(dfi::VertexId Vtx) const { + return Results.factOf(Vtx); + } + + [[nodiscard]] const IntervalReachabilityIndex & + index() const noexcept { + 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/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); } 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/include/phasar/Utils/CsrGraph.h b/include/phasar/Utils/CsrGraph.h new file mode 100644 index 0000000000..8c0de209c0 --- /dev/null +++ b/include/phasar/Utils/CsrGraph.h @@ -0,0 +1,187 @@ +#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/StrongTypeDef.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 { + +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 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, + llvm::ArrayRef RootVtxs = {}) { + CsrGraph Ret; + Ret.assignEdges(NumVertices, Edges, RootVtxs); + 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, + 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.assign(RootVtxs.begin(), RootVtxs.end()); + } + + [[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 { + 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) { + 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..08f9edc50b --- /dev/null +++ b/include/phasar/Utils/IntervalReachability.h @@ -0,0 +1,395 @@ +#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/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" + +#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 (usually small). +/// +/// Built by IntervalReachabilityBuilder +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; + } + + // A sink node + [[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 { + return csrSuccs(SetOffsets, IntervalPool, SCCExtras[SCC]); + } + + 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> + [[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, Out, /*IsTarget=*/psr::TrueFn{}); + } + + /// 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, IndexTy &Out, + std::predicate auto IsTarget) { + 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); + } + } + } + + /// 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, + 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. + auto Range = RankInterval{ + .Lo = RankBegin[Members.back()], + .Hi = 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)}); + } + } + + llvm::sort(Candidates, [](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/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/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..12913ca972 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,13 @@ 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; using psr::EdgeFunctionCacheStats; using psr::EdgeFunctionComposer; @@ -80,7 +88,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..0742480e5c --- /dev/null +++ b/lib/DataFlow/IfdsIde/Solver/DFIExplodedSupergraph.cpp @@ -0,0 +1,281 @@ +/****************************************************************************** + * 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, bool FollowReturnsPastSeeds) + : CGSCCs(std::move(CGSCCs)), + FollowReturnsPastSeeds(FollowReturnsPastSeeds) { + 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(UINT32_MAX); + 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); + FunState[VtxFun[Vtx]].Seeds.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]; +} + +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() && + "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); + } + GraphEdges.append(CallEdges); + GraphEdges.append(UnbalancedReturnEdges.begin(), UnbalancedReturnEdges.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; +} + +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; + + 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]); + } + + // 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( + 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) { + auto &Exits = SummaryExits[Entry]; + auto EntryVtx = EntryVertex[Entry]; + + NewExits.clear(); + 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/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/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); } 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..9aebc03b67 --- /dev/null +++ b/unittests/PhasarLLVM/DataFlow/IfdsIde/DFISolverTest.cpp @@ -0,0 +1,487 @@ +/****************************************************************************** + * 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/IFDSSolver.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/DataFlow/IfdsIde/Problems/IFDSUninitializedVariables.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/InstrTypes.h" +#include "llvm/IR/Instruction.h" +#include "llvm/IR/Value.h" + +#include "TaintTest.h" +#include "TestConfig.h" +#include "gtest/gtest.h" + +#include +#include +#include +#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, 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} + << "\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 + 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()); + } +}; + +TEST_P(DFISolverTaintTest, SameLeaksAsIterativeIDESolver) { + checkSameLeaksAsIterativeIDESolver(PathToLlFiles + + GetParam()); +} + +TEST_P(DFISolverTaintTest, SameLeaksAsIterativeIDESolverBackwardIndex) { + checkSameLeaksAsIterativeIDESolver( + PathToLlFiles + GetParam()); +} + +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)); + +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)); + +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) { + ::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..2f45daf9e1 --- /dev/null +++ b/unittests/Utils/IntervalReachabilityTest.cpp @@ -0,0 +1,314 @@ +/****************************************************************************** + * 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::ArrayRef{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}); + + 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}); + + 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}); + + 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}); + + 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}); + + 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}); + + 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); }); + + 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, {}); + + 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, Idx, + [&](VtxId Vtx) { return IsTarget.contains(Vtx); }); + 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(); +}