Repro
const a = new Uint8Array([10, 20, 30, 40]);
console.log(a[0], a[1], a[2], a[3]); // works: 10 20 30 40
const collected: number[] = [];
for (const b of a) collected.push(b);
console.log("for…of:", collected);
console.log("Array.from:", Array.from(a));
bun main.ts:
10 20 30 40
for…of: [ 10, 20, 30, 40 ]
Array.from: [ 10, 20, 30, 40 ]
perry main.ts:
10 20 30 40
for…of: [ 3.325357495e-315, 0, 0, 0 ]
Array.from: [ 3.325357495e-315, 0, 0, 0 ]
a[i] indexing returns the correct byte values. for…of, Array.from, and the spread operator ([...a]) all produce a 4-element array where the first element is a NaN-boxed float with the raw bit pattern of the underlying NaN-box tag, and subsequent elements are 0.
The denormal 3.325357495e-315 is 0x000000000000A reinterpreted as f64 — the bit pattern of the actual byte values squeezed into the low bits of an f64 with no NaN-box tag at all. So the iterator is reading the raw memory backing the typed array as f64s rather than calling the per-element accessor that does the byte→NaN-box conversion.
Why this matters
Discovered while running @bradenmacdonald/s3-lite-client post #575 + #576 fix (#551). The package's bin2hex helper:
export function bin2hex(binary: Uint8Array) {
return Array.from(binary).map((b) => b.toString(16).padStart(2, "0")).join("");
}
Converts an HMAC output to a hex signature. With this bug, Array.from produces garbage so the hex signature is wrong — the produced URL is structurally identical to bun's but with X-Amz-Signature=0000… (or a similarly wrong value), and would fail authentication against real S3.
Beyond s3-lite, this breaks every package that iterates a Uint8Array (or any typed array) — file I/O, hashing, encryption, base64, anything binary.
Affected APIs (all same root cause, same observed output)
for (const b of typedArray) ...
Array.from(typedArray)
[...typedArray] (spread)
- Likely also:
typedArray.values() / .entries() iteration via the iterator protocol
- Likely also: passing typed arrays to
new Set(typedArray), new Map(...), Promise.all(typedArray.map(...)) — anything that calls Symbol.iterator
Index-based access (a[i], for (let i=0; i<a.length; i++)) is fine — that's the workaround until this lands.
Suggested implementation
The typed-array iterator likely just casts the underlying byte buffer's words directly as f64s without going through the byte→NaN-box conversion that a[i] indexing applies. Search crates/perry-runtime/src/typedarray.rs for the Symbol.iterator / keys / values / entries impl and align with what js_typed_array_index_get (or whatever the indexing path is) does.
Likely a 5-10 line patch.
Acceptance
The repro prints [10, 20, 30, 40] for both for…of and Array.from. Plus regressions:
Uint16Array, Int32Array, Float32Array, Float64Array — all produce element-typed values via iteration
- Spread in array literal:
[0, ...new Uint8Array([1,2,3]), 99] → [0, 1, 2, 3, 99]
- Spread in function call:
Math.max(...new Uint8Array([1,2,3])) → 3
new Set(uint8array) / Array.from(uint8array, fn) / [...uint8array.values()]
Refs #551
Repro
bun main.ts:perry main.ts:a[i]indexing returns the correct byte values.for…of,Array.from, and the spread operator ([...a]) all produce a 4-element array where the first element is a NaN-boxed float with the raw bit pattern of the underlying NaN-box tag, and subsequent elements are 0.The denormal
3.325357495e-315is0x000000000000Areinterpreted as f64 — the bit pattern of the actual byte values squeezed into the low bits of an f64 with no NaN-box tag at all. So the iterator is reading the raw memory backing the typed array as f64s rather than calling the per-element accessor that does the byte→NaN-box conversion.Why this matters
Discovered while running
@bradenmacdonald/s3-lite-clientpost #575 + #576 fix (#551). The package'sbin2hexhelper:Converts an HMAC output to a hex signature. With this bug,
Array.fromproduces garbage so the hex signature is wrong — the produced URL is structurally identical to bun's but withX-Amz-Signature=0000…(or a similarly wrong value), and would fail authentication against real S3.Beyond s3-lite, this breaks every package that iterates a
Uint8Array(or any typed array) — file I/O, hashing, encryption, base64, anything binary.Affected APIs (all same root cause, same observed output)
for (const b of typedArray) ...Array.from(typedArray)[...typedArray](spread)typedArray.values()/.entries()iteration via the iterator protocolnew Set(typedArray),new Map(...),Promise.all(typedArray.map(...))— anything that callsSymbol.iteratorIndex-based access (
a[i],for (let i=0; i<a.length; i++)) is fine — that's the workaround until this lands.Suggested implementation
The typed-array iterator likely just casts the underlying byte buffer's words directly as f64s without going through the byte→NaN-box conversion that
a[i]indexing applies. Searchcrates/perry-runtime/src/typedarray.rsfor theSymbol.iterator/keys/values/entriesimpl and align with whatjs_typed_array_index_get(or whatever the indexing path is) does.Likely a 5-10 line patch.
Acceptance
The repro prints
[10, 20, 30, 40]for bothfor…ofandArray.from. Plus regressions:Uint16Array,Int32Array,Float32Array,Float64Array— all produce element-typed values via iteration[0, ...new Uint8Array([1,2,3]), 99]→[0, 1, 2, 3, 99]Math.max(...new Uint8Array([1,2,3]))→3new Set(uint8array)/Array.from(uint8array, fn)/[...uint8array.values()]Refs #551