Skip the arena clear for record literals that write every field (#2470)

This commit is contained in:
Chris Tate
2026-10-28 09:07:33 -05:00
committed by GitHub
3 changed files with 58 additions and 2 deletions
@@ -67,6 +67,38 @@ console.log(total);
expect(body).not.toContain("ScrArena.closed");
expect(body).not.toContain("@scr_arena_disposal");
}
// A record literal stores every field, and no dispose entry reads them:
// the record's constructor does not clear the object. A class constructor
// may leave fields unset, so the class's does.
const record = constructors.find((body) => body.includes("@sc_rnew_"))!;
const cls = constructors.find((body) => !body.includes("@sc_rnew_"))!;
expect(record).not.toContain("@llvm.memset");
expect(cls).toContain("@llvm.memset");
});
test("record literals in an arena that is not closed keep the clear their dispose entries need", async () => {
// The items hold outside strings, so their groups are not closed.
const llvm = await llvmFor(`
interface Item {
label: string;
next: Item | null;
}
function chain(labels: string[], n: number): Item {
return n === 0 ? { label: labels[0]!, next: null } : { label: labels[n % labels.length]!, next: chain(labels, n - 1) };
}
function length(item: Item): number {
return item.next === null ? item.label.length : item.label.length + length(item.next);
}
const labels = ["a", "bb", "ccc"];
let total = 0;
for (let round = 0; round < 4; round++) total += length(chain(labels, 10 + round));
console.log(total);
`);
expect(llvm).toMatch(/@scr_arena_open\(ptr %[\w.]+, i32 0\)/);
const items = arenaConstructors(llvm).filter((body) => body.includes("@sc_rnew_"));
expect(items.length).toBe(1);
expect(items[0]).toContain(".dispose");
expect(items[0]).toContain("@llvm.memset");
});
test("an arena that is not closed keeps the dispose list in every arena-aware constructor", async () => {
+13 -1
View File
@@ -275,6 +275,12 @@ export function arenaCurrentAddress(host: ArenaHost, dest: string): string[] | n
* the arena is full. The bump, the clear, the header and the dispose entry
* are inline (scr_arena.c's ScrArena ABI); scr_arena_obj takes a new chunk
* and scr_arena_disposal grows the list.
*
* `clear` is what the bump path zeroes: the whole object, or only the
* collector header, for callers that store every word of the object before
* anything reads it (a record literal of a shape with no dispose entries:
* `init` stores the count and the shape number, and the literal every
* field). scr_arena_obj and the heap constructor return zeroed objects.
*/
export function arenaNewLines(
host: ArenaHost,
@@ -284,6 +290,7 @@ export function arenaNewLines(
dispose: string | null,
init: string[],
result: string,
clear: "object" | "header" = "object",
): string[] {
const S = host.sizeType;
// ScrArena's fields and dispose entries are pointer-sized (scr_arena.c).
@@ -328,8 +335,13 @@ export function arenaNewLines(
`carve:`,
` %end = getelementptr inbounds i8, ptr %bp, ${S} %n`,
` store ptr %end, ptr %a`,
` call void @llvm.memset.p0.${S}(ptr align 8 %bp, i8 0, ${S} %n, i1 false)`,
);
if (clear === "object")
lines.push(` call void @llvm.memset.p0.${S}(ptr align 8 %bp, i8 0, ${S} %n, i1 false)`);
else if (cyc !== null)
lines.push(
` call void @llvm.memset.p0.${S}(ptr align 8 %bp, i8 0, ${S} ${CYC_HDR}, i1 false)`,
);
if (cyc !== null)
lines.push(
` %id = trunc i32 %tid to i16`,
+13 -1
View File
@@ -2387,8 +2387,20 @@ export function emitRecordShapes(
` %shp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${SHAPE_WORD_INDEX}`,
` store i32 ${shapeNumber}, ptr %shp ; shape number`,
);
// A record literal covers every field of its shape (ir.ts recordLit),
// so without dispose entries, which read the fields at the drop, no
// field is read before the literal stores it.
defs.push(
...arenaNewLines(host, newSym, sizeOf, traced ? cycType : null, dispose, init, "%o"),
...arenaNewLines(
host,
newSym,
sizeOf,
traced ? cycType : null,
dispose,
init,
"%o",
dispose === null ? "header" : "object",
),
);
if (dispose !== null)
defs.push(