diff --git a/packages/compiler/src/frontend/lowering/class-descriptors.ts b/packages/compiler/src/frontend/lowering/class-descriptors.ts index cb980f0d0..0b807e450 100644 --- a/packages/compiler/src/frontend/lowering/class-descriptors.ts +++ b/packages/compiler/src/frontend/lowering/class-descriptors.ts @@ -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(); const collect = (owner: ClassInfo): void => { diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index 1c5c84cdf..dfb8bd7a5 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -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(), diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index cb5c28444..d9ef0de62 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -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>(); + const candidateSet = (method: string): ReadonlySet => { + 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(); 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; } diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts index 1197d9bdd..6e8256513 100644 --- a/packages/compiler/src/frontend/lowering/class-method-values.ts +++ b/packages/compiler/src/frontend/lowering/class-method-values.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 7b4b58ae3..d2919e202 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -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; diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 371541df8..f312e9229 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -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`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 543149f83..be5db5cc6 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -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(); + private liftedFnsIndexed = 0; /** Runtime-provided classes used as values (defineClassValueConstruct), * by IR class name. */ readonly runtimeClassValues = new Map(); @@ -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 | 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;