From addecf6a02ad0729a24c11ebfd4ce9666d6b3438 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 26 Oct 2026 05:17:10 -0500 Subject: [PATCH] Skip repeated probes in lowering --- .../frontend/lowering/completion-callbacks.ts | 4 +- .../compiler/src/frontend/lowering/lowerer.ts | 85 +++++++++++++++---- packages/compiler/src/frontend/type-mapper.ts | 6 +- packages/compiler/src/ir/runtime-features.ts | 13 ++- 4 files changed, 86 insertions(+), 22 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/completion-callbacks.ts b/packages/compiler/src/frontend/lowering/completion-callbacks.ts index 1f4013d5eb..3296b3979a 100644 --- a/packages/compiler/src/frontend/lowering/completion-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/completion-callbacks.ts @@ -11,7 +11,7 @@ import { BOOL, type IrExpr, type IrModule, type IrStmt } from "../../ir/ir.js"; import type { IrLibFn } from "../../ir/builtin-signatures.js"; import { mapExprChildren, mapStmtChildren } from "../../ir/traverse.js"; -import { moduleRuntimeFeatures } from "../../ir/runtime-features.js"; +import { moduleUsesAsyncContext } from "../../ir/runtime-features.js"; /** The argument that is the completion callback: its index; "last" for * calls whose callback is the final argument when one is passed; or @@ -148,7 +148,7 @@ function bindCall(e: IrExpr): IrExpr { /** Binds the completion callbacks of `mod` when its scheduling carries * AsyncLocalStorage contexts. */ export function bindCompletionCallbacks(mod: IrModule): void { - if (!moduleRuntimeFeatures(mod).asyncContext) return; + if (!moduleUsesAsyncContext(mod)) return; const rewriteExpr = (e: IrExpr): IrExpr => bindCall(mapExprChildren(e, rewriteExpr, rewriteStmt)); const rewriteStmt = (s: IrStmt): IrStmt => mapStmtChildren(s, rewriteExpr, rewriteStmt); for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 169d66e90b..2347cf3718 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2006,6 +2006,9 @@ function byName(fns: readonly IrFunction[]): IrFunction[] { ); } +/** The program module a createRequire binding requires. */ +type CreateRequireTarget = NonNullable>; + export class Lowerer { readonly frontendServices: FrontendServices | undefined; readonly checker: ts.TypeChecker; @@ -3478,7 +3481,7 @@ export class Lowerer { * tag. */ readonly unions = new UnionRegistry(); /** Context-free successful type mappings. See TypeMapperCtx.typeMemo. */ - readonly typeMemo = new Map(); + readonly typeMemo = new Map(); readonly ambient = ambientDtsPath(); readonly overridesAmbient = overridesDtsPath(); readonly fallbackAmbient = fallbackDtsPath(); @@ -4942,6 +4945,55 @@ export class Lowerer { return createRequireProgramModuleOf(this, decl.initializer)?.dep ?? null; } + private readonly createRequireBindingTargets = new Map(); + private readonly createRequireElementTargets = new Map< + ts.Symbol, + { element: ts.BindingElement; target: CreateRequireTarget } | null + >(); + + /** The program module a `const x = createRequire(...)("./m")` binding + * requires, by the binding's symbol; null for every other binding. Every + * reference resolves it (resolveValueSymbol), so it is cached. */ + private createRequireBindingTarget(binding: ts.Symbol): CreateRequireTarget | null { + const cached = this.createRequireBindingTargets.get(binding); + if (cached !== undefined) return cached; + const decl = binding.declarations.find(ts.isVariableDeclaration); + const target = + decl && ts.isIdentifier(decl.name) && decl.initializer + ? createRequireProgramModuleOf(this, decl.initializer) + : null; + this.createRequireBindingTargets.set(binding, target); + return target; + } + + /** The binding element and required program module of a + * `const { m } = createRequire(...)("./m")` binding, by the binding's + * symbol; null for every other binding. Cached like + * createRequireBindingTarget. */ + private createRequireElementTarget( + binding: ts.Symbol, + ): { element: ts.BindingElement; target: CreateRequireTarget } | null { + const cached = this.createRequireElementTargets.get(binding); + if (cached !== undefined) return cached; + let found: { element: ts.BindingElement; target: CreateRequireTarget } | null = null; + const element = binding.declarations.find(ts.isBindingElement); + if ( + element && + element.name !== undefined && + ts.isIdentifier(element.name) && + ts.isObjectBindingPattern(element.parent) + ) { + const declaration = element.parent.parent; + const target = + ts.isVariableDeclaration(declaration) && declaration.initializer + ? createRequireProgramModuleOf(this, declaration.initializer) + : null; + if (target !== null) found = { element, target }; + } + this.createRequireElementTargets.set(binding, found); + return found; + } + /** The represented single value behind a createRequire binding, or null * when the target is a namespace/table module. Collection has already * registered executable exports before entry-module uses lower, so the @@ -4949,9 +5001,7 @@ export class Lowerer { * static storage or callable/class metadata. */ private createRequireWholeExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const decl = binding ? binding.declarations.find(ts.isVariableDeclaration) : undefined; - if (!decl || !ts.isIdentifier(decl.name) || !decl.initializer) return null; - const target = createRequireProgramModuleOf(this, decl.initializer); + const target = binding ? this.createRequireBindingTarget(binding) : null; if (target === null) return null; const moduleSym = this.checker.getSymbolAtLocation(target.dep); let symbol = moduleSym?.getExports().get("export=" as ts.__String); @@ -4996,18 +5046,9 @@ export class Lowerer { * target module's real export symbol. */ private createRequireDestructuredExportSymbolOf(ident: ts.Identifier): ts.Symbol | null { const binding = this.checker.getSymbolAtLocation(ident); - const element = binding ? binding.declarations.find(ts.isBindingElement) : undefined; - if ( - !element || - element.name === undefined || - !ts.isIdentifier(element.name) || - !ts.isObjectBindingPattern(element.parent) - ) - return null; - const declaration = element.parent.parent; - if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) return null; - const target = createRequireProgramModuleOf(this, declaration.initializer); - if (target === null) return null; + const found = binding ? this.createRequireElementTarget(binding) : null; + if (found === null) return null; + const { element, target } = found; const property = element.propertyName; const name = property && (ts.isIdentifier(property) || ts.isStringLiteralLike(property)) @@ -15102,6 +15143,18 @@ export class Lowerer { * member access that IS a module in its own right (`fs.promises` — the * same object as node:fs/promises, Node's rule). Null otherwise. */ builtinNamespaceModuleOf(expr: ts.Expression): string | null { + // A function declaration is never a module namespace. Answering that + // from the symbol skips the type mapping below (every direct call asks). + if (ts.isIdentifier(expr)) { + const symbol = this.checker.getSymbolAtLocation(expr); + if ( + symbol !== undefined && + (symbol.flags & ts.SymbolFlags.Alias) === 0 && + symbol.declarations.length > 0 && + symbol.declarations.every(ts.isFunctionDeclaration) + ) + return null; + } // Default and CommonJS process bindings alias the global object, whose // methods and properties keep their existing process lowerings. if (stdlibGlobalNameOf(this, expr) === "process") return null; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 4a648d2f3a..86ff001292 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1349,7 +1349,7 @@ export interface TypeMapperCtx { * member graph each time is quadratic work. Generic/mixin contexts opt out * through canMemoizeType because the same checker type can map differently * under different instantiation bindings. */ - typeMemo?: Map; + typeMemo?: Map; canMemoizeType?: () => boolean; /** True for files the Lowerer actually compiles (its module order). A * class type can reach the entry through the TYPE world alone — a jsdoc @@ -1862,8 +1862,8 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const memoizable = typeId !== undefined && (ctx.canMemoizeType?.() ?? true); // An empty literal's type maps to the empty record as an unreduced union's // arm (emptyLiteralArm) and to the checked-dynamic value elsewhere: the two - // answers keep separate memo entries. - const memoKey = memoizable ? `${ctx.emptyLiteralArm ? 1 : 0}:${typeId}` : null; + // answers keep separate memo entries. Type ids are non-negative integers. + const memoKey = memoizable ? typeId * 2 + (ctx.emptyLiteralArm ? 1 : 0) : null; if (memoKey !== null) { const memo = ctx.typeMemo?.get(memoKey); if (memo !== undefined) return memo; diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index a4c1e8c35a..82071fde2f 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -9,7 +9,7 @@ import { RUNTIME_EVENT_TARGET_CLASS, RUNTIME_STREAM_CLASSES, } from "./ir.js"; -import { everyModuleNode } from "./traverse.js"; +import { everyModuleNode, everyStmtList } from "./traverse.js"; import { eachNodeOfKinds, type IrNodeIndex } from "./node-index.js"; import { JsIcSites, hasJsIcSite } from "./js-ic.js"; @@ -1383,6 +1383,17 @@ function scanRuntimeFeatures( * switch that pulls scr_regex.c + the vendored libregexp into the binary * (native-toolchain.ts); regex-free programs keep the historical command line. The * typed traversal visits type slots as well as executable nodes. */ +/** True when the module enters AsyncLocalStorage stores (RuntimeFeatures + * asyncContext): a walk for those libCalls alone, without the other + * features' rules. */ +export function moduleUsesAsyncContext(mod: IrModule): boolean { + const visitor = { + expr: (e: IrExpr): boolean => e.kind !== "libCall" || !ASYNC_CONTEXT_LIB_FNS.has(e.fn), + stmt: (): boolean => true, + }; + return !mod.functions.every((fn) => everyStmtList(fn.body, visitor)); +} + export function moduleUsesRegex(mod: IrModule): boolean { return scanRuntimeFeatures(mod, "regex").regex; }