perf(scope): gate namespace-call lookups and cheapen per-site drop bookkeeping

- `namespaceCallReturnType` runs for every two-part dotted binding during
  the chain follow. Skip the scope-chain lookup unless the head is a local
  name some namespace import binds (built once per run from the finalized
  import edges), and parse the name with one anchored match instead of
  split + per-part regex.
- The receiver drop gate checks the cheap hidden-by-earlier-site condition
  first and evaluates the namespace-callable test last, only for sites that
  would otherwise not be recorded.
- Per-site duplicate tracking keeps one map entry per position key (a Set
  only when distinct calls share a key) instead of two Sets and a
  concatenated key per site.
- Type `CALLABLE_TYPES` against the definition type; key the Zig namespace
  handle cache on the tree type.

Behavior is unchanged: the full lang-resolution corpus emits the identical
2,869 CALLS/ACCESSES edges, the receiver bench matches its baseline, and
the resolver and scope-resolution suites pass (3,549 tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Gergo Magyar 2026-10-21 05:36:26 +00:00 • committed by GitHub
parent 603137d7dc
commit c686065afd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 74 additions and 26 deletions

View file

@ -82,7 +82,7 @@ export function zigImportRootOf(value: SyntaxNode | null): SyntaxNode | null {
/** Module-level `const ns = @import("ns.zig")` handles on a lowercase file: a
* namespace module, not a file-struct (`@import("Page.zig")`) whose methods
* live in its struct scope. Cached per tree. */
const zigNamespaceHandlesByTree = new WeakMap<object, ReadonlySet<string>>();
const zigNamespaceHandlesByTree = new WeakMap<SyntaxNode['tree'], ReadonlySet<string>>();
function zigNamespaceHandles(node: SyntaxNode): ReadonlySet<string> {
const cached = zigNamespaceHandlesByTree.get(node.tree);
if (cached !== undefined) return cached;

View file

@ -115,7 +115,30 @@ function producerTypeRef(defId: string, scopes: ScopeResolutionIndexes): TypeRef
*/
const RECHAIN_MAX_DEPTH = 8;
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
/** `head.member`, both plain identifiers. */
const QUALIFIED_MEMBER_RE = /^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/;
/**
* Every local name a namespace import binds anywhere in the workspace. Most
* dotted bindings are member chains (`user.address`) whose head is no such
* name, so they skip the scope-chain lookup entirely. Import edges are fixed
* once finalize has run.
*/
const namespaceLocalNamesByIndexes = new WeakMap<ScopeResolutionIndexes, ReadonlySet<string>>();
function namespaceLocalNames(scopes: ScopeResolutionIndexes): ReadonlySet<string> {
let names = namespaceLocalNamesByIndexes.get(scopes);
if (names === undefined) {
const collected = new Set<string>();
for (const scope of scopes.scopeTree.byId?.values() ?? []) {
for (const edge of scopes.imports?.get(scope.id) ?? scope.imports ?? []) {
if (edge.kind === 'namespace') collected.add(edge.localName);
}
}
names = collected;
namespaceLocalNamesByIndexes.set(scopes, names);
}
return names;
}
/**
* `ns.f` where `ns` is a namespace import and `f` has a return annotation in
@ -129,9 +152,10 @@ export function namespaceCallReturnType(
scopes: ScopeResolutionIndexes,
position?: TypeRef['lookupPosition'],
): TypeRef | undefined {
const parts = rawName.split('.');
if (parts.length !== 2 || !parts.every((part) => IDENTIFIER.test(part))) return undefined;
const [head, member] = parts as [string, string];
const [, head, member] = QUALIFIED_MEMBER_RE.exec(rawName) ?? [];
if (head === undefined || member === undefined || !namespaceLocalNames(scopes).has(head)) {
return undefined;
}
const claim = lookupNameClaim(inScope, head, scopes, { position, purpose: 'value' });
if (claim.bindings.some((binding) => binding.origin === 'local')) return undefined;
const targets = new Set<ScopeId>();

View file

@ -213,7 +213,10 @@ type ReceiverBoundProviderSubset = Pick<
/** A bare, undecorated identifier and nothing else — see {@link isBareTypeName}. */
const BARE_TYPE_NAME_RE = /^[A-Za-z_$][\w$]*$/;
const CALLABLE_TYPES: ReadonlySet<string> = new Set(['Function', 'Method']);
const CALLABLE_TYPES: ReadonlySet<SymbolDefinition['type']> = new Set<SymbolDefinition['type']>([
'Function',
'Method',
]);
/**
* A type name a built-in test may be asked about: a bare, undecorated
@ -487,11 +490,12 @@ export function emitReceiverBoundCalls(
typeRef: TypeRef,
namespaceTargets: ReadonlyMap<string, readonly string[]>,
): boolean => {
const [head, member, ...rest] = typeRef.rawName.split('.');
if (head === undefined || member === undefined || rest.length > 0) return false;
return (namespaceTargets.get(head) ?? []).some((file) =>
CALLABLE_TYPES.has(lookupNamespaceMember(file, member)?.type ?? ''),
);
const [, head, member] = TWO_PART_RECEIVER_RE.exec(typeRef.rawName) ?? [];
if (head === undefined || member === undefined) return false;
return (namespaceTargets.get(head) ?? []).some((file) => {
const def = lookupNamespaceMember(file, member);
return def !== undefined && CALLABLE_TYPES.has(def.type);
});
};
// A class-like def NESTED in `owner` (`A.Item` inside `A`): its qualified
// name is the owner's plus the segment — the identity the structure phase
@ -1162,8 +1166,7 @@ export function emitReceiverBoundCalls(
// Duplicate captures of one site share a key and a member name. Distinct
// calls can share only the key (`ns.make().m()` starts where `ns.make()`
// does), so one's resolution must not hide the other's drop (#3498).
const keysSeen = new Set<string>();
const sitesSeen = new Set<string>();
const memberNamesByKey = new Map<string, string | Set<string>>();
for (const site of parsed.referenceSites) {
if (site.kind !== 'call' && site.kind !== 'read' && site.kind !== 'write') continue;
if (site.explicitReceiver === undefined) continue;
@ -1206,10 +1209,7 @@ export function emitReceiverBoundCalls(
const receiverName = site.explicitReceiver.name;
const memberName = site.name;
const siteKey = `${parsed.filePath}:${site.atRange.startLine}:${site.atRange.startCol}`;
const sameKeyEarlier = keysSeen.has(siteKey);
const siteSeenEarlier = sitesSeen.has(`${siteKey}\0${memberName}`);
keysSeen.add(siteKey);
sitesSeen.add(`${siteKey}\0${memberName}`);
const earlierAtKey = recordSiteMember(memberNamesByKey, siteKey, memberName);
if (provider.suppressReceiverLookup !== undefined) {
const baseName =
@ -3175,21 +3175,19 @@ export function emitReceiverBoundCalls(
// keyed by the MEMBER name, which is the only thing still known about a
// dropped site (its callee is unknown by definition, so the drop cannot
// be attributed to any target symbol).
// #3498: a plain receiver bound to a namespace call the analyzer could
// not type (`x = ns.untyped()`) is a drop too, not an exact miss.
const typedReceiverUnresolved =
typeRef !== undefined &&
BARE_TYPE_NAME_RE.test(receiverName) &&
bindsNamespaceCallable(typeRef, namespaceTargets);
// Every resolving case `continue`s, so a handled key here came from an
// earlier site or pre-seeding; it hides this drop only when that site
// was a duplicate capture of this one.
const hiddenByEarlierSite = handledSites.has(siteKey) && (siteSeenEarlier || !sameKeyEarlier);
const hiddenByEarlierSite = handledSites.has(siteKey) && earlierAtKey !== 'shared-key';
if (
!hiddenByEarlierSite &&
(compoundReceiverUnresolved ||
receiverNamespace !== undefined ||
typedReceiverUnresolved) &&
!hiddenByEarlierSite
// #3498: a plain receiver bound to a namespace call the analyzer
// could not type (`x = ns.untyped()`) is a drop too, not an exact miss.
(typeRef !== undefined &&
BARE_TYPE_NAME_RE.test(receiverName) &&
bindsNamespaceCallable(typeRef, namespaceTargets)))
) {
// Decoded once: both the shape census and the origin classifier read the
// same chain, and this is inside the drop guard so a resolved site pays
@ -3279,6 +3277,32 @@ function splitClassLevelMemberReceiver(
/** `Holder.repo` and nothing looser — see {@link splitClassLevelMemberReceiver}. */
const TWO_PART_RECEIVER_RE = /^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)$/;
/** How an earlier site in the same file relates to this one's position key. */
type EarlierSiteAtKey = 'new-key' | 'shared-key' | 'repeated-site';
/** Record `memberName` at `siteKey` and say what was there before: nothing, a
* different call starting at the same position, or this same member (a
* duplicate capture). One map entry per key; a Set only on a collision. */
function recordSiteMember(
memberNamesByKey: Map<string, string | Set<string>>,
siteKey: string,
memberName: string,
): EarlierSiteAtKey {
const seen = memberNamesByKey.get(siteKey);
if (seen === undefined) {
memberNamesByKey.set(siteKey, memberName);
return 'new-key';
}
if (typeof seen === 'string') {
if (seen === memberName) return 'repeated-site';
memberNamesByKey.set(siteKey, new Set([seen, memberName]));
return 'shared-key';
}
if (seen.has(memberName)) return 'repeated-site';
seen.add(memberName);
return 'shared-key';
}
/** Resolve a member by name on a class def, narrowing by argument
* types when multiple overloads share the name. Falls back to the
* first-seen def (legacy `findOwnedMember` semantics) when there's