diff --git a/codeanalyzer/dataflow/builder.py b/codeanalyzer/dataflow/builder.py index 8520848..497cd73 100644 --- a/codeanalyzer/dataflow/builder.py +++ b/codeanalyzer/dataflow/builder.py @@ -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( diff --git a/codeanalyzer/dataflow/sdg.py b/codeanalyzer/dataflow/sdg.py index e2e9d5e..276943f 100644 --- a/codeanalyzer/dataflow/sdg.py +++ b/codeanalyzer/dataflow/sdg.py @@ -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 @@ -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 diff --git a/codeanalyzer/dataflow/summaries.py b/codeanalyzer/dataflow/summaries.py index f7d0d2c..8d25d2c 100644 --- a/codeanalyzer/dataflow/summaries.py +++ b/codeanalyzer/dataflow/summaries.py @@ -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