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 1 commit into
Conversation
…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.
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. |
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.