Under -mm=rc and -mm=own, a record literal that has a field known only at run time never releases the array in one of its fields. In the repro this happens when another local that owns memory follows the record.
function f(i: number) {
const o = { items: [i, 2], n: i };
o.items.push(3);
const b: number[] = [i];
b.push(4);
return o.items.length;
}
function main() {
let s = 0;
for (let i = 0; i < 1000000; i++) s += f(i);
print(s);
}
Peak memory, ahead of time (tslang/test/tester/tools/measure.ps1, built from #484's branch; this path is the same on main):
| gc |
rc |
none |
own |
| 5.9 MB |
96.5 MB |
4.3 MB |
96.5 MB |
Expected: rc and own flat, about 4.4 MB. Under none LLVM removes the allocations entirely, which is why none is low.
Under the JIT on main (77d9a6a), rc peaks at 144 MB and own at 150 MB. #484's build gives the same.
Without const b (or another owning local such as const s = "x" + i) the leak does not show, but the IR is the same. LLVM elides the allocation in that case, so a flat reading there proves nothing.
What the IR shows (--emit=mlir-affine -mm=rc): the record is built in a temporary slot (ts.Variable initialised from a const_tuple with unit holes, then ts.Store of each run-time field). const o reads that slot (ts.Load {__owned_result_named}). The ts.CreateArray stored into items gets no ts.Release, and the slot gets no ReleaseSlot. Nothing gives back the array's reference.
Since #484, a record whose constant array field is built in a slot takes this path too: { items: [1, 2], n: i } leaks the same way.
Found while testing #484 (#479).
🤖 Generated with Claude Code
Under
-mm=rcand-mm=own, a record literal that has a field known only at run time never releases the array in one of its fields. In the repro this happens when another local that owns memory follows the record.Peak memory, ahead of time (
tslang/test/tester/tools/measure.ps1, built from #484's branch; this path is the same on main):Expected: rc and own flat, about 4.4 MB. Under none LLVM removes the allocations entirely, which is why none is low.
Under the JIT on main (77d9a6a), rc peaks at 144 MB and own at 150 MB. #484's build gives the same.
Without
const b(or another owning local such asconst s = "x" + i) the leak does not show, but the IR is the same. LLVM elides the allocation in that case, so a flat reading there proves nothing.What the IR shows (
--emit=mlir-affine -mm=rc): the record is built in a temporary slot (ts.Variableinitialised from aconst_tuplewithunitholes, thents.Storeof each run-time field).const oreads that slot (ts.Load {__owned_result_named}). Thets.CreateArraystored intoitemsgets nots.Release, and the slot gets noReleaseSlot. Nothing gives back the array's reference.Since #484, a record whose constant array field is built in a slot takes this path too:
{ items: [1, 2], n: i }leaks the same way.Found while testing #484 (#479).
🤖 Generated with Claude Code