fix(pascal): resolve calls through a global singleton variable across files (#3101) - #3119
fix(pascal): resolve calls through a global singleton variable across files (#3101)#3119abhay-codes07 wants to merge 2 commits into
Conversation
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. No changes could be formally verified in this run.
Graphify review — findings
Adds cross-file resolution of Delphi global-singleton method calls (var mm: TMainModule; in one unit's interface, mm.ServerReport(...) from another). The Pascal extractor now collects interface-section var/threadvar declarations into pascal_globals (dropping names declared with conflicting types), splits qualified callees into name and receiver via _pascal_call_parts, and carries the receiver on unresolved raw calls — including the statement -> exprDot shape like om.Flush;. resolve_pascal_inherited_calls joins receiver → declared type → single class of that name → single same-named method, emitting an edge only when every step is unambiguous, and skips the caller's ancestor chain for receiver-qualified calls since they aren't inherited.
Worth a look
- Qualified receiver calls are resolved as unqualified calls —
graphify/extractors/pascal.py:717· 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 — 93 functions depend on the 77 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract_pascal()— 17 callers, 11 callees - new:
_extract_pascal_regex()— 4 callers, 15 callees - new:
resolve_pascal_inherited_calls()— 4 callers, 3 callees - new:
walk()— 1 callers, 7 callees
Verification — 93 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: 93 function(s) in the blast radius were not formally verified this run
Formal verification
Could not verify: Could not verify extract\_pascal.
The verifier did not have enough to check extract\_pascal, 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
Could not verify: Could not verify \_extract\_pascal\_regex.
The verifier did not have enough to check \_extract\_pascal\_regex, 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
Could not verify: Could not verify resolve\_pascal\_inherited\_calls.
The verifier did not have enough to check resolve\_pascal\_inherited\_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
· 1 grounded finding(s) anchored inline below; 3 more finding(s) on lines outside this diff (see the check run).
| "pascal_globals": _pascal_interface_globals(stripped), | ||
| } | ||
|
|
||
| def extract_pascal(path: Path) -> dict: |
There was a problem hiding this comment.
extract_pascal()
fans out to 11 callees (efferent coupling); 17 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
|
Bro, how are you fixing issues so fast. I picked 2 issues today and you already closed them as I was understanding them. |
…raphify-Labs#3101) `var mm: TMainModule;` in one unit's interface and `mm.ServerReport(...)` from another is the standard Delphi "shared main module" shape. The per-file pass discarded the receiver (`mm.ServerReport` -> `serverreport`) before resolution even started, and the cross-file resolver only walked the CALLER's ancestor chain - so whenever caller and callee lived in different files the edge was silently absent, and "who calls X" was empty for exactly the most-used class in the project. Both extractors (tree-sitter and the regex fallback) now keep the receiver on an unresolved qualified call (`receiver: "mm"`; Self/inherited are not receivers) and export the unit's interface-section var/threadvar declarations as `pascal_globals` - the only variables another unit can name. The tree-sitter walk also recognises the bare qualified statement form `om.Flush;` (statement -> exprDot), which had no exprCall wrapper and was never seen as a call at all. graphify.pascal_resolution joins the two across the corpus: receiver -> declared type (unique across every unit's globals) -> the one class of that name -> its one method of that name, emitting an EXTRACTED `calls` edge at the call site. Ambiguity at any step - a global declared with two types, two same-named classes, two same-named methods - yields no edge rather than a guess, the same god-node guard the inherited-call pass uses. A qualified call no longer falls back to the caller's ancestor chain: it names its receiver, it is not an inherited call.
…own method (Graphify-Labs#3101) Review follow-up: _emit_or_report ran the caller-scoped, receiver-blind resolve_callee before parking, so a qualified call om.Flush resolved to the caller's own Flush whenever the name collided in the caller's class/ancestor/module scope — a false edge on the receiver's method. A call with an explicit (non-Self/inherited) receiver now skips that resolver and parks for the receiver-aware pass, which types the receiver via the global-singleton table before matching; an unresolvable one stays unresolved rather than mis-bound. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
1cea7a3 to
723edca
Compare
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 4 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Adds cross-file resolution for Delphi global-singleton calls (mm.ServerReport where mm is a var typed in another unit's interface section): the per-file pass now collects interface-section var/threadvar globals via _pascal_interface_globals and reports qualified calls carrying their receiver, and resolve_pascal_inherited_calls joins receiver→type→class→method across the corpus, emitting an edge only when every step is unambiguous. Splits callees with _pascal_call_parts so Self/inherited/Result receivers are ignored, and handles the om.Flush; no-args statement shape (exprDot). Stops the receiver-blind resolver from binding a qualified call to the caller's own same-named method — such calls are parked for the receiver-aware pass and left unresolved rather than mis-bound if no receiver type is known.
Worth a look
- Interface global extraction treats anonymous record fields as exported globals —
graphify/extractors/pascal.py:191· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- _pascal_call_parts drops receiver on Self/inherited but keeps method name, changing resolution behavior for inherited calls —
graphify/extractors/pascal.py:214· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Regex fallback still locally resolves qualified receiver calls —
graphify/extractors/pascal.py:471· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Behavior change: qualified call to caller's own method no longer resolved locally —
graphify/extractors/pascal.py:722· 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 — 95 functions depend on the 79 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract_pascal()— 17 callers, 11 callees - new:
_extract_pascal_regex()— 4 callers, 15 callees - new:
resolve_pascal_inherited_calls()— 4 callers, 3 callees - new:
walk()— 1 callers, 7 callees
Verification — 95 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: 95 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
274 of 274 test file(s) selected (100%) via static blast radius.
Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.
tests/test_affected_cli.py— full-run-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— full-run-safetytests/test_astro_import_ids.py— full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— full-run-safetytests/test_build_merge_dedup_scope.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— full-run-safetytests/test_cpp_objc_cross_file_calls.py— full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— full-run-safetytests/test_cross_language_call_resolution.py— full-run-safetytests/test_cross_repo_member_calls.py— full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/test_csharp_call_site_generic_args.py— full-run-safetytests/test_csharp_enum_members.py— full-run-safety- … and 224 more
changed code file(s) with no mapped test (
tests/fixtures/pascal_singleton/Form1.pas,tests/fixtures/pascal_singleton/MainModule.pas,tests/fixtures/pascal_singleton/OtherModule.pas) — a coverage gap or a missing link — running the full suite rather than only the selected tests
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.
Formal verification
Could not verify: Could not verify extract\_pascal.
The verifier did not have enough to check extract\_pascal, 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
Could not verify: Could not verify \_extract\_pascal\_regex.
The verifier did not have enough to check \_extract\_pascal\_regex, 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
Could not verify: Could not verify resolve\_pascal\_inherited\_calls.
The verifier did not have enough to check resolve\_pascal\_inherited\_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
· 1 grounded finding(s) anchored inline below; 3 more finding(s) on lines outside this diff (see the check run).
| "pascal_globals": _pascal_interface_globals(stripped), | ||
| } | ||
|
|
||
| def extract_pascal(path: Path) -> dict: |
There was a problem hiding this comment.
extract_pascal()
fans out to 11 callees (efferent coupling); 17 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Closes #3101.
The problem
var mm: TMainModule;declared in one unit's interface,mm.ServerReport(...)called from another — the standard Delphi "shared main module" shape. The edge was emitted only when caller and callee were in the same file; across files it was silently absent. Two causes, both traced in the issue:callee_text.split(".")[-1]), somm.ServerReportbecame justserverreport;pascal_resolution) only walked the caller'sinheritschain — the right thing for an unqualified inherited call, and nothing at all for a call on a global.A third, unreported: the bare qualified statement form
om.Flush;(no parentheses) isstatement → exprDotin tree-sitter-pascal, notexprCall, and the walk only handled a loneidentifierthere — so it was never seen as a call at all.The change
Per-file (both extractors, tree-sitter and the regex fallback):
receiver: "mm");Self./inheritedare not receivers and unqualified calls are reported exactly as before;var/threadvardeclarations are exported aspascal_globals({"mm": "tmainmodule"}) — only interface vars, because they are the only variables another unit can name; implementation-section and procedure-local vars are not collected, and a name declared twice with different types is dropped;exprDotstatement as a qualified call.Corpus (
pascal_resolution): receiver → declared type (unique across every unit's globals) → the one class of that name → its one method of that name, emitting anEXTRACTEDcallsedge at the call site (a variable's declared static type is deterministic, matching the confidence the inherited-call pass uses). Ambiguity at any step — a global declared with two types, two same-named classes, two same-named methods — yields no edge rather than a guess: the same god-node guard the inherited pass applies. A qualified call no longer falls back to the caller's ancestor chain; it names its receiver, so it is not an inherited call.The
callsedges from the shared direct-call pass, and the raw-call plumbing through the id-remap passes, are untouched —pascal_globalsholds type names, never ids.Tests
tests/test_pascal_singleton_calls.py(20 tests; 8 fail with the fix reverted) with a three-unit fixture undertests/fixtures/pascal_singleton/(same static-fixture convention astest_pascal_resolution.py): receiver parsing; interface-globals collection (implementation/local vars excluded, conflicting names dropped, programs export nothing); both extractors reporting the receiver and exporting globals; and at corpus level the edge existing across files, landing on the right class when two classes share a method name (mm.ServerReport→TMainModule, neverTOtherModule), carrying the call site withEXTRACTEDconfidence, an unqualified unresolvable call still producing nothing, an ambiguous receiver producing nothing, and a qualified call never binding through the caller's ancestors.test_pascal,test_pascal_call_scopingandtest_pascal_resolutionare unchanged (72 passed together); the full suite matches thev8baseline.