Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions codeanalyzer/dataflow/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,8 +428,13 @@ def build_program_graphs(
for t in cs.targets:
call_edges.append((sig, t))

summaries = compute_summaries(infos, sorted(set(call_edges)))
return assemble_sdg(infos, summaries, k)
# The converged (facts, ddg) per function are threaded straight into the
# assembler rather than re-derived there (#155).
solutions: Dict[str, object] = {}
summaries = compute_summaries(
infos, sorted(set(call_edges)), solutions=solutions
)
return assemble_sdg(infos, summaries, k, solutions=solutions)


def emit_l4(
Expand Down
19 changes: 17 additions & 2 deletions codeanalyzer/dataflow/sdg.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,18 @@ def assemble_sdg(
infos: Dict[str, FunctionInfo],
summaries: Dict[str, FunctionSummary],
k: int,
*,
solutions: Optional[Dict[str, Tuple[Dict[int, object], List[object]]]] = None,
) -> ProgramGraphsIR:
"""Stitch every function's PDG into the whole-program SDG."""
"""Stitch every function's PDG into the whole-program SDG.

*solutions* optionally carries the converged ``(facts, ddg)`` that
:func:`~codeanalyzer.dataflow.summaries.compute_summaries` already
derived, sparing a second identical solve per function (#155). Omit it and
every function is re-solved, which is the historical behaviour and the
right posture whenever *summaries* did not come from an immediately
preceding run over these same *infos*.
"""
ir = ProgramGraphsIR(k_limit=k)

# Pass 1: solve each function against the final summaries and lay out its
Expand All @@ -396,7 +406,12 @@ def assemble_sdg(
formal_ids: Dict[str, Dict[str, int]] = {}
for sig in sorted(infos):
info = infos[sig]
summary, facts, ddg = solve_function(info, summaries)
cached = solutions.get(sig) if solutions is not None else None
if cached is None:
summary, facts, ddg = solve_function(info, summaries)
else:
facts, ddg = cached
summary = summaries[sig]
asm = _FunctionAssembler(info, summary, facts, ddg)
asm.build_formals()
assemblers[sig] = asm
Expand Down
31 changes: 27 additions & 4 deletions codeanalyzer/dataflow/summaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,19 +199,42 @@ def reach(start: Set[int]) -> Set[int]:
def compute_summaries(
infos: Dict[str, FunctionInfo],
call_edges: List[Tuple[str, str]],
*,
solutions: Optional[Dict[str, Tuple[Dict[int, object], List[DDGEdge]]]] = None,
) -> Dict[str, FunctionSummary]:
"""Bottom-up composition over the SCC condensation DAG, monotone fixpoint
within each SCC."""
within each SCC.

A **singleton SCC with no self-edge** is solved exactly once: the
condensation is processed bottom-up, so every callee summary it reads is
already final and a second pass could only recompute the same answer to
observe that nothing changed. Genuinely recursive SCCs (several members,
or one member calling itself) still iterate to fixpoint.

When *solutions* is supplied it receives each signature's converged
``(facts, ddg)`` — the by-products of the final solve, which
:func:`~codeanalyzer.dataflow.sdg.assemble_sdg` would otherwise recompute
from scratch. They are the same values that a fresh solve against the
final summaries produces, because a converged pass is by definition one
in which no member's summary changed (#155).
"""
order = strongly_connected_components(sorted(infos), call_edges)
self_calls = {src for src, dst in call_edges if src == dst}
summaries: Dict[str, FunctionSummary] = {}
for scc in order:
members = [s for s in scc if s in infos]
changed = True
while changed:
if not members:
continue
recursive = len(members) > 1 or members[0] in self_calls
while True:
changed = False
for sig in members:
new, _, _ = solve_function(infos[sig], summaries)
new, facts, ddg = solve_function(infos[sig], summaries)
if solutions is not None:
solutions[sig] = (facts, ddg)
if summaries.get(sig) != new:
summaries[sig] = new
changed = True
if not (recursive and changed):
break
return summaries