Skip to content

spread: a replaced Array.prototype[Symbol.iterator] is ignored by [...arr] #7542

Closed

Description

@proggeramlug

Replacing Array.prototype[Symbol.iterator] changes what [...arr] yields in node. In Perry the spread ignores the replacement and returns the builtin element copy.

const arrProto: any = Array.prototype;
const original = arrProto[Symbol.iterator];
arrProto[Symbol.iterator] = function* () { yield "patched"; };

console.log([...[1, 2, 3]]);   // node: [ 'patched' ]   perry: [ 1, 2, 3 ]

arrProto[Symbol.iterator] = original;
console.log([...[1, 2, 3]]);   // node: [ 1, 2, 3 ]     perry: [ 1, 2, 3 ]

An own [Symbol.iterator] on the instance is honoured correctly, so this is specific to the prototype-level replacement:

const a: any = [1, 2, 3];
a[Symbol.iterator] = function* () { yield "own"; };
console.log([...a]);           // node: [ 'own' ]       perry: [ 'own' ]  ✓

Where

The machinery already exists and is not consulted on this path. array/indexing.rs keeps a sticky flag:

static ARRAY_PROTO_ITERATOR_MODIFIED: AtomicBool = AtomicBool::new(false);
pub(crate) fn note_array_proto_iterator_write(obj: usize, sym_key: usize) { … }
pub(crate) fn array_proto_iterator_modified() -> bool { … }

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:

  1. 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.
  2. %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.

Activity

  1. proggeramlug commented on Aug 10, 2026

    @proggeramlug
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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