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:
- 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.
Replacing
Array.prototype[Symbol.iterator]changes what[...arr]yields in node. In Perry the spread ignores the replacement and returns the builtin element copy.An own
[Symbol.iterator]on the instance is honoured correctly, so this is specific to the prototype-level replacement:Where
The machinery already exists and is not consulted on this path.
array/indexing.rskeeps a sticky flag:js_get_iterator(symbol/iterator.rs) honours it — when the flag is set it reads the patched method offArray.prototypeand calls it withthis === val.array_from_spread_valuenever asks. Its generic symbol lookup reads own symbol props only (hence the own-@@iteratorcase working), so for a plain array it misses and falls to thejs_array_is_arrayarm, which drives the builtinarray_values_iterunconditionally.Two things to check when fixing:
note_array_proto_iterator_writeactually fires forArray.prototype[Symbol.iterator] = fn— if the flag never flips, both this path andjs_get_iterator's are dead.%ArrayIteratorPrototype%.nextreplacement is a separate, currently-unmodelled case: builtin array iterators dispatch.next()through the class-id tower, so a patched prototypenextis bypassed there too.Provenance
Found while building a 32-case
[...arr]semantics matrix for #7533. Pre-existing: reproduces byte-identically atf06270d06and is unchanged by #7540's dense fast path, which gates onarray_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.