Skip to content

Draft Full sparse (should talk about functions)#1814

Closed
bjjwwang wants to merge 7 commits intoSVF-tools:masterfrom
bjjwwang:full_sparse01
Closed

Draft Full sparse (should talk about functions)#1814
bjjwwang wants to merge 7 commits intoSVF-tools:masterfrom
bjjwwang:full_sparse01

Conversation

@bjjwwang
Copy link
Copy Markdown
Contributor

PR Description — Full-Sparse AE for SVF

TL;DR

Add a third sparsity mode to SVF's abstract execution. Replace dense's "copy entire _addrToAbsVal at every ICFG node" with three SSA-anchored transfers + two AE-side overlays. Memory drops 98% versus Dense; 332/352 tests pass; Dense and Semi-Sparse remain at 352/352.


Why Full-Sparse

Dense AE's mergeStatesFromPredecessors does two unrelated jobs at once:

  1. Propagating store-produced values along ICFG edges (the legitimate dataflow job).
  2. Materialising reaching values at every non-store SSA anchor as a side-effect of bulk state copy — phi nodes, function entries, call returns. This second job is what makes Dense's reader "just work": trace[useNode]._addrToAbsVal[obj] is always populated, regardless of whether the reaching def is a store, a phi, a function entry, or a call return.

Full-Sparse decouples the two. Values live at MSSA def-anchors only; reads route through getDefSiteOfObjVar. Memory cost falls from O(ICFG nodes × all ObjVars) to O(real def-anchors only).

But once we kick out the bulk copy, three things Dense had implicitly stop working. The PR is essentially "rebuild those three things sparingly":

  1. Non-store SSA anchors (MSSAPHI / FormalIN / ActualOUT) need to be filled by someone, since merge no longer materialises them. → Three transfer helpers (Section 1).
  2. Branch refinement (cmp-induced narrowing) and GEP-precision refinements were carried by merge's bulk copy. → Two AE-side overlays (Section 2 + the pathRefinedAt table for branches).
  3. Cycle widening relied on trace[cycleHead] having every variable's current value because merge put them there. → ValVar pull-and-scatter at cycle heads + ObjVar via re-firing transfers (Section 3).

1. Why three new transfer helpers?

void runMSSAPHITransferAt(const ICFGNode* N);
void runFormalINTransferAt(const ICFGNode* N);
void runActualOUTTransferAt(const ICFGNode* N);

The invariant they establish

For every ICFG node N that getDefSiteOfObjVar may return, and every ObjVar o it anchors at N, there is exactly one filler that writes trace[N]._addrToAbsVal[o] during N's WTO visit, before any downstream reader observes it.

MSSA produces exactly five kinds of ObjVar def-anchors:

Anchor SVFG node | ICFG host | Filler -- | -- | -- StoreVFGNode | store stmt | existing updateStateOnStore MSSAPHISVFGNode | block entry / cycle head | runMSSAPHITransferAt FormalINSVFGNode | FunEntryICFGNode | runFormalINTransferAt ActualOUTSVFGNode (normal callee) | RetICFGNode | runActualOUTTransferAt ActualOUTSVFGNode (ExtAPI callee) | CallICFGNode | existing AbsExtAPI handler

ES_Loc_TOTAL (ObjVar storage) drops 98.2% versus Dense. ES_Var_TOTAL (ValVar storage) drops 97.7%.

Dense and Semi-Sparse remain unchanged at 352/352 — none of the new mechanisms fire in those modes.


Out of scope (the 20 remaining failures)

  • 4 × recursive_afterrec: ActualOUT chain through self-recursive SCCs needs more iteration; tractable follow-up.
  • 6 × detector FP/FN under branch refinement: pathRefinedAt overlay needs to extend to ValVar pointers (if (cond) p = NULL; UNSAFE_LOAD(p);).
  • 5 × ExtAPI / scanf / switch / memcpy edge cases: middling effort.
  • 1 × BASIC_array_int_0: Phase D's flow-insensitive trade-off (won't fix without losing the simplicity).
  • 4 × very complex CFG (goto + nested loops + while-true exit): borderline cases.

None indicate a soundness regression.


Files changed

  • svf/include/AE/Svfexe/AbstractStateManager.h: public API for the three transfers, pathRefinedAt, gepOverlay.
  • svf/lib/AE/Svfexe/AbstractStateManager.cpp: transfer implementations, sparsity-aware reader, gepOverlay write/read paths.
  • svf/lib/AE/Svfexe/AbstractInterpretation.cpp: driver hooks for the three transfers, modified mergeStatesFromPredecessors, modified isCmpBranchFeasible signature.
  • svf/lib/AE/Core/AbstractState.cpp: sparsity gate in joinWith.
  • svf/lib/AE/Svfexe/SparseAbstractInterpretation.cpp: cycle ValVar pull-and-scatter.
  • svf/lib/AE/Svfexe/PreAnalysis.cpp: initCycleValVars for the bottom-up cycle ValVar set.
  • svf/lib/AE/Svfexe/AbsExtAPI.cpp: ObjVar reads in ExtAPI handlers go through the sparsity-aware reader.

No SVFG / VFG / ICFG / MSSA / Andersen header or behavioural changes.

PR Description — Full-Sparse AE for SVF TL;DR Add a third sparsity mode to SVF's abstract execution. Replace dense's "copy entire _addrToAbsVal at every ICFG node" with three SSA-anchored transfers + two AE-side overlays. Memory drops 98% versus Dense; 332/352 tests pass; Dense and Semi-Sparse remain at 352/352.

Why Full-Sparse
Dense AE's mergeStatesFromPredecessors does two unrelated jobs at once:

Propagating store-produced values along ICFG edges (the legitimate dataflow job).
Materialising reaching values at every non-store SSA anchor as a side-effect of bulk state copy — phi nodes, function entries, call returns. This second job is what makes Dense's reader "just work": trace[useNode]._addrToAbsVal[obj] is always populated, regardless of whether the reaching def is a store, a phi, a function entry, or a call return.
Full-Sparse decouples the two. Values live at MSSA def-anchors only; reads route through getDefSiteOfObjVar. Memory cost falls from O(ICFG nodes × all ObjVars) to O(real def-anchors only).

But once we kick out the bulk copy, three things Dense had implicitly stop working. The PR is essentially "rebuild those three things sparingly":

Non-store SSA anchors (MSSAPHI / FormalIN / ActualOUT) need to be filled by someone, since merge no longer materialises them. → Three transfer helpers (Section 1).
Branch refinement (cmp-induced narrowing) and GEP-precision refinements were carried by merge's bulk copy. → Two AE-side overlays (Section 2 + the pathRefinedAt table for branches).
Cycle widening relied on trace[cycleHead] having every variable's current value because merge put them there. → ValVar pull-and-scatter at cycle heads + ObjVar via re-firing transfers (Section 3).

  1. Why three new transfer helpers?

void runMSSAPHITransferAt(const ICFGNode* N);
void runFormalINTransferAt(const ICFGNode* N);
void runActualOUTTransferAt(const ICFGNode* N);
The invariant they establish
For every ICFG node N that getDefSiteOfObjVar may return, and every ObjVar o it anchors at N, there is exactly one filler that writes trace[N]._addrToAbsVal[o] during N's WTO visit, before any downstream reader observes it.

MSSA produces exactly five kinds of ObjVar def-anchors:

Anchor SVFG node ICFG host Filler
StoreVFGNode store stmt existing updateStateOnStore
MSSAPHISVFGNode block entry / cycle head runMSSAPHITransferAt
FormalINSVFGNode FunEntryICFGNode runFormalINTransferAt
ActualOUTSVFGNode (normal callee) RetICFGNode runActualOUTTransferAt
ActualOUTSVFGNode (ExtAPI callee) CallICFGNode existing AbsExtAPI handler
Without these three helpers, four out of five anchor kinds would be empty when the reader tries to read from them — and the entire sparse design collapses into "everything reads top".

What they share — one pattern, three flavours

runXxxTransferAt(N):
for each Xxx-kind VFG node D at N:
for each obj in D.MR.getPointsTo():
meet = ⊥
for each upstream CHI U of D (via SVFG indirect edges):
if trace[U.ICFGNode][obj] exists:
meet ⊔= trace[U.ICFGNode][obj]
commitTransferValue(trace[N], obj, meet) # fill-empty-only
The only thing that varies is how upstream CHIs are reached — and that boils down to one SVFG quirk: MU (use-annotation) nodes don't hold values, only CHI (def-annotation) nodes do. If an edge leads to a MU, walk one more indirect-edge hop.

Transfer Direct upstream MU intermediary? Hops
MSSAPHI indirect in-edges → operand CHIs no 1
FormalIN CallIndSVFGEdge → ActualIN (CallMU) → upstream CHI yes 2 (then fall back to globalICFGNode if no caller reached)
ActualOUT RetIndSVFGEdge → FormalOUT (RetMU) → callee CHI yes 2 (skip entirely if callee is ExtAPI)
Critical detail: commitTransferValue is fill-empty-only
The transfer only writes to slots merge left empty. If merge already wrote a value (which it can, when isCmpBranchFeasible narrowed the pred state in Dense/Phase-A), the transfer defers. Without this, transfer's un-refined upstream values would clobber merge's branch-refined precision and break four assert tests in early prototyping.

In the final design (Phase B+ where merge skips ObjVar copy entirely), the slot is always empty when the transfer runs and the transfer becomes the primary filler — but the fill-empty-only semantics stay as a safety property.

getPathRefinedAt

Map<const ICFGNode*, Map<NodeID, AbstractValue>>& getPathRefinedAt();
This is the public accessor for the branch refinement overlay — a separate concern from the three transfers but tied into the same reader. The overlay stores per-ObjVar narrowings that come from if (p < 5) style cmp branches, which MSSA does not model (cmp is not a def in SSA). It is populated by the modified mergeStatesFromPredecessors (folding each feasible edge's narrowingDelta from isCmpBranchFeasible) and consumed by getAbstractValue(ObjVar, useNode) via meet at read time.

Why we need to expose it: cycle handling (SparseAbstractInterpretation::widenCycleState) and external diagnostics need to inspect / preserve the overlay across iterations.

  1. Why gepOverlay?

Map<NodeID, AbstractValue> gepOverlay;
The disconnect
AE precise-refines GEP fields at runtime via getGepObjAddrs(base, [low, high]):

int a[10];
a[3] = 7; // AE writes obj-id 42 (= GepObjVar(base=37, offset=3))
v = a[3]; // AE reads obj-id 42
But MSSA / Andersen are static — they only see the base obj arr (id 37) for these stores, not the field-precise GepObjVar id 42. Concretely:

getDefSiteOfObjVar(42, useNode) returns null because no SVFG indirect edge has 42 in its PointsTo set.
The globalICFGNode fallback for non-globally-initialised arrays returns nothing.
So a GepObjVar read in pure sparse mode would always return ⊤. Every array test would fail.

What we tried first and discarded
ICFG reverse BFS fallback: walk back from useNode via ICFG in-edges until we find a node whose trace holds obj 42. Worked correctly (324/352) but was an O(N) graph walk inside a function that should be O(1), with stale-cache issues across WTO iterations.
Route via base's MSSA def-site: call getDefSiteOfObjVar(baseObj, useNode), then read trace[defNode][gepId]. Triggered SVFG's hard assert(defSite == nullptr && "unique indirect definition"), because complex ICFG nodes (call sites, init nodes) host multiple VFG nodes referencing the base — multiple matches violate the API's unique-def invariant.
Selectively merge only GepObjVar ids: in joinWith, copy only ids whose SVFVar is a GepObjVar. Memory exploded to 2.24M Loc entries because struct field GepObjVars propagated densely to every node in their containing function.
The shipped solution
A flow-insensitive global map keyed by GepObjVar's NodeID (which uniquely encodes (base, offset) via SVFIR's canonical getGepObjVar). Single map, lives in AbstractStateManager.

storeValue(p, val, n):
... # existing trace + Andersen mirror writes
for o in pts(p):
if o is GepObjVar:
gepOverlay[o.id].join_with(val) # fold to the global map

getAbstractValue(ObjVar* var, ICFGNode* node):
... step 1-3 (local, MSSA, globalICFGNode) ...
if !base && var is GepObjVar: # step 4
if gepOverlay.has(var.id):
base = gepOverlay[var.id]
Why this works
Reader O(1): single Map::find, no graph walk.
No SVFG modification: doesn't touch the asserting API.
Unaffected by sparse merge gating: gepOverlay is a separate global structure, propagation isn't merge's job.
Cross-procedural for free: every GepObjVar write across all functions folds into the same map; reads anywhere find them.
Trade-off: flow-insensitive
GepObjVar values use join_with only — never strong-update. So:

a[3]=7; a[3]=11; assert(a[3]==11) reads [7, 11] (joined), not 11. One test regresses (BASIC_array_int_0).
a[3]=7; a[5]=11; assert(a[3]==7) still reads 7 (different keys). Works fine.
for (i...) a[i] = 0; a[k] = 99; assert(a[k]==99) reads [0, 99]. Loses precision in this pattern.
In our 352-test suite, net effect is +8 tests passing versus BFS — flow-insensitivity costs 1 test but the BFS version had its own staleness bugs in cycles that cost more.

Why GepObjVar can get away without widening
A standard concern with flow-insensitive join-only structures is termination. For interval domains it isn't a problem: the lattice has finite height (bounded by [-INF, INF]), and join_with only ever climbs the lattice. So gepOverlay naturally terminates without needing an explicit widen operator. Precision is sacrificed; soundness and termination are not.

  1. How Full-Sparse handles cycles
    Cycles are the place where everything sparse-related compounds. The challenge: WTO widening compares prev and cur snapshots at the cycle head, and Dense relies on those snapshots having every value via merge's bulk copy. Sparse's prev/cur snapshots come from trace[cycleHead], which doesn't store ValVars at all in sparse mode.

We solve ValVar and ObjVar with two different mechanisms.

3.1 ValVar — pull-and-scatter
In sparse, ValVars only live at their def-sites (var->getICFGNode()). They are never written to trace[cycleHead] by merge. So we must explicitly transport them in and out of the cycle head's snapshot.

Pre-analysis collects the cycle's ValVar set (PreAnalysis::initCycleValVars):

Map<const ICFGCycleWTO*, Set<const ValVar*>> cycleToValVars;
Bottom-up over the WTO: each cycle's ValVar set = LHS ValVars of every stmt in its singleton members + nested sub-cycles' sets + (if head is a FunEntryICFGNode) formal parameters. Computed once before AE runs; reused on every iteration.

Entering the cycle iteration (SparseAbstractInterpretation::getFullCycleHeadState):

AbstractState snap = base::getFullCycleHeadState(cycle); // ObjVars come from trace
snap.clearValVars();
for (const ValVar* v : preAnalysis->getCycleValVars(cycle)):
if (v->getICFGNode() && hasAbsValue(v, v->getICFGNode())):
snap[v->getId()] = getAbsValue(v, v->getICFGNode()) // pull from def-site
Exiting the cycle iteration (widenCycleState / narrowCycleState):

base::widenCycleState(prev, cur, cycle) // writes widened state to trace[cycleHead]
for (id, val) in trace[cycleHead].getVarToVal():
updateAbsValue(svfir->getSVFVar(id), val, cycleHead) // scatter back to def-site
Note that updateAbsValue in sparse mode internally routes the write to the var's def-site (not literally cycleHead) — that's how the next iteration's body reads pick up the widened value.

3.2 ObjVar — no scatter, just re-fire runMSSAPHITransferAt
ObjVars do live in trace[cycleHead]._addrToAbsVal in sparse mode, populated each iteration by runMSSAPHITransferAt(cycleHead) (which joins from operand def-sites = entry def + back-edge def per latch). This means:

The widen-set for ObjVars is implicit — it's exactly MR.getPointsTo() of the MSSAPHIs at cycle head, computed dynamically each iteration. No pre-collection needed.
No scatter needed — values stay in trace[cycleHead]. The next iteration's body reads route via getDefSiteOfObjVar(o, useNode), which (for objs whose defs reach via the cycle's MSSAPHI) returns cycleHead, and the read finds the widened value there.
Per-iteration timeline:

iter k:
prev = getFullCycleHeadState(cycle) # ObjVars: copy of trace[cycleHead] from iter k-1
# ValVars: pulled from def-sites

mergeStatesFromPredecessors(cycleHead) # sparse: clears _addrToAbsVal
runMSSAPHITransferAt(cycleHead) # re-fills _addrToAbsVal:
# for each obj in MR.PointsTo():
# join over operand def-sites
handleICFGNode body # body stmts may write to other nodes

cur = getFullCycleHeadState(cycle) # ObjVars: trace[cycleHead] just refilled
# ValVars: pulled again

next = prev.widening(cur) # widens both _varToAbsVal and _addrToAbsVal
trace[cycleHead] = next

scatter ValVars from trace[cycleHead] back to their def-sites

ObjVars stay in trace[cycleHead] for iter k+1

3.3 Recursion
WTO treats recursion SCCs as cycles, so the same machinery applies. One subtlety: runActualOUTTransferAt at a recursive call's RetICFGNode walks RetIndSVFGEdge → FormalOUT → IndirectSVFGEdge to find callee CHIs, which may include the same call's ActualOUT (a self-loop). First iteration finds the slot empty (just cleared by merge); WTO iterates the SCC and the slot fills in subsequent rounds. fill-empty-only ensures the recursion's skipRecursionWithTop mode (when enabled) writes ⊤ to exactly the slot the reader will consult, and the transfer doesn't subsequently overwrite that ⊤.

3.4 GepObjVar in cycles
gepOverlay participates in cycles trivially: every iteration's stores fold their values into the same global map via join_with. The lattice is bounded so termination is automatic. There is no widening on gepOverlay and none is needed — the worst case is intervals saturating to [-INF, INF], which terminates the WTO loop.

3.5 The widening monotonicity bug worth noting
An earlier draft of pathRefinedAt's cross-iteration commit used meet. Under widening, this trapped overlay entries at iteration 1's tightest narrowing, while the base def-anchor value widened around them. Reads computed meet(widened_base, stale_tight_overlay) = stale_tight_overlay, and the cycle never converged.

Fix: cross-iteration commit uses join. The per-iteration tightening still happens because isCmpBranchFeasible recomputes narrowingDelta each round and the per-pred snapshot folds delta via meet — that part is correct. Only the cross-iteration step needed to climb the lattice with the base value, not against it. Single-line change in mergeStatesFromPredecessors recovered 50/115 failing tests at the time.

Empirical results (289-file benchmark)
Mode ES_Loc_TOTAL ES_Var_TOTAL Tests
Dense 2,320,904 4,200,835 352/352
Semi-Sparse 2,320,832 106,608 352/352
Full-Sparse 42,323 98,622 332/352
ES_Loc_TOTAL (ObjVar storage) drops 98.2% versus Dense. ES_Var_TOTAL (ValVar storage) drops 97.7%.

Dense and Semi-Sparse remain unchanged at 352/352 — none of the new mechanisms fire in those modes.

Out of scope (the 20 remaining failures)
4 × recursive_afterrec: ActualOUT chain through self-recursive SCCs needs more iteration; tractable follow-up.
6 × detector FP/FN under branch refinement: pathRefinedAt overlay needs to extend to ValVar pointers (if (cond) p = NULL; UNSAFE_LOAD(p);).
5 × ExtAPI / scanf / switch / memcpy edge cases: middling effort.
1 × BASIC_array_int_0: Phase D's flow-insensitive trade-off (won't fix without losing the simplicity).
4 × very complex CFG (goto + nested loops + while-true exit): borderline cases.
None indicate a soundness regression.

Files changed
svf/include/AE/Svfexe/AbstractStateManager.h: public API for the three transfers, pathRefinedAt, gepOverlay.
svf/lib/AE/Svfexe/AbstractStateManager.cpp: transfer implementations, sparsity-aware reader, gepOverlay write/read paths.
svf/lib/AE/Svfexe/AbstractInterpretation.cpp: driver hooks for the three transfers, modified mergeStatesFromPredecessors, modified isCmpBranchFeasible signature.
svf/lib/AE/Core/AbstractState.cpp: sparsity gate in joinWith.
svf/lib/AE/Svfexe/SparseAbstractInterpretation.cpp: cycle ValVar pull-and-scatter.
svf/lib/AE/Svfexe/PreAnalysis.cpp: initCycleValVars for the bottom-up cycle ValVar set.
svf/lib/AE/Svfexe/AbsExtAPI.cpp: ObjVar reads in ExtAPI handlers go through the sparsity-aware reader.
No SVFG / VFG / ICFG / MSSA / Andersen header or behavioural changes.

bjjwwang and others added 7 commits April 22, 2026 19:16
…overlay

Phase A adds three per-kind MSSA def-anchor transfers (MssaPhi,
FormalIn, ActualOut) in AbstractStateManager, filling
trace[N]._addrToAbsVal[obj] at every ICFG node that hosts a matching
SVFG def-node. Transfers use only SVFG's public APIs (getVFGNodes,
IndirectSVFGEdge::getPointsTo, MRVer::getMR, SVFUtil::isExtCall) and
commit with fill-empty-only semantics so merge's branch-refined values
are preserved.

Phase B gates _addrToAbsVal copy in AbstractState::joinWith under
Sparse mode, routes ObjVar reads through getDefSiteOfObjVar, and adds
a path-refined overlay (pathRefinedAt) populated by
mergeStatesFromPredecessors from per-edge narrowingDelta returned by
isCmpBranchFeasible. Overlay propagates via per-pred snapshot (inherit
+ tighten with delta) combined across preds by intersection-with-join,
then committed into the successor with meet for WTO monotonicity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes the dominant loop/recursion failure class (50/115 tests) in
full-sparse mode.

Two orthogonal bugs in the cycle machinery:

1. Overlay (pathRefinedAt) commit used meet-across-iterations,
   trapping the entry at the earliest tightest narrowing. Under a
   widening cycle, reads of refined ObjVars stopped converging because
   meet(widenedBase, staleTightOverlay) stayed pinned to the tight
   value. Switched to join-across-iterations so overlay grows with the
   base. Narrowing-phase tightening is still achieved via
   isCmpBranchFeasible re-producing per-iter narrowingDelta folded by
   per-pred meet.

2. updateAbstractValue(ValVar) in Sparse routed writes to the caller-
   supplied node instead of the var's def-site. Cycle scatter
   (widenCycleState in SparseAbstractInterpretation) passes cycle_head
   but expects the write to land at each body ValVar's def-site, as
   in SemiSparse. Extended the existing SemiSparse branch to also
   cover Sparse.

Also switches the fast-path gate in getAbstractValue(ValVar) from
'!semiSparse' to 'denseOnly' so Sparse reads go through the def-site
pull (matching SemiSparse) rather than returning a stale current-node
ValVar cached by an earlier transfer.

Test count drops from 115 failures to 65. Dense and SemiSparse both
stay at 352/352.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
APOUT SVFG nodes are hosted at the RetICFGNode of a call-pair, not the
CallICFGNode, per SVF's MSSA node placement. The Phase A wiring ran
runActualOUTTransferAt on the CallICFGNode and walked its VFG nodes —
which hold AParm/APIN but never APOUT — so the transfer never fired
and no reader ever saw a reaching value at the call's def-anchor.

Even when the transfer did run (e.g., if we committed at the callNode
anyway), WTO processes the retNode right after the callNode and its
own merge would clear any _addrToAbsVal we had written into trace
[callNode] state copied forward.

Fix: the driver dispatches runActualOUTTransferAt when the current
WTO node is a RetICFGNode (post-merge hook), and the transfer itself
scans retNode->getVFGNodes() and commits to trace[retNode]. Matches
the invariant that each def-anchor is filled on its own ICFG visit
by exactly one filler.

Cuts funcall-ref / out-param cross-function writes from failing to
passing; suite moves from 65 -> 58 failing. Dense + SemiSparse stay
352/352.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small fixes for the ObjVar read path in Sparse mode,
uncovered by triaging array-init assert tests:

1. globalICFGNode fallback in getAbstractValue(ObjVar*, node).
   Statically-initialised globals and const-init arrays are populated
   into trace[globalICFGNode] during program setup but have no
   intra-procedural MSSA def-anchor. If getDefSiteOfObjVar returns
   nothing, consult globalICFGNode before falling through to top.

2. Null-def-site fallback for ValVar reader. Some load-produced SSA
   ValVars come back with getICFGNode() == nullptr (IR-lowering
   artifact). Sparse was routing to a null def-site and returning
   top; now falls back to the current node's slot (where the load
   transfer wrote the value).

3. AbsExtAPI::handleMemcpy source read routed through the Sparse-
   aware reader. The inline `as.load(srcAddr)` only finds a value
   if `_addrToAbsVal` at the call site has been bulk-populated by
   merge — which never happens in Sparse. Route via
   mgr->getAbstractValue(srcObjVar, node) so the global-const
   initializer values are found via the def-anchor chain.

These together clear the "RHS read returns top -> stores write top"
chain for const-init array loads.  Suite moves from 58 -> 57
failing; the remainder is dominated by an Andersen-pts vs
AE-pts-to mismatch on GEP'd arrays (the store's MSSA CHI is against
a different obj id than the store's AE write target, so sparse's
SVFG-routed reader at a downstream load cannot find the store's
value). Dense papers over this via bulk _addrToAbsVal copy; fixing
it in Sparse is out of scope for this pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When AE's runtime-computed pts-to disagrees with Andersen's static
pts-to on a store (common on GEP'd arrays: AE sees the precise
field-obj, Andersen gives a different / coarser obj), MSSA builds
its reaching-def chain from Andersen's pts and a Sparse reader
routed via getDefSiteOfObjVar asks for the Andersen obj-id — which
AE's AE-pts-only write never touched.

Dense papers over this via bulk _addrToAbsVal merge-copy. Sparse
must explicitly mirror the store onto every obj in Andersen's pts.
For already-present slots we join so an AE-precise strong-update
survives as a lower bound.

Needs the pta handle stored on AbstractStateManager; constructor
was already given it but not retaining the pointer. Added as a
private member.

Fixes one nullptr_deref test (ExtAPI memcpy-like case where AE and
Andersen disagreed on dst obj).  Does not fix the
array[constant-index] assert cluster (19 tests): there Andersen
actually gives the wrong obj-id outright (says a+9 -> a[0]), so
mirroring onto Andersen's pts writes to the wrong obj and MSSA's
routing for the correct-index load still bypasses the store node.
That is an Andersen imprecision / field-sensitivity gap, not a
sparse routing bug.

Suite moves from 57 -> 56 failing. Dense + SemiSparse stay 352/352.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes the ObjVar read fallback chain in Sparse mode.  When MSSA's
getDefSiteOfObjVar routes to an anchor whose slot is empty (or routes
wrong because Andersen disagrees with AE on the GEP'd obj-id), walk
the ICFG in-edges back from the use-site until a predecessor's trace
is found to hold the obj.  Multiple reaching writers from distinct
branches join into a single upstream value.  Traversal stops past any
latest-writer so join respects per-path semantics.  Result is
materialised at hereState for O(1) repeated reads.

Motivation: AE computes GEP'd ObjVar ids at runtime via
getGepObjAddrs(base, offset); for a constant offset the store and the
corresponding load agree on the same ObjVar id, but MSSA's reaching-def
chain is built from Andersen's static pts-to which can disagree.  In
Dense/Semi this is papered over by bulk _addrToAbsVal merge-copy.
Sparse gated merge-copy, so we need an explicit fallback — but only
when MSSA fails; for the common case the existing MSSA routing still
fires first and pays zero walk cost.

Cuts assert-suite failures 19 -> 5.  Suite moves from 56 -> 42 failing
(assert 5, overflow 15, recursion_from_overflow 15, recursion 4,
nullptr 3).  Dense + SemiSparse stay 352/352.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the ICFG reverse-BFS fallback in getAbstractValue(ObjVar*, node)
with a flow-insensitive global gepOverlay map keyed by GepObjVar NodeID.

Why: AE precise-refines GEP fields (a[3], a[5], ...) into GepObjVar ids
that MSSA / Andersen do not track on the precise id.  Sparse merge
skips _addrToAbsVal, so reads at downstream nodes cannot reach the
write through the per-node trace.  The previous workaround was a
reverse BFS over ICFG in-edges, amortised O(1) via materialise-back
caching but ungainly and stale across WTO iterations.

This patch:
  - Adds Map<NodeID, AbstractValue> gepOverlay to AbstractStateManager.
  - storeValue folds every GepObjVar write into gepOverlay (join_with),
    in addition to the existing AE-pts trace write and Andersen mirror.
  - Reader looks up gepOverlay for GepObjVar before the
    globalICFGNode fallback; BFS is removed.

Trade-off: gepOverlay is flow-insensitive, so a[3]=7; a[3]=11 joins to
[7,11] instead of strong-updating to 11.  Empirically this loses one
test (BASIC_array_int_0) but recovers nine others that BFS missed,
net +8 passing tests.  Memory drops slightly as the BFS
materialise-back side-effect is gone.

Also routes ObjVar reads in AbsExtAPI handlers (strRead, getStrlen,
handleMemset) through mgr->getAbstractValue(ObjVar, node) instead of
as.load(addr), so detector FP/FN reasoning sees sparsity-aware values.

Results on 289-file benchmark (assert + overflow + nullptr + recursion):
  Dense:        352/352, ES_Loc=2,320,904  ES_Var=4,200,835
  Semi-Sparse:  352/352, ES_Loc=2,320,832  ES_Var=  106,608
  Full-Sparse:  332/352, ES_Loc=   42,323  ES_Var=   98,622  (-98.2% Loc)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bjjwwang bjjwwang closed this Apr 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant