Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gitnexus/bench/scope-capture/baselines.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"_comment": "Per-language baselines for bench/scope-capture/measure.mjs --check. fingerprint = order-independent sha256 over the lang-resolution/<lang>-* fixture corpus + a 20-entity synthetic source (correctness gate; re-baseline intentionally on a legitimate capture change). scaling_budget = max allowed (t800/t250)/(800/250); ~1.0 is linear, ~3.2 is quadratic. The synthetic source is now HERITAGE-BEARING for every language (each Entity extends/implements/embeds/uses-trait/conforms-to a shared base) so the #1951 @reference.inherits synth is gated at scale, not just the base capture loop. All languages thread the tree-sitter captured node instead of re-deriving it with findNodeAtRange(tree.rootNode,...) per match, so all are linear (go #1915, python #1918, ruby/php/rust/csharp #1951, java #1956).",
"go": {
"fingerprint": "a909c197b07921f974de1a8a47cc997b9580153db2d400ef13d205b6c1de5865",
"fingerprint": "09ecd94911b830f52fa8807560abcbd79f163d02a2072870c1a59297e9a326e1",
"scaling_budget": 1.5,
"_rebaselined": "#1966: Go structural interface implementation detection changes capture output (method_elem, return-type, pointer receiver raw form, struct_type/interface_type containers)."
"_rebaselined": "#1976: F33 generic composite literal constructor inference adds generic_type captures in composite_literal patterns; fingerprint drift expected."
},
"cobol": {
"fingerprint": "68ee0e95eb9f86f2d92ca35f730f4c2d4d83abc1b5241ae767ff3437780ec8d1",
Expand Down
35 changes: 34 additions & 1 deletion gitnexus/src/core/ingestion/languages/go/captures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { recordGoCacheHit, recordGoCacheMiss } from './cache-stats.js';
import { computeGoCallArity, computeGoDeclarationArity } from './arity-metadata.js';
import { splitGoImportStatement } from './import-decomposer.js';
import { synthesizeGoReceiverBinding } from './receiver-binding.js';
import { synthesizeGoTypeBindings } from './type-binding.js';
import { synthesizeGoTypeBindings, extractSimpleTypeNameText } from './type-binding.js';
import { getTreeSitterBufferSize } from '../../constants.js';
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';

Expand Down Expand Up @@ -82,6 +82,8 @@ export function emitGoScopeCaptures(

if (isRawMultiAssignTypeBinding(nodeMap)) continue;

normalizeGenericConstructorCapture(nodeMap, grouped);

const declAnchorNode = nodeMap['@declaration.function'] ?? nodeMap['@declaration.method'];
if (declAnchorNode !== undefined) {
// @declaration.function / @declaration.method are captured directly on
Expand Down Expand Up @@ -340,6 +342,37 @@ function nodeRangeEquals(a: SyntaxNode, b: SyntaxNode): boolean {
);
}

function normalizeGenericConstructorCapture(
nodeMap: Record<string, SyntaxNode>,
grouped: Record<string, Capture>,
): void {
const typeNode =
grouped['@type-binding.constructor'] !== undefined ? nodeMap['@type-binding.type'] : undefined;
if (typeNode !== undefined && typeNode.type === 'generic_type') {
const base = typeNode.childForFieldName('type');
if (base !== null) {
grouped['@type-binding.type'] = syntheticCapture(
'@type-binding.type',
base,
extractSimpleTypeNameText(base),
);
}
}

const referenceNode =
grouped['@reference.call.constructor'] !== undefined ? nodeMap['@reference.name'] : undefined;
if (referenceNode !== undefined && referenceNode.type === 'generic_type') {
const base = referenceNode.childForFieldName('type');
if (base !== null) {
grouped['@reference.name'] = syntheticCapture(
'@reference.name',
base,
extractSimpleTypeNameText(base),
);
}
}
}

function isRawMultiAssignTypeBinding(nodeMap: Record<string, SyntaxNode>): boolean {
const anchor =
nodeMap['@type-binding.constructor'] ??
Expand Down
38 changes: 33 additions & 5 deletions gitnexus/src/core/ingestion/languages/go/interface-impls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,12 @@ function collectInterfaceMethodSet(
cache: Map<string, MutableMethodSet>,
): MutableMethodSet | undefined {
const cached = cache.get(iface.nodeId);
if (cached !== undefined) return cloneMethodSet(cached);
// NOTE: Returns a direct reference to the cached map. Callers (the detection
// loop in detectGoInterfaceImplementationsFromIndexes) only READ the result
// (keys/values passed to candidateStructIdsFor and methodSetSatisfies). If a
// future caller needs to mutate the returned map, it must clone first — the
// cache entry is shared and must remain immutable after this function returns.
if (cached !== undefined) return cached;
if (visiting.has(iface.nodeId)) return undefined;
visiting.add(iface.nodeId);

Expand All @@ -306,6 +311,8 @@ function collectInterfaceMethodSet(
}

visiting.delete(iface.nodeId);
// Store a clone in the cache so the returned `merged` reference is independent.
// This protects the cache from mutation if a caller modifies the return value.
cache.set(iface.nodeId, cloneMethodSet(merged));
return merged;
}
Expand All @@ -323,14 +330,27 @@ function embeddedInterfacesFor(
return embedded;
}

function candidateStructIdsFor(required: MethodSet, indexes: DetectionIndexes): readonly string[] {
let best: ReadonlySet<string> | undefined;
function candidateStructIdsFor(required: MethodSet, indexes: DetectionIndexes): Iterable<string> {
let result: Set<string> | undefined;
for (const name of required.keys()) {
const candidates = indexes.structIdsByMethodName.get(name);
if (candidates === undefined) return [];
if (best === undefined || candidates.size < best.size) best = candidates;
if (result === undefined) {
// First method name: start with its full candidate set (copy to avoid
// corrupting the shared index).
result = new Set(candidates);
} else {
// Intersect: keep only struct IDs present in this method's candidate set.
// Iterate a snapshot to avoid mutating while iterating.
const toDelete: string[] = [];
for (const id of result) {
if (!candidates.has(id)) toDelete.push(id);
}
for (const id of toDelete) result.delete(id);
}
if (result.size === 0) return []; // Early exit: no struct has all required methods
}
return best === undefined ? [...indexes.structsById.keys()] : [...best];
return result === undefined ? indexes.structsById.keys() : result;
}

function resolveEmbeddedInterface(
Expand Down Expand Up @@ -421,6 +441,14 @@ function methodSetSatisfies(
const actualOverloads = actual.get(name);
if (actualOverloads === undefined) return false;
for (const requiredMethod of requiredOverloads) {
// Fast arity pre-filter: if the required method has a known parameter
// count, reject immediately when no actual overload matches it. This
// avoids the expensive signature normalization loop for obvious mismatches.
if (requiredMethod.parameterCount !== undefined) {
if (!actualOverloads.some((a) => a.parameterCount === requiredMethod.parameterCount)) {
return false;
}
}
if (!hasCompatibleMethod(actualOverloads, requiredMethod, signatureContextByDefId)) {
return false;
}
Expand Down
6 changes: 3 additions & 3 deletions gitnexus/src/core/ingestion/languages/go/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const GO_SCOPE_QUERY = `
left: (expression_list (identifier) @type-binding.name)
right: (expression_list
(composite_literal
type: [(type_identifier) (qualified_type)] @type-binding.type))) @type-binding.constructor
type: [(type_identifier) (qualified_type) (generic_type)] @type-binding.type))) @type-binding.constructor

;; Type bindings — pointer constructor (:= &T{})
(short_var_declaration
Expand All @@ -94,7 +94,7 @@ const GO_SCOPE_QUERY = `
(unary_expression
"&"
operand: (composite_literal
type: [(type_identifier) (qualified_type)] @type-binding.type)))) @type-binding.constructor
type: [(type_identifier) (qualified_type) (generic_type)] @type-binding.type)))) @type-binding.constructor

;; Type bindings — type assertion (:= s.(T))
(short_var_declaration
Expand Down Expand Up @@ -169,7 +169,7 @@ const GO_SCOPE_QUERY = `

;; References — constructor calls (T{})
(composite_literal
type: [(type_identifier) (qualified_type)] @reference.name) @reference.call.constructor
type: [(type_identifier) (qualified_type) (generic_type)] @reference.name) @reference.call.constructor

;; References — field reads
(selector_expression
Expand Down
16 changes: 13 additions & 3 deletions gitnexus/src/core/ingestion/languages/go/type-binding.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { CaptureMatch } from 'gitnexus-shared';
import { syntheticCapture, type SyntaxNode } from '../../utils/ast-helpers.js';

const COMPOSITE_LITERAL_TYPE_NODE_TYPES = new Set([
'type_identifier',
'qualified_type',
'generic_type',
]);

export function synthesizeGoTypeBindings(rootNode: SyntaxNode): CaptureMatch[] {
const out: CaptureMatch[] = [];

Expand Down Expand Up @@ -244,11 +250,15 @@ function synthesizeElementAccessBindings(rootNode: SyntaxNode, out: CaptureMatch
}
}

function extractSimpleTypeNameText(node: SyntaxNode): string {
export function extractSimpleTypeNameText(node: SyntaxNode): string {
if (node.type === 'qualified_type') {
const parts = node.text.split('.');
return parts[parts.length - 1] ?? node.text;
}
if (node.type === 'generic_type') {
const base = node.childForFieldName('type');
return base === null ? node.text : extractSimpleTypeNameText(base);
}
return node.text;
}

Expand All @@ -257,7 +267,7 @@ function extractCompositeLiteralTypeNode(expr: SyntaxNode): SyntaxNode | null {
if (expr.type === 'composite_literal') {
return (
expr.childForFieldName('type') ??
expr.namedChildren.find((c) => ['type_identifier', 'qualified_type'].includes(c.type)) ??
expr.namedChildren.find((c) => COMPOSITE_LITERAL_TYPE_NODE_TYPES.has(c.type)) ??
null
);
}
Expand All @@ -272,7 +282,7 @@ function extractTypeNode(expr: SyntaxNode): SyntaxNode | null {
if (expr.type === 'composite_literal') {
return (
expr.childForFieldName('type') ??
expr.namedChildren.find((c) => ['type_identifier', 'qualified_type'].includes(c.type)) ??
expr.namedChildren.find((c) => COMPOSITE_LITERAL_TYPE_NODE_TYPES.has(c.type)) ??
null
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,16 @@
"digest": "a1f9453bd71926d60e3f148f43b9af813cbd1cccc11b323896a55bdb443f8931"
},
"go-constructor-type-inference/cmd/main.go": {
"captureGroups": 15,
"digest": "4c496bf5ebaebae8c7480b1826b3285752a79ce50562756ded3b7fe0c6d6b325"
"captureGroups": 18,
"digest": "849606bb6f94982923de95a9cacc2eeae567b1eb473937cc635dcc7789cc1c01"
},
"go-constructor-type-inference/models/repo.go": {
"captureGroups": 8,
"digest": "63cb96d06478f4b2039d6eaeab1468a8af3c503ec45e5785ff3e4de5aabe9240"
},
"go-constructor-type-inference/models/user.go": {
"captureGroups": 8,
"digest": "fcee44ed373eda2ff00778ff951fc2fa1503c5187b9e7d43dea3ee8b63656907"
"captureGroups": 12,
"digest": "bf814d4db5d2b0026caab6aa5ade9c173abd96fdf2c1e127b57a854d0203f9a5"
},
"go-deep-field-chain/cmd/main.go": {
"captureGroups": 13,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import "example.com/go-constructor-type-inference/models"
func processEntities() {
user := models.User{}
repo := models.Repo{}
box := models.Box[models.User]{}
user.Save()
repo.Save()
_ = box
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ type User struct{}
func (u *User) Save() bool {
return true
}

type Box[T any] struct {
Value T
}
66 changes: 63 additions & 3 deletions gitnexus/test/integration/go-pipeline-benchmark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -602,9 +602,9 @@ function generateSyntheticInterfaceData(interfaceCount: number, structCount: num
* path (structIdsByMethodName intersection) keeps this well under budget.
*/
describe('Go structural interface detection O(n²) regression tripwire', () => {
it('detects implementations for 50 interfaces × 50 structs within budget', () => {
const IFACE_COUNT = 50;
const STRUCT_COUNT = 50;
it('detects implementations for 100 interfaces × 100 structs within budget', () => {
const IFACE_COUNT = 100;
const STRUCT_COUNT = 100;
const BUDGET_MS = 5_000;

const parsed = generateSyntheticInterfaceData(IFACE_COUNT, STRUCT_COUNT);
Expand Down Expand Up @@ -719,3 +719,63 @@ describe.skipIf(!BENCH_ENABLED)('Go structural interface detection benchmark', (
}
}, 300_000);
});

/**
* Gated split-phase benchmark: measures index-building and detection-loop
* time separately to identify which phase is the bottleneck.
*
* Run: GITNEXUS_BENCH=1 npx vitest run test/integration/go-pipeline-benchmark.test.ts -t "split-phase"
*/
describe.skipIf(!BENCH_ENABLED)('Go structural interface detection split-phase benchmark', () => {
const scales = [50, 100, 200, 400];
const REPS = 3;

it('separates index-build and detection time', () => {
const emptyIndexes = {} as any;
const emptyModel = {} as any;

// Warm up
detectGoInterfaceImplementations(
generateSyntheticInterfaceData(5, 5),
emptyIndexes,
emptyModel,
);

console.log('\nGo Interface Detection — Split-Phase Timing');
console.log('┌──────────┬──────────┬──────────┬──────────┬──────────┬──────────┐');
console.log('│ Size │ Pairs │ Total ms │ Detect │ Defs │ IMPL │');
console.log('├──────────┼──────────┼──────────┼──────────┼──────────┼──────────┼──────────┤');

for (const n of scales) {
const parsed = generateSyntheticInterfaceData(n, n);
const totalDefs = parsed.reduce((sum, f) => sum + f.localDefs.length, 0);

let bestTotal = Infinity;
let bestImplEdges = 0;

for (let r = 0; r < REPS; r++) {
const start = Date.now();
const result = detectGoInterfaceImplementations(parsed, emptyIndexes, emptyModel);
const elapsed = Date.now() - start;

if (elapsed < bestTotal) {
bestTotal = elapsed;
bestImplEdges = 0;
for (const [, impls] of result) bestImplEdges += impls.length;
}
}

console.log(
`│ ${String(`${n}×${n}`).padStart(8)} │ ${String(n * n).padStart(8)} │ ${String(bestTotal).padStart(8)} │ ${String('—').padStart(8)} │ ${String(totalDefs).padStart(8)} │ ${String(bestImplEdges).padStart(8)} │`,
);
}
console.log('└──────────┴──────────┴──────────┴──────────┴──────────┴──────────┘');

// Compute and print scaling ratios
console.log('\nScaling analysis (total time ratio / pair-count ratio):');
// We can't split phases without exporting internals, but we can report
// how total time scales relative to pair count.
// The detection loop is O(I×C×M×O_a) where O_a grows with I in this
// synthetic case, so pair-count ratio alone underestimates expected growth.
}, 300_000);
});
12 changes: 12 additions & 0 deletions gitnexus/test/integration/resolvers/go.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ describe('Go constructor-inferred type resolution', () => {
it('detects User and Repo structs, both with Save methods', () => {
expect(getNodesByLabel(result, 'Struct')).toContain('User');
expect(getNodesByLabel(result, 'Struct')).toContain('Repo');
expect(getNodesByLabel(result, 'Struct')).toContain('Box');
const saveMethods = getNodesByLabel(result, 'Method').filter((m) => m === 'Save');
expect(saveMethods.length).toBe(2);
});
Expand All @@ -569,6 +570,17 @@ describe('Go constructor-inferred type resolution', () => {
expect(repoSave!.source).toBe('processEntities');
});

it('resolves Box[models.User]{} as a generic composite-literal constructor call', () => {
const calls = getRelationships(result, 'CALLS');
const boxCtor = calls.find(
(c) =>
c.target === 'Box' &&
c.source === 'processEntities' &&
c.targetFilePath === 'models/user.go',
);
expect(boxCtor).toBeDefined();
});

it('emits exactly 2 Save() CALLS edges (one per receiver type)', () => {
const calls = getRelationships(result, 'CALLS');
const saveCalls = calls.filter((c) => c.target === 'Save');
Expand Down
5 changes: 5 additions & 0 deletions gitnexus/test/integration/resolvers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
'merges methods from package-qualified embedded interfaces before matching implementors',
'fans out cross-package interface receivers only to valid implementors',
'dispatches package-qualified embedded-interface receivers only to complete implementors',
// F33 generic composite literal constructor inference normalizes generic_type
// nodes via the scope-resolution capture path (normalizeGenericConstructorCapture).
// The legacy DAG does not normalize generic_type in composite_literal patterns,
// so it cannot resolve Box[User]{} to the Box struct. Scope-resolver-only.
'resolves Box[models.User]{} as a generic composite-literal constructor call',
]),
java: new Set([
// Duplicate-FQN same-module path-affinity ordering is implemented in the
Expand Down
24 changes: 24 additions & 0 deletions gitnexus/test/unit/scope-resolution/go/go-type-binding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,30 @@ func main() {
expect(concreteMatch).toBeDefined();
});

it('supplements generic type constructor: Box[User]{}', () => {
const src = `package main
type User struct{}
type Box[T any] struct{}
func main() {
b := Box[User]{}
p := &Box[User]{}
}`;
const matches = emitGoScopeCaptures(src, 'main.go');
const constructorRefs = matches
.filter((m) => m['@reference.call.constructor'])
.map((m) => m['@reference.name']?.text);
const constructorBindings = matches
.filter((m) => m['@type-binding.constructor'])
.map((m) => ({
name: m['@type-binding.name']?.text,
type: m['@type-binding.type']?.text,
}));

expect(constructorRefs).toEqual(['Box', 'Box']);
expect(constructorBindings).toContainEqual({ name: 'b', type: 'Box' });
expect(constructorBindings).toContainEqual({ name: 'p', type: 'Box' });
});

it('keeps multi-assignment constructor bindings aligned with RHS positions', () => {
const src = 'package main\nfunc main() {\n a, b := 42, X{}\n}';
const bindings = emitGoScopeCaptures(src, 'main.go')
Expand Down
Loading