Keep int32 fields of classes whose prototypes are named for inspection (#1736)

This commit is contained in:
Chris Tate
2026-10-21 21:31:22 -05:00
committed by GitHub
3 changed files with 88 additions and 1 deletions
+3 -1
View File
@@ -262,7 +262,8 @@ test("dynamic stores disqualify the field names they can reach", () => {
expect([readOnly.isField("C", "a"), readOnly.isField("C", "b")]).toEqual([true, true]);
// Reflection that only reads the view (Object.keys/values/entries, for-in,
// own-key lists, typeof, toString and inspection) stores nothing, nor does
// creating an object from a prototype or reading a prototype.
// creating an object from a prototype, reading a prototype or naming a
// class prototype for inspection.
for (const reader of [
"dyn.objKeys",
"dyn.objKeyStrings",
@@ -282,6 +283,7 @@ test("dynamic stores disqualify the field names they can reach", () => {
"insp.dynS",
"dyn.objCreate",
"dyn.getPrototype",
"dyn.nameClassPrototype",
// Registering or removing a process event listener stores nothing either.
"process.onSignal",
"process.offSignal",
+1
View File
@@ -376,6 +376,7 @@ const DYN_READ_ONLY = new Set<string>([
"dyn.collectionIteratorIndex",
"dyn.mapSeedEntries",
"dyn.mapSeedEntry",
"dyn.nameClassPrototype",
"dyn.numberConstructor",
"dyn.objCreate",
"dyn.objEntries",
@@ -0,0 +1,84 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule } from "../src/index.js";
import { analyzeInt32Slots, dynamicFieldStores } from "../src/ir/int32-slots.js";
import type { IrModule } from "../src/ir/ir.js";
// Instances of these classes reach checked-dynamic code (an unknown[] array,
// instanceof, inspection, JSON and string conversion), but the program never
// stores a property through a dynamic value. The helpers the compiler emits
// for those reads (prototype objects, inspection names) store nothing, so the
// classes keep their int32 fields.
const source = `
import { inspect } from "node:util";
class Node {
kind: number;
flags = 0;
constructor(kind: number) {
this.kind = kind;
}
}
class Identifier extends Node {
text = "id";
}
class Type {
constructor(
public flags: number,
public id: number,
) {}
}
const values: unknown[] = [new Node(1), new Identifier(2), new Type(4, 7), "s", 3];
let total = 0;
for (const value of values) {
if (value instanceof Identifier) total += value.kind | value.flags;
else if (value instanceof Node) total += value.kind;
else if (value instanceof Type) total += value.flags & 6;
}
console.log(total, values, inspect(new Type(1, 2)));
console.log(JSON.stringify(values), String(values[0]), typeof values[2]);
`;
async function lowered(): Promise<IrModule> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-int32-dynamic-"));
try {
const entry = join(dir, "main.ts");
await writeFile(entry, source);
const outPath = join(dir, "main.ir.json");
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
return deserializeModule(await readFile(outPath, "utf8"));
} finally {
await rm(dir, { recursive: true, force: true });
}
}
test("classes that only reach dynamic reads keep their int32 fields", async () => {
const mod = await lowered();
// Name the functions whose dynamic helpers count as stores that can name
// any property: such a helper either stores into the value or is missing
// from the analysis's read-only list (DYN_READ_ONLY in int32-slots.ts).
const storing = mod.functions
.filter((fn) => dynamicFieldStores({ ...mod, functions: [fn] }).all)
.map((fn) => fn.name);
expect(storing).toEqual([]);
expect(dynamicFieldStores(mod).names.size).toBe(0);
const slots = analyzeInt32Slots(mod);
const className = (jsName: string): string =>
mod.classes!.find((c) => (c.jsName ?? c.name) === jsName || c.name.endsWith(`.${jsName}`))!
.name;
expect([
slots.isField(className("Node"), "kind"),
slots.isField(className("Node"), "flags"),
slots.isField(className("Identifier"), "kind"),
slots.isField(className("Type"), "flags"),
slots.isField(className("Type"), "id"),
]).toEqual([true, true, true, true, true]);
// The program reaches the helper that names class prototypes for inspection.
expect(mod.functions.some((fn) => fn.name.startsWith("%prototype.data."))).toBe(true);
});