Repository navigation
codegen: pointer_locals types Expr::Uint8ArrayGet as Number unconditionally — a symbol-keyed read binds a heap value into a local with no shadow slot #6998
Description
Activity
Fixed in #7699.
Reachability — the thing this issue said to establish first — is established
Not argued: read off
--print-hir.lower/expr_member/member_tail.rsfolds every non-STRING key on aUint8Array/Buffer-typed local onto this node, and a symbol key is not a string, soconst it = u8[Symbol.iterator]gives exactly:Let { id: 1, name: "it", ty: Any, mutable: false, init: Some(Uint8ArrayGet { array: LocalGet(0), index: SymbolFor(String("@@__perry_wk_iterator")) }) }A dynamic key reaches it too:
const k: any = "byteLength"; u8[k]givesUint8ArrayGet { index: LocalGet(4) }.The fix
expr_value_type's arm answersSome(Type::Number)only for a structurally numeric key — the sameindex_is_definitely_numericproof theIndexGettyped-array arm next to it already uses — andNoneotherwise.Structural on purpose. A
number-declared index local is not evidence, because Perry does not enforce annotations, andexpr_is_known_non_pointer_shadow_value's sharper three-part test needs anFnCtxthis collector runs before.Noneis the conservative direction: the local keeps a slot the collector rewrites harmlessly.Both directions are pinned, and the sabotage arm was run — restoring the unconditional
Numberturnsa_symbol_keyed_uint8array_read_keeps_its_shadow_slotred whilea_numeric_keyed_uint8array_read_still_pays_no_slot(the #6996 cost this must not reintroduce) stays green.★ The runtime consequence is currently masked by a second, behavioural bug
Filed as #7700, and it is worth more than this fix. Five other collectors and
type_analysis/numeric.rsmake the same unconditional "aUint8ArrayGetis a number" assumption, and they force the i32-context lowering — so a non-numeric key on a typed-array-typed local reads a byte instead of the property:node 26.5.1 perry typeof u8[Symbol.iterator]functionnumberconst k: any = "byteLength"; u8[k]40u8.tag = {…}; const k: any = "tag"; u8[k]{"kind":"buffer"}0expr/arrays_finds.rs's JS-value lowering handles all three key kinds correctly; the value never reaches it.So no heap value binds into such a local today, which is why this classification bug has never bitten. It is a latent-soundness fix that becomes load-bearing the moment #7700 is repaired — which is the order the two should land in. No gap test was added here for that reason: the behavioural one would be red, and a red gap test is not acceptance.
Fixed by #7699 (commit
9965eb3f6), verified in current code:collectors/pointer_locals.rs:263 Expr::Uint8ArrayGet { index, .. } if index_is_definitely_numeric(index) => Some(Type::Number) collectors/pointer_locals.rs:266 Expr::Uint8ArrayGet { .. } => Nonewith a
#6998:comment at line 240 reproducing this issue's own reasoning. A symbol-keyed read no longer binds a heap value into a local classifiedNumber, so it gets a shadow slot.Filing the residue rather than folding it in here: that fix's changelog flags a separate, still-open defect — five sibling collectors plus
type_analysis/numeric.rsstill assumeUint8ArrayGetis always numeric. That is new scope discovered by the fix, not part of this issue as written, and it should not ride along on a closed ticket.
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.