Remove repeated lookups from the class dynamic-dispatch passes (#1663)

This commit is contained in:
Chris Tate
2026-10-21 10:59:12 -05:00
committed by GitHub
7 changed files with 94 additions and 25 deletions
@@ -177,7 +177,7 @@ export function lowerClassDataDescriptor(
function descriptorGuard(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): string {
const name = `%descriptorGuard:${info.def.name}`;
const existing = lowerer.liftedFns.find((fn) => fn.name === name);
const existing = lowerer.liftedFnNamed(name);
const key = varRef("key", DYN, loc);
const fields = new Set<string>();
const collect = (owner: ClassInfo): void => {
@@ -32,6 +32,9 @@ function context() {
unions: new Map(),
classes: new Map(),
liftedFns,
liftedFnNamed: (name: string | undefined) => liftedFns.find((fn) => fn.name === name),
subclassLinks: 0,
constructedClassCount: () => -1,
diags: [],
prototypeMethodAccesses: new Map(),
protocolDispatches: new Map(),
@@ -366,7 +366,7 @@ function readOnlyBagBody(body: IrStmt[], orEmpty = false): IrStmt[] {
export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc, readOnly = false): IrFunction {
const name = readOnly ? READ_PROPERTIES : "%dyn.class.properties";
const existing = lowerer.liftedFns.find((fn) => fn.name === name);
const existing = lowerer.liftedFnNamed(name);
if (existing) return existing;
const helper: IrFunction = {
name,
@@ -1003,7 +1003,7 @@ export class ClassDynamicDispatch {
const loc = this.propertyBag.loc;
const method = classMethodValue(lowerer, info.decl, info, "toJSON", loc);
const prototype = method ? classPrototypeData(lowerer, info, loc) : null;
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper);
const helper = lowerer.liftedFnNamed(info.def.prototypeDataHelper);
const init = helper?.body[0];
if (
!info.prototypeReflectionReady &&
@@ -1236,6 +1236,15 @@ export class ClassDynamicDispatch {
byMethod.set(method, matching);
return matching;
};
const candidateSets = new Map<string, ReadonlySet<ClassInfo>>();
const candidateSet = (method: string): ReadonlySet<ClassInfo> => {
let set = candidateSets.get(method);
if (!set) {
set = new Set(candidates(method));
candidateSets.set(method, set);
}
return set;
};
for (const fn of functions) {
// Discovery already visits every expression. Preserve bodies without
// dispatch sites instead of rebuilding their entire typed IR tree on
@@ -2220,16 +2229,16 @@ export class ClassDynamicDispatch {
: expr.kind === "dynInvoke"
? expr
: null;
const staticCandidates =
invoke &&
// Static methods matter only for methods no instance answers.
const staticCandidates = (method: string): boolean =>
[...this.boxedConstructors].some((name) => {
const info = lowerer.classes.get(name);
return info && findStaticOn(lowerer, info, invoke.method)?.method;
return info && findStaticOn(lowerer, info, method)?.method;
});
if (
!invoke ||
(candidates(invoke.method).length === 0 &&
!staticCandidates &&
!staticCandidates(invoke.method) &&
!(invoke.method === "toString" && lowerer.classes.has("%Error")))
)
return expr;
@@ -2501,10 +2510,13 @@ export class ClassDynamicDispatch {
});
changed = true;
}
for (const plan of this.propertyReceivers(lowerer, true)) {
// Compute the candidates where the first plan would have, so the
// memoized set is the same whichever plans are already answered.
const plans = this.propertyReceivers(lowerer, true);
const matching = plans.length ? candidateSet(dispatch.source.method) : null;
for (const plan of plans) {
const { info } = plan;
if (!candidates(dispatch.source.method).includes(info) || dispatch.classes.has(plan.key))
continue;
if (dispatch.classes.has(plan.key) || !matching!.has(info)) continue;
dispatch.classes.add(plan.key);
const loc = dispatch.source.loc;
const value = varRef("p.0", DYN, loc);
@@ -2648,7 +2660,7 @@ export class ClassDynamicDispatch {
if (
plan.info !== plan.capsule ||
dispatch.classes.has(plan.key) ||
(!dispatch.field && !candidates(dispatch.method).includes(plan.info))
(!dispatch.field && !candidateSet(dispatch.method).has(plan.info))
)
continue;
dispatch.classes.add(plan.key);
@@ -2738,6 +2750,8 @@ export class ClassDynamicDispatch {
}
for (const plan of this.propertyReceivers(lowerer, true)) {
const { info } = plan;
// Classes answered in an earlier pass skip the member lookups.
if (dispatch.classes.has(plan.key)) continue;
if (
!isClassOwnEnumerableFieldName(dispatch.name) ||
(!(info.builtinStream !== undefined && streamProperty(dispatch.name)) &&
@@ -2751,7 +2765,6 @@ export class ClassDynamicDispatch {
!findMethodOn(lowerer, info, `set:${dispatch.name}`))
)
continue;
if (dispatch.classes.has(plan.key)) continue;
dispatch.classes.add(plan.key);
const loc = dispatch.fn.loc;
const { receiver, condition } = this.propertyReceiver(plan, loc);
@@ -3310,7 +3323,7 @@ export class ClassDynamicDispatch {
loc: SrcLoc,
): IrExpr {
const name = fn === "dyn.hasKey" ? "%dyn.class.bagHas" : "%dyn.class.bagHasComputed";
let helper = lowerer.liftedFns.find((item) => item.name === name);
let helper = lowerer.liftedFnNamed(name);
if (!helper) {
const keyType = fn === "dyn.hasKey" ? STRING : DYN;
const value = varRef("p.0", DYN, loc);
@@ -3907,7 +3920,7 @@ export class ClassDynamicDispatch {
private readPropertyBagOrEmpty(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr {
// One call per read site: large JS programs have a branch per class and
// property, so the absent-bag handling lives in the shared helper.
let helper = lowerer.liftedFns.find((fn) => fn.name === READ_PROPERTIES_OR_EMPTY);
let helper = lowerer.liftedFnNamed(READ_PROPERTIES_OR_EMPTY);
if (!helper) {
// Settled by finalize; until then, the read-only bag's own dispatch.
helper = {
@@ -4256,17 +4269,38 @@ export class ClassDynamicDispatch {
add(root);
}
/** propertyReceivers results by mode, with the class graph they were
* computed from: the boxed classes, the registered classes, the subclass
* links and the constructed classes only grow, so equal counts and the
* same capsules give the same plans. */
private readonly receiverPlans = new Map<
string,
{ revision: string; capsules: (ClassInfo | undefined)[]; plans: readonly PropertyReceiver[] }
>();
/** A capsule retains its static type, while its object may be a subclass.
* Preorder insertion lets later branches test the most derived layout first. */
private propertyReceivers(
lowerer: Lowerer,
streams = false,
normalize = true,
): PropertyReceiver[] {
): readonly PropertyReceiver[] {
const mode = `${streams}:${normalize}`;
const revision = `${this.boxed.size}:${lowerer.classes.size}:${lowerer.subclassLinks}:${lowerer.constructedClassCount()}`;
const cached = this.receiverPlans.get(mode);
if (cached?.revision === revision) {
let i = 0;
let same = true;
for (const name of this.boxed)
if (lowerer.classes.get(name) !== cached.capsules[i++]) same = false;
if (same) return cached.plans;
}
const capsules: (ClassInfo | undefined)[] = [];
const plans: PropertyReceiver[] = [];
const seen = new Set<string>();
for (const name of this.boxed) {
const capsule = lowerer.classes.get(name);
capsules.push(capsule);
if (
!capsule ||
((capsule.builtinEmitter || capsule.builtinStream) && !streams) ||
@@ -4275,10 +4309,10 @@ export class ClassDynamicDispatch {
continue;
const canonical = normalize && !capsule.builtinStream && !capsule.builtinEmitter;
const visit = (info: ClassInfo): void => {
if (
(!canonical || !seen.has(info.def.name)) &&
(info === capsule || lowerer.classCanBeConstructed(info))
) {
// A seen class was visited with its whole subtree, and a canonical
// visit adds no plan for a seen class.
if (canonical && seen.has(info.def.name)) return;
if (info === capsule || lowerer.classCanBeConstructed(info)) {
seen.add(info.def.name);
plans.push({
info,
@@ -4290,6 +4324,7 @@ export class ClassDynamicDispatch {
};
visit(capsule);
}
this.receiverPlans.set(mode, { revision, capsules, plans });
return plans;
}
@@ -426,7 +426,7 @@ function reflectedMethodValue(
);
}
if (value.kind === "closure" && value.captures.length === 0) {
const body = lowerer.liftedFns.find((fn) => fn.name === value.fnName);
const body = lowerer.liftedFnNamed(value.fnName);
if (body?.captures?.length === 0) delete body.captures;
}
const cache = staticMethod
@@ -172,7 +172,7 @@ export function classPrototypeData(
}
if (!metadataOnly && !info.prototypeObservedMethodsReady) {
info.prototypeObservedMethodsReady = true;
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper)!;
const helper = lowerer.liftedFnNamed(info.def.prototypeDataHelper)!;
const value = varRef(`%g.${info.def.prototypeDataHelper}`, DYN, loc);
// Materialize only observed method slots. Own declarations stop lookup
// at the correct prototype even if an ancestor is replaced later. An
@@ -307,7 +307,7 @@ export function reflectClassPrototype(
if (info.base.prototypeReflectionRefusal)
return refuse("prototype reflection with this base class is not supported yet");
}
const helper = lowerer.liftedFns.find((fn) => fn.name === info.def.prototypeDataHelper);
const helper = lowerer.liftedFnNamed(info.def.prototypeDataHelper);
const initialization = helper?.body[0];
if (!helper || initialization?.kind !== "if" || info.prototypeReflectionReady) return prototype;
info.prototypeReflectionReady = true;
@@ -946,7 +946,10 @@ export function registerBuiltinErrorClasses(lowerer: Lowerer): void {
throwingSetters: [],
staticFields: [],
};
if (base) base.subclasses.push(info);
if (base) {
base.subclasses.push(info);
lowerer.subclassLinks++;
}
lowerer.classes.set(irName, info);
}
}
@@ -1026,7 +1029,10 @@ export function registerBuiltinStreamClasses(lowerer: Lowerer): void {
throwingSetters: [],
staticFields: [],
};
if (base) base.subclasses.push(info);
if (base) {
base.subclasses.push(info);
lowerer.subclassLinks++;
}
lowerer.classes.set(irName, info);
}
}
@@ -4471,7 +4477,10 @@ export function collectClassShapeInner(
};
lowerer.genericClassByDecl.set(decl, info.generic);
}
if (base) base.subclasses.push(info);
if (base) {
base.subclasses.push(info);
lowerer.subclassLinks++;
}
lowerer.classes.set(className, info);
if (computedBase && !functionLocalClass(decl)) {
info.def.baseValueGlobal = `%g.${className}.computedBase`;
@@ -3042,6 +3042,10 @@ export class Lowerer {
// top level, top = the innermost nested function currently lowering).
fnStack: FnCtx[] = [];
readonly liftedFns: IrFunction[] = [];
/** The first lifted function of each name (liftedFnNamed), for the
* liftedFns indexed so far. liftedFns only grows. */
private readonly liftedFnsByName = new Map<string, IrFunction>();
private liftedFnsIndexed = 0;
/** Runtime-provided classes used as values (defineClassValueConstruct),
* by IR class name. */
readonly runtimeClassValues = new Map<string, ClassInfo>();
@@ -3113,6 +3117,10 @@ export class Lowerer {
* can contain any retained descendant, but importing its declaration
* alone does not make every descendant constructible. */
private constructedClasses: Set<string> | null = null;
/** Classes added to some ClassInfo's subclasses (lower-classes.ts). With
* the class and constructed-class counts, it tells caches over the class
* graph when the graph changed. */
subclassLinks = 0;
/** Set while a subclass constructor's super() call notes its base
* constructor edge: that edge does not instantiate the base exactly. */
private superConstructorEdge = false;
@@ -3129,6 +3137,20 @@ export class Lowerer {
info.mixinInstance !== undefined
);
}
/** How many classes classCanBeConstructed admits by name, or -1 while
* it admits every class. The count only grows. */
constructedClassCount(): number {
return this.constructedClasses?.size ?? -1;
}
/** The first lifted function named `name`, the one
* `liftedFns.find((fn) => fn.name === name)` returns. */
liftedFnNamed(name: string | undefined): IrFunction | undefined {
for (; this.liftedFnsIndexed < this.liftedFns.length; this.liftedFnsIndexed++) {
const fn = this.liftedFns[this.liftedFnsIndexed]!;
if (!this.liftedFnsByName.has(fn.name)) this.liftedFnsByName.set(fn.name, fn);
}
return name === undefined ? undefined : this.liftedFnsByName.get(name);
}
/** Coverage remainder mode: the reachability gate inverts (see wantBody)
* and no module is built. */
readonly remainder: boolean;