Skip to content

Commit 882ea0b

Browse files
authored
Merge pull request #156 from codellm-devkit/fix/issue-155-serial-tail-redundant-solves
perf(dataflow): solve each function once instead of three times
2 parents 2459cbe + f40827f commit 882ea0b

3 files changed

Lines changed: 51 additions & 8 deletions

File tree

codeanalyzer/dataflow/builder.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,13 @@ def build_program_graphs(
442442
for t in cs.targets:
443443
call_edges.append((sig, t))
444444

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

448453

449454
def emit_l4(

codeanalyzer/dataflow/sdg.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,18 @@ def assemble_sdg(
386386
infos: Dict[str, FunctionInfo],
387387
summaries: Dict[str, FunctionSummary],
388388
k: int,
389+
*,
390+
solutions: Optional[Dict[str, Tuple[Dict[int, object], List[object]]]] = None,
389391
) -> ProgramGraphsIR:
390-
"""Stitch every function's PDG into the whole-program SDG."""
392+
"""Stitch every function's PDG into the whole-program SDG.
393+
394+
*solutions* optionally carries the converged ``(facts, ddg)`` that
395+
:func:`~codeanalyzer.dataflow.summaries.compute_summaries` already
396+
derived, sparing a second identical solve per function (#155). Omit it and
397+
every function is re-solved, which is the historical behaviour and the
398+
right posture whenever *summaries* did not come from an immediately
399+
preceding run over these same *infos*.
400+
"""
391401
ir = ProgramGraphsIR(k_limit=k)
392402

393403
# Pass 1: solve each function against the final summaries and lay out its
@@ -396,7 +406,12 @@ def assemble_sdg(
396406
formal_ids: Dict[str, Dict[str, int]] = {}
397407
for sig in sorted(infos):
398408
info = infos[sig]
399-
summary, facts, ddg = solve_function(info, summaries)
409+
cached = solutions.get(sig) if solutions is not None else None
410+
if cached is None:
411+
summary, facts, ddg = solve_function(info, summaries)
412+
else:
413+
facts, ddg = cached
414+
summary = summaries[sig]
400415
asm = _FunctionAssembler(info, summary, facts, ddg)
401416
asm.build_formals()
402417
assemblers[sig] = asm

codeanalyzer/dataflow/summaries.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,19 +199,42 @@ def reach(start: Set[int]) -> Set[int]:
199199
def compute_summaries(
200200
infos: Dict[str, FunctionInfo],
201201
call_edges: List[Tuple[str, str]],
202+
*,
203+
solutions: Optional[Dict[str, Tuple[Dict[int, object], List[DDGEdge]]]] = None,
202204
) -> Dict[str, FunctionSummary]:
203205
"""Bottom-up composition over the SCC condensation DAG, monotone fixpoint
204-
within each SCC."""
206+
within each SCC.
207+
208+
A **singleton SCC with no self-edge** is solved exactly once: the
209+
condensation is processed bottom-up, so every callee summary it reads is
210+
already final and a second pass could only recompute the same answer to
211+
observe that nothing changed. Genuinely recursive SCCs (several members,
212+
or one member calling itself) still iterate to fixpoint.
213+
214+
When *solutions* is supplied it receives each signature's converged
215+
``(facts, ddg)`` — the by-products of the final solve, which
216+
:func:`~codeanalyzer.dataflow.sdg.assemble_sdg` would otherwise recompute
217+
from scratch. They are the same values that a fresh solve against the
218+
final summaries produces, because a converged pass is by definition one
219+
in which no member's summary changed (#155).
220+
"""
205221
order = strongly_connected_components(sorted(infos), call_edges)
222+
self_calls = {src for src, dst in call_edges if src == dst}
206223
summaries: Dict[str, FunctionSummary] = {}
207224
for scc in order:
208225
members = [s for s in scc if s in infos]
209-
changed = True
210-
while changed:
226+
if not members:
227+
continue
228+
recursive = len(members) > 1 or members[0] in self_calls
229+
while True:
211230
changed = False
212231
for sig in members:
213-
new, _, _ = solve_function(infos[sig], summaries)
232+
new, facts, ddg = solve_function(infos[sig], summaries)
233+
if solutions is not None:
234+
solutions[sig] = (facts, ddg)
214235
if summaries.get(sig) != new:
215236
summaries[sig] = new
216237
changed = True
238+
if not (recursive and changed):
239+
break
217240
return summaries

0 commit comments

Comments
 (0)