From 7918ec43f74f8537aa7279b0cb568396a93fe0c6 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Wed, 28 Oct 2026 08:20:06 -0500 Subject: [PATCH] Skip the arena clear for record literals that write every field An arena-aware record constructor zeroed the whole object after the bump, then stored the count and shape number, and the record literal that called it then stored every field. A record literal covers every field of its shape, so the zeroing only mattered for the dispose entries of an arena that is not closed, which read the fields when the arena drops. The arena-aware constructor of a record shape without dispose entries now clears only the collector header, when the shape has one. Its object is never read before the literal's stores: a release of an arena object only lowers its count, and scr_arena_obj and the heap fallback still return zeroed objects. Shapes with dispose entries and classes, whose constructors need not store every field, keep the whole clear. --- .../compiler/src/backend/llvm/arenas.test.ts | 32 +++++++++++++++++++ packages/compiler/src/backend/llvm/arenas.ts | 14 +++++++- packages/compiler/src/backend/llvm/shapes.ts | 14 +++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/compiler/src/backend/llvm/arenas.test.ts b/packages/compiler/src/backend/llvm/arenas.test.ts index 1ab4b010a..a0885bba4 100644 --- a/packages/compiler/src/backend/llvm/arenas.test.ts +++ b/packages/compiler/src/backend/llvm/arenas.test.ts @@ -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 () => { diff --git a/packages/compiler/src/backend/llvm/arenas.ts b/packages/compiler/src/backend/llvm/arenas.ts index 3e3954856..426d3a9e3 100644 --- a/packages/compiler/src/backend/llvm/arenas.ts +++ b/packages/compiler/src/backend/llvm/arenas.ts @@ -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`, diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts index 489b5d028..993a1ffa5 100644 --- a/packages/compiler/src/backend/llvm/shapes.ts +++ b/packages/compiler/src/backend/llvm/shapes.ts @@ -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(