Skip to content

Extract direct recursive calls as calls self loops - #3364

Open
ayushcodes10 wants to merge 1 commit into
Graphify-Labs:v8from
ayushcodes10:fix-3350-python-recursive-calls
Open

Extract direct recursive calls as calls self loops#3364
ayushcodes10 wants to merge 1 commit into
Graphify-Labs:v8from
ayushcodes10:fix-3350-python-recursive-calls

Conversation

@ayushcodes10

Copy link
Copy Markdown
Contributor

What

_extract_generic's call resolution required tgt_nid != caller_nid before emitting a calls edge, so a function calling itself by name was silently dropped, no matter which language sharing this engine it was written in.

build_from_json already preserves a supplied recursive calls self loop rather than stripping it (test_recursive_call_self_loop_is_preserved), so the intent to keep genuine recursion visible already existed one layer up. Extraction just never produced the edge for that layer to preserve.

Repro (from the issue, reproduced before this fix)

def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)

def entry(n):
    return factorial(n)

Before: calls: [('repro_entry', 'repro_factorial')] (recursive call missing)
After: calls: [('repro_factorial', 'repro_factorial'), ('repro_entry', 'repro_factorial')]

Verified this also fixes recursion for Java, Ruby, and other _extract_generic-routed languages, since the guard removed was language agnostic.

Scope

Several dedicated per-language extractors (Go, Rust, CommonLisp, DreamMaker, Elixir, PowerShell, Zig) carry the same tgt_nid != caller_nid guard in their own separate files. Left those untouched here since the issue's repro and my verification are both Python/_extract_generic-scoped, and fixing those too would be a materially larger, separate change across files this report didn't exercise. Happy to open a follow-up if that's wanted.

Fixes #3350.

_extract_generic's call resolution required tgt_nid != caller_nid
before emitting a calls edge, so a function calling itself by name
was silently dropped no matter which of the many languages sharing
this engine it was written in (Python, Java, Ruby, C#, PHP, and the
rest of the LanguageConfig set).

build_from_json already preserves a supplied recursive calls self
loop rather than stripping it (test_recursive_call_self_loop_is_preserved
in test_import_self_loops.py), so the design intent to keep genuine
recursion visible already existed one layer up; extraction just never
produced the edge for that layer to preserve. Name resolution binds a
self call the same way as any other call, so there is nothing
special casing a caller equal to its own target should do beyond what
seen_call_pairs already dedupes.

Scoped to the shared engine only: several dedicated per language
extractors (Go, Rust, CommonLisp, DreamMaker, Elixir, PowerShell,
Zig) carry the same tgt_nid != caller_nid guard in their own files
and were left untouched here, since that is a separate, larger change
across files this report did not exercise.

Fixes Graphify-Labs#3350.
@ayushcodes10

Copy link
Copy Markdown
Contributor Author

@safishamsi opened this against a report of direct Python recursion never producing a calls self-loop (#3350), traced to a shared tgt_nid != caller_nid guard in _extract_generic used by many languages, not just Python. Verified the same fix also restores recursion for Java and Ruby. Flagging the scope choice in the PR description: a few dedicated per-language extractors carry the same guard separately and weren't touched here. Happy to address any feedback.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Preserves recursive call self-loops during extraction by dropping the tgt_nid != caller_nid guard in _extract_generic, so a function or method that calls itself now emits a calls edge to itself instead of having it silently discarded (#3350). Adds test_recursive_call_produces_self_loop and test_recursive_method_call_produces_self_loop covering plain and self.<method>() recursion, and documents in test_calls_no_self_loops that its fixture just has no recursive functions.

No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1009 functions depend on the 608 functions this change touches.

Health — this change adds coupling hotspots:

  • new: _extract_generic() — 18 callers, 26 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_objc() — 27 callers, 9 callees
  • new: extract_julia() — 17 callers, 7 callees
  • new: extract_cpp() — 27 callers, 3 callees
  • new: extract_vue() — 10 callers, 7 callees
  • new: walk() — 1 callers, 58 callees
  • …and 8 more — each is listed as a finding

Verification — 1009 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 949 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 16 more finding(s) on lines outside this diff (see the check run).

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.

Python AST extraction omits direct recursive calls while ordinary calls are emitted

1 participant