Skip repeated probes in lowering

This commit is contained in:
Chris Tate
2026-10-26 05:17:10 -05:00
parent 9c3b336f76
commit addecf6a02
4 changed files with 86 additions and 22 deletions
@@ -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);
@@ -2006,6 +2006,9 @@ function byName(fns: readonly IrFunction[]): IrFunction[] {
);
}
/** The program module a createRequire binding requires. */
type CreateRequireTarget = NonNullable<ReturnType<typeof createRequireProgramModuleOf>>;
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<string, IrType>();
readonly typeMemo = new Map<number, IrType>();
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<ts.Symbol, CreateRequireTarget | null>();
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;
@@ -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<string, IrType>;
typeMemo?: Map<number, IrType>;
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;
+12 -1
View File
@@ -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;
}