Skip to content

rc: a getter read is settled like a call - #408

Merged
ASDAlexander77 merged 5 commits into
mainfrom
rc-getter-result-temporary
Sep 29, 2026
Merged

ASDAlexander77 merged 5 commits into
mainfrom
rc-getter-result-temporary

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Under -mm=rc, a value read through a getter was never released. The first leftover from phase 4 (spec §15.3).

Cause

A getter retains its result, as every function does (+1 convention, §9.24). OwnedReturnConsumptionPass settles who owns that reference, but it only looked at ts.CallIndirect. A getter read stays an accessor op (ts.Accessor, ThisAccessor, ThisIndirect*, BoundIndirect*) until the affine lowering turns it into a call. So:

  • h.cc.x kept the getter's reference, and nothing gave it back.
  • let b = h.cc retained it a second time.

Fix

Accessor reads are classified the way calls are:

  • a getter named outright;
  • a virtual slot: every method with that member name;
  • an interface slot: what the vtable globals put there;
  • otherwise, the whole-module (closed-world) answer.

Each classified read is then marked, taken over by its receiver, or released at the end of its block.

A getter read with no live use is skipped. Assigning through an accessor first builds a getter read, then rebuilds the op as a setter and leaves the read for the canonicalizer to delete. A release would keep that read alive, which would add a getter call the program never made.

Measured (AOT, measure.ps1, MB)

program gc rc before rc after own
own_getter_borrow 6.5 12.6 4.8 4.8
h.cc.x in a loop 6.3 10.9 4.6 4.6

Tests

  • New own/rc_getter_temporary.ts, JIT + AOT under rc, gc and none. It covers:

    • class getters returning a field and a fresh object;
    • an override that goes through super;
    • a static getter;
    • an interface accessor;
    • a getter result taken by a let, passed to a call, and assigned through.

    Each read comes after a churn(), so a block freed too early would be reused.

  • Windows Release: 3235/3235.

  • Linux (WSL): 3222/3222.

  • -mm=own corpus: unchanged at 303 compiling files.

Independent of #407. Both edit interfaceCallReturnsOwned, so whichever lands second needs a small rebase.

🤖 Generated with Claude Code

ASDAlexander77 and others added 3 commits September 29, 2026 21:48
A getter retains what it returns, as every function does (§9.24), but
OwnedReturnConsumptionPass only looked at ts.CallIndirect. A getter read
is one of the accessor ops (ts.Accessor, ThisAccessor, ThisIndirect*,
BoundIndirect*) until the affine lowering turns it into a call, so its
+1 was never taken over or given back: `h.cc.x` leaked the C, and
`let b = h.cc` retained a second time. own_getter_borrow read 12.6 MB
under rc; it now reads 4.8 MB, as gc's 6.5 and own's 4.8 do.

The accessor ops are classified as calls are - the getter named outright,
every method of that member name for a virtual slot, the vtables for an
interface slot, or the whole-module answer - and then marked, consumed
by their receiver, or released at the end of the block.

A getter read with no live use is left alone: an assignment through an
accessor builds a read before rebuilding the op as a setter, and a
release would keep that read, a call the program never made.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ASDAlexander77 and others added 2 commits September 29, 2026 22:19
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit bc33db7 into main Sep 29, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the rc-getter-result-temporary branch September 29, 2026 21:55
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.

1 participant