From d2eafdb7c0a89411b001413e42ce40268f016f5d Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Thu, 13 Aug 2026 22:24:50 -0600 Subject: [PATCH] fix(native): cha dispatch missing rta evidence from new expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native engine's collect_cha_instantiated_types only sourced typeMap entries (confidence >= 0.9) as RTA instantiation evidence for CHA dispatch, unlike WASM/TS's collectInstantiatedTypes (cha.ts) which also unions every new_expressions entry regardless of assignment shape. A class instantiated ONLY via an object-literal property value or bare non-`this.` assignment never produced a confidence>=0.9 typeMap entry, so it was invisible to native's CHA/RTA filter even after #2139's dispatch-edge fix — WASM already handled this correctly. Adds new_expressions: Vec to FileSymbols/FileEdgeInput, populated unconditionally in handle_new_expr (mirroring the TS extractor's newExpressions collection), and unions it into collect_cha_instantiated_types alongside the existing typeMap source. New fixture (ObjWorker.ts) instantiated only via an object-literal property value, verified via describe.each(['wasm','native']) that both engines now emit the CHA-expanded dispatch edge to it. docs check acknowledged: this is an internal RTA-evidence parity fix closing a narrow instantiation-evidence gap in one engine to match the other's already-correct, already-documented CHA+RTA behavior — no new feature, language, or architecture change, so README/CLAUDE/ ROADMAP do not need updates. Closes #2346 Impact: 3 functions changed, 4 affected --- .../src/domain/graph/builder/pipeline.rs | 1 + .../graph/builder/stages/build_edges.rs | 29 +++++++++++++++---- .../graph/builder/stages/import_edges.rs | 1 + .../src/extractors/javascript.rs | 11 +++++++ crates/codegraph-core/src/types.rs | 9 ++++++ tests/fixtures/cha-dispatch/ObjWorker.ts | 29 +++++++++++++++++++ .../phase-8.5-cha-dispatch.test.ts | 21 ++++++++++++++ 7 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/cha-dispatch/ObjWorker.ts diff --git a/crates/codegraph-core/src/domain/graph/builder/pipeline.rs b/crates/codegraph-core/src/domain/graph/builder/pipeline.rs index 288f907c1..69aa7a076 100644 --- a/crates/codegraph-core/src/domain/graph/builder/pipeline.rs +++ b/crates/codegraph-core/src/domain/graph/builder/pipeline.rs @@ -2120,6 +2120,7 @@ fn build_and_insert_call_edges( object_rest_param_bindings: non_empty(&symbols.object_rest_param_bindings), object_prop_bindings: non_empty(&symbols.object_prop_bindings), computed_dispatch_table_evidence: non_empty(&symbols.computed_dispatch_table_evidence), + new_expressions: non_empty(&symbols.new_expressions), }); } diff --git a/crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs b/crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs index bea574c09..33564e7b9 100644 --- a/crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs +++ b/crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs @@ -156,6 +156,12 @@ pub struct FileEdgeInput { /// `src/types.ts` — see its doc comment for the full rationale. #[napi(js_name = "computedDispatchTableEvidence")] pub computed_dispatch_table_evidence: Option>, + /// RTA instantiation evidence (issue #2346): every constructor type name + /// that appears in ANY `new X()` expression in this file, regardless of + /// assignment shape. Mirrors `ExtractorOutput.newExpressions` in + /// `src/types.ts` — see `collect_cha_instantiated_types`'s doc comment. + #[napi(js_name = "newExpressions")] + pub new_expressions: Option>, } #[napi(object)] @@ -380,14 +386,26 @@ fn add_to_file_scoped<'a>( } } -/// RTA: collect instantiated class names from every file's typeMap, keeping -/// only high-confidence (>= 0.9) entries — mirrors the typeMap fallback -/// branch of `collectInstantiatedTypes` in `cha.ts` (constructor-confidence -/// 1.0 and type-annotation-confidence 0.9 entries both qualify; native has no -/// dedicated `newExpressions` list, so this is the only RTA evidence source). +/// RTA: collect instantiated class names from every file, unioning two +/// sources — mirrors `collectInstantiatedTypes` in `cha.ts` exactly: +/// (a) the dedicated `new_expressions` list (issue #2346): every constructor +/// type name that appears in ANY `new X()` expression in the file, regardless +/// of assignment shape (object-literal property value, bare non-`this.` +/// assignment, etc.) — no confidence threshold applies to this source, same +/// as the TS side; and +/// (b) the typeMap fallback: high-confidence (>= 0.9) entries only +/// (constructor-confidence 1.0 and type-annotation-confidence 0.9 entries +/// both qualify) — covers instantiation evidence inferred indirectly (e.g. +/// cross-file return-type propagation) that never produces a literal +/// `new X()` in this file. fn collect_cha_instantiated_types(files: &[FileEdgeInput]) -> HashSet<&str> { let mut instantiated = HashSet::new(); for file in files { + if let Some(new_expressions) = &file.new_expressions { + for type_name in new_expressions { + instantiated.insert(type_name.as_str()); + } + } for tm in &file.type_map { if tm.confidence >= 0.9 { instantiated.insert(tm.type_name.as_str()); @@ -4598,6 +4616,7 @@ mod call_edge_tests { object_rest_param_bindings: None, object_prop_bindings: None, computed_dispatch_table_evidence: None, + new_expressions: None, } } diff --git a/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs b/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs index 77bf147a3..e7928cedd 100644 --- a/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs +++ b/crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs @@ -1037,6 +1037,7 @@ mod tests { object_rest_param_bindings: vec![], object_prop_bindings: vec![], computed_dispatch_table_evidence: vec![], + new_expressions: vec![], } } diff --git a/crates/codegraph-core/src/extractors/javascript.rs b/crates/codegraph-core/src/extractors/javascript.rs index 7927bb2d2..8f9b8cb76 100644 --- a/crates/codegraph-core/src/extractors/javascript.rs +++ b/crates/codegraph-core/src/extractors/javascript.rs @@ -2887,6 +2887,17 @@ fn handle_call_expr( } fn handle_new_expr(node: &Node, source: &[u8], symbols: &mut FileSymbols) { + // RTA instantiation evidence (issue #2346): record every constructor type + // name that appears in a `new X()` expression, regardless of whether the + // result is ever assigned to anything — mirrors the WASM engine's + // unconditional `newExpressions` collection in `src/extractors/javascript.ts`, + // and gives `collect_cha_instantiated_types` (build_edges.rs) coverage for + // instantiation shapes (e.g. object-literal property values, bare + // non-`this.` assignments) that never produce a confidence>=0.9 typeMap + // entry. + if let Some(type_name) = extract_new_expr_type_name(node, source) { + symbols.new_expressions.push(type_name.to_string()); + } let ctor = node .child_by_field_name("constructor") .or_else(|| node.child(1)); diff --git a/crates/codegraph-core/src/types.rs b/crates/codegraph-core/src/types.rs index d4f5b283c..7989f7b2f 100644 --- a/crates/codegraph-core/src/types.rs +++ b/crates/codegraph-core/src/types.rs @@ -576,6 +576,14 @@ pub struct FileSymbols { /// `src/types.ts`. #[napi(js_name = "computedDispatchTableEvidence")] pub computed_dispatch_table_evidence: Vec, + /// Every constructor type name that appears in ANY `new X()` expression in + /// this file, regardless of whether the result is assigned to anything — + /// RTA (Rapid Type Analysis) instantiation evidence for CHA dispatch + /// (issue #2346). Mirrors `ExtractorOutput.newExpressions` in + /// `src/types.ts` / `symbols.newExpressions` populated by + /// `new_expression` handling in `src/extractors/javascript.ts`. + #[napi(js_name = "newExpressions")] + pub new_expressions: Vec, } impl FileSymbols { @@ -603,6 +611,7 @@ impl FileSymbols { object_rest_param_bindings: Vec::new(), object_prop_bindings: Vec::new(), computed_dispatch_table_evidence: Vec::new(), + new_expressions: Vec::new(), } } } diff --git a/tests/fixtures/cha-dispatch/ObjWorker.ts b/tests/fixtures/cha-dispatch/ObjWorker.ts new file mode 100644 index 000000000..c6fb006f6 --- /dev/null +++ b/tests/fixtures/cha-dispatch/ObjWorker.ts @@ -0,0 +1,29 @@ +import type { IWorker } from './IWorker.js'; + +// Issue #2346: instantiated ONLY as an object-literal property value — never +// as `const x = new ObjWorker()` (every other IWorker implementor in this +// fixture set already uses that variable-declarator shape, which both engines' +// typeMap seeding has always recognized as confidence-1.0 instantiation +// evidence). The WASM engine's `newExpressions` list (Phase 8.5) already +// captures every `new X()` in a file regardless of assignment shape, so RTA +// already treated ObjWorker as instantiated there. The native engine's RTA +// evidence, before the #2346 fix, came ONLY from typeMap confidence>=0.9 +// entries — and constructor typeMap seeding only fires when a `new_expression` +// is the direct value of a variable declarator or a `this.prop = ` assignment, +// so a `new X()` buried inside an object-literal property value was invisible +// to native's CHA/RTA filter even though `dispatch(worker: IWorker)` in +// Dispatcher.ts should CHA-expand to it just like ConcreteWorker/MockWorker. +export class ObjWorker implements IWorker { + doWork(): string { + return 'obj'; + } +} + +// Object-literal-property-value instantiation — NOT a variable declarator. +const objWorkerTable: Record = { w: new ObjWorker() }; + +// Keep the table referenced so it isn't dead code — mirrors the real-world +// object-literal dispatch-table shape this RTA gap was found in. +export function describeObjWorkerTable(): string { + return Object.keys(objWorkerTable).join(','); +} diff --git a/tests/integration/phase-8.5-cha-dispatch.test.ts b/tests/integration/phase-8.5-cha-dispatch.test.ts index 53d2ab9c9..4ad6639a2 100644 --- a/tests/integration/phase-8.5-cha-dispatch.test.ts +++ b/tests/integration/phase-8.5-cha-dispatch.test.ts @@ -113,6 +113,27 @@ describe.each(ENGINES)('Phase 8.5 CHA dispatch (%s)', (engine) => { expect(edge?.technique).toBe('cha'); }); + // ── RTA evidence from non-typeMap instantiation shapes (issue #2346) ─── + // ObjWorker is instantiated ONLY as an object-literal property value + // (`{ w: new ObjWorker() }`), never as `const x = new ObjWorker()`. The + // WASM engine's `newExpressions` list already covers this; the native + // engine's RTA evidence, before #2346, came only from typeMap + // confidence>=0.9 entries, which this instantiation shape never produces. + + it('CHA: emits dispatch → ObjWorker.doWork (instantiated only via object-literal-property-value, issue #2346)', () => { + const edge = callEdges.find( + (e) => + e.caller_name === 'dispatch' && + e.callee_name === 'ObjWorker.doWork' && + e.callee_file === 'ObjWorker.ts', + ); + expect( + edge, + `Expected dispatch → ObjWorker.doWork edge (RTA must treat a new-expression used only as an object-literal property value as instantiation evidence, not just typeMap-tracked variable declarators).\nActual edges:\n${JSON.stringify(callEdges, null, 2)}`, + ).toBeDefined(); + expect(edge?.technique).toBe('cha'); + }); + // ── RTA filter ───────────────────────────────────────────────────────── it('RTA: does NOT emit dispatch → GhostWorker.doWork (never instantiated)', () => {