Skip to content

fix(native): persist this/super dispatch via hybrid WASM post-pass#1337

Open
carlos-alm wants to merge 6 commits into
mainfrom
fix/native-cha-this-super-dispatch-1326
Open

fix(native): persist this/super dispatch via hybrid WASM post-pass#1337
carlos-alm wants to merge 6 commits into
mainfrom
fix/native-cha-this-super-dispatch-1326

Conversation

@carlos-alm
Copy link
Copy Markdown
Contributor

Summary

  • The native orchestrator path resolved typed receiver calls but did not persist raw unresolved call site receiver info (this, super) to the DB, so this.method() and super.method() dispatch was silently dropped on the native path
  • Adds runPostNativeThisDispatch: after the Rust pipeline completes, WASM-re-parses JS/TS/TSX files to collect call sites with this/super receivers, then resolves them through the DB class hierarchy (extends edges) using the existing resolveThisDispatch function
  • Removes the skipIf(engine === 'native') guards on the this-dispatch and super-dispatch integration tests — both engines now produce identical edges

Test plan

  • this-dispatch: emits ConcreteWorker.doWork → ConcreteWorker.prepare passes for both wasm and native
  • super-dispatch: emits Lion.speak → Animal.speak passes for both wasm and native
  • All 622 integration tests pass, 2 skipped (CHA transitive tests pending native binary abstract_class_declaration fix)
  • No regressions in existing CHA, RTA, or import resolution tests

Closes #1326

…1326)

The native orchestrator resolves typed receiver calls but does not persist
raw unresolved call site receiver info (this/super) to the DB, so
runPostNativeCha could not resolve this.method() or super.method() calls.

Add runPostNativeThisDispatch: after the Rust pipeline completes, WASM-re-
parses JS/TS/TSX files to collect call sites with this/super receivers, then
resolves them through the DB class hierarchy (extends edges) using the
existing resolveThisDispatch function. Only runs when extends edges exist.

Removes the skipIf(engine === 'native') guards on the this-dispatch and
super-dispatch integration tests — both engines now produce identical edges
for ConcreteWorker.doWork → ConcreteWorker.prepare and Lion.speak →
Animal.speak. The two CHA transitive skips remain (pending abstract_class_
declaration fix in a future native binary).

Closes #1326
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 5, 2026

Greptile Summary

This PR closes the native orchestrator's this/super dispatch gap (#1326): the Rust pipeline resolves typed receiver calls but never persists raw call-site receiver info to the DB, so this.method() and super.method() edges were silently dropped on the native path. The fix adds a hybrid post-pass (runPostNativeThisDispatch) that WASM-re-parses only inheritance-related JS/TS files, collects call sites with this/super receivers, and resolves them through the existing DB class hierarchy via resolveThisDispatch.

  • native-orchestrator.ts: New runPostNativeThisDispatch function filters to files involved in extends edges (avoiding a full-project re-parse), seeds a seen set from existing call edges to prevent duplicates, and inserts new cha-technique edges inside a transaction. Uses COALESCE(end_line - line, 999999999) (fixed from a prior review) to correctly pick the tightest enclosing method.
  • types.ts: Adds optional thisDispatchMs to BuildResult.phases for timing visibility on the native path.
  • phase-8.5-cha-dispatch.test.ts: Removes the skipIf(engine === 'native') guards from the this-dispatch and super-dispatch integration tests, making both run unconditionally against both engines.

Confidence Score: 5/5

Safe to merge — the post-pass is additive and gated by a fast extends-edge check, so projects without inheritance are completely unaffected.

The implementation is tightly scoped: it only re-parses files already involved in the DB class hierarchy, deduplicates via the seen set, and runs inside an existing transaction. Both correctness bugs flagged in the previous review round have been resolved. The integration tests now cover both engines unconditionally.

No files require special attention.

Important Files Changed

Filename Overview
src/domain/graph/builder/stages/native-orchestrator.ts Adds runPostNativeThisDispatch: WASM re-parses inheritance-related JS/TS files to collect this/super call sites, resolves them via DB class hierarchy, and inserts cha-technique call edges — bridging the gap where the Rust pipeline did not persist raw receiver info. NULL ordering and self-receiver bugs from the prior round are both fixed.
src/types.ts Adds optional thisDispatchMs field to BuildResult.phases; correctly marked optional so callers that don't supply it (WASM path) remain valid.
tests/integration/phase-8.5-cha-dispatch.test.ts Removes the skipIf(engine === 'native') guards for this-dispatch and super-dispatch tests; both tests now run unconditionally against both WASM and native engines, validating the parity fix.

Sequence Diagram

sequenceDiagram
    participant T as tryNativeOrchestrator
    participant C as runPostNativeCha
    participant D as runPostNativeThisDispatch
    participant W as parseFilesWasmForBackfill
    participant DB as SQLite DB

    T->>C: run CHA post-pass (interface dispatch)
    C->>DB: insert cha call edges
    T->>D: run this/super dispatch post-pass
    D->>DB: SELECT extends edges - build parents map
    D->>DB: SELECT files in extends hierarchy
    D->>DB: seed seen set from existing call edges
    D->>W: WASM-re-parse inheritance-related files
    W-->>D: Map of relPath to ExtractorOutput with call sites
    loop each call site with this or super receiver
        D->>DB: findCallerByLineStmt innermost method at line
        D->>D: resolveThisDispatch walk parents map
        D->>DB: byName lookup for qualified method
        D->>D: computeConfidence minus CHA_DISPATCH_PENALTY
    end
    D->>DB: batchInsertEdges transaction
    D->>D: free WASM parse trees
    T->>T: backfillEdgeTechniques labels all new edges
    T-->>T: formatNativeTimingResult includes thisDispatchMs
Loading

Reviews (7): Last reviewed commit: "fix(native): document incremental limita..." | Re-trigger Greptile

Comment on lines +653 to +661
// Find the innermost containing method/function for a call at `line` in `file`.
// NULL end_line sorts last in SQLite ASC → only selected when no bounded node exists.
const findCallerByLineStmt = db.prepare(`
SELECT id, name FROM nodes
WHERE file = ? AND kind IN ('method', 'function')
AND line <= ? AND (end_line IS NULL OR end_line >= ?)
ORDER BY (end_line - line) ASC
LIMIT 1
`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 SQLite NULL ordering inverted — wrong caller selected when end_line is absent

The comment says "NULL end_line sorts last in SQLite ASC", but SQLite actually sorts NULLs first in ASC order (NULLs are considered less than any other value). When any method/function node has end_line IS NULL, the expression (end_line - line) evaluates to NULL, which ranks before every positive span — so that unbounded node is picked by LIMIT 1 instead of the tightest-bounded enclosing method. The intent (use unbounded nodes only as a last resort) is directly contradicted by the actual SQLite behaviour, meaning this/super edges can silently be attributed to the wrong caller.

Fix: replace the ORDER BY with COALESCE(end_line - line, 999999999) ASC or (end_line - line) ASC NULLS LAST (supported since SQLite 3.30.0).

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — replaced (end_line - line) ASC with COALESCE(end_line - line, 999999999) ASC. SQLite ASC orders NULLs first so the original query would pick unbounded nodes before any bounded node, inverting the intent. The COALESCE sentinel pushes them last, which is what the comment was trying to say. Committed in fd15d3c.

Comment on lines +671 to +672
if (call.receiver !== 'this' && call.receiver !== 'self' && call.receiver !== 'super')
continue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Including 'self' as a this-like receiver for JS/TS-family files may produce false-positive dispatch edges. In JavaScript and TypeScript self is not a class-instance receiver — it refers to the global WindowOrWorkerGlobalScope object. Any call of the form self.postMessage() or similar inside a Worker would be misidentified as a this-dispatch and resolved against the class hierarchy, yielding spurious calls edges. If self support is intentional (e.g. for CoffeeScript or other transpiled sources), a comment explaining the rationale would help future readers avoid accidentally widening this further.

Suggested change
if (call.receiver !== 'this' && call.receiver !== 'self' && call.receiver !== 'super')
continue;
if (call.receiver !== 'this' && call.receiver !== 'super')
continue;

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed — removed 'self' from the receiver check in runPostNativeThisDispatch. This function only runs on JS/TS-family files where self is WindowOrWorkerGlobalScope, not a class instance. Keeping it would have produced spurious dispatch edges for any self.postMessage() or similar Worker call. Applied the suggested fix plus an explanatory comment. Committed in fd15d3c.

…f receiver

SQLite ASC ordering puts NULL values first, so (end_line - line) ASC would
pick unbounded nodes before any bounded node — inverting the intent. Replace
with COALESCE(end_line - line, 999999999) ASC so unbounded nodes sort last.

Also remove 'self' from the this/super receiver filter in runPostNativeThisDispatch.
In JS/TS files 'self' refers to WindowOrWorkerGlobalScope, not a class instance —
including it would produce spurious dispatch edges from Worker call sites.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 6, 2026

Codegraph Impact Analysis

4 functions changed6 callers affected across 6 files

  • runPostNativeThisDispatch in src/domain/graph/builder/stages/native-orchestrator.ts:578 (4 transitive callers)
  • formatNativeTimingResult in src/domain/graph/builder/stages/native-orchestrator.ts:754 (4 transitive callers)
  • tryNativeOrchestrator in src/domain/graph/builder/stages/native-orchestrator.ts:1171 (5 transitive callers)
  • BuildResult.phases in src/types.ts:1155 (0 transitive callers)

…files only

On a full native build, runPostNativeThisDispatch was WASM-re-parsing every
JS/TS file in the project, adding a costly second parse pass on top of the
native Rust parse (measured: +358% ms/file on codegraph itself).

Narrow the file set to only files that appear in the class inheritance graph
(sources and targets of 'extends' edges). Files outside the hierarchy have no
class relationship, so this/super calls in them either resolve locally or are
skipped by resolveThisDispatch anyway — WASM re-parsing them adds cost with
zero benefit.

Also replace the hardcoded 0.1 confidence penalty with the CHA_DISPATCH_PENALTY
named constant (already imported), matching every other CHA confidence
calculation in native-orchestrator.ts and build-edges.ts.

Fixes: regression-guard failure "Build ms/file: 3.6 → 16.5 (+358%)" (#1337)
@carlos-alm
Copy link
Copy Markdown
Contributor Author

Addressed Greptile's remaining finding from the summary: replaced the hardcoded 0.1 confidence penalty with CHA_DISPATCH_PENALTY (already imported) so it matches every other CHA confidence calculation in the file.

Also fixed the performance regression caught by the benchmark gate: the WASM re-parse on full native builds was scanning every JS/TS file in the project (+358% ms/file). Narrowed the file set to only files that participate in the class inheritance graph (sources and targets of extends edges) — files outside the hierarchy can't produce meaningful this/super dispatch edges, so re-parsing them was pure overhead.

Committed in 4a5c5b9.

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

…Ms timing

Add a comment to the incremental-build branch of runPostNativeThisDispatch
documenting the known gap: if a parent-class method is replaced (new node ID)
but the child file is unchanged, the stale super.method() edge is not
refreshed until the next full rebuild.

Add wall-clock timing for the this/super dispatch post-pass. The function
now returns the elapsed milliseconds (Promise<number>), and the result is
threaded through formatNativeTimingResult as a new thisDispatchMs phase.
For large class hierarchies the WASM re-parse can be non-trivial, so
surfacing it in build diagnostics makes performance regressions visible.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

Addressed the two remaining findings from the Greptile summary:

Incremental build limitation (lines 643–645): Added a comment to the else branch of runPostNativeThisDispatch documenting the known gap — if a parent-class method is replaced (new node ID) but the child file is unchanged, the stale super.method() edge will not be refreshed until the next full rebuild.

Timing capture: Changed runPostNativeThisDispatch return type from Promise<void> to Promise<number> (elapsed wall-clock ms). The return value is now captured at the call site and threaded through formatNativeTimingResult as a new thisDispatchMs phase field (optional in BuildResult). For projects with large class hierarchies this WASM re-parse can be non-trivial, so it now shows up in build diagnostics.

Committed in 773caf0.

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

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.

fix(native): persist raw call-site receiver info to DB for this/super dispatch on native path

1 participant