Context-Sensitive Andersen Pointer Analysis - #856
Open
fabianbs96 wants to merge 80 commits into
Open
Conversation
…ing in AndersenOTFSolver
- grow() may reallocate Nodes; all constraint methods now call every
grow() before indexing Nodes[X], and snapshot pts sets before any
addAssignEdge call that fires inside a foreach callback
- onNewPointee snapshots all four constraint lists upfront for the
same reason
- merge() snapshots NonRep vectors before any addAssignEdge call, and
retroactively fires load/store/memcopy constraints for Rep's merged
pts set (previously those constraints were silently dropped for
already-existing pointees)
- ConnectKnownTargets and checkUnresolvedFPCalls snapshot pts(FPId)
before iterating: connectCallee->propagate() can grow that set
- handleCall now collects all resolved IDs per argument (not just the
last one) via SmallVector<ValueId,2> per slot; FPCallRecord::Args
and connectCallee updated accordingly
- Add dedup guards (LoadDstSet, StoreSrcSet, MemCopyAs{Src,Dst}Set)
to NodeInfo to avoid redundant constraint firing
- Remove unused NoArgId sentinel and <cstdint> include
- Mark rep() [[nodiscard]]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…o speensgaard sets
- Fix OperandOf::operator< (was comparing R2.Inst instead of R1.Inst) - DeepChainTwoObjectsMerge (context_04_1): three-level id chain with x/y - RecursiveSelfAlias (context_08): SCC collapsing under self-recursion - MutualRecursionAlias (context_10_0): Forth↔Back two-way recursion - ReturnSecondArgContextInsensitive (context_12_1): argretq precision - FuncPtrCallbackIdentity (context_14_1): OTF resolves indirect call - RecursionTwoObjectsMerge (context_09_0): recursive with two objects - MutualRecursionTwoObjects (context_10_1): mutual recursion, two objects - ThreeWayMutualRecursion (context_11_0): Forth↔Back↔Stop recursion - ThreeArgReturnQContextInsensitive (context_13_1): three-param function - FuncPtrCallbackThreeWayMerge (context_14_2): three function pointers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dness with extern functions
XXX: Should we allow passing-in an instance of LLVMFunctionDataFlowFacts?
Root-cause was integral stores being found as reaching definition for a ptr-load
addPointee() resolved the pointee through rep(), letting lazy cycle detection rewrite an abstract object's identity. Merging a cycle is points-to-correct, but object nodes take part in the constraint graph and points-to membership is what defines may-alias: once two objects shared a representative, every pointer to one aliased every pointer to the other. Context-sensitivity multiplied object nodes and grew that conflated SCC, making it a net precision loss. Storing the canonical id costs nothing: cycle collapsing still drives propagation, as every constraint helper re-resolves through rep() itself. Also fixes a latent unsoundness: rep() was applied at insert time while merge() never rewrote other nodes' points-to sets, so pointers recording the same object before and after a merge stored different ids and failed to alias. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gxbHcWF9ZWq82oZP4m6Y1
addFnPtrArgsAsEntries() reads pts(arg) when a declaration call site is first connected, and the ConnectedCallees guard makes that happen exactly once. Points-to sets keep growing afterwards, so a function pointer that reached such an argument later was never discovered and its whole callee subgraph went unanalyzed. Record the argument lists and re-check them per outer round, like the other unresolved-call tables. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019gxbHcWF9ZWq82oZP4m6Y1
Turns the six failing tests from d45ecc2 green, plus A3, which the review had written off as untestable. - A1: resolveFieldWrite(ValueId) resolves through rep(), so a store whose pointer node later collapses into an SCC still poisons its objects. Includes the Poisen -> Poison typo. - A2: a failed addAlias in addPtrAlias now merges the two nodes instead of silently dropping the relation. handlePhi interns loop-carried GEPs and casts before they are translated, which left them with an empty pts-set. - A3: merge() queues Rep when the absorbed diff left it with pending pts. Reachable via A2's new merge site: addAssignEdge only re-marks Rep's pts when it is non-empty, so an empty Rep absorbing a non-empty NonRep stranded the whole diff. - A4: widen handleCall's CSRetVal gate to match handleReturn, add ExtractValue/InsertValue (assign edges from the aggregate operand, as LLVMPointerAssignmentGraph already does) and AtomicRMW/AtomicCmpXchg (store + load). Clang puns pointer atomics through a pointer-sized integer, so isPunnedPointerAccess bypasses the integer-type gate when the accessed pointer resolves to pointer-holding memory. - B4: keep a colliding external id as a second name for the node, so a caller-supplied ValueCompressor no longer loses one side's alias set. - B5: sortUnique(Buf) before insertSorted, whose precondition context clones routinely violated. B1 stays as documented behavior: k is still 1, and the test now pins the merged result so raising k has to break it deliberately. Not measured yet: A2 is where ptaben recall should improve and A4's field-insensitive aggregate merging is where precision may drop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
compileGlobs silently dropped malformed allow-/deny-list patterns, so a typo disabled selection for exactly the functions the user asked for -- and in Mode::Manual disabled the feature entirely. It now names the list and prints the pattern plus the reason to stderr, keeping the rest. PHASAR_LOG is unsuitable here: logging is off unless the client enables it and is compiled out without DYNAMIC_LOG, so the report would stay invisible in a default build. Also fix the __has_cpp_attribute(lifetimebound) spelling in Macros.h and mark the AndersenOTFSolver constructor's reference-like parameters PSR_LIFETIMEBOUND, which catches a temporary Entries container or IRDB at the call site under clang. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured over six programs from a real-world IR corpus, comparing alias-entry counts and wall time against Mode::Off and Mode::All. MaxContextsPerFunction 8 -> 32. Eight was the binding precision constraint: below the cliff the analysis pays for eight clones and still merges the remaining callers into the root clone, i.e. the worst of both. One program improves by 34% once the cap clears its cliff. MaxContextualNodes 200'000 -> 20'000. The old value could never bind (observed peak 89k). Fixing the throttle (below) turns it into the real cost governor; solve time is super-linear in the node count. MaxLocalMergeFunctionSize 32 -> 0 (off). Provably inert: 0, 32 and 256 give identical alias counts everywhere. That tier's only payoff is formal-vs-formal aliasing inside the body, which buildResult unions back together across contexts. Worth revisiting if that is fixed. The budget could not actually throttle anything: budgetExhausted() was only consulted in computeIsSelected, which runs once per function and memoizes before any clone exists, so functions selected in the early rounds kept minting contexts indefinitely. calleeContext now re-checks it. Allow-listed functions stay exempt there -- selected but capped at one context is selected in name only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clang lowers pointer traffic through pointer-sized integers -- notably the atomic builtins -- so gating loads and stores on the value's LLVM type alone drops the whole chain and leaves the loaded pointer unaliased. Ports isPunnedPointerAccess from AndersenOTFAA, which admits an integer access iff the memory it reads from or writes to is a pointer-holding alloca or global. Measured over six programs with computeCtxIndSensUnionFindAARaw; all six recover aliases that were previously missing (alias entries, lower = more precise, so a rise here is recovered unsoundness): bison 99913238 -> 99933230 lepton 214651764 -> 215856283 libpcap 17198183 -> 17206453 lrzip 8801726 -> 8801728 opencv-core 6955391325 -> 6955724937 readelf 157857005 -> 158007805 Runtime unchanged within noise. Unlike Andersen, no escape hatch is needed in the operand traversal: the Punned flag on forEachOpId only suppresses an early-out that forEachOpId adds on top of forEachPointerOperand, and handleOperand has no such early-out. Two limitations remain, shared with Andersen: forEachPointerOperand still filters integer operands inside a ConstantExpr walk, and a pointer laundered through an intermediate integer-typed local breaks the chain at that local, whose alloca is not pointer-holding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither instruction had a case in dispatch, so an atomic pointer exchange
was silently ignored: nothing was stored into the slot and the exchanged-out
value aliased nothing at all.
Field-insensitively an atomicrmw is a store of the new value plus a load of
the old one; cmpxchg likewise, into its { ty, i1 } result. Mirrors
AndersenOTFAA's handleAtomicAccess. Routing the pair through addEdge lets
the existing store/load delaying treat the slot like any other, so the
delayed edges connect each incoming store to each outgoing load.
Only reaches pointer-typed atomics now that the punning gate is in place;
clang lowers the pointer builtins through a pointer-sized integer.
No change on the six-program corpus: every atomicrmw/cmpxchg there is an
integer refcount update, which the gate still correctly ignores. This is a
completeness fix, not a precision win.
The test expectation differs from AndersenOTFAATest's on one point:
Andersen keeps the two stored objects in separate alias sets, a union-find
cannot, since both reach the same loads.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ValueCompressor::addAlias no-ops when the value already owns an id, and four PAG-builder sites ignored that. Since handlePhi and handleCall intern operands eagerly, this fires 13-1856 times per program on real code. Most are benign -- a substitute edge, usually the Assign handlePhi already added, joins the nodes anyway. The exception is handleLoad's single-reaching-def MemSSA path, which returns without emitting any edge, leaving the load aliasing nothing. addAliasOrEquate falls back to a pair of Assign edges, which every strategy treats as an equivalence. Both directions are needed: CallingContextSensUnionFindAA drives the join from the source's contexts. Measured: bison +28 alias entries, five of six programs unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
buildPAG only called onAddValue for freshly inserted values, so a pre-populated ValueCompressor shifted every index in the strategies' per-value tables. Report the already-present ids up front instead, and assert the ascending-id invariant at the three emplace_back sites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LLVMPAGBuilder and AndersenOTFSolver each had their own copy of isPunnedPointerAccess and of the load/store/atomicrmw/cmpxchg taxonomy. Move both into LLVMPointerSemantics.h so the two cannot disagree. That exposed one divergence: Andersen wrapped the multi-def branch of the MemSSA reaching-defs block in an else, so a single reaching def storing a ConstantExpr fell back to a plain Load edge instead of assigning from the expression's leaves. Aligned with the PAG builder; lepton loses 5384 spurious alias entries, the rest of the corpus is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The returned iterator borrows VC, and the strategies borrow CG and IRDB. Annotating only the call sites is inert, because clang stops tracking at MaybeUniquePtr / NonNullPtr, so their pointer-taking constructors are annotated too. computeUnionFindAA*Raw is left alone: its result owns its data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Restore the example tool, which had been replaced by a timing harness. Drop the review's item ids from test names, test inputs and the design doc, and reword the comments that still described the defects as open -- all of them are fixed and the tests pass. Remaining findings are deferred. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fabianbs96
marked this pull request as ready for review
August 16, 2026 11:16
…en results as well + integrate into HelperAnalyses
…nction() more robust
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds selective call-string context-sensitivity to the existing
AndersenOTFSolver+ fixes some bugs/incompleteness in both alias solvers (AndersenOTFAA and union-find-aa).Context-sensitivity is off by default; with
Mode::Offnothing is allocated and the solver behaves exactly as before.Key Features
Call-string limit fixed at k=1 with 4 selection modes:
Off(default)ManualAllowListDynamicAllowListplus functions observed as precision-criticalAllAllowList/DenyListare function-name globs to bypass the selection heuristics.Two budgets bound the cost:
MaxContextualNodes(default: 20k) caps context-qualified nodes globally, andMaxContextsPerFunction(default: 32) caps clones of one function.Mode::Dynamicadditionally skips functions aboveMaxContextualFunctionSize(default: 256).DISCLAIMER: The work in this branch has been largely AI generated, similar to #842, just with a stronger model.