Keep int32 class fields when a program registers process event listeners (#1160)

This commit is contained in:
Chris Tate
2026-10-16 20:36:11 -05:00
committed by GitHub
3 changed files with 93 additions and 0 deletions
@@ -282,6 +282,17 @@ test("dynamic stores disqualify the field names they can reach", () => {
"insp.dynS",
"dyn.objCreate",
"dyn.getPrototype",
// Registering or removing a process event listener stores nothing either.
"process.onSignal",
"process.offSignal",
"process.onUncaughtException",
"process.offUncaughtException",
"process.onUnhandledRejection",
"process.offUnhandledRejection",
"process.onRejectionHandled",
"process.offRejectionHandled",
"process.onWarning",
"process.offWarning",
]) {
const reflected = analyzeInt32Slots(program(write(reader, [view])));
expect([reader, reflected.isField("C", "a"), reflected.isField("C", "b")]).toEqual([
+13
View File
@@ -345,6 +345,19 @@ const DYN_READ_ONLY = new Set<string>([
"json.stringifyReplacer",
"json.stringifyValue",
"num.toStringRadix",
// Process event listeners (signals, uncaughtException, unhandledRejection,
// rejectionHandled, warning): the runtime keeps the callback and calls it
// with fresh event values; off compares callbacks by identity.
"process.offRejectionHandled",
"process.offSignal",
"process.offUncaughtException",
"process.offUnhandledRejection",
"process.offWarning",
"process.onRejectionHandled",
"process.onSignal",
"process.onUncaughtException",
"process.onUnhandledRejection",
"process.onWarning",
"sharedArrayBuffer.new",
"text.decodeStream",
"worker.new",
@@ -0,0 +1,69 @@
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 } from "../src/index.js";
async function classLayout(listeners: string): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-process-listener-int32-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ll");
await writeFile(
entry,
`
class P {
kind: number;
pos: number;
next: P | undefined;
constructor(kind: number, pos: number) {
this.kind = kind | 0;
this.pos = pos | 0;
this.next = undefined;
}
}
const n = process.argv.length;
const p = new P(n, n * 2);
p.next = new P(1, 2);
if (n > 100) console.log(p as unknown);
const listener = (): void => {
console.log("signal");
};
if (n > 100) {
${listeners}
}
console.log(p.kind + p.pos);
`,
);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "llvm" });
expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true);
const llvm = await readFile(outPath, "utf8");
return /^%sc_o_P = type \{[^}]*\}/m.exec(llvm)?.[0] ?? "";
} finally {
await rm(dir, { recursive: true, force: true });
}
}
// uncaughtException listeners need @types/node, which this temporary
// project lacks; the int32-slots unit test covers their helpers.
test.for([
[
"signals",
`process.on("SIGINT", listener);\n process.once("SIGTERM", listener);\n process.off("SIGINT", listener);`,
],
[
"unhandledRejection",
`process.on("unhandledRejection", listener);\n process.off("unhandledRejection", listener);`,
],
[
"rejectionHandled",
`process.on("rejectionHandled", listener);\n process.off("rejectionHandled", listener);`,
],
["warning", `process.on("warning", listener);\n process.off("warning", listener);`],
] as const)(
"%s listeners keep int32 fields of classes that reach dynamic code",
async ([, listeners]) => {
const layout = await classLayout(listeners);
expect(layout).toMatch(/^%sc_o_P = type \{ i64, i32, i32, ptr/);
},
);