Skip to content

Commit 9320ed2

Browse files
committed
fix(wasm): emit receiver edges for declaration-typed locals in C++/CUDA
The JS C++ and CUDA extractors had no handler for 'declaration' AST nodes, so typeMap was never seeded for statically-typed locals (e.g. 'UserService svc;'). Without a typeMap entry for 'svc', resolveReceiverEdge had nothing to look up and silently skipped the receiver edge. Add handleCppDeclaration / handleCudaDeclaration to both extractors. They mirror match_c_family_type_map ('declaration' branch) from the native Rust path: extract the type node text and seed typeMap[varName] = { type, confidence: 0.9 } for each identifier or init_declarator child. Primitive types (int, char, bool, …) are skipped to avoid spurious edges. parity-compare.mjs --langs cpp,cuda --hybrid: PARITY OK (wasm = native = hybrid) All 3044 tests pass.
1 parent 29dd101 commit 9320ed2

2 files changed

Lines changed: 101 additions & 0 deletions

File tree

src/extractors/cpp.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
SubDeclaration,
55
TreeSitterNode,
66
TreeSitterTree,
7+
TypeMapEntry,
78
} from '../types.js';
89
import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js';
910

@@ -50,6 +51,9 @@ function walkCppNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
5051
case 'call_expression':
5152
handleCppCallExpression(node, ctx);
5253
break;
54+
case 'declaration':
55+
handleCppDeclaration(node, ctx);
56+
break;
5357
}
5458

5559
for (let i = 0; i < node.childCount; i++) {
@@ -204,6 +208,36 @@ function handleCppInclude(node: TreeSitterNode, ctx: ExtractorOutput): void {
204208
});
205209
}
206210

211+
/**
212+
* Seed typeMap for declaration-typed locals: `UserService svc;` and
213+
* `UserService svc = makeService();` both yield typeMap["svc"] = "UserService"
214+
* at confidence 0.9. Mirrors `match_c_family_type_map` ("declaration" branch)
215+
* in the native Rust C++ extractor.
216+
*/
217+
function handleCppDeclaration(node: TreeSitterNode, ctx: ExtractorOutput): void {
218+
const typeNode = node.childForFieldName('type');
219+
if (!typeNode) return;
220+
const typeName = typeNode.text;
221+
// Skip primitive types — they are never class/struct receivers
222+
if (isPrimitiveCppType(typeName)) return;
223+
for (let i = 0; i < node.childCount; i++) {
224+
const child = node.child(i);
225+
if (!child) continue;
226+
const kind = child.type;
227+
let nameNode: TreeSitterNode | null = null;
228+
if (kind === 'init_declarator') {
229+
nameNode = child.childForFieldName('declarator') ?? null;
230+
} else if (kind === 'identifier') {
231+
nameNode = child;
232+
}
233+
if (!nameNode) continue;
234+
const varName = unwrapCppDeclaratorName(nameNode);
235+
if (varName) {
236+
ctx.typeMap.set(varName, { type: typeName, confidence: 0.9 });
237+
}
238+
}
239+
}
240+
207241
function handleCppCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void {
208242
const funcNode = node.childForFieldName('function');
209243
if (!funcNode) return;
@@ -324,6 +358,23 @@ function extractCppClassFields(classNode: TreeSitterNode): SubDeclaration[] {
324358
return fields;
325359
}
326360

361+
/**
362+
* Primitive C/C++ types that are never class/struct receivers. Seeding these
363+
* into typeMap would cause spurious receiver edges (e.g. `int x` → `int`).
364+
*/
365+
const CPP_PRIMITIVE_TYPES = new Set([
366+
'int', 'long', 'short', 'unsigned', 'signed', 'float', 'double',
367+
'char', 'bool', 'void', 'wchar_t', 'auto', 'size_t', 'uint8_t',
368+
'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t',
369+
'int64_t', 'ptrdiff_t', 'intptr_t', 'uintptr_t',
370+
]);
371+
372+
function isPrimitiveCppType(typeName: string): boolean {
373+
// Strip qualifiers like `const`, `volatile`, `unsigned` etc.
374+
const base = typeName.split(/\s+/).pop() ?? typeName;
375+
return CPP_PRIMITIVE_TYPES.has(base) || CPP_PRIMITIVE_TYPES.has(typeName);
376+
}
377+
327378
function extractCppEnumEntries(enumNode: TreeSitterNode): SubDeclaration[] {
328379
const entries: SubDeclaration[] = [];
329380
const body = findChild(enumNode, 'enumerator_list');

src/extractors/cuda.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
SubDeclaration,
55
TreeSitterNode,
66
TreeSitterTree,
7+
TypeMapEntry,
78
} from '../types.js';
89
import { extractModifierVisibility, findChild, nodeEndLine } from './helpers.js';
910

@@ -63,6 +64,9 @@ function walkCudaNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
6364
case 'call_expression':
6465
handleCudaCallExpression(node, ctx);
6566
break;
67+
case 'declaration':
68+
handleCudaDeclaration(node, ctx);
69+
break;
6670
}
6771

6872
for (let i = 0; i < node.childCount; i++) {
@@ -204,6 +208,36 @@ function handleCudaInclude(node: TreeSitterNode, ctx: ExtractorOutput): void {
204208
});
205209
}
206210

211+
/**
212+
* Seed typeMap for declaration-typed locals: `UserService svc;` and
213+
* `UserService svc = make();` both yield typeMap["svc"] = "UserService"
214+
* at confidence 0.9. Mirrors `match_c_family_type_map` ("declaration" branch)
215+
* in the native Rust CUDA extractor.
216+
*/
217+
function handleCudaDeclaration(node: TreeSitterNode, ctx: ExtractorOutput): void {
218+
const typeNode = node.childForFieldName('type');
219+
if (!typeNode) return;
220+
const typeName = typeNode.text;
221+
// Skip primitive types — they are never class/struct receivers
222+
if (isCudaPrimitiveType(typeName)) return;
223+
for (let i = 0; i < node.childCount; i++) {
224+
const child = node.child(i);
225+
if (!child) continue;
226+
const kind = child.type;
227+
let nameNode: TreeSitterNode | null = null;
228+
if (kind === 'init_declarator') {
229+
nameNode = child.childForFieldName('declarator') ?? null;
230+
} else if (kind === 'identifier') {
231+
nameNode = child;
232+
}
233+
if (!nameNode) continue;
234+
const varName = extractCudaFieldName(nameNode);
235+
if (varName) {
236+
ctx.typeMap.set(varName, { type: typeName, confidence: 0.9 });
237+
}
238+
}
239+
}
240+
207241
function handleCudaCallExpression(node: TreeSitterNode, ctx: ExtractorOutput): void {
208242
const funcNode = node.childForFieldName('function');
209243
if (!funcNode) return;
@@ -374,6 +408,22 @@ function innerCudaDeclarator(node: TreeSitterNode): TreeSitterNode | null {
374408
return null;
375409
}
376410

411+
/**
412+
* Primitive C/C++/CUDA types that are never class/struct receivers. Seeding
413+
* these into typeMap would produce spurious receiver edges (e.g. `int x` → `int`).
414+
*/
415+
const CUDA_PRIMITIVE_TYPES = new Set([
416+
'int', 'long', 'short', 'unsigned', 'signed', 'float', 'double',
417+
'char', 'bool', 'void', 'wchar_t', 'auto', 'size_t', 'uint8_t',
418+
'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t',
419+
'int64_t', 'ptrdiff_t', 'intptr_t', 'uintptr_t',
420+
]);
421+
422+
function isCudaPrimitiveType(typeName: string): boolean {
423+
const base = typeName.split(/\s+/).pop() ?? typeName;
424+
return CUDA_PRIMITIVE_TYPES.has(base) || CUDA_PRIMITIVE_TYPES.has(typeName);
425+
}
426+
377427
function extractCudaEnumEntries(enumNode: TreeSitterNode): SubDeclaration[] {
378428
const entries: SubDeclaration[] = [];
379429
const body = findChild(enumNode, 'enumerator_list');

0 commit comments

Comments
 (0)