Skip to content

rc: an interface-typed global initialized with a class object holds it - #656

Merged
ASDAlexander77 merged 1 commit into
mainfrom
rc-iface-global-initializer
Oct 23, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
rc-iface-global-initializer

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Follow-up 9 from the own-raytrace rulings ("an interface-typed global given another global's value is never counted").

Bug

let a: I = K.w gave the global no count of its own under rc: claimGlobalInitializers looked through casts and unions to the value a global is given, but not through ts.NewInterface, the view a class object takes for an interface-typed global. Giving either side a new value freed the block the other still read (a = new V(3) freed K.w; rc printed 999 for 1).

Wider than filed:

  • the same for a global, a field or an element (let a: I = G / h.v / arr[0]) and const a: I = G (rc);
  • let a: I = mk() and let a: I = new V(8): the call's or the new's reference was given back as a discarded temporary at the end of the initializer, so the global was born pointing at freed memory - under rc and own (own exited 127 on main).

Fix

claimGlobalInitializers looks through ts.NewInterface to the object (getThisVal()), so a fresh object is claimed and a read gets its retain.

Tests

  • tests/00global_interface_initializer.ts (gc, rc, none; AOT + JIT): each source and each interface global given a new value, after allocations of the same size.
  • own/own_global_interface_fresh.ts under own, rc, none and gc.
  • Teeth: without the fix rc fails a call's result (exit 127), own fails the same.

Verified

Found, not fixed here

let opt: I | null = flag ? mk(5) : null reads garbage under gc and rc: the C | null -> I | null conversion is a plain cast with no ts.NewInterface.

🤖 Generated with Claude Code

`let a: I = K.w` (a static field, another global, a field or an element)
gave the global no count of its own: claimGlobalInitializers looked
through casts and unions to the value a global is given, but not through
`ts.NewInterface`, the view a class object takes for an interface-typed
global. Giving either side a new value freed the block the other still
read (rc printed 999 for 1). A call's or a `new`'s reference
(`let a: I = mk()`, `let a: I = new V(8)`) was given back as a discarded
temporary at the end of the initializer, so the global was born pointing
at freed memory; under own too (`let a: I = mk()` read a freed block).

The initializer now looks through `ts.NewInterface` to the object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 6c1f1d1 into main Oct 23, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the rc-iface-global-initializer branch October 23, 2026 09:19
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