+## v0.5.825 — fix(runtime): `Array.from(string)` splits by Unicode codepoint instead of emitting garbage f64s. Pre-fix `Array.from("hello")` returned five mantissa-shaped numbers like `[2.5e-323, 1.9e+214, ...]` — `js_array_clone` fell through to the array-memcpy path on a StringHeader source, reading the underlying UTF-8 bytes as 8-byte f64 slots. Same path was reached by `[..."abc"]` spread and `for (const c of someString)` iteration. **Root cause.** `crates/perry-runtime/src/array.rs::js_array_clone` had per-receiver early dispatch arms for Set, Map, typed arrays, and Buffer/Uint8Array, but no detection for `GC_TYPE_STRING` or `top16==STRING_TAG` sources. StringHeader and ArrayHeader both start with a u32 length, so the array-memcpy path read what looked like a sensible `length` value (the UTF-16 code-unit count) and then `ptr::copy_nonoverlapping` 8 bytes per element from the string's data region — UTF-8 bytes reinterpreted as IEEE 754 doubles. **Fix.** New string-source arm at the top of `js_array_clone` that detects either a NaN-boxed STRING_TAG (top16=0x7FFF) or a raw pointer whose preceding GcHeader has `obj_type == GC_TYPE_STRING`, then routes to a new private helper `js_array_from_string_codepoints` that iterates the source by Unicode codepoint via `str::chars()` and emits each as a 1-codepoint heap string. Pre-counts the codepoints to size the result exactly, then encodes each `char` back to UTF-8 via `encode_utf8` into a 4-byte scratch buffer and allocates a per-element StringHeader via `js_string_from_bytes`. The element is NaN-boxed with `js_nanbox_string` so the storage slot carries the STRING_TAG. Surrogate-pair handling: `str::chars()` yields one `char` per scalar value, so UTF-16 surrogate pairs (like an emoji's `0xD83C 0xDF89`) materialize as a single element — matches the spec's String Iterator behavior (ECMA-262 §22.1.5). **Validation.** 7-case `/tmp/probe_arr_from_str.ts` (ASCII "hello", BMP "aéz", surrogate-pair "a🎉b", empty, single-char, `[..."abc"]` spread, `typeof arr[0]`) all byte-identical to Bun. 26/28 gap parity tests pass (baseline preserved). Probed while loop-testing real-app patterns; same probe surfaced other Array.from variants (`Array.from(arraylike)`, `Array.from(_, mapFn)`) which remain on the followup backlog. **Out of scope (still on the backlog)**: `Array.from({length, 0:..., 1:...})` (array-like), `Array.from(iter, mapFn)` (map-fn ignored), `flat(Infinity)`, `copyWithin`, `.finally()` closure mutation, `arr[Symbol.iterator]()`, `string.match(rx).groups`, tagged-template `.raw`, async-fn throw-after-await.
0 commit comments