Skip to content

fix(cpp): bind x.m() by the receiver's type (builds on #3240) - #4946

Closed
nothariharan wants to merge 4 commits into
Graphify-Labs:v8from
nothariharan:fix/cpp-receiver-dispatch
Closed

nothariharan wants to merge 4 commits into
Graphify-Labs:v8from
nothariharan:fix/cpp-receiver-dispatch

Conversation

@nothariharan

Copy link
Copy Markdown
Contributor

Builds on #3240 by @abhay-codes07, cherry-picked with authorship kept (it applies cleanly to v8), and fixes the case #3240 doesn't fix on its own.

Problem

In C++, void render(Base* b) { b->draw(); } in a file that defines Derived::draw produced an EXTRACTED edge to Derived::draw, which is the method the static type rules out, and no edge to Base::draw. A member call with a variable receiver fell through to the file-wide bare-name map. This came up in a public C++ firmware benchmark that reported calls into overridden functions weren't linked.

Change

  1. fix(cpp): type member-call receivers from parameters and class fields (#3215) #3240: type receivers from parameters and class fields.
  2. Defer x.m() / x->m() to _resolve_cpp_member_calls, as JS/TS have done since [Bug]: JS/TS: member call binds to a same-named function in the file, so delegating wrappers become false self-loops (#4082 still reproduces on 0.9.93) #4598, but only when the receiver is typed. Untyped receivers (auto, smart pointers, range-for) keep their v8 in-file binding, so they don't lose edges. this->m() and Foo::m() are unchanged.
  3. Each function's receivers are now typed from its own locals and parameters. The file table was first-write-wins, so Response r in one function retyped Request& r in another. A name typed differently in two places, or a field typed differently in two classes, is dropped as ambiguous instead of guessed.

b->draw() now reaches Base::draw (INFERRED).

Evidence

  • tests/test_cpp_member_call_by_receiver_type.py: 5 tests. The base-pointer, cross-function retype and auto cases each fail without the commit that fixes them.
  • 495 C/C++ tests pass. Full suite: 9002 passed, with the same 14 failures plain v8 has on this machine. Ruff is clean, and the pyright count is unchanged from v8.

Not included: an edge from Derived::draw to the Base::draw it overrides. The schema has no such relation today, so adding one is a design decision.

abhay-codes07 and others added 4 commits November 2, 2026 08:38
…Graphify-Labs#3215)

_resolve_cpp_member_calls types a receiver from cpp_type_table, which was
populated ONLY from local variable declarations - parameters never entered
it, so a call through `const Thing& T` / `Thing* P` / `Thing V` (the
dominant C++ idiom for passing state) was silently skipped while the
identical call through a local resolved. TS/JS have an explicit
augmentation pass for exactly this; C++ had none.

Two halves:

  * the per-file table now folds in the enclosing function's parameters
    (_cpp_parameter_types) and class/struct member declarations
    (_cpp_field_types), in C++'s own shadowing order - a local beats a
    parameter beats a field, enforced by the table's first-write-wins;
    the same precision rules as locals apply (class-like type nodes only,
    qualified names keyed by their simple tail, multi-declarator and
    builtin-typed entries contribute nothing);
  * the resolver's capitalized-receiver arm treated `T.IsOk()` as an
    explicit type name and bailed when no type called T exists - but a
    capitalized receiver is very often a VARIABLE (parameter T, field
    Inner). When the explicit-type lookup finds no unique type, it now
    falls back to the file's var->type table exactly like the lowercase
    arm, mirroring what the C# resolver already does for Pascal-cased
    locals; a table miss still bails rather than guessing.

_cpp_declarator_name learns field_identifier (fields use it where locals
use identifier). Chained receivers (B.Inner.IsOk()) stay deferred.
… a decisive edge

Review follow-up: the old test declared a differently-named local (T2, not T),
so it never exercised shadowing, and its assertion passed on an empty result.
Now a parameter Thing T is shadowed by an inner-block local Other T, and the
call binds to Other::Ready — which resolves only if the local wins the
first-write-wins fold (locals before parameters); a params-first regression
leaves Thing (no Ready) and yields no edge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
…med method

A C++ member call with a variable receiver fell through to the file-wide
bare-name map, so `void render(Base* b) { b->draw(); }` in a file that also
defines `Derived::draw` produced an EXTRACTED edge to Derived::draw, the one
the static type rules out, and no edge to Base::draw.

Defer `x.m()` / `x->m()` to _resolve_cpp_member_calls like JS/TS already
do (Graphify-Labs#4598). With the parameter and field typing from the previous commit,
`b` is typed as Base and the call reaches Base::draw (INFERRED). An untyped
receiver now gets no edge instead of a name guess. `this->m()` and
`Foo::m()` keep their existing in-file binding.
…d in-file

Review follow-ups to the previous commit.

The receiver table is one dict for the whole file, filled first-write-wins,
so `void g() { Response r; }` retyped `r` in `void f(Request& r)` and f's
`r.send()` went looking on Response. Build each function's table on its
own (a local still shadows a parameter there), then drop any name the file
types two different ways, including a field declared with different types
in two classes. An ambiguous receiver gets no type rather than a guess.

Defer a member call only when its receiver is typed. `auto w = make();
w.paint()`, smart pointers and range-for variables have no table entry, and
deferring them dropped the in-file edge they had on v8.
@github-actions github-actions Bot added the contributor Pull request from a returning community contributor label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown

Thanks for the pull request, @nothariharan. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@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.

View in Graphify

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.

Not checked: tests were not run; formal verification proved 0 of 3 changed function(s) (3 not verified).

Formal verification. PR-changed functions: 0/3 verified (0 proven, 0 may-equivalent, 0 distinguished) · 3 not verified (2 vacuous, 1 unsupported).

Not verified on this run: \_resolve\_cpp\_member\_calls (vacuous: never exercised), \_cpp\_declarator\_name (vacuous: never exercised), \_extract\_generic (unsupported).


Graphify review — findings

Adds C++ receiver typing from function parameters and class fields, so x.m() / x->m() through a const Thing& parameter or a member field resolves by the receiver's type instead of being skipped. Capitalized qualified receivers such as T::bar() now fall back to the file's variable-to-type table when no unique type of that name exists, and the fallback binding is only as reliable as that file-wide, name-keyed table (a same-named local in another function can retype a parameter). Defers C++ member calls on typed non-this receivers to _resolve_cpp_member_calls, so b->draw() through Base* b stops binding to the file's own Derived::draw, though that defer depends on type_table being populated before the call walk reaches it.

Worth a look

  • File-wide name-keyed C++ type table lets a local in one function retype a same-named parameter in another — graphify/extract.py · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
  • C++ member-call defer may be dead code because type_table is filled after the call walk — graphify/extractors/engine.py:10108 · Escalate · medium
    • NOT verified (no proof, no reproducing execution) — needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 3971 functions depend on the 757 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: extract() — 1101 callers, 64 callees
  • worse: _extract_generic() — 20 callers, 52 callees

Verification — 3971 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: 3709 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

238 of 491 test file(s) selected (48%) via static blast radius.

  • tests/test_4874_jvm_resolution.py — impact
  • tests/test_affected_overrides.py — impact
  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_blade_extractor.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_c_include_resolution.py — impact
  • tests/test_cache.py — impact
  • tests/test_case_only_decl_ids_4904.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cobol_extractor.py — impact
  • tests/test_confidence_score_output.py — impact
  • tests/test_cpp_member_call_by_receiver_type.py — impact, changed-test
  • tests/test_cpp_method_declarations.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cpp_receiver_parameters.py — impact, changed-test
  • tests/test_cpp_template_type_params.py — impact
  • tests/test_cpp_type_alias.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_external_call_guards.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_delegate_declarations.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_foreach_loop_variable.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_chain_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_method_owner_labels.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_overloads.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_tuple_type_refs.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_csharp_underscore_collisions.py — impact
  • tests/test_csharp_var_call_return_type.py — impact
  • tests/test_csharp_verbatim_interpolation.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_edge_also_seen.py — impact
  • tests/test_ejs_extractor.py — impact
  • … and 188 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Docs that may be stale (advisory)

…and 10 more.

Formal verification

Could not verify: Could not verify \_resolve\_cpp\_member\_calls.

The verifier did not have enough to check \_resolve\_cpp\_member\_calls, 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: no capturable inputs from the test suite; property tier: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

Could not verify: Could not verify \_cpp\_declarator\_name.

The verifier did not have enough to check \_cpp\_declarator\_name, 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: no capturable inputs from the test suite; property tier: not verifiable: all 228 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

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: no capturable inputs from the test suite; property tier: parameter `config` is annotated `LanguageConfig` — outside the synthesizable primitive/collection set

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

safishamsi added a commit that referenced this pull request Nov 2, 2026
#4951 (type-alias exact-spelling binding + salted alias contains edge),
#4946 (receiver typing from parameters and fields), #4920 (split-header
owner binding via _cpp_qualified), and #4921 (local-callable suppression)
all change per-file C++ extractor output, so warm caches must re-run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Member

Landed in 0.9.101 (published to PyPI), cherry-picked with your authorship preserved. Thanks @nothariharan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor Pull request from a returning community contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants