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(