gitnexus 1.6.10-rc.146 → 1.6.10-rc.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -58,8 +58,8 @@
58
58
  * - `noAdlSites` — call sites with parenthesized function (capture-time)
59
59
  * - `classToNamespaceQualifiedName` — class def → its enclosing namespace
60
60
  * qualified name (`populateCppAssociatedNamespaces` time)
61
- * - `adlIndex` / `adlIndexSource` — the lazily-built candidate index and the
62
- * `parsedFiles` reference it was built from (first-`pickCppAdlCandidates`
61
+ * - `adlIndexByPass` — the lazily-built candidate index, memoized weakly on
62
+ * the `parsedFiles` array it was built from (first-`pickCppAdlCandidates`
63
63
  * time; see `ensureAdlIndex`)
64
64
  *
65
65
  * The class→namespace map uses qualified names (not scope IDs) because
@@ -58,8 +58,8 @@
58
58
  * - `noAdlSites` — call sites with parenthesized function (capture-time)
59
59
  * - `classToNamespaceQualifiedName` — class def → its enclosing namespace
60
60
  * qualified name (`populateCppAssociatedNamespaces` time)
61
- * - `adlIndex` / `adlIndexSource` — the lazily-built candidate index and the
62
- * `parsedFiles` reference it was built from (first-`pickCppAdlCandidates`
61
+ * - `adlIndexByPass` — the lazily-built candidate index, memoized weakly on
62
+ * the `parsedFiles` array it was built from (first-`pickCppAdlCandidates`
63
63
  * time; see `ensureAdlIndex`)
64
64
  *
65
65
  * The class→namespace map uses qualified names (not scope IDs) because
@@ -103,8 +103,21 @@ function pushFileSiteKey(idx, filePath, key) {
103
103
  }
104
104
  keys.push(key);
105
105
  }
106
- let adlIndex;
107
- let adlIndexSource;
106
+ /**
107
+ * Per-pass memo: `parsedFiles` array identity → the built index. `WeakMap`-keyed
108
+ * so the entry lives only as long as the caller's `parsedFiles` array does, and
109
+ * is reclaimed with the pass — mirrors `qualifiedNsIndexByPass` in
110
+ * `inline-namespaces.ts` and `moduleScopeIndexByPass` in `file-local-linkage.ts`.
111
+ *
112
+ * Weak keying is load-bearing, not stylistic: the index holds
113
+ * `SymbolDefinition` references reaching into every ParsedFile's scopes, so the
114
+ * previous module-level strong `let` pair (index + source array) kept the whole
115
+ * C/C++ ParsedFile set alive past the point `scope-resolution/pipeline/phase.ts`
116
+ * evicts `files`/`contents`/`preExtractedByPath` and calls `forceGc()` to
117
+ * reclaim it — C++ is 7th of 16 `SCOPE_RESOLVERS` entries, so the retention
118
+ * survived nine later language passes plus emit.
119
+ */
120
+ let adlIndexByPass = new WeakMap();
108
121
  function siteKey(filePath, line, col) {
109
122
  return `${filePath}:${line}:${col}`;
110
123
  }
@@ -292,26 +305,36 @@ export function validateAdlSeqCoverage(idx) {
292
305
  return [...missing];
293
306
  }
294
307
  /** Build the ADL index on first use of a given `parsedFiles` set; reuse it for
295
- * all subsequent call sites in the same pipeline run. Reset by
308
+ * every subsequent call site in the same pipeline run. Reset by
296
309
  * `clearCppAdlState`.
297
310
  *
311
+ * Lifetime: the memoized entry is reachable only while the caller still holds
312
+ * the `parsedFiles` array. Once the pipeline drops it, the entry — and the
313
+ * `SymbolDefinition` references it holds into those files' scopes — becomes
314
+ * collectable; nothing here pins it (see {@link adlIndexByPass}).
315
+ *
298
316
  * Staleness is keyed on `parsedFiles` reference identity ONLY, but the index
299
317
  * is a function of THREE inputs: `parsedFiles` (namespace/friend candidates),
300
318
  * `scopes` (`classDefsBySimple`, read from `scopes.defs.byId`), and the
301
- * module-level `classToNamespaceQualifiedName` (friend-candidate keys). This
302
- * is sound for the current pipeline because all three are built together once
303
- * per `runScopeResolution` pass and `clearCppAdlState` runs in
304
- * `loadResolutionConfig` at the start of every pass. Callers MUST call
305
- * `clearCppAdlState` between any two passes that change `scopes` or
306
- * `classToNamespaceQualifiedName` while reusing the same `parsedFiles` array
307
- * reference — otherwise a stale index would be served. (No such caller exists
308
- * today; widening the guard to also key on `scopes` is deferred until one
309
- * does.) */
319
+ * module-level `classToNamespaceQualifiedName` (friend-candidate keys). The
320
+ * WeakMap key covers only the first, so the explicit clear is still REQUIRED
321
+ * for the other two: a later pass may hand back the same `parsedFiles`
322
+ * reference with different `scopes` / class→namespace state, and identity
323
+ * alone would serve the stale index. This is sound for the current pipeline
324
+ * because all three are built together once per `runScopeResolution` pass and
325
+ * `clearCppAdlState` runs in `loadResolutionConfig` at the start of every
326
+ * pass. Callers MUST call `clearCppAdlState` between any two passes that
327
+ * change `scopes` or `classToNamespaceQualifiedName` while reusing the same
328
+ * `parsedFiles` array reference — otherwise a stale index would be served.
329
+ * (No such caller exists today; widening the key to also cover `scopes` is
330
+ * deferred until one does.) */
310
331
  function ensureAdlIndex(scopes, parsedFiles) {
311
- if (adlIndex !== undefined && adlIndexSource === parsedFiles)
312
- return;
313
- adlIndex = buildAdlIndex(scopes, parsedFiles);
314
- adlIndexSource = parsedFiles;
332
+ let index = adlIndexByPass.get(parsedFiles);
333
+ if (index === undefined) {
334
+ index = buildAdlIndex(scopes, parsedFiles);
335
+ adlIndexByPass.set(parsedFiles, index);
336
+ }
337
+ return index;
315
338
  }
316
339
  /** Record per-call-site argument info. Called once per call site from
317
340
  * `emitCppScopeCaptures`. */
@@ -403,8 +426,14 @@ export function clearCppAdlState() {
403
426
  argInfoSiteKeysByFile.clear();
404
427
  noAdlSiteKeysByFile.clear();
405
428
  classToNamespaceQualifiedName.clear();
406
- adlIndex = undefined;
407
- adlIndexSource = undefined;
429
+ // The candidate index is dropped by REASSIGNING a fresh `WeakMap`: `WeakMap`
430
+ // has no `.clear()`, and swapping the instance discards every memoized entry
431
+ // at once — the exact invalidation the previous `adlIndex = undefined` pair
432
+ // provided. Required even though the map is keyed on `parsedFiles` identity,
433
+ // because the index also depends on `scopes` and
434
+ // `classToNamespaceQualifiedName`, which the key cannot observe (see
435
+ // {@link ensureAdlIndex}).
436
+ adlIndexByPass = new WeakMap();
408
437
  }
409
438
  /**
410
439
  * Walk `parsed.scopes` to record each Class def's enclosing namespace
@@ -467,14 +496,14 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
467
496
  if (args === undefined || args.length === 0)
468
497
  return undefined;
469
498
  // Build the workspace-wide ADL candidate index once; reuse for every site.
470
- ensureAdlIndex(scopes, parsedFiles);
499
+ const idx = ensureAdlIndex(scopes, parsedFiles);
471
500
  // Collect associated namespace QNames from every participating class-typed arg
472
501
  // and from function-reference args.
473
502
  const associatedNamespaces = new Set();
474
503
  for (const arg of args) {
475
- collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces);
504
+ collectAssociatedNamespacesForAdlArg(arg, scopes, idx, associatedNamespaces);
476
505
  if (arg.functionRefText !== undefined) {
477
- collectFunctionTypeAssociatedNamespaces(arg.functionRefText, scopes, associatedNamespaces);
506
+ collectFunctionTypeAssociatedNamespaces(arg.functionRefText, scopes, idx, associatedNamespaces);
478
507
  }
479
508
  }
480
509
  if (associatedNamespaces.size === 0)
@@ -487,9 +516,6 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
487
516
  // (`friendCandidates`, ISO C++ `[basic.lookup.argdep]` §2).
488
517
  // Dedup by nodeId and sort by visitation sequence so the candidate list is
489
518
  // byte-for-byte identical to the legacy file-major scan order.
490
- const idx = adlIndex;
491
- if (idx === undefined)
492
- return undefined;
493
519
  const bySeq = new Map();
494
520
  const seenKey = new Set();
495
521
  const collectFrom = (buckets) => {
@@ -515,11 +541,11 @@ export function pickCppAdlCandidates(site, callerParsed, scopes, parsedFiles) {
515
541
  return undefined;
516
542
  return [...bySeq.entries()].sort((a, b) => a[0] - b[0]).map(([, def]) => def);
517
543
  }
518
- function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces) {
544
+ function collectAssociatedNamespacesForAdlArg(arg, scopes, idx, associatedNamespaces) {
519
545
  // For template args this may be the template name itself (e.g. `vector`);
520
546
  // simple-name lookup can match project classes with the same name (known
521
547
  // V1/V2 simplification).
522
- addAssociatedNamespaceForClassName(arg.simpleClassName, scopes, associatedNamespaces);
548
+ addAssociatedNamespaceForClassName(arg.simpleClassName, scopes, idx, associatedNamespaces);
523
549
  // Includes template-owner namespaces (e.g. `std` in std::vector<T>). If
524
550
  // that surfaces extra candidates, merged-candidate overload narrowing in
525
551
  // free-call-fallback suppresses arbitrary edge emission.
@@ -530,13 +556,13 @@ function collectAssociatedNamespacesForAdlArg(arg, scopes, associatedNamespaces)
530
556
  associatedNamespaces.add(ns);
531
557
  }
532
558
  for (const className of arg.templateArgClassNames) {
533
- addAssociatedNamespaceForClassName(className, scopes, associatedNamespaces);
559
+ addAssociatedNamespaceForClassName(className, scopes, idx, associatedNamespaces);
534
560
  }
535
561
  }
536
- function addAssociatedNamespaceForClassName(simpleClassName, scopes, associatedNamespaces) {
562
+ function addAssociatedNamespaceForClassName(simpleClassName, scopes, idx, associatedNamespaces) {
537
563
  if (simpleClassName.length === 0)
538
564
  return;
539
- const classLookup = findCppClassDefBySimpleName(simpleClassName);
565
+ const classLookup = findCppClassDefBySimpleName(idx, simpleClassName);
540
566
  if (classLookup === undefined)
541
567
  return;
542
568
  const { classDef, ambiguous } = classLookup;
@@ -629,10 +655,10 @@ function findNamespaceDefInScope(scope) {
629
655
  *
630
656
  * ISO C++ `[basic.lookup.argdep]` §2: enumerations contribute their
631
657
  * enclosing namespace to the associated set, just like class types. */
632
- function findCppClassDefBySimpleName(simpleName) {
658
+ function findCppClassDefBySimpleName(idx, simpleName) {
633
659
  // `classDefsBySimple` preserves `scopes.defs.byId` order, so `[0]` is the
634
660
  // legacy first-match and `length > 1` is the legacy `ambiguous` flag.
635
- const matches = adlIndex?.classDefsBySimple.get(simpleName);
661
+ const matches = idx.classDefsBySimple.get(simpleName);
636
662
  if (matches === undefined)
637
663
  return undefined;
638
664
  const first = matches[0];
@@ -644,10 +670,7 @@ function findCppClassDefBySimpleName(simpleName) {
644
670
  * Contribute associated namespaces for a function-reference argument by walking
645
671
  * the referenced overload set's parameter and return types.
646
672
  */
647
- function collectFunctionTypeAssociatedNamespaces(refText, scopes, out) {
648
- const idx = adlIndex;
649
- if (idx === undefined)
650
- return;
673
+ function collectFunctionTypeAssociatedNamespaces(refText, scopes, idx, out) {
651
674
  const colonIdx = refText.lastIndexOf('::');
652
675
  if (colonIdx !== -1) {
653
676
  // Qualified ref: extract namespace prefix and normalise :: → dot notation.
@@ -662,7 +685,7 @@ function collectFunctionTypeAssociatedNamespaces(refText, scopes, out) {
662
685
  const matches = idx.nsFunctionsByQName.get(nsText)?.get(simpleName);
663
686
  if (matches !== undefined) {
664
687
  for (const def of matches)
665
- collectAssociatedNamespacesForFunctionDef(def, scopes, out);
688
+ collectAssociatedNamespacesForFunctionDef(def, scopes, idx, out);
666
689
  }
667
690
  return;
668
691
  }
@@ -673,23 +696,23 @@ function collectFunctionTypeAssociatedNamespaces(refText, scopes, out) {
673
696
  const matches = idx.nsFunctionsBySimple.get(refText);
674
697
  if (matches !== undefined) {
675
698
  for (const def of matches)
676
- collectAssociatedNamespacesForFunctionDef(def, scopes, out);
699
+ collectAssociatedNamespacesForFunctionDef(def, scopes, idx, out);
677
700
  }
678
701
  }
679
- function collectAssociatedNamespacesForFunctionDef(def, scopes, out) {
702
+ function collectAssociatedNamespacesForFunctionDef(def, scopes, idx, out) {
680
703
  const parameterTypes = def.parameterTypeClasses?.map((typeClass) => typeClass.base);
681
704
  for (const paramType of parameterTypes ?? def.parameterTypes ?? []) {
682
- collectAssociatedNamespacesForFunctionTypeText(paramType, scopes, out);
705
+ collectAssociatedNamespacesForFunctionTypeText(paramType, scopes, idx, out);
683
706
  }
684
707
  if (def.returnType !== undefined) {
685
- collectAssociatedNamespacesForFunctionTypeText(def.returnType, scopes, out);
708
+ collectAssociatedNamespacesForFunctionTypeText(def.returnType, scopes, idx, out);
686
709
  }
687
710
  }
688
- function collectAssociatedNamespacesForFunctionTypeText(typeText, scopes, out) {
711
+ function collectAssociatedNamespacesForFunctionTypeText(typeText, scopes, idx, out) {
689
712
  for (const token of extractCppTypeNameTokens(typeText)) {
690
713
  if (isIgnoredCppAdlNamespace(token.namespaceName))
691
714
  continue;
692
- addAssociatedNamespaceForClassName(token.simpleName, scopes, out);
715
+ addAssociatedNamespaceForClassName(token.simpleName, scopes, idx, out);
693
716
  if (token.namespaceName !== '')
694
717
  out.add(token.namespaceName);
695
718
  }
@@ -14,12 +14,14 @@
14
14
  * 2. **Transitive qualified visibility.** `outer::foo()` resolves to
15
15
  * `outer::v1::foo()` when `v1` is inline. The qualified-namespace
16
16
  * receiver resolver (`resolveCppQualifiedNamespaceMember`) walks
17
- * inline-namespace children transitively when collecting candidates.
17
+ * inline-namespace children transitively when collecting candidates —
18
+ * once per pipeline run, into {@link QualifiedNsMemberIndex} (#2788).
18
19
  *
19
20
  * State lifecycle: capture-time `markCppInlineNamespaceRange` records each
20
21
  * inline namespace's source range; `populateCppInlineNamespaceScopes`
21
22
  * resolves ranges to `ScopeId`s during `populateOwners`. Cleared via
22
- * `clearCppInlineNamespaces`, called from `clearFileLocalNames`.
23
+ * `clearCppInlineNamespaces`, called from
24
+ * `cppScopeResolver.loadResolutionConfig` at the start of every pass.
23
25
  *
24
26
  * STL idiom this enables: `std::__1::vector` (libc++) and `std::__cxx11`
25
27
  * (libstdc++) are inline namespaces of `std`. With this support,
@@ -46,7 +48,14 @@ export declare function markCppInlineNamespaceRange(filePath: string, range: Ran
46
48
  export declare function collectCppInlineNamespaceSideChannel(filePath: string): readonly string[];
47
49
  /** Restore this file's captured inline-namespace ranges from the side-channel. */
48
50
  export declare function applyCppInlineNamespaceSideChannel(filePath: string, ranges: readonly string[]): void;
49
- /** Clear all inline-namespace state. Called from `clearFileLocalNames`. */
51
+ /** Clear all inline-namespace state. Called from
52
+ * `cppScopeResolver.loadResolutionConfig` at the start of every pass.
53
+ *
54
+ * The qualified-namespace index is dropped by REASSIGNING a fresh `WeakMap`
55
+ * (`WeakMap` has no `.clear()`) so its entries — and the `SymbolDefinition`
56
+ * references they hold — are released here rather than waiting on the key.
57
+ * Correctness does not rest on that: {@link inlineNamespaceEpoch} invalidates
58
+ * any surviving entry. */
50
59
  export declare function clearCppInlineNamespaces(): void;
51
60
  /** Resolve captured ranges to actual ScopeIds by matching scope ranges
52
61
  * against the inline-namespace ranges recorded for this file. Run from
@@ -59,17 +68,28 @@ export declare function populateCppInlineNamespaceScopes(parsed: ParsedFile): vo
59
68
  * namespace's level). */
60
69
  export declare function isCppInlineNamespaceScope(scopeId: ScopeId): boolean;
61
70
  /**
62
- * Walk every parsed file looking for a Namespace scope whose qualified
63
- * name matches `receiverName`, collect its callable ownedDefs matching
64
- * `memberName`, transitively descending into any inline-namespace
65
- * children (since they're members of the enclosing namespace under ISO
66
- * C++).
71
+ * Find the Namespace scopes whose simple name matches `receiverName` and
72
+ * return their callable members matching `memberName`, transitively
73
+ * including inline-namespace children (since they're members of the
74
+ * enclosing namespace under ISO C++). Served from a per-pipeline index
75
+ * ({@link QualifiedNsMemberIndex}), not a per-call-site workspace scan.
67
76
  *
68
77
  * Returns the most specific (innermost) match — for `outer::foo()`
69
78
  * where `inline namespace v1` declares `foo`, returns `v1::foo`. When
70
79
  * multiple inline-namespace children declare the same name, ISO C++
71
80
  * leaves the call ambiguous; returns `'ambiguous'` so the caller
72
81
  * suppresses edge emission rather than picking arbitrarily (#1564).
82
+ *
83
+ * Two production call sites, both in `scope-resolver.ts`:
84
+ * - `resolveQualifiedReceiverMember` — the `outer::foo()` qualified-receiver
85
+ * hook. Passes `callsite`, so a multi-candidate set can be narrowed by
86
+ * arity and argument types.
87
+ * - `resolveAdlCandidates` — resolving a `using ns::name;` named import back
88
+ * to its namespace member, for template-class method bodies where the
89
+ * lexical walk misses that visibility. Passes NO `callsite`, which makes
90
+ * the narrowing filters below pass-throughs: on that path any receiver
91
+ * whose member has more than one candidate returns `'ambiguous'` (and the
92
+ * caller then skips it) rather than being disambiguated.
73
93
  */
74
94
  export declare function resolveCppQualifiedNamespaceMember(receiverName: string, memberName: string, parsedFiles: readonly ParsedFile[], _scopes: ScopeResolutionIndexes, callsite?: Callsite): SymbolDefinition | 'ambiguous' | undefined;
75
95
  export {};
@@ -14,12 +14,14 @@
14
14
  * 2. **Transitive qualified visibility.** `outer::foo()` resolves to
15
15
  * `outer::v1::foo()` when `v1` is inline. The qualified-namespace
16
16
  * receiver resolver (`resolveCppQualifiedNamespaceMember`) walks
17
- * inline-namespace children transitively when collecting candidates.
17
+ * inline-namespace children transitively when collecting candidates —
18
+ * once per pipeline run, into {@link QualifiedNsMemberIndex} (#2788).
18
19
  *
19
20
  * State lifecycle: capture-time `markCppInlineNamespaceRange` records each
20
21
  * inline namespace's source range; `populateCppInlineNamespaceScopes`
21
22
  * resolves ranges to `ScopeId`s during `populateOwners`. Cleared via
22
- * `clearCppInlineNamespaces`, called from `clearFileLocalNames`.
23
+ * `clearCppInlineNamespaces`, called from
24
+ * `cppScopeResolver.loadResolutionConfig` at the start of every pass.
23
25
  *
24
26
  * STL idiom this enables: `std::__1::vector` (libc++) and `std::__cxx11`
25
27
  * (libstdc++) are inline namespaces of `std`. With this support,
@@ -27,9 +29,16 @@
27
29
  * declaration transparently.
28
30
  */
29
31
  import { isOverloadAmbiguousAfterNormalization, narrowOverloadCandidates, } from '../../scope-resolution/passes/overload-narrowing.js';
32
+ import { isOverloadableCallable } from '../../utils/callable-labels.js';
33
+ import { isSemanticModelValidatorEnabled } from '../../utils/env.js';
30
34
  import { CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES, cppConversionRank } from './conversion-rank.js';
31
35
  const inlineNamespaceRangesByFile = new Map();
32
36
  const inlineNamespaceScopeIds = new Set();
37
+ /** Bumped by every writer of {@link inlineNamespaceScopeIds}. The qualified-ns
38
+ * index is a function of both `parsedFiles` and that Set; its WeakMap key sees
39
+ * only the first, so the memo stamps this epoch and a missed
40
+ * {@link clearCppInlineNamespaces} degrades to a rebuild, not a stale answer. */
41
+ let inlineNamespaceEpoch = 0;
33
42
  function rangeKey(r) {
34
43
  return `${r.startLine}:${r.startCol}:${r.endLine}:${r.endCol}`;
35
44
  }
@@ -65,10 +74,19 @@ export function applyCppInlineNamespaceSideChannel(filePath, ranges) {
65
74
  for (const r of ranges)
66
75
  set.add(r);
67
76
  }
68
- /** Clear all inline-namespace state. Called from `clearFileLocalNames`. */
77
+ /** Clear all inline-namespace state. Called from
78
+ * `cppScopeResolver.loadResolutionConfig` at the start of every pass.
79
+ *
80
+ * The qualified-namespace index is dropped by REASSIGNING a fresh `WeakMap`
81
+ * (`WeakMap` has no `.clear()`) so its entries — and the `SymbolDefinition`
82
+ * references they hold — are released here rather than waiting on the key.
83
+ * Correctness does not rest on that: {@link inlineNamespaceEpoch} invalidates
84
+ * any surviving entry. */
69
85
  export function clearCppInlineNamespaces() {
70
86
  inlineNamespaceRangesByFile.clear();
71
87
  inlineNamespaceScopeIds.clear();
88
+ inlineNamespaceEpoch++;
89
+ qualifiedNsIndexByPass = new WeakMap();
72
90
  }
73
91
  /** Resolve captured ranges to actual ScopeIds by matching scope ranges
74
92
  * against the inline-namespace ranges recorded for this file. Run from
@@ -78,6 +96,7 @@ export function populateCppInlineNamespaceScopes(parsed) {
78
96
  const ranges = inlineNamespaceRangesByFile.get(parsed.filePath);
79
97
  if (ranges === undefined || ranges.size === 0)
80
98
  return;
99
+ inlineNamespaceEpoch++;
81
100
  for (const scope of parsed.scopes) {
82
101
  if (scope.kind !== 'Namespace')
83
102
  continue;
@@ -94,45 +113,234 @@ export function isCppInlineNamespaceScope(scopeId) {
94
113
  return inlineNamespaceScopeIds.has(scopeId);
95
114
  }
96
115
  /**
97
- * Walk every parsed file looking for a Namespace scope whose qualified
98
- * name matches `receiverName`, collect its callable ownedDefs matching
99
- * `memberName`, transitively descending into any inline-namespace
100
- * children (since they're members of the enclosing namespace under ISO
101
- * C++).
116
+ * Dev/test-only mutation tripwire on the arrays this module shares across call
117
+ * sites ({@link NO_DEFS} and every `membersByReceiver` memo entry): an in-place
118
+ * `.sort()`/`.splice()` ever added to `overload-narrowing.ts` would corrupt
119
+ * every LATER resolution of the same pair, not just its own call, and freezing
120
+ * makes that a loud `TypeError` instead. DEV-ONLY because the types already
121
+ * reject it — the memo and `narrowOverloadCandidates`' parameter are both
122
+ * `readonly SymbolDefinition[]`, so only a cast gets past — while the freeze is
123
+ * not free: V8 moves a frozen array to `PACKED_FROZEN_ELEMENTS`, off the
124
+ * builtin fast path for the `.filter`/`.map`/`.some` narrowing runs over it at
125
+ * every multi-candidate call site (measured 4.6×; large bench arm 100.3 → 70.6ms
126
+ * with it off). Gated on `isSemanticModelValidatorEnabled()` — the OPT-IN form,
127
+ * not `adl.ts`'s opt-out `NODE_ENV !== 'production'`: `NODE_ENV` is unset in a
128
+ * CLI `analyze`, so the opt-out form would keep paying the freeze exactly where
129
+ * the cost lands. Read once at load because this one is per-call-site.
130
+ */
131
+ const FREEZE_SHARED_CANDIDATES = isSemanticModelValidatorEnabled();
132
+ /** Shared empties: most namespace scopes declare no callables, and most
133
+ * qualified receivers name no namespace at all. `NO_DEFS` is process-wide, so
134
+ * it gets the same dev-only freeze the memoized arrays do. */
135
+ const NO_MEMBERS = new Map();
136
+ const NO_DEFS = FREEZE_SHARED_CANDIDATES ? Object.freeze([]) : [];
137
+ /** Simple (last-segment) name of a def, matching the legacy scan exactly —
138
+ * including the empty-string fallback for a def with no `qualifiedName`, and
139
+ * the empty last segment of a trailing-dot name. `lastIndexOf` + `slice`
140
+ * rather than `split('.').pop()`: same result, no intermediate array, and this
141
+ * runs per callable def and per namespace scope at build. */
142
+ function simpleNameOf(def) {
143
+ const qualified = def.qualifiedName;
144
+ if (qualified == null)
145
+ return '';
146
+ const lastDot = qualified.lastIndexOf('.');
147
+ return lastDot === -1 ? qualified : qualified.slice(lastDot + 1);
148
+ }
149
+ /**
150
+ * Per-pass memo: `parsedFiles` array identity → the built index. `WeakMap`-keyed
151
+ * so the entry lives only as long as the caller's `parsedFiles` array does, and
152
+ * is reclaimed with the pass — mirrors `moduleScopeIndexByPass` in
153
+ * `file-local-linkage.ts`.
102
154
  *
103
- * Returns the most specific (innermost) match — for `outer::foo()`
104
- * where `inline namespace v1` declares `foo`, returns `v1::foo`. When
105
- * multiple inline-namespace children declare the same name, ISO C++
106
- * leaves the call ambiguous; returns `'ambiguous'` so the caller
107
- * suppresses edge emission rather than picking arbitrarily (#1564).
155
+ * Weak keying is load-bearing, not stylistic: the index holds
156
+ * `SymbolDefinition` references reaching into every ParsedFile's scopes, so a
157
+ * module-level strong `let` pair (index + source array) kept the whole C/C++
158
+ * ParsedFile set alive past the point `scope-resolution/pipeline/phase.ts`
159
+ * evicts `files`/`contents`/`preExtractedByPath` and calls `forceGc()` to
160
+ * reclaim it — C++ is 7th of 16 `SCOPE_RESOLVERS` entries, so the retention
161
+ * survived nine later language passes plus emit (104.2 MB measured).
108
162
  */
109
- export function resolveCppQualifiedNamespaceMember(receiverName, memberName, parsedFiles, _scopes, callsite) {
110
- const allHits = [];
111
- const seenNodeId = new Set();
163
+ let qualifiedNsIndexByPass = new WeakMap();
164
+ /** Build the index in two linear passes per file: one to make a node per
165
+ * `Namespace` scope, one to link inline children and register receivers.
166
+ * Linking needs both endpoints to exist and `parsed.scopes` does not promise
167
+ * parents precede children, hence two passes rather than one — but only the
168
+ * first pass filters `parsed.scopes`; it hands the second the `[scope, node]`
169
+ * pairs it already found. */
170
+ function buildQualifiedNsMemberIndex(parsedFiles) {
171
+ const rootsByReceiver = new Map();
112
172
  for (const parsed of parsedFiles) {
113
- const scopesById = new Map();
114
- for (const sc of parsed.scopes)
115
- scopesById.set(sc.id, sc);
116
- for (const scope of parsed.scopes) {
117
- if (scope.kind !== 'Namespace')
173
+ const nodesByScope = new Map();
174
+ const namespaces = [];
175
+ for (const sc of parsed.scopes) {
176
+ if (sc.kind !== 'Namespace')
118
177
  continue;
119
- const nsDef = findNamespaceDefInScope(scope);
178
+ const node = { ownMembers: bucketOwnMembers(sc), inlineChildren: [] };
179
+ nodesByScope.set(sc.id, node);
180
+ namespaces.push([sc, node]);
181
+ }
182
+ for (const [sc, node] of namespaces) {
183
+ // A non-`Namespace` parent (a Module scope, say) has no node, so the
184
+ // inline child links to nothing — hence the `?.` below.
185
+ if (sc.parent !== null && inlineNamespaceScopeIds.has(sc.id)) {
186
+ nodesByScope.get(sc.parent)?.inlineChildren.push(node);
187
+ }
188
+ const nsDef = findNamespaceDefInScope(sc);
120
189
  if (nsDef === undefined)
121
190
  continue;
122
- const nsName = nsDef.qualifiedName?.split('.').pop() ?? nsDef.qualifiedName ?? '';
123
- if (nsName !== receiverName)
124
- continue;
125
- // Found a matching namespace scope in this file. Collect ALL
126
- // members transitively through any inline-namespace children.
127
- const hits = findMemberInNamespaceTransitive(scope, scopesById, memberName);
128
- for (const hit of hits) {
129
- if (seenNodeId.has(hit.nodeId))
130
- continue;
131
- seenNodeId.add(hit.nodeId);
132
- allHits.push(hit);
191
+ const nsName = simpleNameOf(nsDef);
192
+ let roots = rootsByReceiver.get(nsName);
193
+ if (roots === undefined) {
194
+ roots = [];
195
+ rootsByReceiver.set(nsName, roots);
133
196
  }
197
+ roots.push(node);
198
+ }
199
+ }
200
+ return { rootsByReceiver, membersByReceiver: new Map() };
201
+ }
202
+ /** Bucket a namespace scope's own callable `ownedDefs` by member simple name.
203
+ * No descent: inline children are separate nodes reached by the walk. */
204
+ function bucketOwnMembers(scope) {
205
+ let members;
206
+ for (const def of scope.ownedDefs) {
207
+ if (!isOverloadableCallable(def.type))
208
+ continue;
209
+ const simple = simpleNameOf(def);
210
+ members ??= new Map();
211
+ let arr = members.get(simple);
212
+ if (arr === undefined) {
213
+ arr = [];
214
+ members.set(simple, arr);
215
+ }
216
+ arr.push(def);
217
+ }
218
+ return members ?? NO_MEMBERS;
219
+ }
220
+ /** Candidates for `receiver::member`, memoized on the index per pair so a
221
+ * repeated call site costs two Map lookups. */
222
+ function qualifiedNsMembers(index, receiverName, memberName) {
223
+ const roots = index.rootsByReceiver.get(receiverName);
224
+ // No namespace by that name — the most common outcome in real source. Left
225
+ // unmemoized deliberately: it is already O(1), and memoizing would grow the
226
+ // index by an entry per unresolved receiver name in the workspace.
227
+ if (roots === undefined)
228
+ return NO_DEFS;
229
+ let byMember = index.membersByReceiver.get(receiverName);
230
+ if (byMember === undefined) {
231
+ byMember = new Map();
232
+ index.membersByReceiver.set(receiverName, byMember);
233
+ }
234
+ let hits = byMember.get(memberName);
235
+ if (hits === undefined) {
236
+ // Memoization makes this array shared by every call site in the pass rather
237
+ // than rebuilt per call, so it carries the dev-only mutation tripwire (see
238
+ // {@link FREEZE_SHARED_CANDIDATES}).
239
+ hits = gatherQualifiedNsMember(roots, memberName);
240
+ if (FREEZE_SHARED_CANDIDATES)
241
+ hits = Object.freeze(hits);
242
+ byMember.set(memberName, hits);
243
+ }
244
+ return hits;
245
+ }
246
+ /** Pre-order depth-first walk of every scope named `receiverName` and its
247
+ * transitive inline-namespace children, collecting callables named
248
+ * `memberName` — the index twin of the legacy
249
+ * `findMemberInNamespaceTransitive`.
250
+ *
251
+ * Iterative, not recursive: nesting depth is whatever the input file says, and
252
+ * a recursive descent threw `RangeError` past ~8k levels (see
253
+ * {@link QualifiedNsMemberIndex}). */
254
+ function gatherQualifiedNsMember(roots, memberName) {
255
+ const hits = [];
256
+ const stack = [];
257
+ // `visited` spans ALL roots, reproducing the legacy per-receiver `seenNodeId`
258
+ // dedup: `namespace ns { inline namespace ns { … } }` registers both scopes
259
+ // under receiver `ns`, and the outer walk already collected the inner's defs.
260
+ // Node identity is equivalent to the legacy nodeId key because a node always
261
+ // contributes the same defs. It doubles as the guard that a malformed parent
262
+ // cycle terminates instead of spinning — the one regression an explicit stack
263
+ // could otherwise introduce over recursion's `RangeError`.
264
+ const visited = new Set();
265
+ for (const root of roots) {
266
+ stack.push(root);
267
+ while (stack.length > 0) {
268
+ const node = stack.pop();
269
+ if (visited.has(node))
270
+ continue;
271
+ visited.add(node);
272
+ const own = node.ownMembers.get(memberName);
273
+ if (own !== undefined)
274
+ for (const def of own)
275
+ hits.push(def);
276
+ // Reverse push so children pop in declaration order — the legacy walk's
277
+ // pre-order depth-first candidate order (see
278
+ // {@link QualifiedNsMemberIndex.rootsByReceiver}).
279
+ const kids = node.inlineChildren;
280
+ for (let i = kids.length - 1; i >= 0; i--)
281
+ stack.push(kids[i]);
134
282
  }
135
283
  }
284
+ return hits.length === 0 ? NO_DEFS : hits;
285
+ }
286
+ /** Build the index on first use of a given `parsedFiles` set; reuse it for
287
+ * every subsequent call site that passes the same array reference AND the same
288
+ * {@link inlineNamespaceEpoch}.
289
+ *
290
+ * Both keys are needed because the index is a function of TWO inputs:
291
+ * `parsedFiles` and the module-level `inlineNamespaceScopeIds` (which inline
292
+ * children get descended into). A later pass may hand back the same
293
+ * `parsedFiles` reference with different inline-namespace state, so identity
294
+ * alone would serve a stale index — the epoch is what makes that a checked
295
+ * rebuild instead of a documented obligation on the clear site.
296
+ *
297
+ * Lifetime: the entry is reachable only while the caller still holds the
298
+ * `parsedFiles` array; nothing here pins it (see {@link qualifiedNsIndexByPass}).
299
+ *
300
+ * Same STALENESS contract as `ensureAdlIndex` in `adl.ts`, minus that
301
+ * sibling's dev-gated `validateAdlSeqCoverage` guard: this index has no `?? 0`
302
+ * defaulting read to silently collide on, so there is nothing for such a guard
303
+ * to catch. */
304
+ function qualifiedNsMemberIndex(parsedFiles) {
305
+ const memo = qualifiedNsIndexByPass.get(parsedFiles);
306
+ if (memo !== undefined && memo.epoch === inlineNamespaceEpoch)
307
+ return memo.index;
308
+ const index = buildQualifiedNsMemberIndex(parsedFiles);
309
+ qualifiedNsIndexByPass.set(parsedFiles, { epoch: inlineNamespaceEpoch, index });
310
+ return index;
311
+ }
312
+ /** Constant narrowing hooks for the C++ qualified-receiver path — hoisted so
313
+ * the literal is not reallocated at every multi-candidate call site. */
314
+ const CPP_NARROWING_HOOKS = {
315
+ conversionRankFn: cppConversionRank,
316
+ conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES,
317
+ };
318
+ /**
319
+ * Find the Namespace scopes whose simple name matches `receiverName` and
320
+ * return their callable members matching `memberName`, transitively
321
+ * including inline-namespace children (since they're members of the
322
+ * enclosing namespace under ISO C++). Served from a per-pipeline index
323
+ * ({@link QualifiedNsMemberIndex}), not a per-call-site workspace scan.
324
+ *
325
+ * Returns the most specific (innermost) match — for `outer::foo()`
326
+ * where `inline namespace v1` declares `foo`, returns `v1::foo`. When
327
+ * multiple inline-namespace children declare the same name, ISO C++
328
+ * leaves the call ambiguous; returns `'ambiguous'` so the caller
329
+ * suppresses edge emission rather than picking arbitrarily (#1564).
330
+ *
331
+ * Two production call sites, both in `scope-resolver.ts`:
332
+ * - `resolveQualifiedReceiverMember` — the `outer::foo()` qualified-receiver
333
+ * hook. Passes `callsite`, so a multi-candidate set can be narrowed by
334
+ * arity and argument types.
335
+ * - `resolveAdlCandidates` — resolving a `using ns::name;` named import back
336
+ * to its namespace member, for template-class method bodies where the
337
+ * lexical walk misses that visibility. Passes NO `callsite`, which makes
338
+ * the narrowing filters below pass-throughs: on that path any receiver
339
+ * whose member has more than one candidate returns `'ambiguous'` (and the
340
+ * caller then skips it) rather than being disambiguated.
341
+ */
342
+ export function resolveCppQualifiedNamespaceMember(receiverName, memberName, parsedFiles, _scopes, callsite) {
343
+ const allHits = qualifiedNsMembers(qualifiedNsMemberIndex(parsedFiles), receiverName, memberName);
136
344
  if (allHits.length === 0)
137
345
  return undefined;
138
346
  if (allHits.length === 1)
@@ -142,12 +350,7 @@ export function resolveCppQualifiedNamespaceMember(receiverName, memberName, par
142
350
  // can disambiguate via exact-type match and, when available, conversion-rank
143
351
  // scoring (`cppConversionRank`). Same-signature ambiguity is still detected
144
352
  // by `isOverloadAmbiguousAfterNormalization` below.
145
- const narrowed = narrowOverloadCandidates(allHits, callsite?.arity, callsite?.argumentTypes, callsite !== undefined
146
- ? {
147
- conversionRankFn: cppConversionRank,
148
- conversionOnlyArgTypePrefixes: CPP_CONVERSION_ONLY_ARG_TYPE_PREFIXES,
149
- }
150
- : undefined);
353
+ const narrowed = narrowOverloadCandidates(allHits, callsite?.arity, callsite?.argumentTypes, callsite !== undefined ? CPP_NARROWING_HOOKS : undefined);
151
354
  if (narrowed.length === 1)
152
355
  return narrowed[0];
153
356
  if (narrowed.length === 0)
@@ -158,35 +361,6 @@ export function resolveCppQualifiedNamespaceMember(receiverName, memberName, par
158
361
  // suppress because we lack call-site info to disambiguate.
159
362
  return 'ambiguous';
160
363
  }
161
- /** Recursively search a namespace scope and any inline-namespace
162
- * descendants for callable defs with the given simple name. Non-inline
163
- * nested namespaces are NOT traversed — they require explicit
164
- * qualification (`outer::nested::foo`). Returns ALL matches so the
165
- * caller can detect same-name ambiguity across inline children (#1564). */
166
- function findMemberInNamespaceTransitive(scope, scopesById, memberName) {
167
- const results = [];
168
- // Check this scope's own ownedDefs first.
169
- for (const def of scope.ownedDefs) {
170
- if (def.type !== 'Function' && def.type !== 'Method' && def.type !== 'Constructor')
171
- continue;
172
- const simple = def.qualifiedName?.split('.').pop() ?? def.qualifiedName ?? '';
173
- if (simple === memberName)
174
- results.push(def);
175
- }
176
- // Descend into inline-namespace children.
177
- for (const childScope of scopesById.values()) {
178
- if (childScope.parent !== scope.id)
179
- continue;
180
- if (childScope.kind !== 'Namespace')
181
- continue;
182
- if (!inlineNamespaceScopeIds.has(childScope.id))
183
- continue;
184
- const childHits = findMemberInNamespaceTransitive(childScope, scopesById, memberName);
185
- for (const hit of childHits)
186
- results.push(hit);
187
- }
188
- return results;
189
- }
190
364
  function findNamespaceDefInScope(scope) {
191
365
  for (const def of scope.ownedDefs) {
192
366
  if (def.type === 'Namespace')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.146",
3
+ "version": "1.6.10-rc.147",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",