Summary
codegen/closure.rs allocates the closure's captured this (:704) and new.target (:687) into plain alloca_entry slots and never binds either. They hold a heap receiver for the whole closure body, across every this_stack.last() reader (~14 sites), so an evacuating minor neither marks nor rewrites them and every load below a collection point names from-space.
Found by the #7202 bare-alloca enumeration (PR #7207), which fixed the sibling this slot on the inline-constructor path.
It is a structural asymmetry, not one missing line
codegen/method.rs:316 lf.enable_shadow_frame(m.len() as u32 + 1); // the +1 IS the `this` slot
codegen/method.rs:1344 lf.enable_shadow_frame(m.len() as u32 + 1);
codegen/closure.rs:589 lf.enable_shadow_frame(m.len() as u32); // no slot reserved
method.rs:334/:1372 then bind that reserved slot (js_shadow_slot_bind(this_shadow_slot_idx, this_slot)). Closures reserve nothing, so there is no index to bind.
The in-tree comment at codegen/closure.rs:815-817 — "The this / new.target capture reads are exempt … they run in the entry-block prologue, ahead of any statement that could collect" — justifies the timing of the read, not the lifetime of the slot, which spans the whole body.
Fix shape
expr::scalar_slot_root::root_entry_alloca (generalized in #7207 from #6968's machinery) is the mechanism: seed the alloca to undefined in entry_allocas, reserve_shadow_slot(), bind once in entry setup. It grows the frame on demand, so enable_shadow_frame's pre-lowering count does not have to predict it.
A reproducer needs a closure whose body allocates between two this uses, with the closure's receiver reachable only from the capture cell. It is worth writing one before fixing: #7207's inline-ctor equivalent turned out to be masked at runtime (the class-field inline guard fails on the forwarded header and the runtime fallback resolves forwarding), so a static fix without a witness should say so.
Refs #7202, #7154, #7161, #7207.
Summary
codegen/closure.rsallocates the closure's capturedthis(:704) andnew.target(:687) into plainalloca_entryslots and never binds either. They hold a heap receiver for the whole closure body, across everythis_stack.last()reader (~14 sites), so an evacuating minor neither marks nor rewrites them and every load below a collection point names from-space.Found by the
#7202bare-alloca enumeration (PR #7207), which fixed the siblingthisslot on the inline-constructor path.It is a structural asymmetry, not one missing line
method.rs:334/:1372then bind that reserved slot (js_shadow_slot_bind(this_shadow_slot_idx, this_slot)). Closures reserve nothing, so there is no index to bind.The in-tree comment at
codegen/closure.rs:815-817— "Thethis/new.targetcapture reads are exempt … they run in the entry-block prologue, ahead of any statement that could collect" — justifies the timing of the read, not the lifetime of the slot, which spans the whole body.Fix shape
expr::scalar_slot_root::root_entry_alloca(generalized in #7207 from #6968's machinery) is the mechanism: seed the alloca toundefinedinentry_allocas,reserve_shadow_slot(), bind once in entry setup. It grows the frame on demand, soenable_shadow_frame's pre-lowering count does not have to predict it.A reproducer needs a closure whose body allocates between two
thisuses, with the closure's receiver reachable only from the capture cell. It is worth writing one before fixing: #7207's inline-ctor equivalent turned out to be masked at runtime (the class-field inline guard fails on the forwarded header and the runtime fallback resolves forwarding), so a static fix without a witness should say so.Refs #7202, #7154, #7161, #7207.