diff --git a/packages/compiler/src/ir/int32-slots.test.ts b/packages/compiler/src/ir/int32-slots.test.ts index 6b75e203b..79042a063 100644 --- a/packages/compiler/src/ir/int32-slots.test.ts +++ b/packages/compiler/src/ir/int32-slots.test.ts @@ -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([ diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 3dec3397d..b8c33de0b 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -345,6 +345,19 @@ const DYN_READ_ONLY = new Set([ "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", diff --git a/packages/compiler/test/process-listener-int32-emission.test.ts b/packages/compiler/test/process-listener-int32-emission.test.ts new file mode 100644 index 000000000..1d7303b97 --- /dev/null +++ b/packages/compiler/test/process-listener-int32-emission.test.ts @@ -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 { + 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/); + }, +);