Summary
collect_pointer_typed_locals's expr_value_type classifies Expr::Uint8ArrayGet as Type::Number unconditionally, with no regard for the key kind (crates/perry-codegen/src/collectors/pointer_locals.rs:232):
Expr::Number(_)
| Expr::Integer(_)
| Expr::Uint8ArrayLength(_)
| Expr::Uint8ArrayGet { .. } // <-- every key kind, including symbol keys
| Expr::BufferLength(_)
| Expr::BufferIndexGet { .. }
... => Some(Type::Number),
But Uint8ArrayGet is not always numeric. PR #6997's analysis enumerated three heap-capable arms of that node — a symbol key, an unproven key routed to js_typed_array_index_get_dynamic, and an i32-context js_object_get_index_polymorphic arm — and gated its own rooting decision on the lowering rather than the node kind for exactly this reason.
Consequence
const x = u8[Symbol.iterator] binds a heap value into a local that the collector has classified as non-pointer. A non-pointer local gets no shadow slot, so it is not a GC root: the value is live in the program and invisible to the collector. Same class as #6951, rarer shape.
Note the classification is also what sizes the shadow frame, so this is adjacent to #6995 (frame sized by HashMap len while indices come from a counter) — both are ways the shadow-slot map can end up not describing the frame it is used with. They are separate defects; fixing one does not fix the other.
Status — verified statically, reachability NOT verified
I confirmed by reading origin/main (aa1c15028) that the match arm is unconditional. I have not exhibited a TypeScript program that produces a symbol-keyed Uint8ArrayGet bound to a local, and that is the first thing to establish. The obvious candidate is const it = u8[Symbol.iterator], but AST→HIR lowering may route symbol keys to a different node before this collector sees them — check before assuming.
The soundness principle PR #6997 established should be applied here too: decide from the lowering, not from the node kind or the declared type. Perry does not enforce type annotations at runtime, so a declared type is never sufficient evidence that a value cannot be a pointer.
Verification guidance
A reproducer must be checked on the evacuating precise-roots arm, where a missing root actually bites:
PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off
with [gc-copy-minor] copied_objects=N, N > 0. A green run where nothing moved proves nothing (#6942/#6946/#6950). Also note #6993: the per-PR matrix arms cannot reach this configuration, so PR CI would not catch a regression here even once a reproducer exists.
Found while fixing #6996 (PR #6997), which deliberately declined to copy the pattern. Filed separately rather than folded in.
Summary
collect_pointer_typed_locals'sexpr_value_typeclassifiesExpr::Uint8ArrayGetasType::Numberunconditionally, with no regard for the key kind (crates/perry-codegen/src/collectors/pointer_locals.rs:232):But
Uint8ArrayGetis not always numeric. PR #6997's analysis enumerated three heap-capable arms of that node — a symbol key, an unproven key routed tojs_typed_array_index_get_dynamic, and an i32-contextjs_object_get_index_polymorphicarm — and gated its own rooting decision on the lowering rather than the node kind for exactly this reason.Consequence
const x = u8[Symbol.iterator]binds a heap value into a local that the collector has classified as non-pointer. A non-pointer local gets no shadow slot, so it is not a GC root: the value is live in the program and invisible to the collector. Same class as #6951, rarer shape.Note the classification is also what sizes the shadow frame, so this is adjacent to #6995 (frame sized by
HashMaplen while indices come from a counter) — both are ways the shadow-slot map can end up not describing the frame it is used with. They are separate defects; fixing one does not fix the other.Status — verified statically, reachability NOT verified
I confirmed by reading
origin/main(aa1c15028) that the match arm is unconditional. I have not exhibited a TypeScript program that produces a symbol-keyedUint8ArrayGetbound to a local, and that is the first thing to establish. The obvious candidate isconst it = u8[Symbol.iterator], but AST→HIR lowering may route symbol keys to a different node before this collector sees them — check before assuming.The soundness principle PR #6997 established should be applied here too: decide from the lowering, not from the node kind or the declared type. Perry does not enforce type annotations at runtime, so a declared type is never sufficient evidence that a value cannot be a pointer.
Verification guidance
A reproducer must be checked on the evacuating precise-roots arm, where a missing root actually bites:
with
[gc-copy-minor] copied_objects=N, N > 0. A green run where nothing moved proves nothing (#6942/#6946/#6950). Also note #6993: the per-PR matrix arms cannot reach this configuration, so PR CI would not catch a regression here even once a reproducer exists.Found while fixing #6996 (PR #6997), which deliberately declined to copy the pattern. Filed separately rather than folded in.