Skip to content

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

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions