Skip to content

fix(array,hir): a replaced Array.prototype[Symbol.iterator] is honoured everywhere, and the method is a real own property (#7760) - #7761

Merged
proggeramlug merged 3 commits into
mainfrom
fix/7760-array-iterator-protocol
Aug 10, 2026
Merged

fix(array,hir): a replaced Array.prototype[Symbol.iterator] is honoured everywhere, and the method is a real own property (#7760)#7761
proggeramlug merged 3 commits into
mainfrom
fix/7760-array-iterator-protocol

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #7760all three items. (Item 1 was added after the first review pass; the original scope note is gone.) #7759 has merged, so this now applies to main directly.

Items 2 and 3: one root cause

Array.prototype[Symbol.iterator] was not an own property at alljs_object_get_symbol_property synthesized a receiver-bound method on every read — and Array.prototype.values was installed by install_noop_proto_methods, so it existed and did nothing. Array-specific; Map/Set/String/%TypedArray% already had real descriptors.

That explains both symptoms. No own property ⇒ no descriptor, hasOwnProperty false, absent from getOwnPropertySymbols (item 3). And the synthesized closure is bound to the prototype at read time, so storing it back and calling it with this === arr throws next is not a function (item 2).

One real array_prototype_values_thunk reading this at CALL time, installed as values and as the own [Symbol.iterator] with the spec descriptor { writable: true, enumerable: false, configurable: true }.

Item 1: for…of over an array

for…of desugars to an index loop that never consults the protocol. Two parallel lowerings had to be fixed — lower::stmt_loops (module init) and lower_decl::body_stmt (function bodies) — which is why a for…of over an array parameter was still wrong after the first half worked, and is worth knowing about for anything else in this area.

The patch is a RUNTIME fact and the index-vs-lazy choice is COMPILE-TIME, so both forms are emitted and selected by a branch on a new Expr::ArrayIterationPatched, lowering to a single volatile i8 load of PERRY_ARRAY_PROTO_ITERATOR_PATCHED — the shape PERRY_ARRAY_INDEX_FAST_PATH_INVALIDATED already uses. Three properties shaped it:

Cost

Interleaved, 7 reps each, 20k-iteration loop fixture (three for…of loops over number[] / object / string arrays), on a host at load average 12:

median   1.85 s (guard off)   1.86 s (guard on)
best     1.85 s               1.82 s

No measurable difference. Recording a near-miss because it is the more useful part: a first non-interleaved best-of-5 on the same box read 2.64 s vs 2.90 s and I nearly reported a 10% regression from it. On a machine at this load only interleaved runs mean anything, and the number still wants confirming on the pinned bench host before anyone quotes it as a bound.

Validation

  • test-files/test_gap_array_proto_iterator_replaced_7542.tsPASS through the harness, byte-identical to node 26.5.1. Covers the descriptor shape, values.call, all four spread forms from spread: a replaced Array.prototype[Symbol.iterator] is ignored by [...arr] #7542, own-@@iterator precedence, for…of over a module const / typed local / array parameter, the lazy-break pull count, restore-by-reference, and a getOwnPropertyDescriptordefineProperty round-trip.
  • cargo test -p perry-runtime --lib: 1991 passed. cargo test -p perry-hir --lib: 290 passed.
  • Targeted parity across array / iterator / for / spread / generator / map / destructuring gap tests: all pass.
  • cargo fmt --all --check clean; scripts/check_file_size.sh clean — the guard wrapper moved to lower/for_of_guard.rs because stmt_loops.rs crossed the 2000-line cap at 2025.

Note on the test

This test could not exist before this PR. Patching Array.prototype[Symbol.iterator] takes the oracle down — node's primordials build a SafeMap from an iterable and get the patched value — so the test must restore the slot, and restoring is exactly what item 2 broke.

No version bump (the branch carries the maintainer's 0.5.1446 bump from the earlier push).

Summary by CodeRabbit

  • Bug Fixes

    • Fixed Array.prototype[Symbol.iterator] and .values to behave like standard configurable, writable, non-enumerable properties.
    • Improved borrowed and direct calls, spreading, Array.from, and for…of behavior when iterators are replaced.
    • Preserved lazy iteration and correct early termination.
    • Restoring or overriding array iterators now works correctly.
  • Tests

    • Added regression coverage for iterator replacement and descriptor round-tripping.
  • Chores

    • Updated the package version to 0.5.1446.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4fa0da04-0afc-41e6-9dcc-b679d79ab2ef

📥 Commits

Reviewing files that changed from the base of the PR and between 78f5c0f and bae9306.

📒 Files selected for processing (16)
  • changelog.d/7761-array-proto-iterator-own-property.md
  • crates/perry-codegen/src/expr/dispatch.rs
  • crates/perry-codegen/src/expr/literals_vars.rs
  • crates/perry-codegen/src/runtime_decls/objects.rs
  • crates/perry-hir/src/ir/expr.rs
  • crates/perry-hir/src/lower/context.rs
  • crates/perry-hir/src/lower/for_of_guard.rs
  • crates/perry-hir/src/lower/lowering_context.rs
  • crates/perry-hir/src/lower/mod.rs
  • crates/perry-hir/src/lower/stmt_loops.rs
  • crates/perry-hir/src/lower_decl/body_stmt.rs
  • crates/perry-hir/src/stable_hash/expr.rs
  • crates/perry-hir/src/walker/expr_mut.rs
  • crates/perry-hir/src/walker/expr_ref.rs
  • crates/perry-runtime/src/array/indexing.rs
  • test-files/test_gap_array_proto_iterator_replaced_7542.ts

📝 Walkthrough

Walkthrough

The runtime now installs Array.prototype[Symbol.iterator] as a descriptor-correct, receiver-aware thunk. Array for…of lowering detects patched iterators and selects lazy iteration. Regression tests cover replacement, restoration, spread, Array.from, borrowed calls, and lazy termination.

Changes

Array iterator patch handling

Layer / File(s) Summary
Install the Array iterator thunk
crates/perry-runtime/src/object/global_this/array_error.rs, crates/perry-runtime/src/object/global_this/proto_methods.rs
Array.prototype.values reads this at call time. Prototype setup installs it as the own Symbol.iterator property with writable, non-enumerable, and configurable attributes.
Publish the iterator patch signal
crates/perry-runtime/src/array/indexing.rs, crates/perry-hir/src/ir/expr.rs, crates/perry-hir/src/stable_hash/expr.rs, crates/perry-hir/src/walker/*, crates/perry-codegen/src/expr/*, crates/perry-codegen/src/runtime_decls/objects.rs
The runtime publishes an atomic patch flag. HIR and code generation read the flag as Expr::ArrayIterationPatched.
Guard proven-array for-of loops
crates/perry-hir/src/lower/*
Lowering preserves the indexed loop and emits a lazy iterator fallback when the iterator is patched.
Validate replacement and restoration behavior
test-files/test_gap_array_proto_iterator_replaced_7542.ts, changelog.d/7761-array-proto-iterator-own-property.md, CLAUDE.md, Cargo.toml
Tests cover descriptors, borrowed calls, patched and restored iterators, own-iterator precedence, spread, Array.from, and lazy termination. The changelog and project version are updated.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ArrayPrototype
  participant RuntimePatchTracker
  participant ForOfLowering
  participant IteratorProtocol
  ArrayPrototype->>RuntimePatchTracker: record iterator replacement
  RuntimePatchTracker->>ForOfLowering: publish patch flag
  ForOfLowering->>ForOfLowering: read flag at loop entry
  ForOfLowering->>IteratorProtocol: use lazy iterator path when patched
Loading

Possibly related issues

Possibly related PRs

Suggested labels: type:bug

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the Array iterator fix and the related values() behavior.
Description check ✅ Passed The description thoroughly explains the changes, related issues, test coverage, validation results, and the remaining limitation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7760-array-iterator-protocol

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changelog.d/7759-array-proto-iterator-spread.md`:
- Around line 13-15: Update the “No gap test, deliberately” note to reflect that
test-files/test_gap_array_proto_iterator_replaced_7542.ts now covers the patched
Array.prototype iterator cases and passes through the harness, as documented by
changelog.d/7761-array-proto-iterator-own-property.md. Remove the stale claim
that no such test exists unless it specifically refers to an unrelated,
separately tracked case.

In `@crates/perry-runtime/src/array/iterator.rs`:
- Around line 820-825: Update the fast path around js_array_is_array and
array_has_own_iterator so Proxy-wrapped arrays with a custom Symbol.iterator
from a get trap use ordinary proxy-aware GetIterator behavior. Do not route
these values through the direct Array.prototype lookup in js_get_iterator;
either exclude Proxy arrays from this optimization or perform the iterator
property read through the proxy.

In `@crates/perry-runtime/src/object/global_this/array_error.rs`:
- Around line 655-660: Update array_prototype_values_thunk to retain the
existing native-array fast path while routing non-array object receivers to an
array-like iterator implementation. Ensure generic calls such as
Array.prototype.values.call(...) and the Symbol.iterator alias return an
iterator rather than undefined.

In `@test-files/test_gap_array_proto_iterator_replaced_7542.ts`:
- Around line 47-54: After Object.defineProperty(arrProto, Symbol.iterator,
desc) in the iterator restoration test, read the property descriptor again and
assert that its value, writable, enumerable, and configurable fields all match
desc, in addition to the existing spread check.
- Around line 14-22: Update the iterator verification around original so it
calls arrProto.values.call(...) directly, confirming the Array.prototype.values
behavior with the expected array output. Also assert that
arrProto[Symbol.iterator] and arrProto.values reference the same shared
function, rather than relying only on the iterator property.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b77ca02-6b35-4df2-bd1d-10e80bcef1e5

📥 Commits

Reviewing files that changed from the base of the PR and between 88aa492 and 2f2ca31.

📒 Files selected for processing (10)
  • changelog.d/7759-array-proto-iterator-spread.md
  • changelog.d/7761-array-proto-iterator-own-property.md
  • crates/perry-runtime/src/array/from_concat.rs
  • crates/perry-runtime/src/array/iterator.rs
  • crates/perry-runtime/src/array/mod.rs
  • crates/perry-runtime/src/array/push_pop.rs
  • crates/perry-runtime/src/object/arguments.rs
  • crates/perry-runtime/src/object/global_this/array_error.rs
  • crates/perry-runtime/src/object/global_this/proto_methods.rs
  • test-files/test_gap_array_proto_iterator_replaced_7542.ts

Comment thread changelog.d/7759-array-proto-iterator-spread.md
Comment thread crates/perry-runtime/src/array/iterator.rs
Comment on lines +655 to +660
pub(crate) extern "C" fn array_prototype_values_thunk(
_c: *const crate::closure::ClosureHeader,
_a: f64,
) -> f64 {
let this = crate::object::js_implicit_this_get();
crate::array::array_values_iter(this)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make Array.prototype.values generic.

array_values_iter returns undefined when this is not a native ArrayHeader. Therefore, Array.prototype.values.call({ 0: "x", length: 1 }) and Array.prototype[Symbol.iterator].call(...) return undefined instead of an iterator.

Add an array-like iterator path for non-array object receivers. Keep the native-array fast path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/global_this/array_error.rs` around lines 655
- 660, Update array_prototype_values_thunk to retain the existing native-array
fast path while routing non-array object receivers to an array-like iterator
implementation. Ensure generic calls such as Array.prototype.values.call(...)
and the Symbol.iterator alias return an iterator rather than undefined.

Comment on lines +14 to +22
const desc: any = Object.getOwnPropertyDescriptor(arrProto, Symbol.iterator);
console.log("descriptor:", typeof desc.value, desc.writable, desc.enumerable, desc.configurable);
console.log("hasOwn:", Object.prototype.hasOwnProperty.call(arrProto, Symbol.iterator));
console.log("in ownSymbols:", Object.getOwnPropertySymbols(arrProto).indexOf(Symbol.iterator) >= 0);
console.log("name:", desc.value.name);

const original = arrProto[Symbol.iterator];
// #7760: the value reads `this` at CALL time, so a borrowed reference works.
console.log("values.call:", JSON.stringify(Array.from(original.call([7, 8]) as any)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Call Array.prototype.values directly.

original comes from arrProto[Symbol.iterator], so this test does not verify Array.prototype.values. A no-op values implementation would still pass. Call arrProto.values.call(...) and verify that both properties reference the shared function.

Suggested coverage
 const original = arrProto[Symbol.iterator];
+console.log("values alias:", arrProto.values === original);
 
-console.log("values.call:", JSON.stringify(Array.from(original.call([7, 8]) as any)));
+console.log("values.call:", JSON.stringify(Array.from(arrProto.values.call([7, 8]) as any)));
+console.log("iterator.call:", JSON.stringify(Array.from(original.call([7, 8]) as any)));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const desc: any = Object.getOwnPropertyDescriptor(arrProto, Symbol.iterator);
console.log("descriptor:", typeof desc.value, desc.writable, desc.enumerable, desc.configurable);
console.log("hasOwn:", Object.prototype.hasOwnProperty.call(arrProto, Symbol.iterator));
console.log("in ownSymbols:", Object.getOwnPropertySymbols(arrProto).indexOf(Symbol.iterator) >= 0);
console.log("name:", desc.value.name);
const original = arrProto[Symbol.iterator];
// #7760: the value reads `this` at CALL time, so a borrowed reference works.
console.log("values.call:", JSON.stringify(Array.from(original.call([7, 8]) as any)));
const desc: any = Object.getOwnPropertyDescriptor(arrProto, Symbol.iterator);
console.log("descriptor:", typeof desc.value, desc.writable, desc.enumerable, desc.configurable);
console.log("hasOwn:", Object.prototype.hasOwnProperty.call(arrProto, Symbol.iterator));
console.log("in ownSymbols:", Object.getOwnPropertySymbols(arrProto).indexOf(Symbol.iterator) >= 0);
console.log("name:", desc.value.name);
const original = arrProto[Symbol.iterator];
console.log("values alias:", arrProto.values === original);
// `#7760`: the value reads `this` at CALL time, so a borrowed reference works.
console.log("values.call:", JSON.stringify(Array.from(arrProto.values.call([7, 8]) as any)));
console.log("iterator.call:", JSON.stringify(Array.from(original.call([7, 8]) as any)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-files/test_gap_array_proto_iterator_replaced_7542.ts` around lines 14 -
22, Update the iterator verification around original so it calls
arrProto.values.call(...) directly, confirming the Array.prototype.values
behavior with the expected array output. Also assert that
arrProto[Symbol.iterator] and arrProto.values reference the same shared
function, rather than relying only on the iterator property.

Comment on lines +47 to +54
// #7760: restore by reference, and by descriptor round-trip.
arrProto[Symbol.iterator] = original;
console.log("restored spread:", JSON.stringify([...src]));
console.log("restored Array.from:", JSON.stringify(Array.from(src as any)));
console.log("restored call spread:", count(...src));

Object.defineProperty(arrProto, Symbol.iterator, desc);
console.log("after defineProperty:", JSON.stringify([...src]));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Check descriptor fields after the round-trip.

After Object.defineProperty, the test checks only spread behavior. A regression that restores the method but changes writable, enumerable, or configurable would pass. Read the descriptor again and compare its value and all three flags with desc.

Suggested assertion
 Object.defineProperty(arrProto, Symbol.iterator, desc);
+const roundTripDesc: any = Object.getOwnPropertyDescriptor(arrProto, Symbol.iterator);
+console.log(
+  "round-trip descriptor:",
+  roundTripDesc.value === desc.value,
+  roundTripDesc.writable === desc.writable,
+  roundTripDesc.enumerable === desc.enumerable,
+  roundTripDesc.configurable === desc.configurable,
+);
 console.log("after defineProperty:", JSON.stringify([...src]));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// #7760: restore by reference, and by descriptor round-trip.
arrProto[Symbol.iterator] = original;
console.log("restored spread:", JSON.stringify([...src]));
console.log("restored Array.from:", JSON.stringify(Array.from(src as any)));
console.log("restored call spread:", count(...src));
Object.defineProperty(arrProto, Symbol.iterator, desc);
console.log("after defineProperty:", JSON.stringify([...src]));
// `#7760`: restore by reference, and by descriptor round-trip.
arrProto[Symbol.iterator] = original;
console.log("restored spread:", JSON.stringify([...src]));
console.log("restored Array.from:", JSON.stringify(Array.from(src as any)));
console.log("restored call spread:", count(...src));
Object.defineProperty(arrProto, Symbol.iterator, desc);
const roundTripDesc: any = Object.getOwnPropertyDescriptor(arrProto, Symbol.iterator);
console.log(
"round-trip descriptor:",
roundTripDesc.value === desc.value,
roundTripDesc.writable === desc.writable,
roundTripDesc.enumerable === desc.enumerable,
roundTripDesc.configurable === desc.configurable,
);
console.log("after defineProperty:", JSON.stringify([...src]));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test-files/test_gap_array_proto_iterator_replaced_7542.ts` around lines 47 -
54, After Object.defineProperty(arrProto, Symbol.iterator, desc) in the iterator
restoration test, read the property descriptor again and assert that its value,
writable, enumerable, and configurable fields all match desc, in addition to the
existing spread check.

@proggeramlug
proggeramlug force-pushed the fix/7760-array-iterator-protocol branch from 2f2ca31 to 78f5c0f Compare August 10, 2026 11:23
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Merging as v0.5.1446

A/B'd against a post-#7759 build, so this isolates this PR:

                node        after #7759 only    this PR
desc            function    undefined           function
hasOwn          true        false               true
inSymbols       true        false               true
name            values      undefined           values
restore         [1,2,3]     TypeError:          [1,2,3]
                            next is not a fn

The best thing here is that it makes the previous PR testable

#7759 shipped without a gap test, and the reason was this bug: a test that patches Array.prototype[Symbol.iterator] must restore the slot before returning or it takes node's own primordials down — and restoring was exactly what was broken. So the untestable thing became testable by fixing the actual defect rather than by working around it. Four workarounds were tried on #7759 and each failure was itself a symptom of this.

I verified that end-to-end rather than taking it: test_gap_array_proto_iterator_replaced_7542.ts is byte-identical to node 26.5.1, and node itself now exits 0 on it — the crash that made the comparison meaningless is gone. One test now covers both issues, through the harness.

One root cause, and the cross-builtin table proves it

Array.prototype[Symbol.iterator] was not an own property at all — synthesized per read, with values installed by install_noop_proto_methods, i.e. present and doing nothing. The table showing Map/Set/String/%TypedArray% all with hasOwn: true and Array alone with false is what turns "a bug" into "Array was missed": the mechanism (install_collection_iterator_symbol) already existed.

Both symptoms then fall out of the one cause rather than needing separate fixes — item 3 because there is no own property to describe, and item 2 because a closure bound to the prototype at read time iterates the prototype when called with this === arr, which is exactly next is not a function.

Reading this at call time in one real array_prototype_values_thunk, installed with the spec descriptor { writable: true, enumerable: false, configurable: true }, is the correct shape — and the defineProperty round-trip row is the one that proves the descriptor is real rather than merely present.

Scope

Correctly not closing #7760 — item 1 stays open, and saying so is right. Same discipline as #7759's three filed follow-ups.

cargo test -p perry-runtime --lib: 2023 passed, 0 failed. 38 targeted parity tests across array / iterator / symbol / object / map / set / typed-array. Gates 21/21.

@proggeramlug proggeramlug changed the title fix(array): Array.prototype[Symbol.iterator] is a real own property and values() iterates (#7760 items 2-3) fix(array,hir): a replaced Array.prototype[Symbol.iterator] is honoured everywhere, and the method is a real own property (#7760) Aug 10, 2026
@proggeramlug
proggeramlug merged commit 293e72f into main Aug 10, 2026
0 of 18 checks passed
@proggeramlug
proggeramlug deleted the fix/7760-array-iterator-protocol branch August 10, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant