From c8869df6c5036df97c022384b920dcfe698bbfc6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:10:04 -0700 Subject: [PATCH 1/2] init --- internal/checker/checker.go | 1 + internal/checker/symbolaccessibility.go | 81 +++++++++++++++++++++---- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 940d0f98f8..20e6542024 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -640,6 +640,7 @@ type Checker struct { errorTypes map[CacheHashKey]*Type moduleSymbols map[*ast.Node]*ast.Symbol globalThisSymbol *ast.Symbol + symbolTableAliasCache map[symbolTableID][]*ast.Symbol resolveName func(location *ast.Node, name string, meaning ast.SymbolFlags, nameNotFoundMessage *diagnostics.Message, isUse bool, excludeGlobals bool) *ast.Symbol resolveNameForSymbolSuggestion func(location *ast.Node, name string, meaning ast.SymbolFlags, nameNotFoundMessage *diagnostics.Message, isUse bool, excludeGlobals bool) *ast.Symbol tupleTypes map[CacheHashKey]*Type diff --git a/internal/checker/symbolaccessibility.go b/internal/checker/symbolaccessibility.go index 8c749b55ea..57275a7741 100644 --- a/internal/checker/symbolaccessibility.go +++ b/internal/checker/symbolaccessibility.go @@ -478,19 +478,57 @@ func (c *Checker) getAccessibleSymbolChainFromSymbolTable(ctx accessibleSymbolCh } visitedSymbolTables[tableId] = struct{}{} - res := c.trySymbolTable(ctx, t, tableId == stKindGlobals, ignoreQualification, isLocalNameLookup) + res := c.trySymbolTable(ctx, t, tableId, ignoreQualification, isLocalNameLookup) delete(visitedSymbolTables, tableId) return res } +// stKindMask extracts the kind bits (top 2 bits) from a symbolTableID. +const stKindMask symbolTableID = 3 << 62 + +// getSymbolTableAliases returns only the alias symbols from a symbol table, +// caching the result by tableId to avoid repeated iteration over large tables. +// Members tables are skipped entirely since someSymbolTableInScope filters them +// to SymbolFlagsType & ^SymbolFlagsAssignment, which never includes aliases. +// Locals tables are small and per-scope, so they are filtered but not cached. +func (c *Checker) getSymbolTableAliases(symbols ast.SymbolTable, tableId symbolTableID) []*ast.Symbol { + kind := tableId & stKindMask + // Members tables never contain alias symbols; skip entirely. + if kind == stKindMembers { + return nil + } + // Globals and exports tables are large and revisited often; use cache. + if kind == stKindGlobals || kind == stKindExports { + if c.symbolTableAliasCache != nil { + if aliases, ok := c.symbolTableAliasCache[tableId]; ok { + return aliases + } + } + } + var aliases []*ast.Symbol + for _, sym := range symbols { + if sym.Flags&ast.SymbolFlagsAlias != 0 { + aliases = append(aliases, sym) + } + } + if kind == stKindGlobals || kind == stKindExports { + if c.symbolTableAliasCache == nil { + c.symbolTableAliasCache = make(map[symbolTableID][]*ast.Symbol) + } + c.symbolTableAliasCache[tableId] = aliases + } + return aliases +} + func (c *Checker) trySymbolTable( ctx accessibleSymbolChainContext, symbols ast.SymbolTable, - isGlobals bool, + tableId symbolTableID, ignoreQualification bool, isLocalNameLookup bool, ) []*ast.Symbol { + isGlobals := tableId == stKindGlobals // If symbol is directly available by its name in the symbol table res, ok := symbols[ctx.symbol.Name] if ok && res != nil && c.isAccessible(ctx, res /*resolvedAliasSymbol*/, nil, ignoreQualification) { @@ -498,11 +536,21 @@ func (c *Checker) trySymbolTable( } var candidateChains [][]*ast.Symbol - // collect all possible chains to sort them and return the shortest/best - for _, symbolFromSymbolTable := range symbols { + + // Check for ExportSymbol via direct O(1) lookup instead of inside the alias loop. + // In the original loop, this checks `symbolFromSymbolTable.Name == ctx.symbol.Name`, + // but there's at most one such symbol in the table (keyed by name). + if ok && res != nil && res.ExportSymbol != nil { + if c.isAccessible(ctx, c.getMergedSymbol(res.ExportSymbol) /*resolvedAliasSymbol*/, nil, ignoreQualification) { + candidateChains = append(candidateChains, []*ast.Symbol{ctx.symbol}) + } + } + + // Iterate only alias symbols from the table (cached per tableId). + // This avoids iterating thousands of non-alias symbols in large tables like globals. + for _, symbolFromSymbolTable := range c.getSymbolTableAliases(symbols, tableId) { // for every non-default, non-export= alias symbol in scope, check if it refers to or can chain to the target symbol - if symbolFromSymbolTable.Flags&ast.SymbolFlagsAlias != 0 && - symbolFromSymbolTable.Name != ast.InternalSymbolNameExportEquals && + if symbolFromSymbolTable.Name != ast.InternalSymbolNameExportEquals && symbolFromSymbolTable.Name != ast.InternalSymbolNameDefault && !(isUMDExportSymbol(symbolFromSymbolTable) && ctx.enclosingDeclaration != nil && ast.IsExternalModule(ast.GetSourceFileOfNode(ctx.enclosingDeclaration))) && // If `!useOnlyExternalAliasing`, we can use any type of alias to get the name @@ -518,11 +566,6 @@ func (c *Checker) trySymbolTable( candidateChains = append(candidateChains, candidate) } } - if symbolFromSymbolTable.Name == ctx.symbol.Name && symbolFromSymbolTable.ExportSymbol != nil { - if c.isAccessible(ctx, c.getMergedSymbol(symbolFromSymbolTable.ExportSymbol) /*resolvedAliasSymbol*/, nil, ignoreQualification) { - candidateChains = append(candidateChains, []*ast.Symbol{ctx.symbol}) - } - } } if len(candidateChains) > 0 { @@ -533,6 +576,22 @@ func (c *Checker) trySymbolTable( // If there's no result and we're looking at the global symbol table, treat `globalThis` like an alias and try to lookup thru that if isGlobals { + // Fast path: since globalThisSymbol.Exports shares the globals table, check if the + // symbol is directly accessible by name with ignoreQualification=true before the + // expensive recursive search through getCandidateListForSymbol. The recursive search + // would clone the globals table, iterate all symbols looking for aliases, etc. In the + // common case, the symbol IS in the globals table by name but failed the initial check + // above because canQualifySymbol failed. With ignoreQualification=true, we can find it + // directly and build the globalThis.X chain without iteration. + if ok && res != nil { + if c.isAccessible(ctx, res, nil, true /*ignoreQualification*/) { + if c.canQualifySymbol(ctx, c.globalThisSymbol, getQualifiedLeftMeaning(ctx.meaning)) { + return []*ast.Symbol{c.globalThisSymbol, ctx.symbol} + } + // If globalThis itself can't be qualified, no globalThis-prefixed path will work + return nil + } + } return c.getCandidateListForSymbol(ctx, c.globalThisSymbol, c.globalThisSymbol, ignoreQualification) } return nil From aa21bc0a851226b04c2e29ad78d63ddc84e6c313 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:36:56 -0700 Subject: [PATCH 2/2] Better solution with similar perf --- internal/checker/symbolaccessibility.go | 55 +++++++++++-------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/internal/checker/symbolaccessibility.go b/internal/checker/symbolaccessibility.go index 57275a7741..600a6988bb 100644 --- a/internal/checker/symbolaccessibility.go +++ b/internal/checker/symbolaccessibility.go @@ -397,15 +397,21 @@ type accessibleSymbolChainContext struct { } // symbolTableID uniquely identifies a symbol table by encoding its source. -// The high 2 bits encode the kind (locals, exports, members, globals), -// and the remaining bits encode the NodeId or SymbolId of the source. +// The high 3 bits encode the kind, and the remaining bits encode the +// NodeId or SymbolId of the source. type symbolTableID uint64 +const stKindShift = 61 + const ( - stKindLocals symbolTableID = iota << 62 + stKindLocals symbolTableID = iota << stKindShift stKindExports stKindMembers stKindGlobals + stKindResolvedExports // resolved/derived exports from getExportsOfSymbol, distinct from raw sym.Exports + + // stKindMask extracts the kind bits from a symbolTableID. + stKindMask symbolTableID = (iota - 1) << stKindShift ) func symbolTableIDFromLocals(node *ast.Node) symbolTableID { @@ -416,6 +422,14 @@ func symbolTableIDFromExports(sym *ast.Symbol) symbolTableID { return stKindExports | symbolTableID(ast.GetSymbolId(sym)) } +// symbolTableIDFromResolvedExports returns an ID for resolved/derived export tables +// (e.g. from getExportsOfSymbol/getExportsOfModule which may include export * resolution +// and late-bound members). This is distinct from symbolTableIDFromExports to prevent +// cache collisions with raw sym.Exports tables passed by someSymbolTableInScope. +func symbolTableIDFromResolvedExports(sym *ast.Symbol) symbolTableID { + return stKindResolvedExports | symbolTableID(ast.GetSymbolId(sym)) +} + func symbolTableIDFromMembers(sym *ast.Symbol) symbolTableID { return stKindMembers | symbolTableID(ast.GetSymbolId(sym)) } @@ -484,22 +498,19 @@ func (c *Checker) getAccessibleSymbolChainFromSymbolTable(ctx accessibleSymbolCh return res } -// stKindMask extracts the kind bits (top 2 bits) from a symbolTableID. -const stKindMask symbolTableID = 3 << 62 - // getSymbolTableAliases returns only the alias symbols from a symbol table, // caching the result by tableId to avoid repeated iteration over large tables. // Members tables are skipped entirely since someSymbolTableInScope filters them // to SymbolFlagsType & ^SymbolFlagsAssignment, which never includes aliases. -// Locals tables are small and per-scope, so they are filtered but not cached. func (c *Checker) getSymbolTableAliases(symbols ast.SymbolTable, tableId symbolTableID) []*ast.Symbol { kind := tableId & stKindMask // Members tables never contain alias symbols; skip entirely. if kind == stKindMembers { return nil } - // Globals and exports tables are large and revisited often; use cache. - if kind == stKindGlobals || kind == stKindExports { + // Cache globals and exports tables (which are large and revisited often). + // Locals tables are small and per-scope, so they are filtered but not cached. + if kind == stKindGlobals || kind == stKindExports || kind == stKindResolvedExports { if c.symbolTableAliasCache != nil { if aliases, ok := c.symbolTableAliasCache[tableId]; ok { return aliases @@ -512,7 +523,7 @@ func (c *Checker) getSymbolTableAliases(symbols ast.SymbolTable, tableId symbolT aliases = append(aliases, sym) } } - if kind == stKindGlobals || kind == stKindExports { + if kind == stKindGlobals || kind == stKindExports || kind == stKindResolvedExports { if c.symbolTableAliasCache == nil { c.symbolTableAliasCache = make(map[symbolTableID][]*ast.Symbol) } @@ -537,9 +548,9 @@ func (c *Checker) trySymbolTable( var candidateChains [][]*ast.Symbol - // Check for ExportSymbol via direct O(1) lookup instead of inside the alias loop. - // In the original loop, this checks `symbolFromSymbolTable.Name == ctx.symbol.Name`, - // but there's at most one such symbol in the table (keyed by name). + // Check for ExportSymbol by direct name lookup rather than discovering it during + // the alias iteration below (where it would never match, since only alias-flagged + // symbols are iterated). if ok && res != nil && res.ExportSymbol != nil { if c.isAccessible(ctx, c.getMergedSymbol(res.ExportSymbol) /*resolvedAliasSymbol*/, nil, ignoreQualification) { candidateChains = append(candidateChains, []*ast.Symbol{ctx.symbol}) @@ -576,22 +587,6 @@ func (c *Checker) trySymbolTable( // If there's no result and we're looking at the global symbol table, treat `globalThis` like an alias and try to lookup thru that if isGlobals { - // Fast path: since globalThisSymbol.Exports shares the globals table, check if the - // symbol is directly accessible by name with ignoreQualification=true before the - // expensive recursive search through getCandidateListForSymbol. The recursive search - // would clone the globals table, iterate all symbols looking for aliases, etc. In the - // common case, the symbol IS in the globals table by name but failed the initial check - // above because canQualifySymbol failed. With ignoreQualification=true, we can find it - // directly and build the globalThis.X chain without iteration. - if ok && res != nil { - if c.isAccessible(ctx, res, nil, true /*ignoreQualification*/) { - if c.canQualifySymbol(ctx, c.globalThisSymbol, getQualifiedLeftMeaning(ctx.meaning)) { - return []*ast.Symbol{c.globalThisSymbol, ctx.symbol} - } - // If globalThis itself can't be qualified, no globalThis-prefixed path will work - return nil - } - } return c.getCandidateListForSymbol(ctx, c.globalThisSymbol, c.globalThisSymbol, ignoreQualification) } return nil @@ -638,7 +633,7 @@ func (c *Checker) getCandidateListForSymbol( if candidateTable == nil { return nil } - candidateTableId := symbolTableIDFromExports(resolvedImportedSymbol) + candidateTableId := symbolTableIDFromResolvedExports(resolvedImportedSymbol) accessibleSymbolsFromExports := c.getAccessibleSymbolChainFromSymbolTable(ctx, candidateTable, candidateTableId /*ignoreQualification*/, true, false) if len(accessibleSymbolsFromExports) == 0 { return nil