From 70aed0f1339e455140c436fcfc158fe3d25a72ef Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 21 Oct 2026 17:17:02 -0500 Subject: [PATCH] Walk a file once for the references of its function declarations --- .../src/frontend/lowering/var-hoisting.ts | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.ts b/packages/compiler/src/frontend/lowering/var-hoisting.ts index 6276ab671..3f950f3ca 100644 --- a/packages/compiler/src/frontend/lowering/var-hoisting.ts +++ b/packages/compiler/src/frontend/lowering/var-hoisting.ts @@ -41,7 +41,10 @@ export function earlyReadVarSymbols( if (sf.isDeclarationFile) continue; const bindings = new Map(); const names = new Set(); + const functionNames = new Set(); ts.walkPreorder(sf, (node) => { + if (ts.isFunctionDeclaration(node) && node.name !== undefined) + functionNames.add(node.name.text); if (!ts.isVariableDeclarationList(node)) return undefined; if ((node.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const | ts.NodeFlags.Using)) !== 0) return undefined; @@ -66,7 +69,7 @@ export function earlyReadVarSymbols( }); if (bindings.size === 0) continue; const refs = referencesByName(lowerer, sf, names); - const analysis = new ScopeAnalysis(lowerer, sf); + const analysis = new ScopeAnalysis(lowerer, sf, functionNames); for (const [symbol, binding] of bindings) { const settle = analysis.settleIndex(binding.scope, symbol); const early = (refs.get(symbol) ?? []).some( @@ -151,6 +154,31 @@ function referencesByName( if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node); return undefined; }); + return referencesBySymbol(lowerer, idents); +} + +/** The identifiers of `sf` named in `names`, by name, each list in source + * order. */ +function identifiersByName( + sf: ts.SourceFile, + names: ReadonlySet, +): Map { + const out = new Map(); + ts.walkPreorder(sf, (node) => { + if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined; + const list = out.get(node.text); + if (list) list.push(node); + else out.set(node.text, [node]); + return undefined; + }); + return out; +} + +/** `idents` by resolved symbol, each list in the order of `idents`. */ +function referencesBySymbol( + lowerer: Lowerer, + idents: readonly ts.Identifier[], +): Map { lowerer.checker.prefetchSymbolNodesExact(idents); const out = new Map(); for (const id of idents) { @@ -173,10 +201,14 @@ class ScopeAnalysis { * its own body names is never called through that name). */ private readonly functionMemo = new Map(); private readonly functionRefs = new Map(); + /** The file's identifiers named like one of its function declarations, + * by name, collected in one walk on first use. */ + private functionNameIdents: Map | null = null; constructor( private readonly lowerer: Lowerer, private readonly sf: ts.SourceFile, + private readonly functionNames: ReadonlySet, ) { this.js = isJsSourceFile(sf); } @@ -263,12 +295,19 @@ class ScopeAnalysis { const refs = symbol === undefined ? [] - : (referencesByName(this.lowerer, this.sf, new Set([name.text])).get(symbol) ?? []).filter( - (ref) => ref !== name, - ); + : ( + referencesBySymbol(this.lowerer, this.identifiersNamed(name.text)).get(symbol) ?? [] + ).filter((ref) => ref !== name); this.functionRefs.set(fn, refs); return refs; } + + /** The identifiers named `text`, the name of a function declaration of + * the file. */ + private identifiersNamed(text: string): readonly ts.Identifier[] { + this.functionNameIdents ??= identifiersByName(this.sf, this.functionNames); + return this.functionNameIdents.get(text) ?? []; + } } /** A loop statement that settles the binding settles the references in