feat(extract): capture assignment/return indirect_call edges (#1566 slice 2)#1569
feat(extract): capture assignment/return indirect_call edges (#1566 slice 2)#1569sheik-hiiobd wants to merge 1 commit into
Conversation
…y-Labs#1566 slice 2) A function bound to a name (cb = handler) or returned from a factory (def make(): return handler) is a real reference, but indirect_call only covered call arguments and dispatch tables, so `affected` still dropped these callers. Emit indirect_call (context "assignment"/"return", INFERRED) for the value-side identifiers of a Python assignment RHS and a return, at function scope (owner = enclosing function) and module scope (owner = file node). Reuses the shared _emit_indirect_ref guard. Scans the VALUE side only -- the assignment target is a new local binding, not a reference -- so the existing param/local shadow guard still rejects the false edges Graphify-Labs#1565 fixed. Negatives covered: param-shadow, local-shadow, non-callable emit nothing. Full suite green; ruff clean.
safishamsi
left a comment
There was a problem hiding this comment.
Reviewed and tested locally — this is clean, approving.
What I checked
- Rebases cleanly on current
v8(your branch is on6dc1cb0; the only newer commit touches__main__.py, no overlap). - Reuses the shared
_emit_indirect_refguard with no new emit path — so callable-target-only, cross-file deferral, and dedup all carry over for free. Good call keeping it to one path. - The inverted-shadow handling is sound by construction: scanning only the value side means a param/local named on the RHS still hits the existing shadow guard. Confirmed
def via(handler): cb = handlerand the local-rebind case both emit nothing. - Full suite green; your negatives (param-shadow, local-shadow, non-callable) all pass.
Adversarial cases I ran beyond the PR's tests, all correct:
- annotated assignment (
cb: Callable = handler) -> captured - alias chain (
a = handler; b = a; return b) -> one edge at the bind site only (no dataflow, as intended) return f, compute()-> onlyf- self-return (
def rec(): return rec) -> nothing (self-ref guard) - augmented (
cb += handler), walrus, and RHS-call (cb = handler()) -> nothing - multi-target (
a = b = handler) -> resolves
Minor, non-blocking (fine to leave as follow-ups):
- Class-level attribute assignments (
class C: ref = handler) aren't captured — same gap as the slice-1 dispatch tables (the module scan stops at class bodies), so it's consistent, just worth knowing. - Walrus / augmented assignment aren't handled — rare for this shape, agreed it's not worth the surface area.
Nice work, especially the up-front negatives. Merging into v8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Merged into Note: the slice-1 dispatch-table work (your #1567) had already landed on |
Closes slice 2 of #1566 (assignment + return references).
What
indirect_callcovered call arguments (#1565) and dispatch tables (#1566 slice 1). This adds the assignment/return shape:graphify affected handlerpreviously dropped these; now they're in the blast radius.How
indirect_call(context"assignment"/"return",INFERRED) for the value-side identifiers of a Python assignment RHS and a return — a bare name or a bare unpack (a, b = f, g) — at function scope (owner = enclosing function) and module scope (owner = file node)._emit_indirect_refguard (callable-target-only, shadow-skip, cross-file deferral) — no new emit path.cb = [f],cb = (f, g)) stay with the dispatch-table scan, not double-counted (_python_ref_value_identstakes only a bare name or an unbracketedexpression_list).Scope / limits
cb()is not followed) — per the issue's non-goals.Tests
tests/test_indirect_dispatch_assign_return.py— positives (assignment, return, multi-assign, module-level alias,affected) and the negatives: param-shadow, local-shadow, non-callable each emit nothing. Full suite green; ruff clean.