mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Reuse the class dispatch passes' per-function scans while bodies are unchanged
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user