mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
perf: reuse recursive class type analysis during final lowering
- Reuse completed type graph checks across globals and local variables. - Preserve cyclic class validation and native cleanup behavior.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { RuntimeOptionalLocals } from "./runtime-optional-locals.js";
|
||||
import { sanitizeUnregisteredClassTypes } from "./sanitize-class-types.js";
|
||||
import { UnregisteredClassTypes } from "./unregistered-class-types.js";
|
||||
import { buildUnionNarrow } from "./union-narrow.js";
|
||||
import { planUnionRetag, buildUnionRetag, planRecordUnionWrap, buildRecordUnionWrap } from "./union-retag.js";
|
||||
import type { WidthLift } from "./width-lift.js";
|
||||
@@ -3765,9 +3766,12 @@ export class Lowerer {
|
||||
// fences), so drop it — guarded by a reference scan, with the
|
||||
// validator's registration check as the backstop for anything that
|
||||
// does slip through with a live reference.
|
||||
const brokenGlobals = this.globalsList.filter((g) => this.typeNamesUnregisteredClass(g.type));
|
||||
const unregistered = new UnregisteredClassTypes(
|
||||
(id) => this.shapes.get(id), (id) => this.unions.get(id), (name) => this.classes.has(name),
|
||||
);
|
||||
const brokenGlobals = this.globalsList.filter((g) => unregistered.has(g.type));
|
||||
const brokenLocalFns = functions.filter((fn) =>
|
||||
fn.locals.some((l) => this.typeNamesUnregisteredClass(l.type)),
|
||||
fn.locals.some((l) => unregistered.has(l.type)),
|
||||
);
|
||||
if (brokenGlobals.length > 0 || brokenLocalFns.length > 0) {
|
||||
const referencedIn = (root: unknown): Set<string> => {
|
||||
@@ -3810,7 +3814,7 @@ export class Lowerer {
|
||||
// those out from under them.
|
||||
const referenced = referencedIn([fn.body, fn.params, fn.captures ?? [], fn.classCaptures ?? []]);
|
||||
fn.locals = fn.locals.filter(
|
||||
(l) => referenced.has(l.id) || !this.typeNamesUnregisteredClass(l.type),
|
||||
(l) => referenced.has(l.id) || !unregistered.has(l.type),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, VOID, type IrRecordShape, type IrType, type IrUnionDef } from "../../ir/ir.js";
|
||||
import { UnregisteredClassTypes } from "./unregistered-class-types.js";
|
||||
|
||||
const record = (shapeId: string): IrType => ({ kind: "record", shapeId });
|
||||
const union = (unionId: string): IrType => ({ kind: "union", unionId });
|
||||
const missing: IrType = { kind: "object", className: "Missing" };
|
||||
|
||||
test("cyclic references wait for sibling checks before caching safe types", () => {
|
||||
const records = new Map<string, IrRecordShape>([
|
||||
["a", { id: "a", fields: [{ name: "cycle", type: record("b") }, { name: "missing", type: missing }] }],
|
||||
["b", { id: "b", fields: [{ name: "back", type: union("u") }, { name: "safe", type: record("safe") }] }],
|
||||
["safe", { id: "safe", fields: [{ name: "value", type: F64 }] }],
|
||||
]);
|
||||
const unions = new Map<string, IrUnionDef>([["u", { id: "u", arms: [record("a")] }]]);
|
||||
const index = new UnregisteredClassTypes((id) => records.get(id), (id) => unions.get(id), () => false);
|
||||
expect(index.has(record("a"))).toBe(true);
|
||||
expect(index.has(record("b"))).toBe(true);
|
||||
expect(index.has(union("u"))).toBe(true);
|
||||
expect(index.has(record("safe"))).toBe(false);
|
||||
});
|
||||
|
||||
test("shared recursive graphs are traversed once across repeated local types", () => {
|
||||
const records = new Map<string, IrRecordShape>();
|
||||
const count = 2048;
|
||||
for (let i = 0; i < count; i++) records.set(String(i), {
|
||||
id: String(i), fields: [{ name: "next", type: record(String((i + 1) % count)) }],
|
||||
});
|
||||
let lookups = 0;
|
||||
const index = new UnregisteredClassTypes((id) => { lookups++; return records.get(id); }, () => undefined, () => true);
|
||||
for (let i = 0; i < count; i++) {
|
||||
expect(index.has({ kind: "array", elem: record(String(i)) })).toBe(false);
|
||||
}
|
||||
expect(lookups).toBe(count);
|
||||
});
|
||||
|
||||
test("nested containers, signatures and index values expose missing instances", () => {
|
||||
const shape: IrRecordShape = { id: "r", fields: [], indexValue: missing };
|
||||
const definition: IrUnionDef = { id: "u", arms: [record("r")] };
|
||||
const index = new UnregisteredClassTypes(() => shape, () => definition, () => false);
|
||||
const types: IrType[] = [
|
||||
{ kind: "array", elem: missing }, { kind: "set", elem: missing },
|
||||
{ kind: "map", key: missing, value: F64 }, { kind: "map", key: F64, value: missing },
|
||||
{ kind: "promise", inner: missing },
|
||||
{ kind: "func", params: [missing], ret: VOID }, { kind: "func", params: [], ret: missing },
|
||||
record("r"), union("u"),
|
||||
];
|
||||
for (const type of types) expect(index.has(type)).toBe(true);
|
||||
expect(index.has({ kind: "classval", className: "Missing" })).toBe(false);
|
||||
});
|
||||
|
||||
test("record and union IDs have independent identities", () => {
|
||||
const index = new UnregisteredClassTypes(
|
||||
(id) => ({ id, fields: [] }), (id) => ({ id, arms: [missing] }), () => false,
|
||||
);
|
||||
expect(index.has(record("same"))).toBe(false);
|
||||
expect(index.has(union("same"))).toBe(true);
|
||||
expect(index.has(record("same"))).toBe(false);
|
||||
});
|
||||
|
||||
test("fresh indexes observe later class registration and type-table completion", () => {
|
||||
let shape: IrRecordShape | undefined;
|
||||
const classes = new Set<string>();
|
||||
const fresh = () => new UnregisteredClassTypes(() => shape, () => undefined, (name) => classes.has(name));
|
||||
expect(fresh().has(record("r"))).toBe(false);
|
||||
shape = { id: "r", fields: [{ name: "value", type: missing }] };
|
||||
expect(fresh().has(record("r"))).toBe(true);
|
||||
classes.add("Missing");
|
||||
expect(fresh().has(record("r"))).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { IrRecordShape, IrType, IrUnionDef } from "../../ir/ir.js";
|
||||
|
||||
/** Reuse class-reference checks while the type tables and class registry are
|
||||
* fixed. Create a new index after lowering or retention changes those tables. */
|
||||
export class UnregisteredClassTypes {
|
||||
private readonly safeRecords = new Set<string>();
|
||||
private readonly safeUnions = new Set<string>();
|
||||
private readonly badRecords = new Set<string>();
|
||||
private readonly badUnions = new Set<string>();
|
||||
|
||||
constructor(
|
||||
private readonly record: (id: string) => IrRecordShape | undefined,
|
||||
private readonly union: (id: string) => IrUnionDef | undefined,
|
||||
private readonly hasClass: (name: string) => boolean,
|
||||
) {}
|
||||
|
||||
has(root: IrType): boolean {
|
||||
const pending: IrType[] = [root];
|
||||
const records = new Set<string>();
|
||||
const unions = new Set<string>();
|
||||
let bad = false;
|
||||
while (pending.length > 0 && !bad) {
|
||||
const type = pending.pop()!;
|
||||
switch (type.kind) {
|
||||
case "object":
|
||||
bad = !this.hasClass(type.className);
|
||||
break;
|
||||
case "array":
|
||||
case "set":
|
||||
pending.push(type.elem);
|
||||
break;
|
||||
case "map":
|
||||
pending.push(type.value, type.key);
|
||||
break;
|
||||
case "promise":
|
||||
pending.push(type.inner);
|
||||
break;
|
||||
case "func":
|
||||
pending.push(type.ret);
|
||||
for (let i = type.params.length - 1; i >= 0; i--) pending.push(type.params[i]!);
|
||||
break;
|
||||
case "record": {
|
||||
if (this.badRecords.has(type.shapeId)) { bad = true; break; }
|
||||
if (this.safeRecords.has(type.shapeId) || records.has(type.shapeId)) break;
|
||||
records.add(type.shapeId);
|
||||
const shape = this.record(type.shapeId);
|
||||
if (shape !== undefined) {
|
||||
for (let i = shape.fields.length - 1; i >= 0; i--) pending.push(shape.fields[i]!.type);
|
||||
if (shape.indexValue !== undefined) pending.push(shape.indexValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "union": {
|
||||
if (this.badUnions.has(type.unionId)) { bad = true; break; }
|
||||
if (this.safeUnions.has(type.unionId) || unions.has(type.unionId)) break;
|
||||
unions.add(type.unionId);
|
||||
const definition = this.union(type.unionId);
|
||||
if (definition !== undefined) {
|
||||
for (let i = definition.arms.length - 1; i >= 0; i--) pending.push(definition.arms[i]!);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bad) {
|
||||
// Only the root is proven bad; visited sibling types may be safe.
|
||||
if (root.kind === "record") this.badRecords.add(root.shapeId);
|
||||
if (root.kind === "union") this.badUnions.add(root.unionId);
|
||||
} else {
|
||||
// A cycle back-edge is provisional. Cache negative answers only after
|
||||
// the entire reachable graph has been checked, including its siblings.
|
||||
for (const id of records) this.safeRecords.add(id);
|
||||
for (const id of unions) this.safeUnions.add(id);
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
}
|
||||
+22
-1
@@ -1,5 +1,6 @@
|
||||
import { F64, typeKey, type IrExpr, type IrModule, type IrType } from "../../../packages/compiler/src/ir/ir.js";
|
||||
import { F64, typeKey, type IrExpr, type IrModule, type IrRecordShape, type IrType, type IrUnionDef } from "../../../packages/compiler/src/ir/ir.js";
|
||||
import { sanitizeUnregisteredClassTypes } from "../../../packages/compiler/src/frontend/lowering/sanitize-class-types.js";
|
||||
import { UnregisteredClassTypes } from "../../../packages/compiler/src/frontend/lowering/unregistered-class-types.js";
|
||||
|
||||
const loc = { file: "class-types.ts", start: 0, end: 1 };
|
||||
const missing: IrType = { kind: "object", className: "Fenced" };
|
||||
@@ -21,6 +22,26 @@ for (const record of module.records ?? []) {
|
||||
if (record.indexValue) console.log(typeKey(record.indexValue));
|
||||
}
|
||||
for (const union of module.unions ?? []) console.log(union.arms.map(typeKey).join(","));
|
||||
const records = new Map<string, IrRecordShape>();
|
||||
const unions = new Map<string, IrUnionDef>();
|
||||
for (let i = 0; i < 1024; i++) records.set(String(i), { id: String(i), fields: [
|
||||
{ name: "next", type: { kind: "record", shapeId: String((i + 1) % 1024) } },
|
||||
] });
|
||||
let lookups = 0;
|
||||
const references = new UnregisteredClassTypes(
|
||||
(id) => { lookups++; return records.get(id); }, (id) => unions.get(id), (name) => name === "Registered",
|
||||
);
|
||||
let bad = false;
|
||||
for (let i = 0; i < 1024; i++) bad = references.has({ kind: "record", shapeId: String(i) }) || bad;
|
||||
console.log(bad, lookups);
|
||||
records.set("a", { id: "a", fields: [
|
||||
{ name: "cycle", type: { kind: "record", shapeId: "b" } }, { name: "missing", type: missing },
|
||||
] });
|
||||
records.set("b", { id: "b", fields: [{ name: "back", type: { kind: "union", unionId: "u" } }] });
|
||||
unions.set("u", { id: "u", arms: [{ kind: "record", shapeId: "a" }] });
|
||||
const cyclic = new UnregisteredClassTypes((id) => records.get(id), (id) => unions.get(id), () => false);
|
||||
console.log(cyclic.has({ kind: "record", shapeId: "a" }), cyclic.has({ kind: "record", shapeId: "b" }),
|
||||
cyclic.has({ kind: "union", unionId: "u" }), cyclic.has({ kind: "record", shapeId: "0" }));
|
||||
console.log(missing.kind, kept.kind, F64.kind);
|
||||
sanitizeUnregisteredClassTypes(module, (name) => name === "Registered");
|
||||
console.log(expr.type.kind);
|
||||
|
||||
@@ -25,6 +25,7 @@ test("native class type cleanup updates shared IR without dynamic snapshots", as
|
||||
}
|
||||
expect(native.stdout).toBe(oracle.stdout);
|
||||
expect(native.stdout).toMatch(/^f64 f64\n/);
|
||||
expect(native.stdout).toContain("false 1024\ntrue true true false\n");
|
||||
expect(native.stdout).toMatch(/object object f64\nf64\n$/);
|
||||
} finally { rmSync(directory, { recursive: true, force: true }); }
|
||||
}, 300_000);
|
||||
|
||||
Reference in New Issue
Block a user