You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
spread: a replaced Array.prototype[Symbol.iterator] is ignored by [...arr] #7542
Replacing Array.prototype[Symbol.iterator] changes what [...arr] yields in node. In Perry the spread ignores the replacement and returns the builtin element copy.
js_get_iterator (symbol/iterator.rs) honours it — when the flag is set it reads the patched method off Array.prototype and calls it with this === val. array_from_spread_value never asks. Its generic symbol lookup reads own symbol props only (hence the own-@@iterator case working), so for a plain array it misses and falls to the js_array_is_array arm, which drives the builtin array_values_iter unconditionally.
Two things to check when fixing:
Whether note_array_proto_iterator_write actually fires for Array.prototype[Symbol.iterator] = fn — if the flag never flips, both this path and js_get_iterator's are dead.
%ArrayIteratorPrototype%.next replacement is a separate, currently-unmodelled case: builtin array iterators dispatch .next() through the class-id tower, so a patched prototype next is bypassed there too.
Provenance
Found while building a 32-case [...arr] semantics matrix for #7533. Pre-existing: reproduces byte-identically at f06270d06 and is unchanged by #7540's dense fast path, which gates on array_proto_iterator_modified() and so cannot make this worse — with the flag set it declines and falls through to the same (already wrong) protocol path.
Investigated; not fixed. Three findings, two of which correct the issue as written — posting them so the next attempt does not start from the same map I did.
1. note_array_proto_iterator_write DOES fire (check #1, answered)
Instrumented the hook. Across a run it is called 49 times (builtin prototype setup), and exactly one call matches — obj == proto == 0x…a552b8, sym == wk == 0x…b40180 — and sets the flag:
[7542] note_write obj=0x34814a552b8 proto=0x34814a552b8 sym=0x34814b40180 wk=0x34814b40180 already=false
[7542] FLAG SET
So neither this path nor js_get_iterator's is dead, and the flag is not the bug. (Worth knowing for anyone else instrumenting: the early calls report proto=0x0 because array_prototype_addr() has not materialised yet — a write during runtime init could never match. The user write happens well after, so that is not the issue here, but it is a real hole in the hook for anything that patches during init.)
2. The affected surface is wider than "spread"
Measured against node 26.5.1 with the prototype patched:
node
perry
[...arr] (variable)
["patched"]
[1,2,3]
[...[1,2,3]] (literal)
["patched"]
[1,2,3]
Array.from(arr)
["patched"]
[1,2,3]
f(...arr)
1 arg
3 args
for (const x of arr) — arr a typed local
["patched"]
[1,2,3]
for (const x of [1,2,3]) — literal
["patched"]
["patched"] ✓
const [p] = [1,2,3] — literal
"patched"
"patched" ✓
own arr[Symbol.iterator]
["own"]
["own"] ✓
So Array.from and call-spread are equally affected, and for…of is affected too — but only over a typed local; over an array literal it is correct. The literal/local split points at a codegen dense-loop lowering bypassing the protocol, which is a second, independent bypass from the one in the title.
3. The "Where" section's mechanism does not hold — the js_array_is_array arm is never reached
The issue says the generic walk "reads own symbol props only … so for a plain array it misses and falls to the js_array_is_array arm, which drives the builtin array_values_iter unconditionally."
I added the flag check to exactly that arm (delegating to js_get_iterator, which already implements the prototype consult) and it changed nothing. Instrumenting the arm shows why: with the flag set, it is never reached. array_from_spread_value returns earlier.
It is not the #7533 dense fast path — dense_spread_source checks array_proto_iterator_modified() at flat_clone.rs:86 and correctly declines. What is left between the two is the [Symbol.iterator] walk itself, which therefore does not miss for a plain array — it resolves something and returns it, and that something is evidently the builtin rather than the patched method. That is where I would look next: what array_prototype_property_value → default_object_prototype_property_value returns for @@iterator after the prototype slot has been overwritten, and whether it is reading a cached/snapshotted builtin table rather than the live property.
So the one-line fix implied by the current "Where" is not available, and a fix confined to array_from_spread_value cannot address for…of over a local either.
I have reverted my change rather than land a no-op with a plausible comment attached to it.
Activity
docs(changelog): name the two pre-existing spread divergences by issue (
docs(changelog): name the two pre-existing spread divergences by issue (
perf(array): copy an ordinary dense array's spread instead of driving…
proggeramlug commentedon Aug 10, 2026
Investigated; not fixed. Three findings, two of which correct the issue as written — posting them so the next attempt does not start from the same map I did.
1.
note_array_proto_iterator_writeDOES fire (check #1, answered)Instrumented the hook. Across a run it is called 49 times (builtin prototype setup), and exactly one call matches —
obj == proto == 0x…a552b8,sym == wk == 0x…b40180— and sets the flag:So neither this path nor
js_get_iterator's is dead, and the flag is not the bug. (Worth knowing for anyone else instrumenting: the early calls reportproto=0x0becausearray_prototype_addr()has not materialised yet — a write during runtime init could never match. The user write happens well after, so that is not the issue here, but it is a real hole in the hook for anything that patches during init.)2. The affected surface is wider than "spread"
Measured against node 26.5.1 with the prototype patched:
[...arr](variable)["patched"][1,2,3][...[1,2,3]](literal)["patched"][1,2,3]Array.from(arr)["patched"][1,2,3]f(...arr)for (const x of arr)—arra typed local["patched"][1,2,3]for (const x of [1,2,3])— literal["patched"]["patched"]✓const [p] = [1,2,3]— literal"patched""patched"✓arr[Symbol.iterator]["own"]["own"]✓So
Array.fromand call-spread are equally affected, andfor…ofis affected too — but only over a typed local; over an array literal it is correct. The literal/local split points at a codegen dense-loop lowering bypassing the protocol, which is a second, independent bypass from the one in the title.3. The "Where" section's mechanism does not hold — the
js_array_is_arrayarm is never reachedThe issue says the generic walk "reads own symbol props only … so for a plain array it misses and falls to the
js_array_is_arrayarm, which drives the builtinarray_values_iterunconditionally."I added the flag check to exactly that arm (delegating to
js_get_iterator, which already implements the prototype consult) and it changed nothing. Instrumenting the arm shows why: with the flag set, it is never reached.array_from_spread_valuereturns earlier.It is not the #7533 dense fast path —
dense_spread_sourcechecksarray_proto_iterator_modified()atflat_clone.rs:86and correctly declines. What is left between the two is the[Symbol.iterator]walk itself, which therefore does not miss for a plain array — it resolves something and returns it, and that something is evidently the builtin rather than the patched method. That is where I would look next: whatarray_prototype_property_value→default_object_prototype_property_valuereturns for@@iteratorafter the prototype slot has been overwritten, and whether it is reading a cached/snapshotted builtin table rather than the live property.So the one-line fix implied by the current "Where" is not available, and a fix confined to
array_from_spread_valuecannot addressfor…ofover a local either.I have reverted my change rather than land a no-op with a plausible comment attached to it.
fix(array): a replaced Array.prototype[Symbol.iterator] drives spread…
fix(array): a replaced Array.prototype[Symbol.iterator] drives spread…
fix(array): a replaced Array.prototype[Symbol.iterator] drives spread…