From 17fa1a4ba4d7be2046a4ed4627fac5aa9baa20ed Mon Sep 17 00:00:00 2001 From: mathuraditya724 Date: Tue, 20 Oct 2026 16:16:56 -0700 Subject: [PATCH] perf(compiler): Reuse function reference indexing Co-Authored-By: GPT-6 --- .../frontend/lowering/var-hoisting.test.ts | 125 ++++++++++++++++++ .../src/frontend/lowering/var-hoisting.ts | 10 +- 2 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/var-hoisting.test.ts diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.test.ts b/packages/compiler/src/frontend/lowering/var-hoisting.test.ts new file mode 100644 index 0000000000..fb29e5d72b --- /dev/null +++ b/packages/compiler/src/frontend/lowering/var-hoisting.test.ts @@ -0,0 +1,125 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test, vi } from "vitest"; +import * as ast from "../ts7/ast.js"; +import * as ts from "../ts7/adapter.js"; +import { Ts7Host } from "../ts7/program-host.js"; +import { Ts7Api } from "../ts7/rpc-api.js"; +import { Lowerer } from "./lowerer.js"; +import { earlyReadVarSymbols } from "./var-hoisting.js"; + +function fixture(source: string) { + const directory = mkdtempSync(join(tmpdir(), "scriptc-hoisting-references-")); + const entry = join(directory, "main.cjs"); + writeFileSync(entry, source); + const host = new Ts7Host((options) => new Ts7Api(options), { cwd: directory }); + const program = host.createProgram([entry], { + noLib: true, + types: [], + strict: true, + allowJs: true, + checkJs: true, + }); + const file = program.getSourceFile(entry)!; + const lowerer = new Lowerer(program, file, [file]); + const early = () => { + const result = earlyReadVarSymbols(lowerer, [file]); + const names: string[] = []; + ts.walkPreorder(file, (node) => { + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) { + const symbol = lowerer.checker.getSymbolAtLocation(node.name); + if (symbol !== undefined && result.has(symbol)) names.push(node.name.text); + } + }); + return names; + }; + return { + file, + early, + dispose() { + program.dispose(); + host.close(); + rmSync(directory, { recursive: true, force: true }); + }, + }; +} + +test("whole-file reference scans do not grow with the number of functions", () => { + const fixtures = [12, 48].map((count) => + fixture( + "var value = 7;\nfunction f0() { return value; }\n" + + Array.from({ length: count }, (_, i) => `function f${i + 1}() { return f${i}(); }\n`).join( + "", + ) + + `f${count}();\n`, + ), + ); + const walk = vi.spyOn(ast, "walkPreorder"); + try { + const scans = fixtures.map((f) => { + walk.mockClear(); + expect(f.early()).toEqual([]); + return walk.mock.calls.filter(([node]) => node === f.file).length; + }); + expect(scans[1]).toBe(scans[0]); + } finally { + walk.mockRestore(); + for (const f of fixtures) f.dispose(); + } +}); + +test("reference indexing preserves shadowed function names and early calls", () => { + const f = fixture(` +function read() { return late; } +read(); +var late = 7; +var settled = 8; +function holder() { + function read() { return settled; } + return read(); +} +holder(); +`); + try { + expect(f.early()).toEqual(["late"]); + } finally { + f.dispose(); + } +}); + +test("reference indexes belong to each immutable source file snapshot", () => { + const before = fixture("var value = 7; function read() { return value; } read();\n"); + const after = fixture("read(); var value = 7; function read() { return value; }\n"); + try { + expect(before.early()).toEqual([]); + expect(after.early()).toEqual(["value"]); + expect(before.early()).toEqual([]); + } finally { + before.dispose(); + after.dispose(); + } +}); + +test("duplicate declarations and shorthand references retain symbol identity", () => { + const duplicate = fixture(` +function read() { return first; } +function read() { return second; } +read(); +var first = 7; +var second = 8; +`); + const shorthand = fixture(` +var object = { read }; +object.read(); +var value = 7; +function read() { return value; } +`); + try { + expect(duplicate.early()).toEqual(["first", "second"]); + expect(shorthand.early()).toEqual(["value"]); + } finally { + duplicate.dispose(); + shorthand.dispose(); + } +}); diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.ts b/packages/compiler/src/frontend/lowering/var-hoisting.ts index 6276ab671d..64e158a0a0 100644 --- a/packages/compiler/src/frontend/lowering/var-hoisting.ts +++ b/packages/compiler/src/frontend/lowering/var-hoisting.ts @@ -42,6 +42,7 @@ export function earlyReadVarSymbols( const bindings = new Map(); const names = new Set(); ts.walkPreorder(sf, (node) => { + if (ts.isFunctionDeclaration(node) && node.name !== undefined) names.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 +67,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, refs); for (const [symbol, binding] of bindings) { const settle = analysis.settleIndex(binding.scope, symbol); const early = (refs.get(symbol) ?? []).some( @@ -177,6 +178,7 @@ class ScopeAnalysis { constructor( private readonly lowerer: Lowerer, private readonly sf: ts.SourceFile, + private readonly references: ReadonlyMap, ) { this.js = isJsSourceFile(sf); } @@ -261,11 +263,7 @@ class ScopeAnalysis { const name = fn.name!; const symbol = this.lowerer.checker.getSymbolAtLocation(name); const refs = - symbol === undefined - ? [] - : (referencesByName(this.lowerer, this.sf, new Set([name.text])).get(symbol) ?? []).filter( - (ref) => ref !== name, - ); + symbol === undefined ? [] : (this.references.get(symbol) ?? []).filter((ref) => ref !== name); this.functionRefs.set(fn, refs); return refs; }