The EventEmitterAsyncResource native backing Box caches the subclass emitter's this in its event_emitter field as a raw heap address, and no GC root scanner visits that field. After a collection that moves the emitter, the cache points at the old from-space copy, so sub.asyncResource.eventEmitter === sub becomes false.
Found while repairing #10952. It exists on main too, independent of that PR, so #10952's new gap test deliberately doesn't assert it.
Fix: register the field with a mutable root scanner, the same way #10952 did for EventEmitterHandle.async_resource_handle in both perry-stdlib/src/events.rs and crates/perry-ext-events/src/lib.rs. Or store a rooted handle instead of a raw address. Reproduce with PERRY_GC_SCHEDULE_SEED=<n> PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_PROTECT_FROMSPACE=1 and an assertion on sub.asyncResource.eventEmitter === sub after some allocation.
See CLAUDE.md, "A runtime-side cache of a raw heap pointer is a GC root".
The
EventEmitterAsyncResourcenative backingBoxcaches the subclass emitter'sthisin itsevent_emitterfield as a raw heap address, and no GC root scanner visits that field. After a collection that moves the emitter, the cache points at the old from-space copy, sosub.asyncResource.eventEmitter === subbecomesfalse.Found while repairing #10952. It exists on main too, independent of that PR, so #10952's new gap test deliberately doesn't assert it.
Fix: register the field with a mutable root scanner, the same way #10952 did for
EventEmitterHandle.async_resource_handlein bothperry-stdlib/src/events.rsandcrates/perry-ext-events/src/lib.rs. Or store a rooted handle instead of a raw address. Reproduce withPERRY_GC_SCHEDULE_SEED=<n> PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_PROTECT_FROMSPACE=1and an assertion onsub.asyncResource.eventEmitter === subafter some allocation.See CLAUDE.md, "A runtime-side cache of a raw heap pointer is a GC root".