Finish Objective-C member calls across a repository boundary (#3152) - #3385
Finish Objective-C member calls across a repository boundary (#3152)#3385xiongjianxu wants to merge 2 commits into
Conversation
…s#3228) Every other extractor stamps its definitions through the generic engine: `_callable` for "a real callable, not a same-named data symbol" (Graphify-Labs#2438), narrowed to `_callable_class` for a type, which is callable only through a constructor (Graphify-Labs#2137). The ObjC extractor builds its nodes by hand and set neither, so an ObjC class was invisible to every pass that indexes declarations by those markers — the `same_type_as` pass from Graphify-Labs#3007, the qualified-name indexes, and anything new that needs to tell a declaration from a reference — and an ObjC method could not be told apart from a data symbol of the same name. `@interface`, `@implementation` and `@protocol` nodes now carry both markers (a protocol is a type declaration, the same way the engine marks a Java or C# interface), and method nodes carry `_callable`. The file node and the stubs minted for dangling references stay unmarked: a stub has no declaration behind it.
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Formal verification. 2 change(s) tested, no difference found (not proven).
Graphify review — findings
Adds Objective-C support to cross-repo member-call linking: registers .h/.m/.mm suffixes for the objc language, and resolves parked ObjC selectors against both -/+ sigil variants of a member key while treating a type that declares both as an ambiguity rather than a hit. Parks typed ObjC receivers whose class is declared nowhere in the corpus via _park_absent so a merged graph can answer them later, while skipping builtin types (NSString, DispatchQueue) that no other repo owns.
Worth a look
- Already-sigiled Objective-C callees are double-prefixed —
graphify/cross_repo_calls.py:157· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1942 functions depend on the 324 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 546 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
extract_objc()— 27 callers, 9 callees - new:
link_cross_repo_member_calls()— 22 callers, 8 callees - new:
_get_extractor()— 26 callers, 6 callees - …and 37 more — each is listed as a finding
Verification — 1942 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: 1777 function(s) in the blast radius were not formally verified this run
Formal verification
No difference found (not proven): No behavior difference found in \_key (not a proof).
The verifier ran both versions of \_key on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: concolic exploration (CrossHair). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in link\_cross\_repo\_member\_calls (not a proof).
The verifier ran both versions of link\_cross\_repo\_member\_calls on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_resolve\_objc\_member\_calls.
The verifier did not have enough to check \_resolve\_objc\_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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify extract\_objc.
The verifier did not have enough to check extract\_objc, 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
· 3 grounded finding(s) anchored inline below; 42 more finding(s) on lines outside this diff (see the check run).
| return (f"-{callee}", f"+{callee}") | ||
| return (callee,) | ||
|
|
||
|
|
||
| def link_cross_repo_member_calls(merged: "nx.Graph") -> int: |
There was a problem hiding this comment.
link_cross_repo_member_calls()
fans out to 8 callees (efferent coupling); 22 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| PARKED_OBJC = [{"callee": "greet", "receiver_type": "Greeter", "lang": "objc", "line": "L5"}] | ||
|
|
||
|
|
||
| def test_an_objc_selector_answers_through_either_sigil(): |
There was a problem hiding this comment.
test_an_objc_selector_answers_through_either_sigil()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| ) | ||
| assert link_cross_repo_member_calls(G) == 0 | ||
|
|
||
|
|
||
| def test_the_definition_answers_before_a_same_named_declaration(): |
There was a problem hiding this comment.
test_the_definition_answers_before_a_same_named_declaration()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
…y-Labs#3152) `_resolve_objc_member_calls` types a receiver from `self`/`super`, an explicit class name, the file's local table or the class's @property/ivar table, then requires exactly one declaration of that type. Zero declarations and more than one shared a single bail, so a call into a class another repository declares was dropped with the receiver type already in hand — the case Graphify-Labs#3152 parks for the merge in Java, C++, C# and Swift. Split the three type-known bails: zero declarations parks `{callee, receiver_type, lang: "objc", line}` on the caller, more than one stays dropped as before (a local ambiguity that merging only widens). A builtin receiver type is filtered out at the parking sites, matching what the Swift and C++ resolvers already skip before resolving. On the merge side an ObjC method label keeps its `+`/`-` sigil where every other extractor writes `.name()`, and a parked selector does not say which was meant, so both spellings are tried and a class declaring both counts as ambiguous. The sigil is what keeps ObjC and C++ members disjoint inside a shared `.h`, which is in both languages' suffix sets. Depends on the `_callable` / `_callable_class` markers (Graphify-Labs#3228) that `_index_declarations` indexes declarations by; the commit before this one carries them.
1416986 to
dab3a70
Compare
|
Addressed the one advisory finding ("already-sigiled Objective-C callees are double-prefixed", Not reachable from graphify's own output — Also dropped a local test log ( Full suite after the change: 4 failed, 5322 passed, 93 skipped — the 4 are the pre-existing The three inline findings are coupling-delta advisories on |
There was a problem hiding this comment.
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.
Formal verification. 2 change(s) tested, no difference found (not proven).
Graphify review — findings
Adds Objective-C support to the cross-repo member-call flow: the ObjC resolver now parks a call whose receiver type is declared nowhere in the corpus (skipping builtins like NSString), and link_cross_repo_member_calls resolves it after a merge, trying both - and + selector spellings via _member_keys since a parked selector carries no class/instance sigil. Marks ObjC classes, implementations, protocols, and methods with the _callable/_callable_class node markers the generic engine puts on every other language's declarations, so they become visible to the passes that index declarations. Keeps the single-definition guard and builtin exclusion so a name collision or ambiguous +greet/-greet pair still won't fabricate an edge.
Worth a look
- objc protocol label '' breaks bare-name declaration index lookup —
graphify/extractors/objc.py:381· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Protocol declarations now indexed as receiver types via _callable_class, contradicting documented exclusion —
graphify/extractors/objc.py:381· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1943 functions depend on the 325 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 546 callers, 43 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
extract_objc()— 27 callers, 9 callees - new:
link_cross_repo_member_calls()— 23 callers, 8 callees - new:
_get_extractor()— 26 callers, 6 callees - …and 38 more — each is listed as a finding
Verification — 1943 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: 1778 function(s) in the blast radius were not formally verified this run
Formal verification
No difference found (not proven): No behavior difference found in \_key (not a proof).
The verifier ran both versions of \_key on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: concolic exploration (CrossHair). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in link\_cross\_repo\_member\_calls (not a proof).
The verifier ran both versions of link\_cross\_repo\_member\_calls on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
Could not verify: Could not verify \_resolve\_objc\_member\_calls.
The verifier did not have enough to check \_resolve\_objc\_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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous
Could not verify: Could not verify extract\_objc.
The verifier did not have enough to check extract\_objc, 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
· 4 grounded finding(s) anchored inline below; 42 more finding(s) on lines outside this diff (see the check run).
| return (callee,) | ||
|
|
||
|
|
||
| def link_cross_repo_member_calls(merged: "nx.Graph") -> int: |
There was a problem hiding this comment.
link_cross_repo_member_calls()
fans out to 8 callees (efferent coupling); 23 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| PARKED_OBJC = [{"callee": "greet", "receiver_type": "Greeter", "lang": "objc", "line": "L5"}] | ||
|
|
||
|
|
||
| def test_an_objc_selector_answers_through_either_sigil(): |
There was a problem hiding this comment.
test_an_objc_selector_answers_through_either_sigil()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert _added_calls(G) == {("a::app_run", "b::greeter_greet")} | ||
|
|
||
|
|
||
| def test_an_objc_callee_that_already_carries_a_sigil_still_binds(): |
There was a problem hiding this comment.
test_an_objc_callee_that_already_carries_a_sigil_still_binds()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
| assert link_cross_repo_member_calls(G) == 0 | ||
|
|
||
|
|
||
| def test_the_definition_answers_before_a_same_named_declaration(): |
There was a problem hiding this comment.
test_the_definition_answers_before_a_same_named_declaration()
fans out to 6 callees (efferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Self-audit pass over the bot review. No code change on this branch: the two correctness findings do not reproduce, and the remaining notes are coupling deltas. Already handled — a callee arriving with its own Does not reproduce — On the coupling deltas for Verification: |
Fixes #3384. Extends #3152 (cross-repo member calls) to Objective-C.
The change
Extraction (
graphify/extract.py)._resolve_objc_member_callsended each of itsthree typing arms with a single
if len(type_defs) != 1: continue, so "the receiver'stype is declared nowhere in this corpus" — the parking case — was indistinguishable
from "declared more than once", which must stay dropped. Each arm now splits the guard
the way Java's does: zero declarations parks
{callee, receiver_type, lang: "objc", line}on the caller node, more than one still bails on the god-node guard. Theself/superarm is untouched: its type is the caller's own class, which is bydefinition in this corpus.
A builtin receiver type is filtered out at the parking sites, mirroring what the Swift
and C++ resolvers already skip before resolving — a
NSStringorDispatchQueueisnot what another repo declares, so parking one would only let a same-named user class
answer for it.
Merge (
graphify/cross_repo_calls.py). Adds"objc": {".h", ".m", ".mm"}to_LANG_SUFFIXESand a_member_keys(lang, callee)hook next to the existing_member_relations. An ObjC method label keeps its+/-sigil (-greet) where everyother extractor writes
.greet(), and a parked selector does not record whether thesend was to a class or an instance, so both spellings are tried; a class declaring both
+greetand-greetcounts as ambiguous and binds nothing.The sigil is deliberately kept rather than normalized away:
.his in the C++ suffixset too, and the sigil is what keeps an ObjC
-greetand a C++.greet()fromanswering for each other inside one shared header. It also means
@protocoldeclarations self-exclude — they are labelled
<Greeter>, which no parked receiver typespells — so no extra guard is needed there.
The
_callablemarkers._index_declarationsindexes declarations by_callable_class, whichextractors/objc.pynever wrote, so every ObjC type would beskipped. The first commit here is #3229 (
7a6b0e5) cherry-picked unchanged so thisbranch stands alone; if #3229 lands first, that commit drops out on rebase.
Tests
tests/test_cross_repo_member_calls.py, +5 cases:.greet()in a shared.h, and a C++ call doesnot bind to an ObjC
-greetin one either;@protocoldoes not answer a parked call;objc-property-receiveradded to the end-to-end parametrization: two real reposextracted and merged,
[self.greeter greet]across the boundary.Full suite: 4 failed, 5321 passed, 93 skipped. The 4 failures are the pre-existing
tests/test_ollama_retry_cap.pyones, identical to theorigin/v8baseline(4 failed, 5309 passed, 93 skipped) on the same venv.
ruff check graphify testspasses.
Ceiling, measured
On a 15,754-file ObjC corpus in 26 units, 12,866 sends have a typed receiver whose
class is absent from its own unit. Of those, 5,127 are framework classes, 1,911 are
business classes declared in no unit, 2,634 find the class but not the selector on it,
2,769 are ambiguous across units, and 425 are genuinely cross-unit resolvable. This
PR is aimed at those 425; the builtin filter keeps the framework names from being parked
at all.