Reuse the class dispatch passes' per-function scans while bodies are unchanged

This commit is contained in:
Chris Tate
2026-10-22 09:17:49 -05:00
parent 90d74a3193
commit 0318671352
9 changed files with 417 additions and 168 deletions
@@ -11,6 +11,7 @@ import {
} from "../../ir/ir.js";
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { classInstanceOf } from "./class-dynamic-dispatch.js";
import type { DispatchSites } from "./dispatch-sites.js";
import type { ClassInfo } from "./lower-classes.js";
import type { Lowerer } from "./lowerer.js";
@@ -26,25 +27,16 @@ export class CaughtValueDispatch {
private readonly payloadHelpers = new Map<string, IrFunction>();
private readonly payloadHelperFns = new Set<IrFunction>();
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
process(lowerer: Lowerer, functions: readonly IrFunction[], sites: DispatchSites): boolean {
const consumers: IrFunction[] = [];
for (const fn of functions) {
if (fn === this.helper) continue;
let consumes = false;
everyStmtList(fn.body, {
stmt: (stmt) => {
if (stmt.kind === "throw" && stmt.value.type.kind === "object") {
const info = lowerer.classes.get(stmt.value.type.className);
if (info) this.thrownClasses.add(info.def.name);
}
return true;
},
expr: (expr) => {
if (expr.kind === "caughtToDyn") consumes = true;
return true;
},
});
if (consumes) consumers.push(fn);
const found = sites.of(fn);
for (const className of found.thrown) {
const info = lowerer.classes.get(className);
if (info) this.thrownClasses.add(info.def.name);
}
if (found.caughtValues) consumers.push(fn);
}
if (!this.helper && consumers.length === 0) return false;
let changed = false;
@@ -13,7 +13,8 @@ import {
type IrType,
type SrcLoc,
} from "../../ir/ir.js";
import { everyStmtList, transformExpr, transformStmtList } from "../../ir/traverse.js";
import { transformExpr, transformStmtList } from "../../ir/traverse.js";
import type { DispatchSites } from "./dispatch-sites.js";
import type { Lowerer } from "./lowerer.js";
import type { ParamShape } from "./call-signatures.js";
import { classPrototypeData } from "./class-prototypes.js";
@@ -254,22 +255,17 @@ export class ClassConstructionDispatch {
private prototypeHelper: IrFunction | null = null;
private readonly prototypes = new Set<string>();
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
process(lowerer: Lowerer, functions: readonly IrFunction[], sites: DispatchSites): boolean {
const discover = (type: IrType): void => {
if (type.kind === "classval") this.constructors.add(type.className);
else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover);
};
const prototypeReads = new Set<IrFunction>();
for (const fn of functions)
everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
if (expr.kind === "dynFrom") discover(expr.value.type);
if (expr.kind === "libCall" && expr.fn === "dyn.classBasePrototype")
prototypeReads.add(fn);
return true;
},
});
for (const fn of functions) {
const found = sites.of(fn);
found.values.forEach(discover);
if (found.basePrototypes) prototypeReads.add(fn);
}
let changed = false;
for (const fn of functions) {
if (fn === this.prototypeHelper || !prototypeReads.has(fn)) continue;
@@ -77,6 +77,7 @@ import { isClassCallback } from "./class-callbacks.js";
import { CaughtValueDispatch } from "./caught-value-dispatch.js";
import { refreshDescriptorGuards } from "./class-descriptors.js";
import { ClassConstructionDispatch } from "./class-construction.js";
import { DispatchSites } from "./dispatch-sites.js";
import {
emitterRooted,
lowerDynamicEmitterEmit,
@@ -226,7 +227,7 @@ const KEY_CALLS = new Map<string, "object" | "any" | "value">([
["dyn.keySetComputed", "value"],
]);
function toPrimitiveHint(expr: IrExpr): ToPrimitiveHint | null {
export function toPrimitiveHint(expr: IrExpr): ToPrimitiveHint | null {
if (expr.kind === "toString") return expr.operand.type.kind === "dyn" ? "string" : null;
if (expr.kind === "dynKeyGet") return expr.key.type.kind === "dyn" ? "string" : null;
if (expr.kind !== "libCall") return null;
@@ -780,6 +781,8 @@ export class ClassDynamicDispatch {
private readonly generatorDispatch = new GeneratorDynamicDispatch();
private readonly caughtDispatch = new CaughtValueDispatch();
private readonly constructionDispatch = new ClassConstructionDispatch();
/** What each function's body holds for this pass and its sub-passes. */
readonly sites = new DispatchSites();
/** Property names a delete can remove (null: any name), for the field
* creation tracking decision. */
private deletedNames: Set<string> | null | undefined;
@@ -862,12 +865,13 @@ export class ClassDynamicDispatch {
)
fn.body.shift();
}
let changed = this.caughtDispatch.process(lowerer, functions);
let changed = this.caughtDispatch.process(lowerer, functions, this.sites);
changed = refreshErrorMethodDispatch(lowerer, functions) || changed;
changed = refreshClassMethodValueSelections(lowerer) || changed;
changed = refreshErrorPropertyDispatch(lowerer, functions) || changed;
changed = this.generatorDispatch.process(lowerer, functions, this.generated) || changed;
changed = this.constructionDispatch.process(lowerer, functions) || changed;
changed =
this.generatorDispatch.process(lowerer, functions, this.generated, this.sites) || changed;
changed = this.constructionDispatch.process(lowerer, functions, this.sites) || changed;
const seenTypes = new Set<string>();
const rewrite = new Set<IrFunction>();
const toPrimitiveSites = new Set<IrFunction>();
@@ -891,100 +895,18 @@ export class ClassDynamicDispatch {
};
const boxedBefore = this.boxed.size;
const boxedConstructorsBefore = this.boxedConstructors.size;
for (const fn of functions)
everyStmtList(fn.body, {
stmt: (statement) => {
// Field-creation instrumentation can box a base-constructor receiver
// even when only its derived instances otherwise cross the boundary.
if (
statement.kind === "fieldSet" &&
lowerer.classes.get(statement.className)?.def.tracksOwnFields &&
isClassOwnEnumerableFieldName(statement.field)
)
discover(statement.obj.type);
return true;
},
expr: (expr) => {
if (
expr.kind === "jsonStringify" ||
(expr.kind === "libCall" &&
(expr.fn === "json.stringifyValue" || expr.fn === "json.stringifyReplacer"))
) {
this.serializesClasses = true;
}
if (
toPrimitiveHint(expr) !== null ||
(expr.kind === "libCall" && (expr.fn === "dyn.valueOf" || expr.fn === "dyn.toString"))
)
toPrimitiveSites.add(fn);
switch (expr.kind) {
case "dynFrom":
discover(expr.value.type);
break;
case "fieldIncDec":
if (
lowerer.classes.get(expr.className)?.def.tracksOwnFields &&
isClassOwnEnumerableFieldName(expr.field)
)
discover(expr.obj.type);
break;
case "call":
if (
(expr.callee === "%dyn.class.properties" ||
expr.callee === "%dyn.class.readProperties") &&
expr.args[0]?.kind === "dynFrom" &&
isDynTypedRefType(expr.args[0].value.type)
)
rewrite.add(fn);
break;
case "dynHasKey":
hasKeySites.add(fn);
break;
case "caughtTest":
if (expr.test === "instanceof") caughtSites.add(fn);
break;
case "caughtNarrow":
case "caughtCheck":
if (expr.type.kind === "object") caughtSites.add(fn);
break;
case "dynKeyGet":
case "dynInvoke":
rewrite.add(fn);
break;
case "dynCheck":
if (isDynTypedRefType(expr.type)) rewrite.add(fn);
// A dynamic function checked into a native function type
// receives its arguments as dynamic values: class instances
// passed to it cross the boundary when it is called (a JS
// `list.map((rec) => rec.match(x))` callback with an untyped
// parameter).
if (expr.type.kind === "func") expr.type.params.forEach(discover);
break;
case "libCall":
if (
expr.fn === "bytes.construct" ||
expr.fn === "dyn.keySet" ||
expr.fn === "dyn.keySetComputed" ||
expr.fn === "dyn.iterator" ||
expr.fn === "dyn.arrayFromIterator" ||
expr.fn === "dyn.toString" ||
expr.fn === "dyn.stringConstructor" ||
expr.fn === "dyn.forInKeys" ||
expr.fn === "dyn.forInKeyStrings" ||
expr.fn === "dyn.hasKeyComputed" ||
expr.fn === "dyn.hasOwnComputed" ||
expr.fn === "dyn.propertyIsEnumerableComputed" ||
expr.fn === "dyn.defineProperty" ||
expr.fn === "dyn.getPrototype" ||
expr.fn === "dyn.reflectGet" ||
expr.fn === "dyn.reflectSet"
)
rewrite.add(fn);
break;
}
return true;
},
});
for (const fn of functions) {
const found = this.sites.of(fn);
// A class starts tracking its own fields during this pass.
for (const { type, fieldClass } of found.boxing)
if (fieldClass === undefined || lowerer.classes.get(fieldClass)?.def.tracksOwnFields)
discover(type);
if (found.serializes) this.serializesClasses = true;
if (found.conversions) toPrimitiveSites.add(fn);
if (found.dispatches) rewrite.add(fn);
if (found.keyTests) hasKeySites.add(fn);
if (found.caughtClassTests) caughtSites.add(fn);
}
// The classes this pass found join the tables in a fixed order, not in
// the order the scan met them.
sortAdded(lowerer, this.boxed, boxedBefore);
@@ -159,6 +159,15 @@ test("fills every empty copy with the exact file and span while preserving popul
for (const untouched of [otherFile, otherStart, otherEnd]) expect(untouched.body).toEqual([]);
});
test("reports the functions whose placeholders it fills", () => {
const { initializers, pending, lowerer } = fixture();
const functions = [fn("init", [block(loc)]), fn("use", [read("registry")])];
pending.set("registry", [{ loc, lower: () => [value(4)] }]);
const filled: IrFunction[] = [];
expect(initializers.process(lowerer, functions, (f) => filled.push(f))).toBe(true);
expect(filled).toEqual([functions[0]]);
});
test("an update of the binding demands its initializer", () => {
const { initializers, pending, lowerer } = fixture();
const updated = block(loc),
@@ -129,7 +129,14 @@ export class DeferredModuleInitializers {
return block;
}
process(lowerer: Lowerer, functions: readonly IrFunction[]): boolean {
/** Materializes the initializers that reached bodies observe. `filled`
* receives each function whose body changes inside an unchanged statement
* list. */
process(
lowerer: Lowerer,
functions: readonly IrFunction[],
filled?: (fn: IrFunction) => void,
): boolean {
if (this.pending.size === 0) return false;
// Scans keep only the reads of deferred bindings.
for (const key of this.pending.keys()) {
@@ -192,7 +199,10 @@ export class DeferredModuleInitializers {
}
if (holders.length === 0) continue;
// These bodies changed inside unchanged statement lists.
for (const fn of holders) this.scans.delete(fn);
for (const fn of holders) {
this.scans.delete(fn);
filled?.(fn);
}
// An inserted body can introduce a placeholder needed by another
// action in this pass. Unattached bodies are not part of the live IR.
for (const nested of scanStatements(body).placeholders) add(nested, holders);
@@ -0,0 +1,60 @@
import { expect, test, vi } from "vitest";
import { DYN, F64, VOID, type IrFunction, type IrStmt, type SrcLoc } from "../../ir/ir.js";
import * as traversal from "../../ir/traverse.js";
import { DispatchSites } from "./dispatch-sites.js";
const loc: SrcLoc = { file: "sites.ts", start: 0, end: 1 };
const keyRead = (key: string): IrStmt => ({
kind: "exprStmt",
expr: {
kind: "dynKeyGet",
value: { kind: "varRef", localId: "value", type: DYN, loc },
key: { kind: "strLit", value: key, type: DYN, loc },
type: DYN,
loc,
},
loc,
});
const value = (n: number): IrStmt => ({
kind: "exprStmt",
expr: { kind: "numLit", value: n, type: F64, loc },
loc,
});
const fn = (body: IrStmt[]): IrFunction => ({
name: "f",
body,
params: [],
locals: [],
returnType: VOID,
loc,
});
test("reuses a function's sites while its statement list is unchanged", () => {
const sites = new DispatchSites();
const f = fn([value(1)]);
const walk = vi.spyOn(traversal, "everyStmtList");
try {
expect(sites.of(f).dispatches).toBe(false);
expect(sites.of(f).dispatches).toBe(false);
expect(walk).toHaveBeenCalledTimes(1);
// A statement added to the same list is found.
f.body.push(keyRead("next"));
expect(sites.of(f)).toMatchObject({ dispatches: true, generatorSteps: true });
// So is a replaced list.
f.body = [value(2)];
expect(sites.of(f)).toMatchObject({ dispatches: false, generatorSteps: false });
expect(walk).toHaveBeenCalledTimes(3);
} finally {
walk.mockRestore();
}
});
test("scans a body again once a change inside an unchanged list is reported", () => {
const sites = new DispatchSites();
const placeholder: Extract<IrStmt, { kind: "block" }> = { kind: "block", body: [], loc };
const f = fn([placeholder]);
expect(sites.of(f).dispatches).toBe(false);
placeholder.body.push(keyRead("map"));
sites.forget(f);
expect(sites.of(f)).toMatchObject({ dispatches: true, runtimeSteps: true });
});
@@ -0,0 +1,272 @@
import { InternalCompilerError } from "../../errors.js";
import {
isClassOwnEnumerableFieldName,
isDynTypedRefType,
typeKey,
type IrFunction,
type IrStmt,
type IrType,
} from "../../ir/ir.js";
import { everyStmtList } from "../../ir/traverse.js";
import { toPrimitiveHint } from "./class-dynamic-dispatch.js";
import { GENERATOR_STEP_METHODS, HELPER_METHODS } from "./generator-dynamic-dispatch.js";
/** What one function's body holds for the class dispatch pass and its
* caught-value, generator and class construction sub-passes, collected in
* one walk. Lists keep the order the walk meets their entries. */
export interface FunctionSites {
statements: IrStmt[];
length: number;
first: IrStmt | undefined;
last: IrStmt | undefined;
/** The value types of `dynFrom` expressions, once per type key. */
values: IrType[];
/** The types the class dispatch scan boxes, once per type key and class:
* `dynFrom` values, the parameter types of dynamic functions checked into
* native function types, and the receivers of own enumerable field writes
* and updates. A receiver counts only while its class (`fieldClass`)
* tracks its own fields. */
boxing: { type: IrType; fieldClass?: string }[];
/** JSON serialization of dynamic values. */
serializes: boolean;
/** ToPrimitive conversions, and `dyn.valueOf` and `dyn.toString` calls. */
conversions: boolean;
/** Sites the class dispatch rewrite can change. */
dispatches: boolean;
/** `dynHasKey` tests. */
keyTests: boolean;
/** `instanceof` tests of caught values, and caught values narrowed or
* checked to class types. */
caughtClassTests: boolean;
/** Calls and reads of generator steps, and reads of iterator helper
* methods. */
generatorSteps: boolean;
/** Values the runtime steps: iterator helper calls and reads, `dynIterN`,
* and rest destructuring. */
runtimeSteps: boolean;
/** `dyn.classBasePrototype` calls. */
basePrototypes: boolean;
/** The classes of thrown class instances, once each. */
thrown: string[];
/** `caughtToDyn` conversions. */
caughtValues: boolean;
}
/** The `libCall`s the class dispatch rewrite can change. */
const DISPATCH_LIB_CALLS = new Set<string>([
"bytes.construct",
"dyn.keySet",
"dyn.keySetComputed",
"dyn.iterator",
"dyn.arrayFromIterator",
"dyn.toString",
"dyn.stringConstructor",
"dyn.forInKeys",
"dyn.forInKeyStrings",
"dyn.hasKeyComputed",
"dyn.hasOwnComputed",
"dyn.propertyIsEnumerableComputed",
"dyn.defineProperty",
"dyn.getPrototype",
"dyn.reflectGet",
"dyn.reflectSet",
]);
/** Prototype data helpers (classPrototypeData) gain method slots inside
* their initialization block, so their summaries are not reused. */
const PROTOTYPE_DATA_PREFIX = "%prototype.data.";
/** SCRIPTC_CHECK_DISPATCH_SCANS=1 checks each reused summary against a fresh walk. */
const CHECK_SCANS = process.env["SCRIPTC_CHECK_DISPATCH_SCANS"] === "1";
/** The function summaries (FunctionSites) of one class dispatch pass, kept
* across its runs. */
export class DispatchSites {
private readonly sites = new Map<IrFunction, FunctionSites>();
/** The sites of `fn`. A summary is reused while the function's statement
* list is the same array with the same length and the same first and last
* statements: the dispatch passes replace the lists they rewrite and add
* statements to the helpers they extend. Other changes inside an
* unchanged list are reported with `forget`, except in prototype data
* helpers, which are scanned on every use. */
of(fn: IrFunction): FunctionSites {
const cached = this.sites.get(fn);
if (
cached !== undefined &&
!fn.name.startsWith(PROTOTYPE_DATA_PREFIX) &&
cached.statements === fn.body &&
cached.length === fn.body.length &&
cached.first === fn.body[0] &&
cached.last === fn.body[fn.body.length - 1]
) {
if (CHECK_SCANS && !sameSites(cached, scanSites(fn.body)))
throw new InternalCompilerError(
`reused dispatch sites of ${fn.name} disagree with a fresh scan`,
);
return cached;
}
const sites = scanSites(fn.body);
this.sites.set(fn, sites);
return sites;
}
/** Drops the summary of a function whose body changed inside an unchanged
* statement list. */
forget(fn: IrFunction): void {
this.sites.delete(fn);
}
}
function scanSites(statements: IrStmt[]): FunctionSites {
const sites: FunctionSites = {
statements,
length: statements.length,
first: statements[0],
last: statements[statements.length - 1],
values: [],
boxing: [],
serializes: false,
conversions: false,
dispatches: false,
keyTests: false,
caughtClassTests: false,
generatorSteps: false,
runtimeSteps: false,
basePrototypes: false,
thrown: [],
caughtValues: false,
};
const valueKeys = new Set<string>();
const boxingKeys = new Set<string>();
const thrown = new Set<string>();
const box = (type: IrType, fieldClass?: string): void => {
const key = `${fieldClass ?? ""}\0${typeKey(type)}`;
if (boxingKeys.has(key)) return;
boxingKeys.add(key);
sites.boxing.push(fieldClass === undefined ? { type } : { type, fieldClass });
};
everyStmtList(statements, {
stmt: (statement) => {
if (statement.kind === "throw" && statement.value.type.kind === "object") {
const className = statement.value.type.className;
if (!thrown.has(className)) {
thrown.add(className);
sites.thrown.push(className);
}
}
// Field-creation instrumentation can box a base-constructor receiver
// even when only its derived instances otherwise cross the boundary.
if (statement.kind === "fieldSet" && isClassOwnEnumerableFieldName(statement.field))
box(statement.obj.type, statement.className);
return true;
},
expr: (expr) => {
if (
expr.kind === "jsonStringify" ||
(expr.kind === "libCall" &&
(expr.fn === "json.stringifyValue" || expr.fn === "json.stringifyReplacer"))
)
sites.serializes = true;
if (
toPrimitiveHint(expr) !== null ||
(expr.kind === "libCall" && (expr.fn === "dyn.valueOf" || expr.fn === "dyn.toString"))
)
sites.conversions = true;
if (
(expr.kind === "dynInvoke" && GENERATOR_STEP_METHODS.has(expr.method)) ||
(expr.kind === "dynKeyGet" &&
expr.key.kind === "strLit" &&
(GENERATOR_STEP_METHODS.has(expr.key.value) || HELPER_METHODS.has(expr.key.value)))
)
sites.generatorSteps = true;
if (
(expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) ||
(expr.kind === "dynKeyGet" &&
expr.key.kind === "strLit" &&
HELPER_METHODS.has(expr.key.value)) ||
expr.kind === "dynIterN" ||
(expr.kind === "libCall" && expr.fn === "dyn.destructureRest")
)
sites.runtimeSteps = true;
switch (expr.kind) {
case "dynFrom": {
const key = typeKey(expr.value.type);
if (!valueKeys.has(key)) {
valueKeys.add(key);
sites.values.push(expr.value.type);
}
box(expr.value.type);
break;
}
case "fieldIncDec":
if (isClassOwnEnumerableFieldName(expr.field)) box(expr.obj.type, expr.className);
break;
case "call":
if (
(expr.callee === "%dyn.class.properties" ||
expr.callee === "%dyn.class.readProperties") &&
expr.args[0]?.kind === "dynFrom" &&
isDynTypedRefType(expr.args[0].value.type)
)
sites.dispatches = true;
break;
case "dynHasKey":
sites.keyTests = true;
break;
case "caughtTest":
if (expr.test === "instanceof") sites.caughtClassTests = true;
break;
case "caughtNarrow":
case "caughtCheck":
if (expr.type.kind === "object") sites.caughtClassTests = true;
break;
case "caughtToDyn":
sites.caughtValues = true;
break;
case "dynKeyGet":
case "dynInvoke":
sites.dispatches = true;
break;
case "dynCheck":
if (isDynTypedRefType(expr.type)) sites.dispatches = true;
// A dynamic function checked into a native function type
// receives its arguments as dynamic values: class instances
// passed to it cross the boundary when it is called (a JS
// `list.map((rec) => rec.match(x))` callback with an untyped
// parameter).
if (expr.type.kind === "func") expr.type.params.forEach((type) => box(type));
break;
case "libCall":
if (expr.fn === "dyn.classBasePrototype") sites.basePrototypes = true;
if (DISPATCH_LIB_CALLS.has(expr.fn)) sites.dispatches = true;
break;
}
return true;
},
});
return sites;
}
function sameSites(left: FunctionSites, right: FunctionSites): boolean {
const sameList = <T>(a: readonly T[], b: readonly T[], same: (x: T, y: T) => boolean): boolean =>
a.length === b.length && a.every((item, index) => same(item, b[index]!));
return (
sameList(left.values, right.values, (a, b) => a === b) &&
sameList(
left.boxing,
right.boxing,
(a, b) => a.type === b.type && a.fieldClass === b.fieldClass,
) &&
sameList(left.thrown, right.thrown, (a, b) => a === b) &&
left.serializes === right.serializes &&
left.conversions === right.conversions &&
left.dispatches === right.dispatches &&
left.keyTests === right.keyTests &&
left.caughtClassTests === right.caughtClassTests &&
left.generatorSteps === right.generatorSteps &&
left.runtimeSteps === right.runtimeSteps &&
left.basePrototypes === right.basePrototypes &&
left.caughtValues === right.caughtValues
);
}
@@ -13,16 +13,20 @@ import {
type IrStmt,
type IrType,
} from "../../ir/ir.js";
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
import { transformStmtList } from "../../ir/traverse.js";
import { genResultRecord } from "../type-mapper.js";
import type { DispatchSites } from "./dispatch-sites.js";
import type { Lowerer } from "./lowerer.js";
type GeneratorType = Extract<IrType, { kind: "generator" }>;
type Invoke = Extract<IrExpr, { kind: "dynInvoke" }>;
type KeyGet = Extract<IrExpr, { kind: "dynKeyGet" }>;
/** The generator methods a dynamic call or read steps. */
export const GENERATOR_STEP_METHODS = new Set(["next", "return", "throw"]);
/** The iterator helper methods the runtime answers on checked iterators. */
const HELPER_METHODS = new Set([
export const HELPER_METHODS = new Set([
"map",
"filter",
"take",
@@ -101,6 +105,7 @@ export class GeneratorDynamicDispatch {
lowerer: Lowerer,
functions: readonly IrFunction[],
classHelpers: ReadonlySet<IrFunction>,
sites: DispatchSites,
): boolean {
const seen = new Set<string>();
const rewrite = new Set<IrFunction>();
@@ -117,34 +122,14 @@ export class GeneratorDynamicDispatch {
lowerer.shapes.get(type.shapeId)?.fields.forEach((field) => discover(field.type));
else if (type.kind === "union") lowerer.unions.get(type.unionId)?.arms.forEach(discover);
};
for (const fn of functions)
everyStmtList(fn.body, {
stmt: () => true,
expr: (expr) => {
if (expr.kind === "dynFrom") discover(expr.value.type);
if (expr.kind === "dynInvoke" && ["next", "return", "throw"].includes(expr.method))
rewrite.add(fn);
// The runtime steps the values iterator helpers and destructuring
// receive.
if (
(expr.kind === "dynInvoke" && HELPER_METHODS.has(expr.method)) ||
(expr.kind === "dynKeyGet" &&
expr.key.kind === "strLit" &&
HELPER_METHODS.has(expr.key.value)) ||
expr.kind === "dynIterN" ||
(expr.kind === "libCall" && expr.fn === "dyn.destructureRest")
)
runtimeSteps = true;
if (
expr.kind === "dynKeyGet" &&
expr.key.kind === "strLit" &&
(["next", "return", "throw"].includes(expr.key.value) ||
HELPER_METHODS.has(expr.key.value))
)
rewrite.add(fn);
return true;
},
});
for (const fn of functions) {
const found = sites.of(fn);
found.values.forEach(discover);
// The runtime steps the values iterator helpers and destructuring
// receive.
if (found.runtimeSteps) runtimeSteps = true;
if (found.generatorSteps) rewrite.add(fn);
}
let changed = false;
for (const fn of functions) {
// The generator helper's fallback may itself dispatch native classes.
@@ -159,7 +144,7 @@ export class GeneratorDynamicDispatch {
if (
expr.kind === "dynKeyGet" &&
expr.key.kind === "strLit" &&
(["next", "return", "throw"].includes(expr.key.value) ||
(GENERATOR_STEP_METHODS.has(expr.key.value) ||
(HELPER_METHODS.has(expr.key.value) && this.generators.size > 0))
) {
const method = expr.key.value;
@@ -173,8 +158,7 @@ export class GeneratorDynamicDispatch {
loc: expr.loc,
};
}
if (expr.kind !== "dynInvoke" || !["next", "return", "throw"].includes(expr.method))
return expr;
if (expr.kind !== "dynInvoke" || !GENERATOR_STEP_METHODS.has(expr.method)) return expr;
const key = JSON.stringify([expr.method, expr.args.length, expr.calleeName]);
let dispatch = this.dispatches.get(key);
if (!dispatch) {
@@ -8231,13 +8231,17 @@ export class Lowerer {
for (;;) {
drainInstances();
const declared = unitsInOrder();
const initializerChanged = this.deferredModuleInitializers.process(this, [
...declared,
...initFunctions,
...instanceFunctions,
...byName(this.liftedFns),
...byName(this.implicitFns),
]);
const initializerChanged = this.deferredModuleInitializers.process(
this,
[
...declared,
...initFunctions,
...instanceFunctions,
...byName(this.liftedFns),
...byName(this.implicitFns),
],
(fn) => classDispatch.sites.forget(fn),
);
const dispatchChanged = classDispatch.process(this, [
...declared,
...initFunctions,