Summary
Summing a TypedArray field of an object with an element-index for loop returns the wrong result when the object also has a plain-Array field. Reproduces with a single object, synchronously, with no async and no GC pressure — so it is a codegen bug, not a GC bug. In simple cases it's a deterministic wrong answer; under retention/allocation it becomes non-deterministic (mis-reads varying adjacent memory), which is how it surfaced while validating the moving GC.
Repro (deterministic)
const n = { id: 5, payload: [1, 2, 3, 4], buf: new Uint32Array([10, 20, 30]) };
let s = 0;
for (const p of n.payload) s += p; // 10
for (let i = 0; i < n.buf.length; i++) s += n.buf[i]; // +60
console.log(s); // node: 70 | Perry: 10 (the buf loop contributes 0)
Key observations
- Direct field access is CORRECT —
n.buf.length is 3, n.buf[0..2] are 10,20,30, n.buf.constructor.name is Uint32Array, and this holds with the typed array as the only field, first field, or after the array field. So the object literal stores the typed array fine.
- The bug is specifically the element-index loop
for (let i=0; i<n.buf.length; i++) ... n.buf[i] reading a typed-array field of an object that also has an Array field. Perry’s packed-numeric loop optimizations (crates/perry-codegen/src/stmt/loops.rs — match_packed_f64_versioned_loop / packed i32/u32 paths, typed-feedback guards) emit raw element loads; a guard/alias misfire when the receiver object has a sibling Array field is the likely cause.
- Control cases that are CLEAN:
{id, a:number[], b:number[]} (two plain arrays); {id, buf} (typed array only); reading n.buf[i] directly rather than in the summing loop.
Impact
Silent wrong results for a common pattern (objects holding both a JS array and a typed array, iterated by index). Because the mis-read can also read/write adjacent heap memory, it corrupts the heap and makes any subsequent collector (moving or not) fragile — this is what made a moving-GC stress test crash; the GC was choking on a heap this bug had already corrupted.
Next step
Localize with --trace hir --focus on the loop and --trace llvm; check the packed-numeric-loop guard/versioning when the loop receiver is a typed-array field on an object with other pointer fields.
Summary
Summing a TypedArray field of an object with an element-index
forloop returns the wrong result when the object also has a plain-Array field. Reproduces with a single object, synchronously, with no async and no GC pressure — so it is a codegen bug, not a GC bug. In simple cases it's a deterministic wrong answer; under retention/allocation it becomes non-deterministic (mis-reads varying adjacent memory), which is how it surfaced while validating the moving GC.Repro (deterministic)
Key observations
n.buf.lengthis3,n.buf[0..2]are10,20,30,n.buf.constructor.nameisUint32Array, and this holds with the typed array as the only field, first field, or after the array field. So the object literal stores the typed array fine.for (let i=0; i<n.buf.length; i++) ... n.buf[i]reading a typed-array field of an object that also has an Array field. Perry’s packed-numeric loop optimizations (crates/perry-codegen/src/stmt/loops.rs—match_packed_f64_versioned_loop/ packed i32/u32 paths, typed-feedback guards) emit raw element loads; a guard/alias misfire when the receiver object has a sibling Array field is the likely cause.{id, a:number[], b:number[]}(two plain arrays);{id, buf}(typed array only); readingn.buf[i]directly rather than in the summing loop.Impact
Silent wrong results for a common pattern (objects holding both a JS array and a typed array, iterated by index). Because the mis-read can also read/write adjacent heap memory, it corrupts the heap and makes any subsequent collector (moving or not) fragile — this is what made a moving-GC stress test crash; the GC was choking on a heap this bug had already corrupted.
Next step
Localize with
--trace hir --focuson the loop and--trace llvm; check the packed-numeric-loop guard/versioning when the loop receiver is a typed-array field on an object with other pointer fields.