Skip to content

feat(resolver): phase 8.3 + 8.3b — points-to analysis (WASM + native parity)#1296

Merged
carlos-alm merged 21 commits into
mainfrom
feat/phase-8.3-points-to-analysis
Jun 4, 2026
Merged

feat(resolver): phase 8.3 + 8.3b — points-to analysis (WASM + native parity)#1296
carlos-alm merged 21 commits into
mainfrom
feat/phase-8.3-points-to-analysis

Conversation

@carlos-alm
Copy link
Copy Markdown
Contributor

Summary

  • Phase 8.3 (JS/WASM): field-based points-to analysis in buildCallEdgesJS resolves higher-order function calls where a named variable aliases a function (const fn = handler; arr.map(fn) → edge to handler). Andersen-style fixed-point solver with ≤50 iterations.
  • Phase 8.3b (native Rust): extends pts to the native path so both engines produce identical edges. The Rust JS extractor now records const alias = fn / const alias = obj.method bindings; edge_builder.rs implements the same constraint solver; build_pipeline.rs threads bindings through.
  • WASM worker fix: fnRefBindings was silently dropped at the worker serialization boundary since Phase 8.3, preventing the WASM pts path from ever running. Fixed in wasm-worker-entry.ts, wasm-worker-pool.ts, wasm-worker-protocol.ts.

Test plan

  • npx vitest run tests/integration/pts-parity.test.ts — both engines emit processItems → handler for a const alias = handler; items.map(alias) fixture
  • npx vitest run tests/integration/ tests/engines/ — 699 tests pass, no regressions
  • npm run lint — clean (warnings only, no errors)

Closes #1290

carlos-alm and others added 14 commits June 1, 2026 20:28
…ust engine (closes #1280)

Ports Phase 8.2 to the native engine so returnTypeMap and callAssignments
are populated for JS/TS/TSX files regardless of which engine is active.
- Rust: new NativeCallAssignment NAPI struct; FileSymbols gains returnTypeMap
  and callAssignments fields; match_js_return_type_map and
  match_js_call_assignments walk passes mirror the JS extractReturnTypeMapWalk
  and recordCallAssignment logic exactly
- JS: patchReturnTypeMap() in parser.ts converts the native array to a Map
  (same pattern as patchTypeMap) so propagateReturnTypesAcrossFiles sees a
  unified ExtractorOutput regardless of engine
…bols literals

Two test-only FileSymbols struct literals in import_edges.rs and structure.rs
were not updated when the new fields were added to the struct, causing cargo
test --release to fail to compile on all platforms (E0063).
Three parity issues identified in review:

1. find_parent_class climbed through function scope boundaries, causing nested
   function declarations inside class methods to be incorrectly attributed to the
   enclosing class (e.g. UserService.buildQuery instead of buildQuery). Added
   find_parent_class_no_fn_boundary which stops the ancestor walk when it crosses
   a function_declaration, function_expression, arrow_function, or method_definition
   node, matching the WASM extractReturnTypeMapWalk which resets currentClass to null
   before recursing into any function body.

2. The variable_declarator branch included 'function' as a valid value kind, which
   has no counterpart in the TS reference and is not a valid tree-sitter value-
   expression node kind. Removed it, leaving only 'arrow_function' and
   'function_expression' to match the WASM reference exactly.

3. patchReturnTypeMap returned undefined for files with no annotated functions,
   while patchTypeMap always returns a (possibly empty) Map. Changed to always
   assign new Map() to keep both native and WASM paths consistent and safe for
   future callers that iterate without optional-chaining.
… (phase 8.3)

Implements a per-file Andersen-style constraint solver that resolves
higher-order function calls missed by the existing callback heuristic.

The extractor now records FnRefBinding entries (const fn = handler,
const fn = obj.method) via handleVarDeclaratorTypeMap. In the JS edge-
building path, buildPointsToMapForFile seeds the pts map from local
definitions and imported names, then propagates alias constraints to
fixed-point (≤50 iterations). When resolveCallTargets returns empty for
a dynamic, receiver-less call, resolveViaPointsTo is tried as a fallback
and the resolved alias goes back through the normal resolution chain —
so cross-module flows work transparently via importedNames.

Confidence on pts-resolved edges is penalised by PROPAGATION_HOP_PENALTY
(0.1) to reflect the indirection. The fallback is strictly additive and
conservative: it only fires when primary resolution already failed, so no
existing edges are affected.

Expected impact: +5–10pp on caller coverage for callback-heavy code
(Express/Koa middleware, Node.js EventEmitter patterns, strategy objects).

Impact: 14 functions changed, 12 affected
#1289)

- Guard member_expression property binding to static property access only
  (property_identifier | identifier), preventing computed subscript expressions
  like obj[expr] from producing pts keys that can never match
- Add explanatory comment to the partial Call object passed to resolveCallTargets
  in the pts alias fallback path (only name is needed for alias resolution)
- Add DEFAULTS.analysis.pointsToMaxIterations (50) and document the TODO to
  wire MAX_SOLVER_ITERATIONS through config alongside typePropagationDepth
- Document that callable-only seeding in buildPointsToMapForFile is intentional:
  class aliases are handled by Phase 8.2 call-assignment propagation
- Update config unit test to include the new pointsToMaxIterations field
- File issue #1293 to track native Rust engine parity for fn-ref bindings
…dence direct-call edge (#1289)

A pts fallback edge for caller→target (confidence C-0.1) was added to
seenCallEdges before the primary loop could process a later direct call
to the same target. The direct call was silently dropped by the dedup
guard, leaving the graph with a penalised confidence instead of the
correct full-confidence edge.

Fix: pts edges are tracked in a separate ptsEdgeRows map (edgeKey →
allEdgeRows index) instead of seenCallEdges. When the primary loop
later resolves the same caller→target pair directly, it upgrades the
existing row's confidence in-place and promotes the key to seenCallEdges.
…pts edge (#1289)

Two bugs in the pts-edge upgrade path introduced by d794d47:

1. allEdgeRows[ptsIdx][3] = confidence produced TS2532 (Object is possibly
   undefined) because TypeScript cannot prove that ptsIdx is a valid
   allEdgeRows index, even though we set the index ourselves just before
   pushing the row. Fix: assign to a local variable with an explicit
   undefined guard before writing.

2. When a direct call upgrades a pts-fallback edge, only the confidence
   (index 3) was written back; the isDynamic flag (index 4) kept the value
   1 from the original alias call. Downstream consumers (diff-impact, risk
   classifiers) would then misclassify a confirmed direct call as a dynamic
   dispatch. Fix: write isDynamic to index 4 alongside confidence.
Phase 8.3b: closes the parity gap introduced by #1289 where field-based
points-to analysis only ran in the WASM/JS call-edge builder.

**Rust changes (crates/codegraph-core)**
- `types.rs`: add `FnRefBinding` napi struct; add `fn_ref_bindings` field to
  `FileSymbols` so the all-Rust orchestrator path can carry bindings through.
- `extractors/javascript.rs`: extend `handle_var_decl` to detect
  `const alias = identifier` and `const alias = obj.method` patterns and push
  to `fn_ref_bindings`, mirroring the WASM extractor's `handleVarDeclaratorTypeMap`.
- `edge_builder.rs`: add `build_points_to_map` / `resolve_via_points_to`
  (Andersen-style ≤50-iteration fixed-point solver, mirrors `points-to.ts`);
  add pts fallback in `process_file` for dynamic receiver-less calls with no
  primary targets; wire `fn_ref_bindings` into `FileEdgeInput`.
- `build_pipeline.rs`: forward `FileSymbols.fn_ref_bindings` into `FileEdgeInput`
  for the all-Rust build pipeline path.

**TypeScript changes**
- `build-edges.ts`: add `fnRefBindings` to `NativeFileEntry` and serialize
  `symbols.fnRefBindings` when building the JS→Rust FFI entries.
- `wasm-worker-entry.ts` / `wasm-worker-pool.ts` / `wasm-worker-protocol.ts`:
  propagate `fnRefBindings` through the WASM worker serialization boundary so
  the WASM pts path also works end-to-end (was silently dropped before).

**Test**
- `tests/integration/pts-parity.test.ts`: assert both engines emit a
  `processItems → handler` call edge for the `const alias = handler; arr.map(alias)`
  pattern.
@claude
Copy link
Copy Markdown

claude Bot commented Jun 3, 2026

Claude finished @carlos-alm's task —— View job


PR Review: feat(resolver): phase 8.3 + 8.3b — points-to analysis

  • Gather context from PR description and changed files
  • Phase 1: Root cause analysis
  • Phase 2: Configuration change detection
  • Phase 3: Test integrity check
  • Phase 4: Code quality review
  • Phase 5: Backlog compliance assessment

Root Cause Analysis

Problem Being Solved: Higher-order function calls where a named variable aliases a function (e.g. const fn = handler; arr.map(fn)) produce no call edge because the syntactic extractor sees a call to fn (undefined in the graph) rather than to handler. This is a genuine precision gap affecting real codebases that use function references, middleware patterns, and adapter objects.

Why This Approach: An Andersen-style inclusion-based points-to analysis is the standard and correct tool for this class of problem. The field-based (non-field-sensitive) approximation is well-understood and appropriate — 99%+ precision at negligible cost, matching the scope of what a call-graph builder typically needs. The intra-file fixed-point solver is compact (110 LoC in TS, ~50 LoC in Rust) and well-bounded (≤50 iterations). The wasm-worker serialization bug fix is clearly correct: fnRefBindings was being silently dropped at the worker boundary.

Risk Assessment: Low. The PTS path is purely additive — it runs only when targets.length === 0 && call.dynamic (line 714 in build-edges.ts), so it cannot affect calls that already resolve correctly. The confidence penalty (- PROPAGATION_HOP_PENALTY) ensures pts-resolved edges have lower confidence than direct edges, and the ptsEdgeRows promotion mechanism correctly upgrades confidence when a direct edge is later found for the same pair.


Configuration Change Detection

src/infrastructure/config.ts adds two fields to DEFAULTS.analysis:

typePropagationDepth: 3,
pointsToMaxIterations: 50,

These are additive only — no existing rule was relaxed. The TODO comment acknowledges that the constants aren't yet wired through config (the hardcoded MAX_SOLVER_ITERATIONS = 50 in points-to.ts and the for _ in 0..50 in Rust edge_builder.rs currently ignore config). This is a noted technical debt, not a config-silencing change. No linting rules were disabled.

tests/unit/config.test.ts: The single added line (pointsToMaxIterations: 50) extends the DEFAULTS snapshot — no assertions weakened.


Test Integrity

The parity test (tests/integration/pts-parity.test.ts) has one design issue worth raising:

const requireParity = !!process.env.CODEGRAPH_PARITY;
const describeOrSkip = requireParity || hasNative ? describe : describe.skip;

In most CI environments without a native binary, this test suite silently skips (no CODEGRAPH_PARITY env var + no native binary = describe.skip). The WASM-only path — which is the path that had the actual bug (the fnRefBindings drop at the serialization boundary) — only runs the critical edge check when native is available. A dedicated WASM-only test that does not require native would be more robust coverage for the bug fixed in this PR.

The fix itself (wasm-worker-entry.ts:804 + wasm-worker-pool.ts:109 + wasm-worker-protocol.ts:65) is clearly correct. The protocol now includes fnRefBindings in SerializedExtractorOutput, the entry correctly opts it in with a length check, and the pool deserializer propagates it. The bug was real and the fix is minimal and targeted.


Code Quality

points-to.ts — clean module. No issues.

build-edges.tsbuildFileCallEdges / pts fallback (lines 660–738):

The ptsEdgeRows upgrade mechanism (lines 685–696) correctly handles the case where a pts edge is later superseded by a direct-call edge. The logic is sound but reads densely — the inline comment explains it well enough.

One concern: resolveCallTargets is called with a partial Call object on line 719–721:

const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(
  lookup,
  { name: alias },   // ← only `name`, no `line`, `receiver`, or `dynamic`
  ...

resolveCallTargets accepts Partial<Call>-like inputs in some paths, and the comment says "only name is needed here." This is architecturally fragile — if resolveCallTargets's internals start reading call.line or call.dynamic, this silently breaks. Consider typing the second argument as Pick<Call, 'name'> or narrowing the overload so the compiler enforces it.

edge_builder.rs — Rust pts solver (lines 131–191):

The for _ in 0..50 hardcoded limit mirrors the TS constant but isn't a named Rust constant. Low severity — the comment names it as mirroring the JS — but extracting it as const MAX_SOLVER_ITERATIONS: usize = 50; would be cleaner and would make it easier to find when the TODO to wire config through is resolved.

extractors/javascript.rs — Rust extractor (lines 448–467):

The Rust match_js_type_map function records FnRefBinding for identifier and member_expression RHS, matching the TS extractor. Critical parity check: The JS extractor guards on !BUILTIN_GLOBALS.has(valueN.text) (line 1408) and !BUILTIN_GLOBALS.has(obj.text) (line 1420). The Rust extractor has no equivalent BUILTIN_GLOBALS guard. This means patterns like const fn = Array or const fn = Math would generate spurious pts bindings in the Rust path. For the current test fixture this doesn't matter, but it's a latent parity gap.

build_pipeline.rs — fnRefBindings threading:

Clean. None is emitted when empty (line 1385–1388), matching the JS path's symbols.fnRefBindings?.length ? symbols.fnRefBindings : undefined (build-edges.ts:509).


Backlog Compliance

  • Zero-dep: ✓ — No new npm or Cargo dependencies.
  • Foundation-aligned: ✓ — Purely local analysis; no cloud calls, no external toolchain, no code modification. Directly improves the accuracy of the dependency graph that AI agents rely on.
  • Problem-fit: 5/5 — Higher-order function calls are among the most common patterns where dependency graphs produce missing edges. This directly prevents hallucinated "no callee" outputs that mislead AI agents about blast radius.
  • Breaking: No — additive only. fnRefBindings is an optional field; existing extractor outputs without it are unaffected. No CLI output changes, no DB schema changes, no API signature changes.
  • Tier: Tier 1 — zero-dep ✓, foundation-aligned ✓.

Critical Concerns

  1. Missing BUILTIN_GLOBALS guard in Rust extractor (crates/codegraph-core/src/extractors/javascript.rs, lines 448–467): The Rust path will generate FnRefBinding records for const fn = Array, const fn = Math, etc. — patterns the JS extractor explicitly filters out. This is a real parity gap that will produce spurious pts edges in the native engine for codebases that alias built-in globals. Should be fixed before merging.

  2. Hardcoded 0..50 in Rust solver (crates/codegraph-core/src/edge_builder.rs, line 161): Should be a named constant (MAX_SOLVER_ITERATIONS) for consistency with the TS side and to simplify the future config-wiring TODO.

  3. Partial Call object passed to resolveCallTargets (build-edges.ts, line 719): Type-unsafe construction. Low immediate risk but fragile — consider Pick<Call, 'name'> typing or a wrapper function.

  4. Parity test skips silently without native binary: The test that covers the WASM serialization fix (the primary bug fixed in this PR) only runs when hasNative is true or CODEGRAPH_PARITY is set. In a WASM-only CI environment, the regression is not guarded. A dedicated WASM-only test (it('WASM engine records fnRefBindings', ...)) that doesn't depend on native availability would close this gap.


Final Recommendation

## Root Cause Analysis
Problem: Higher-order alias calls produce no call edges (real precision gap)
Approach: Andersen-style pts — correct and proportionate
Risk: Low — pts path activates only on already-failed resolution

## Backlog Compliance
- Zero-dep: ✓
- Foundation-aligned: ✓
- Problem-fit: 5/5 — directly prevents hallucinated "no callee" in AI agent queries
- Breaking: No
- Tier: 1

## Critical Concerns
1. Missing BUILTIN_GLOBALS filter in Rust extractor → spurious native-engine pts edges
2. Hardcoded 50 in Rust (should be a named constant)
3. Partial Call object typing fragility in build-edges.ts
4. WASM parity test skips silently without native binary

## Final Recommendation
Rating: ⭐⭐⭐☆☆ (3/5)
Action: REQUEST CHANGES
Reasoning: The core algorithm is correct and the wasm-worker bug fix is clean.
However, the missing BUILTIN_GLOBALS guard in the Rust extractor is a real
native/WASM parity bug — exactly the class of divergence CLAUDE.md says must
be fixed, not documented. Items 2–4 are minor. Fix item 1 (and optionally 2–4)
and this is a solid APPROVE.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 3, 2026

Greptile Summary

This PR completes Phase 8.3/8.3b points-to analysis across both execution paths: it fixes a serialization bug that prevented fnRefBindings from crossing the WASM worker boundary, and extends the Andersen-style fixed-point pts solver to the native Rust engine so both engines produce identical alias-resolved call edges.

  • WASM worker fix (wasm-worker-entry.ts, wasm-worker-pool.ts, wasm-worker-protocol.ts): fnRefBindings was silently dropped during worker message serialization; three files are updated to carry the field across the boundary.
  • Rust pts solver (edge_builder.rs): Implements build_points_to_map and resolve_via_points_to in Rust, mirrors the JS ptsEdgeRows confidence-upgrade mechanism via an index-based pts_edge_map, and threads FnRefBinding through build_pipeline.rs.
  • JS extractor + parity test (extractors/javascript.rs, pts-parity.test.ts): The Rust extractor now records const alias = fn / const alias = obj.method bindings with a JS_BUILTIN_GLOBALS guard; a 269-line integration test validates engine parity for both alias resolution and the confidence-upgrade path.

Confidence Score: 5/5

Safe to merge — both engine paths produce correct and identical pts-resolved call edges, the WASM serialization bug is properly fixed across all three worker-boundary files, and the prior review's builtin-globals parity gap has been closed.

The Rust pts solver mirrors the JS buildFileCallEdges logic accurately: seeding, constraint propagation, confidence penalisation, and the pts_edge_map upgrade path all behave identically to their TypeScript counterparts. The WASM worker fix correctly propagates fnRefBindings through entry, pool, and protocol layers. The integration test covers the serialization fix, the confidence-upgrade scenario, and cross-engine parity. No logic gaps or data-loss paths were found.

No files require special attention.

Important Files Changed

Filename Overview
crates/codegraph-core/src/edge_builder.rs Adds build_points_to_map, resolve_via_points_to, and the pts_edge_map confidence-upgrade mechanism; logic mirrors the JS buildFileCallEdges ptsEdgeRows path correctly, including the seen_edges/pts_edge_map separation and in-place upgrade on direct-call discovery.
crates/codegraph-core/src/extractors/javascript.rs Records identifier and member-expression fnRefBindings with the JS_BUILTIN_GLOBALS guard added following a prior review; parity with the TS extractor's BUILTIN_GLOBALS set is now enforced.
src/domain/wasm-worker-entry.ts Spreads fnRefBindings into the serialized extractor output when non-empty; correctly fixes the silent-drop bug that blocked the WASM pts path since Phase 8.3.
src/domain/wasm-worker-pool.ts Deserializes fnRefBindings from the worker response when present; symmetrically paired with the wasm-worker-entry.ts serialization fix.
src/domain/wasm-worker-protocol.ts Adds fnRefBindings to SerializedExtractorOutput interface, completing the protocol type update needed for the serialization fix.
tests/integration/pts-parity.test.ts New 269-line integration test covers WASM-only serialization fix, confidence-upgrade path (alias + direct call in same function body), and native/WASM parity; uses tempdir isolation and correctly cleans up shared parent directories.
crates/codegraph-core/src/types.rs Adds FnRefBinding NAPI struct and fnRefBindings field to FileSymbols; mirrors the TypeScript FnRefBinding interface with correct js_name attributes.
crates/codegraph-core/src/build_pipeline.rs Threads fn_ref_bindings from FileSymbols into FileEdgeInput using the same empty→None guard as the JS path.
src/domain/graph/builder/stages/build-edges.ts Adds fnRefBindings to NativeFileEntry and passes it to buildCallEdgesNative with the same empty-guard as build_pipeline.rs; no changes to the JS pts path itself.
src/domain/graph/resolver/ts-resolver.ts Removes double-negation from a bitwise-AND used in a conditional; functionally identical, cosmetic cleanup only.

Reviews (7): Last reviewed commit: "Merge branch 'main' into feat/phase-8.3-..." | Re-trigger Greptile

Comment on lines +448 to +454
} else if name_n.kind() == "identifier" && value_n.kind() == "identifier" {
// Phase 8.3: `const alias = handler` — record for pts analysis.
symbols.fn_ref_bindings.push(FnRefBinding {
lhs: node_text(&name_n, source).to_string(),
rhs: node_text(&value_n, source).to_string(),
rhs_receiver: None,
});
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 The Rust extractor records identifier = identifier bindings without filtering JS builtin globals, while the JS extractor guards both the identifier and member-expression cases with !BUILTIN_GLOBALS.has(valueN.text). For most code this is harmless — builtins like Array or JSON are not seeded in the pts map, so constraints referencing them produce no flow. However, if a file defines a local function whose name matches a builtin (e.g., function Array(x) {...}; const fn = Array), the Rust engine would seed Array in the pts map and produce a caller → Array edge, while the WASM engine would silently drop the binding — directly breaking the engine-parity guarantee the test suite is meant to enforce.

Suggested change
} else if name_n.kind() == "identifier" && value_n.kind() == "identifier" {
// Phase 8.3: `const alias = handler` — record for pts analysis.
symbols.fn_ref_bindings.push(FnRefBinding {
lhs: node_text(&name_n, source).to_string(),
rhs: node_text(&value_n, source).to_string(),
rhs_receiver: None,
});
} else if name_n.kind() == "identifier" && value_n.kind() == "identifier" {
// Phase 8.3: `const alias = handler` — record for pts analysis.
// Mirror the JS BUILTIN_GLOBALS guard: skip well-known JS globals so
// they are never seeded as pts targets (e.g. `const a = Array`).
let rhs_text = node_text(&value_n, source);
if !JS_BUILTIN_GLOBALS.contains(rhs_text) {
symbols.fn_ref_bindings.push(FnRefBinding {
lhs: node_text(&name_n, source).to_string(),
rhs: rhs_text.to_string(),
rhs_receiver: None,
});
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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 — added a JS_BUILTIN_GLOBALS constant (a slice mirroring the TS BUILTIN_GLOBALS set) and guarded both the identifier and member_expression RHS cases in match_js_type_map. Patterns like const fn = Array and const fn = Math.random are now filtered out on the Rust path, closing the parity gap.

Comment thread src/infrastructure/config.ts Outdated
Comment on lines +89 to +90
/** Maximum fixed-point iterations for the Phase 8.3 points-to solver. */
pointsToMaxIterations: 50,
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 pointsToMaxIterations is not read by either solver

DEFAULTS.analysis.pointsToMaxIterations and the matching CodegraphConfig interface field are defined here, but both the TS solver (MAX_SOLVER_ITERATIONS = 50 in points-to.ts) and the Rust solver (literal 50 in edge_builder.rs) use hardcoded constants instead. A user who sets this in .codegraphrc.json will see no effect. The same situation exists for typePropagationDepth, and the TODO comment above already acknowledges this, so this is a known gap — but it may be worth an explicit note in the JSDoc that the config key is reserved and not yet effective.

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.

Acknowledged — added a @reserved JSDoc note to both pointsToMaxIterations in config.ts and the matching field in src/types.ts clarifying that neither the WASM solver nor the Rust solver reads this value yet. The existing TODO comment above in config.ts already tracked the wiring work; the JSDoc makes it explicit at the public interface level.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Codegraph Impact Analysis

17 functions changed21 callers affected across 9 files

  • build_and_insert_call_edges in crates/codegraph-core/src/build_pipeline.rs:1310 (1 transitive callers)
  • build_points_to_map in crates/codegraph-core/src/edge_builder.rs:145 (6 transitive callers)
  • resolve_via_points_to in crates/codegraph-core/src/edge_builder.rs:186 (6 transitive callers)
  • process_file in crates/codegraph-core/src/edge_builder.rs:220 (6 transitive callers)
  • emit_call_edges in crates/codegraph-core/src/edge_builder.rs:441 (6 transitive callers)
  • make_file in crates/codegraph-core/src/edge_builder.rs:1197 (1 transitive callers)
  • handle_var_decl in crates/codegraph-core/src/extractors/javascript.rs:464 (1 transitive callers)
  • make_symbols in crates/codegraph-core/src/import_edges.rs:534 (1 transitive callers)
  • line_count_map_from_symbols in crates/codegraph-core/src/structure.rs:922 (0 transitive callers)
  • FileSymbols.new in crates/codegraph-core/src/types.rs:347 (0 transitive callers)
  • NativeFileEntry.fnRefBindings in src/domain/graph/builder/stages/build-edges.ts:66 (0 transitive callers)
  • buildCallEdgesNative in src/domain/graph/builder/stages/build-edges.ts:458 (3 transitive callers)
  • resolveTypeName in src/domain/graph/resolver/ts-resolver.ts:491 (7 transitive callers)
  • serializeExtractorOutput in src/domain/wasm-worker-entry.ts:787 (2 transitive callers)
  • deserializeResult in src/domain/wasm-worker-pool.ts:89 (1 transitive callers)
  • SerializedExtractorOutput.fnRefBindings in src/domain/wasm-worker-protocol.ts:65 (0 transitive callers)
  • CodegraphConfig.analysis in src/types.ts:1254 (0 transitive callers)

…ers (#1296)

Three test helpers in edge_builder.rs, import_edges.rs and structure.rs
constructed FileEdgeInput / FileSymbols literals without the fn_ref_bindings
field added in Phase 8.3, causing the native host build to fail to compile
on all three CI platforms.
…constant (#1296)

The Rust extractor was recording fn_ref_bindings for patterns like
`const fn = Array` without the guard present in the JS extractor. This
created a parity gap: if a file defines a local function whose name
matches a builtin, the native engine would seed it in the pts map and
produce a spurious call edge while the WASM engine would silently drop
the binding.

- Add JS_BUILTIN_GLOBALS constant mirroring the TS BUILTIN_GLOBALS set
- Guard both the identifier and member_expression paths in match_js_type_map
- Extract MAX_SOLVER_ITERATIONS named constant (was hardcoded 0..50) with
  a TODO noting it should be wired through config alongside typePropagationDepth
…elds (#1296)

The parity test suite only ran when native was available or CODEGRAPH_PARITY
was set, leaving the primary bug (fnRefBindings dropped at WASM worker
boundary) unguarded in WASM-only CI. Add a dedicated describe block that
unconditionally exercises the WASM engine.

Also document that pointsToMaxIterations is reserved and not yet wired to
either solver in config.ts and types.ts, and remove a redundant double-negation
in ts-resolver.ts.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

Addressed all four concerns from the review:

1. Missing BUILTIN_GLOBALS guard in Rust extractor (critical) — Fixed. Added a JS_BUILTIN_GLOBALS constant (slice mirroring the TS BUILTIN_GLOBALS set) and applied it as a guard on both the identifier and member_expression RHS paths in match_js_type_map. Patterns like const fn = Array and const fn = Math.random are now filtered on the Rust path, closing the parity gap.

2. Hardcoded 0..50 in Rust solver — Fixed. Extracted const MAX_SOLVER_ITERATIONS: usize = 50 with a TODO comment noting it should be wired through config alongside typePropagationDepth, consistent with the TS side.

3. Partial Call object passed to resolveCallTargets — No change needed. On review, resolveCallTargets already accepts { name: string; receiver?: string | null } as its parameter type (not a full Call object), so passing { name: alias } is fully type-safe. TypeScript enforces this at compile time.

4. WASM parity test skips silently without native binary — Fixed. Added a new unconditional describe('Phase 8.3 WASM pts: fnRefBindings serialization fix', ...) block in pts-parity.test.ts that exercises the WASM engine alone. This test always runs and directly guards the serialization fix that was the primary bug in this PR.

CI build failures — Also fixed: three test struct initializers in edge_builder.rs, import_edges.rs, and structure.rs were missing the fn_ref_bindings field added in Phase 8.3, causing the native host build to fail on all three platforms.

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@claude

@claude
Copy link
Copy Markdown

claude Bot commented Jun 3, 2026

Claude finished @carlos-alm's task —— View job


PR Re-review: feat(resolver): phase 8.3 + 8.3b — points-to analysis


Verification of All Four Concerns

Fix #1 — BUILTIN_GLOBALS guard in Rust extractor ✅

crates/codegraph-core/src/extractors/javascript.rs:10–21 defines a JS_BUILTIN_GLOBALS: &[&str] slice that exactly mirrors the TS BUILTIN_GLOBALS set in src/extractors/javascript.ts:27–81. The guard is applied in both the identifier case (line 468) and the member_expression case (line 484). The parity gap is closed — const fn = Array and const fn = Math.random are filtered identically in both engines.

Fix #2 — Named MAX_SOLVER_ITERATIONS in Rust ✅

crates/codegraph-core/src/edge_builder.rs:133–137 now declares const MAX_SOLVER_ITERATIONS: usize = 50 with a doc comment that explicitly names the matching TS constant and adds the same TODO annotation for future config wiring. The for _ in 0..MAX_SOLVER_ITERATIONS on line 167 uses it correctly.

Fix #3 — WASM-only test (always runs) ✅

tests/integration/pts-parity.test.ts:71–93 adds an unconditional describe('Phase 8.3 WASM pts: fnRefBindings serialization fix', ...) block — no native check, no CODEGRAPH_PARITY gate. It builds the graph with { engine: 'wasm' }, reads call edges directly from SQLite, and asserts processItems → handler is present. This permanently guards the serialization bug fix regardless of CI environment.

Fix #4@reserved JSDoc in config.ts and types.ts ✅

src/infrastructure/config.ts:89–96 and src/types.ts:1246–1251 both carry explicit @reserved JSDoc on pointsToMaxIterations, stating that neither solver reads the value and pointing to the TODO comment. Users inspecting the public config interface now see the caveat immediately.


One minor observation (non-blocking)

The TS BUILTIN_GLOBALS set includes 'Function' implicitly via the capitalization-guard in the type-map path, but the Rust JS_BUILTIN_GLOBALS slice doesn't include Function as a named entry. For the pts paths specifically (where the guard is used), Function as a pts RHS would be unusual enough in practice that this isn't a blocking concern — but it's worth noting for the next time the list is synced.


Updated Assessment

All four concerns from the previous review have been addressed correctly. The implementation is sound:

  • Andersen-style pts solver: correct on both engines
  • BUILTIN_GLOBALS parity gap: closed
  • Rust constant naming: consistent with TS side
  • Test coverage: WASM regression is now always-on
  • Config interface: honest about reserved status

Rating: ⭐⭐⭐⭐⭐ (5/5) — APPROVE

The changes are additive, low-risk, and improve call-edge precision for higher-order function patterns. Ready to merge.
· Branch: feat/phase-8.3-points-to-analysis

…geRows (#1296)

When a file both aliases and directly calls the same target (e.g. `arr.map(alias)` then
`handler(x)`), the Rust pts fallback was inserting the edge directly into `seen_edges`,
locking in the penalised confidence and silently dropping the subsequent direct call.
The JS/WASM path avoids this via `ptsEdgeRows` — pts edges go into a staging map, and
`emit_call_edges` upgrades confidence in-place when the direct call is found.

Mirrors the same mechanism in `build-edges.ts` lines 660–700:
- `pts_edge_map: HashMap<u64, usize>` (key = edge_key, value = index into `edges`)
  replaces the direct `seen_edges.insert` for pts-resolved edges.
- `emit_call_edges` now checks `pts_edge_map` before inserting a new direct-call edge;
  if a pts edge exists for the same pair, it upgrades confidence and is_dynamic in-place
  and promotes the key to `seen_edges`.
- Adds a confidence-upgrade fixture and two new test suites (WASM-only + parity) that
  assert the edge confidence is >= 0.9 (direct-call level) rather than the penalised
  0.9 - 0.1 = 0.8 value.
carlos-alm added a commit that referenced this pull request Jun 3, 2026
…geRows (#1296)

When a file both aliases and directly calls the same target (e.g. `arr.map(alias)` then `handler(x)`), the Rust pts fallback was inserting the edge directly into `seen_edges`, locking in the penalised confidence and silently dropping the subsequent direct call.  The JS/WASM path avoids this via `ptsEdgeRows` — pts edges go into a staging map, and `emit_call_edges` upgrades confidence in-place when the direct call is found.

Mirrors the same mechanism in `build-edges.ts` lines 660–700:
- `pts_edge_map: HashMap<u64, usize>` (key = edge_key, value = index into `edges`) replaces the direct `seen_edges.insert` for pts-resolved edges.
- `emit_call_edges` now checks `pts_edge_map` before inserting a new direct-call edge; if a pts edge exists for the same pair, it upgrades confidence and is_dynamic in-place and promotes the key to `seen_edges`.
- Adds a confidence-upgrade fixture and two new test suites (WASM-only + parity) that assert the edge confidence is >= 0.9 (direct-call level) rather than the penalised 0.9 - 0.1 = 0.8 value.
@carlos-alm
Copy link
Copy Markdown
Contributor Author

Addressed the pts confidence-upgrade parity gap raised in the Greptile summary.

The Rust pts fallback was inserting edges directly into seen_edges, which locked in the penalised confidence and silently dropped any subsequent direct call to the same caller→target pair in the same function body. The JS/WASM path avoids this via ptsEdgeRows.

Fixed by adding a pts_edge_map: HashMap<u64, usize> in edge_builder.rs (mirrors ptsEdgeRows in build-edges.ts): pts-resolved edges go into this staging map, and emit_call_edges upgrades confidence in-place when a direct call to the same pair is found, then promotes to seen_edges. Added a new confidence-upgrade fixture and two test suites (WASM-only always-on + native parity) that assert the edge confidence reaches direct-call level (>= 0.9) rather than staying at the penalised value.

@carlos-alm
Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm carlos-alm force-pushed the feat/phase-8.3-points-to-analysis branch from 4a6c108 to d0f719b Compare June 4, 2026 00:03
@carlos-alm carlos-alm merged commit 4e11cee into main Jun 4, 2026
28 of 41 checks passed
@carlos-alm carlos-alm deleted the feat/phase-8.3-points-to-analysis branch June 4, 2026 03:11
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 4, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(resolver): phase 8.3b — extend points-to analysis to native Rust path

1 participant