Skip to content

fix(pascal): resolve calls through a global singleton variable across files (#3101) - #3119

Open
abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:fix/pascal-singleton-calls
Open

fix(pascal): resolve calls through a global singleton variable across files (#3101)#3119
abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:fix/pascal-singleton-calls

Conversation

@abhay-codes07

Copy link
Copy Markdown
Contributor

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:

  1. the per-file pass discarded the receiver before resolution started (callee_text.split(".")[-1]), so mm.ServerReport became just serverreport;
  2. the cross-file resolver (pascal_resolution) only walked the caller's inherits chain — 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) is statement → exprDot in tree-sitter-pascal, not exprCall, and the walk only handled a lone identifier there — so it was never seen as a call at all.

The change

Per-file (both extractors, tree-sitter and the regex fallback):

  • an unresolved qualified call keeps its receiver on the raw call (receiver: "mm"); Self./inherited are not receivers and unqualified calls are reported exactly as before;
  • the unit's interface-section var/threadvar declarations are exported as pascal_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;
  • the tree-sitter walk treats a lone exprDot statement 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 an EXTRACTED calls edge 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 calls edges from the shared direct-call pass, and the raw-call plumbing through the id-remap passes, are untouched — pascal_globals holds type names, never ids.

Tests

tests/test_pascal_singleton_calls.py (20 tests; 8 fail with the fix reverted) with a three-unit fixture under tests/fixtures/pascal_singleton/ (same static-fixture convention as test_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.ServerReportTMainModule, never TOtherModule), carrying the call site with EXTRACTED confidence, 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_scoping and test_pascal_resolution are unchanged (72 passed together); the full suite matches the v8 baseline.

…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.
Copilot AI lite review requested due to automatic review settings August 26, 2026 12:56

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

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 callsgraphify/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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionextract_pascal()

fans out to 11 callees (efferent coupling); 17 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

@Vinay2334

Copy link
Copy Markdown

Bro, how are you fixing issues so fast. I picked 2 issues today and you already closed them as I was understanding them.

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.

Pascal/Delphi: cross-file calls via a global singleton variable (mm.Method) produce no calls edges — same pattern as Kotlin #1698, Go #1313

3 participants