Skip to content

A const record whose field is made for it has storage, as a let does (#485) - #490

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-record-literal-field-leak
Oct 4, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-record-literal-field-leak

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Fixes #485: a const record literal leaked what was made for its fields under rc and own.

The bug

A record literal with a field known only at run time is built in a temporary slot and read out whole, and the literal takes no count for what its fields are given: its receiver does, as a let does with RetainSlot. A const of it was folded into that read unless a field was read out of another object or an owning local (#454, #460), so for a field made for the literal nothing took a count and nothing gave one back.

Peak memory, AOT (measure.ps1), 1M iterations of a function holding const o = { <field>, n: i } and one other owning local:

field main rc main own this PR rc this PR own
items: [i, 2] (the issue) 81.2 MB 81.2 MB 4.4 MB 4.4 MB
t: [i, [i]] 81.2 MB 81.2 MB 4.4 MB compile error, see below
items: new Numbers(n) 93.5 MB 93.5 MB 4.4 MB 4.4 MB
inner: { items: [i, 2], k: i } 81.2 MB 81.2 MB 4.4 MB compile error, see below
s: "a" + i 4.4 MB 35.2 MB 4.4 MB 4.4 MB
items: mk(i) 4.4 MB compile error 4.4 MB 4.4 MB
c: new C() 4.4 MB compile error 4.4 MB 4.4 MB

(let o was flat in every row.)

The fix (MLIRGen, readsLiteralSlot)

A const of such a record now has storage, as a let does, when a field owns a block and is made for the literal: an array or tuple built there, a constant array copied for it, a new array, a string, a call's result, a new instance. A value taken out of another record ({ ...o }, ts.ExtractProperty / ts.DeconstructTuple) stays with that record's count, as before, and a literal string is immortal.

Under own, a record whose field is a tuple or record holding an array ({ t: [i, [i]], n: i }) now gives the compile error a let of it already gave on main ("an object literal that holds a value it owns is moved only into a local ..."), where it used to compile and leak.

Tests

  • 00const_record_owned_fields.ts (compile, JIT, rc and none corpora): each field kind above, read after heap churn.
  • test-rc-const-record-release (const-record-release.cmake): no run sees a leak, so the IR is read - under rc each of that file's seven records has a ts.ReleaseSlot of its tuple slot. Main fails it (arrayField does not release its record's slot).

Gates

Gate Result
Windows Release suite (ctest -C Release -j 12 --timeout 300, staged default library) 3788 / 3788 passed
Linux (WSL, on main 86a9ec3, ninja release) 3773 / 3773 passed (1 pre-existing disabled test)
own corpus (--emit=llvm -mm=own --no-default-lib, every file of test/tester/tests) main 459 / 614, this PR 459 / 614 (--opt: 457 / 457). No file goes from ok to failing; 00types, callWithSpread and conditionalTypes2 change only a generated name or a union's member order in their first error
own memory (measure.ps1, AOT) own_fresh_string, own_return_new, own_try_local, own_fresh_array, own_literals, own_array_param_push unchanged
Default library rebuilt (release and debug) with this compiler, its suite gc 159/159, rc 158/159, none 158/159 in each of release/debug x compile/jit - the one failure is weakref_basic, gc-only by design (#420)

Closes #485

🤖 Generated with Claude Code

…485)

A record literal with a field known only at run time is built in a
temporary slot and read out whole, and the literal takes no count for
what its fields are given: its receiver does, as a `let` does with
RetainSlot. A `const` of it was folded into that read unless a field
was read out of another object or an owning local (#454, #460), so for
a field made for the literal nothing took a count and nothing gave one
back:

- an array or tuple built in the literal (`{ items: [i, 2], n: i }`),
  a constant array copied for it, or a new array, born with no count,
  was never freed under rc and own (AOT, 1M iterations: 96.5 MB, 4.4
  with `let`);
- under own, a string, a call's result or a new instance made for it
  leaked as well (35.2 MB for `{ s: "a" + i, n: i }`), and the call and
  the instance did not compile.

Such a const now has storage. A value taken out of another record
(`{ ...o }`, ts.ExtractProperty / ts.DeconstructTuple) stays with that
record's count, as before, and a literal string is immortal.

Under own, a record whose field is a tuple or record holding an array
gives the compile error a `let` of it already gave, where it used to
compile and leak.

Closes #485

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit f588f0a into main Oct 4, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-record-literal-field-leak branch October 4, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rc/own: a record literal with a run-time field leaks the array in its fields

1 participant