From 96dbc6204f26ac380dfa253742812c88d2af2096 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Fri, 23 Oct 2026 21:48:25 -0500 Subject: [PATCH] Keep int32 class fields beside Buffer.from over an ArrayBuffer range Buffer.from(arrayBuffer, byteOffset[, length]) lowers to buffer.poolFromDynRange with its operands as checked values. The int32 slot analysis treats any runtime helper with a checked-value argument as a possible store into a live class instance unless the helper is listed as read-only, so one such call dropped every int32 class field fact in the program (tsc-ts's scanner makes one). buffer.poolFromDynRange and buffer.poolFromDynOffset read their operands (the ArrayBuffer's view after Node's offset and length conversions, or a copy of a string's, array-like's or typed array's values) and store into no class instance, so they join DYN_READ_ONLY. --- packages/compiler/src/ir/int32-slots.test.ts | 46 ++++++++++++++++++++ packages/compiler/src/ir/int32-slots.ts | 5 +++ 2 files changed, 51 insertions(+) diff --git a/packages/compiler/src/ir/int32-slots.test.ts b/packages/compiler/src/ir/int32-slots.test.ts index be933ab38..9b838bb55 100644 --- a/packages/compiler/src/ir/int32-slots.test.ts +++ b/packages/compiler/src/ir/int32-slots.test.ts @@ -357,6 +357,52 @@ test("an Error subclass constructor forwarding dynamic arguments stores no class expect(slots.isField("C", "a")).toBe(true); }); +test("Buffer.from over an ArrayBuffer range and Buffer as a value keep class fields int32", () => { + // `Buffer.from(view.buffer, offset, length)` passes its operands as + // checked values, and a program that reads the Buffer function's value + // installs it in %main; neither stores into a class instance. + const classes = [cls("C", ["a"])]; + const view: IrExpr = { loc, kind: "dynFrom", value: ref("o", obj("C")), type: DYN }; + const libCall = (fnName: string, args: IrExpr[], type: IrType = VOID): IrStmt => + stmt({ loc, kind: "libCall", fn: fnName, args, type } as IrExpr); + const slots = analyzeInt32Slots( + mod(classes, [ + fn( + "load", + [ + { id: "o", type: obj("C") }, + { id: "storage", type: DYN }, + ], + [ + set(ref("o", obj("C")), "C", "a", num(1)), + libCall("dyn.typedRefIs", [ + view, + { loc, kind: "strLit", value: "object:C", type: STRING }, + ]), + libCall("buffer.installValue", []), + libCall("buffer.constructor", [], DYN), + libCall( + "buffer.poolFromDynRange", + [ref("storage", DYN), num(2), num(4)].map((arg): IrExpr => + arg.type.kind === "dyn" ? arg : { loc, kind: "dynFrom", value: arg, type: DYN }, + ), + ), + libCall("buffer.poolFromDynOffset", [ + ref("storage", DYN), + { + loc, + kind: "dynFrom", + value: num(2), + type: DYN, + }, + ]), + ], + ), + ]), + ); + expect(slots.isField("C", "a")).toBe(true); +}); + test("captured bindings share one proof across the declaring function and its closures", () => { const closure: IrExpr = { loc, kind: "closure", fnName: "inner", captures: ["s"], type: DYN }; const outer = (): IrFunction => ({ diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index a359a18c5..2c26fcf0f 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -451,6 +451,11 @@ const DYN_READ_ONLY = new Set([ "fs.utimesSync", // Buffer.prototype members read as values: a lookup by key. "buffer.prototypeMember", + // Buffer.from(value, offset[, length]) over checked values: an + // ArrayBuffer's view after Node's offset and length conversions, or a copy + // of a string, array-like or typed array's values. + "buffer.poolFromDynOffset", + "buffer.poolFromDynRange", "insp.dyn", "insp.dynS", // The dynamic-tier operators run their dyn twins (scr_js_value.c); the