From 5d8dbcfb52f2cf4513a8aed6ab50b9a76880c1fd Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:24:47 -0700 Subject: [PATCH] Compare names before symbols when matching flow references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrowing a reference walks back through every assignment and variable declaration on its flow path, and asks whether each one assigns the reference. For a declaration target that meant loading the declaration's symbol and looking up its late-bound and merged symbols, which is mostly cache misses on unrelated symbols. In long functions this dominated flow analysis. A declaration's symbol is named by its identifier, so a reference with a different name can never match it. Comparing names first skips the symbol work for nearly every unrelated declaration. --checkers 4, n=8, default GC settings: │ before │ after │ │ sec/op │ sec/op vs base │ Tsc/bluesky 2.767 ± 1% 2.752 ± 2% ~ (p=0.314 n=8) Tsc/sentry 10.78 ± 2% 10.76 ± 2% ~ (p=0.701 n=8) Tsc/t3code-server 13.50 ± 4% 12.57 ± 1% -6.85% (p=0.000 n=8) geomean 7.384 7.194 -2.58% │ before │ after │ │ rss-B │ rss-B vs base │ Tsc/bluesky 1.183Gi ± 2% 1.181Gi ± 3% ~ (p=0.878 n=8) Tsc/sentry 3.549Gi ± 6% 3.481Gi ± 5% ~ (p=0.721 n=8) Tsc/t3code-server 2.880Gi ± 2% 2.849Gi ± 2% ~ (p=0.279 n=8) geomean 2.296Gi 2.271Gi -1.06% │ before │ after │ │ heap-B │ heap-B vs base │ Tsc/bluesky 934.5Mi ± 0% 934.8Mi ± 0% +0.04% (p=0.031 n=8) Tsc/sentry 2.608Gi ± 0% 2.608Gi ± 0% ~ (p=0.505 n=8) Tsc/t3code-server 2.301Gi ± 0% 2.302Gi ± 0% ~ (p=0.328 n=8) geomean 1.763Gi 1.763Gi +0.03% --- tsc/internal/checker/flow.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tsc/internal/checker/flow.go b/tsc/internal/checker/flow.go index d2831231772fe..4967fcbe4ec16 100644 --- a/tsc/internal/checker/flow.go +++ b/tsc/internal/checker/flow.go @@ -1610,8 +1610,19 @@ func (c *Checker) isMatchingReference(source *ast.Node, target *ast.Node) bool { if ast.IsThisInTypeQuery(source) { return target.Kind == ast.KindThisKeyword } - return ast.IsIdentifier(target) && c.getResolvedSymbol(source) == c.getResolvedSymbol(target) || - (ast.IsVariableDeclaration(target) || ast.IsBindingElement(target)) && c.getExportSymbolOfValueSymbolIfExported(c.getResolvedSymbol(source)) == c.getSymbolOfDeclaration(target) + if ast.IsIdentifier(target) { + return c.getResolvedSymbol(source) == c.getResolvedSymbol(target) + } + if ast.IsVariableDeclaration(target) || ast.IsBindingElement(target) { + // A declaration's symbol is named by its identifier, so a reference with a + // different name cannot resolve to it. Checking names first avoids loading + // the symbols of every unrelated declaration seen while walking flow nodes. + if name := target.Name(); name != nil && ast.IsIdentifier(name) && name.AsIdentifier().Text != source.Text() { + return false + } + return c.getExportSymbolOfValueSymbolIfExported(c.getResolvedSymbol(source)) == c.getSymbolOfDeclaration(target) + } + return false case ast.KindThisKeyword: return target.Kind == ast.KindThisKeyword case ast.KindSuperKeyword: