Skip to content

feat(extract): capture assignment/return indirect_call edges (#1566 slice 2)#1569

Closed
sheik-hiiobd wants to merge 1 commit into
Graphify-Labs:v8from
sheik-hiiobd:indirect-call-assign-return
Closed

feat(extract): capture assignment/return indirect_call edges (#1566 slice 2)#1569
sheik-hiiobd wants to merge 1 commit into
Graphify-Labs:v8from
sheik-hiiobd:indirect-call-assign-return

Conversation

@sheik-hiiobd

Copy link
Copy Markdown
Contributor

Closes slice 2 of #1566 (assignment + return references).

What

indirect_call covered call arguments (#1565) and dispatch tables (#1566 slice 1). This adds the assignment/return shape:

def bind():
    cb = handler       # assignment reference
    return cb

def make():
    return other       # return reference

CALLBACK = handler     # module-level alias / re-export

graphify affected handler previously dropped these; now they're in the blast radius.

How

  • Emit 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).
  • Reuses the shared _emit_indirect_ref guard (callable-target-only, shadow-skip, cross-file deferral) — no new emit path.
  • The inverted-shadow trap is handled by construction: the scan only looks at the value side, never the assignment target, so a param/local named on the RHS still hits the existing shadow guard and emits nothing — the false edges feat(extract): capture indirect dispatch as indirect_call edges #1565 fixed don't come back.
  • RHS collection literals (cb = [f], cb = (f, g)) stay with the dispatch-table scan, not double-counted (_python_ref_value_idents takes only a bare name or an unbracketed expression_list).

Scope / limits

  • Python (where slice 2 was specced); JS/TS assignment/return is a follow-up like the others.
  • No dataflow: captures the reference at the bind/return site, not later uses of the alias (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.

…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 safishamsi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed and tested locally — this is clean, approving.

What I checked

  • Rebases cleanly on current v8 (your branch is on 6dc1cb0; the only newer commit touches __main__.py, no overlap).
  • Reuses the shared _emit_indirect_ref guard 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 = handler and 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() -> only f
  • 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.

safishamsi added a commit that referenced this pull request Jun 30, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Collaborator

Merged into v8 as 311e63a (cherry-picked with your authorship). Verified clean on current v8 — full suite 2737 passed, plus the adversarial cases from the review (annotated assignment, alias chains, self-return, augmented/walrus/RHS-call, multi-target) all behave correctly. On the next release changelog crediting you. Thanks — clean slice, and good instinct putting the shadow negatives up front.

Note: the slice-1 dispatch-table work (your #1567) had already landed on v8 (8288829) before this, so I'll close #1567 as already-shipped — same credit applies. Slices 3 (getattr-by-string) and 4 (decorator-registry) on #1566 are still open if you want them, though they're the lower-value ones.

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.

2 participants