From 9ca4cf6a4dc9e34bf1f9d48773a0a670dad65a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 30 Aug 2026 15:14:41 +0200 Subject: [PATCH 1/3] fix(runtime): observe prototype replacement in method calls (#9131) --- .../src/object/class_registry.rs | 4 +- .../class_registry/prototype_methods.rs | 1 - .../field_get_set/get_field_by_name_tail.rs | 21 ++++ .../src/object/native_call_method.rs | 34 ++++++ .../object/object_ops/define_properties.rs | 7 ++ .../src/typed_feedback/guards.rs | 5 +- .../src/inline/call_inliner.rs | 108 +++++++++++++++++- ...issue_9131_prototype_method_replacement.rs | 103 +++++++++++++++++ 8 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 crates/perry/tests/issue_9131_prototype_method_replacement.rs diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index a50575f88e..86968241c9 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -117,8 +117,8 @@ pub(crate) use prototype_methods::{ pub(crate) use prototype_methods::{ class_prototype_fast_guard_invalidated_for_method, class_prototype_method_guard_slot, class_prototype_method_root_remove, class_prototype_method_root_store, - invalidate_class_prototype_fast_guards_for_method, mirror_prototype_method_on_object, - synthetic_class_id_for_function, + invalidate_class_prototype_fast_guards, invalidate_class_prototype_fast_guards_for_method, + mirror_prototype_method_on_object, synthetic_class_id_for_function, }; pub use prototype_methods::{ js_class_register_static_field, js_get_function_prototype_method, diff --git a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs index 9a282efccb..936859393f 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs @@ -192,7 +192,6 @@ pub(crate) fn invalidate_class_prototype_fast_guards_for_method(name: &str) { retire_prototype_dependent_caches(); } -#[allow(dead_code)] // Fail-closed escape hatch for a future keyless mutation path. pub(crate) fn invalidate_class_prototype_fast_guards() { #[cfg(not(test))] PERRY_CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED.store(1, std::sync::atomic::Ordering::Release); diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index 8c82bd50b5..66770d1b37 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -1357,6 +1357,17 @@ pub(crate) fn get_field_by_name_object_tail( let keys = crate::object::object_keys_array(obj); if keys.is_null() { + // An explicit per-instance [[Prototype]] replaces the class's + // declaration prototype; it is not an extra link in front of the + // original vtable. Walk that authoritative chain before exposing + // class getters or methods, and do not resurrect the old class + // surface when the custom chain misses. + if !key.is_null() + && super::super::prototype_chain::object_has_prototype_override(obj as usize) + { + return super::super::prototype_chain::resolve_inherited_field(obj as usize, key) + .unwrap_or_else(JSValue::undefined); + } // #809: an object with no own keys (e.g. an `Object.create(proto)` // result, or a `Function.prototype = obj` instance) still has to // resolve inherited props/methods. Pre-fix this returned undefined @@ -1727,6 +1738,16 @@ pub(crate) fn get_field_by_name_object_tail( } } + // A shaped receiver's own-key scan has missed. As in the keyless arm + // above, a user-installed per-instance prototype is now authoritative + // and must win over class-vtable getters and methods. + if !key.is_null() + && super::super::prototype_chain::object_has_prototype_override(obj as usize) + { + return super::super::prototype_chain::resolve_inherited_field(obj as usize, key) + .unwrap_or_else(JSValue::undefined); + } + // Key not found in the keys_array — fall back to the class // vtable's getter map. Refs #486 (hono): cross-module class // getters (e.g. hono Context's `get req()` defined in diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index 84cb811156..4b9d1f6dd9 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -1249,6 +1249,40 @@ pub unsafe extern "C-unwind" fn js_native_call_method( // dispatch tower below it is orders of magnitude more expensive. let object = || object_handle.get_nanbox_f64(); let jsval = || JSValue::from_bits(object().to_bits()); + + // An explicit `Object.setPrototypeOf(instance, proto)` replaces the + // instance's class prototype. Resolve a method value through ordinary + // property lookup before any class/native dispatch: that lookup preserves + // own-property precedence and, for a miss, the per-instance chain is + // authoritative rather than falling back to the original class vtable. + if jsval().is_pointer() { + let candidate = jsval().as_pointer::() as usize; + if crate::value::addr_class::is_above_handle_band(candidate) + && crate::object::is_valid_obj_ptr(candidate as *const u8) + && super::prototype_chain::object_has_prototype_override(candidate) + { + let method_key = + crate::string::js_string_from_bytes(method_name.as_ptr(), method_name.len() as u32); + if !method_key.is_null() { + let receiver = object(); + let receiver_ptr = + JSValue::from_bits(receiver.to_bits()).as_pointer::(); + let method = super::js_object_get_field_by_name(receiver_ptr, method_key); + let method_handle = root_scope.root_nanbox_f64(f64::from_bits(method.bits())); + let receiver = object(); + let bound = crate::closure::clone_closure_rebind_this( + method_handle.get_nanbox_f64().to_bits(), + receiver, + ); + let args = refreshed_args(); + return crate::closure::js_native_call_value( + f64::from_bits(bound), + args.as_ptr(), + args.len(), + ); + } + } + } // RAII recursion depth guard: prevent stack overflow from circular module deps. // The guard auto-decrements on drop, covering all ~20 return points in this function. // When max depth is hit, return a pointer to a static empty object instead of undefined. diff --git a/crates/perry-runtime/src/object/object_ops/define_properties.rs b/crates/perry-runtime/src/object/object_ops/define_properties.rs index 5b0d6afe8d..6c4cb26383 100644 --- a/crates/perry-runtime/src/object/object_ops/define_properties.rs +++ b/crates/perry-runtime/src/object/object_ops/define_properties.rs @@ -431,6 +431,13 @@ pub extern "C" fn js_object_set_prototype_of(obj_value: f64, proto: f64) -> f64 && !crate::closure::is_closure_ptr(obj_ptr_for_record) && is_valid_obj_ptr(obj_ptr_for_record as *const u8) { + // This is keyless prototype surgery: unlike `C.prototype.m = value`, + // there is no method name with which to retire only one direct-method + // guard slot. The lower-level recorder is also used to wire runtime + // builtin prototype objects during startup, so invalidate here at the + // user-visible `Object.setPrototypeOf` entry rather than poisoning the + // fast path for every program during initialization. + crate::object::invalidate_class_prototype_fast_guards(); super::super::prototype_chain::object_set_static_prototype(obj_ptr_for_record, proto_bits); // A grown array's local may still hold the FORWARDED (old) pointer; // the spec [[HasProperty]]/[[Get]] helpers look the prototype up by diff --git a/crates/perry-runtime/src/typed_feedback/guards.rs b/crates/perry-runtime/src/typed_feedback/guards.rs index 61dd016e6d..53f0121659 100644 --- a/crates/perry-runtime/src/typed_feedback/guards.rs +++ b/crates/perry-runtime/src/typed_feedback/guards.rs @@ -107,14 +107,11 @@ fn method_direct_call_contract( }; let name_hash = hash_bytes(method_bytes); let method_guard_slot = crate::object::class_prototype_method_guard_slot(method_name); - if crate::object::class_prototype_fast_guard_invalidated_for_method(method_guard_slot) { - return (shape_addr, class_id, gc_type, name_hash, false); - } - if object_addr == 0 || expected_class_id == 0 || !crate::object::shapes::is_shape_id(expected_shape_id) || expected_func_ptr.is_null() + || crate::object::class_prototype_fast_guard_invalidated_for_method(method_guard_slot) { return (shape_addr, class_id, gc_type, name_hash, false); } diff --git a/crates/perry-transform/src/inline/call_inliner.rs b/crates/perry-transform/src/inline/call_inliner.rs index e641c339ba..a03640ee69 100644 --- a/crates/perry-transform/src/inline/call_inliner.rs +++ b/crates/perry-transform/src/inline/call_inliner.rs @@ -210,14 +210,116 @@ fn returns_only_anonymous_records(function: &Function) -> bool { /// loop bodies were seeded with empty facts, so such calls were never inlined. /// /// `collect_mutated_local_ids` recurses into closures and nested loops and -/// catches every `LocalSet`/`Update`, so "not mutated in the loop" is a sound -/// (conservative) proxy for "fact holds on every iteration". +/// catches every `LocalSet`/`Update`. Prototype surgery is a second kind of +/// loop-carried mutation: the receiver can still hold the same object while +/// its method lookup changes on a later iteration. In that case no exact +/// receiver fact is safe to seed into the body. +fn loop_has_prototype_surgery(stmts: &[Stmt]) -> bool { + fn expr_has_prototype_surgery(expr: &Expr) -> bool { + if matches!( + expr, + Expr::RegisterClassParentDynamic { .. } + | Expr::SetFunctionPrototype { .. } + | Expr::RegisterPrototypeMethod { .. } + | Expr::RegisterFunctionPrototypeMethod { .. } + | Expr::ObjectDefineProperty(_, _, _) + | Expr::ObjectDefineProperties(_, _) + | Expr::ObjectSetPrototypeOf(_, _) + | Expr::ReflectDefineProperty { .. } + ) { + return true; + } + let mut found = false; + perry_hir::walker::walk_expr_children(expr, &mut |child| { + found |= expr_has_prototype_surgery(child); + }); + found + } + + fn stmt_has_prototype_surgery(stmt: &Stmt) -> bool { + match stmt { + Stmt::Let { + init: Some(expr), .. + } + | Stmt::Expr(expr) + | Stmt::Return(Some(expr)) + | Stmt::Throw(expr) => expr_has_prototype_surgery(expr), + Stmt::If { + condition, + then_branch, + else_branch, + } => { + expr_has_prototype_surgery(condition) + || loop_has_prototype_surgery(then_branch) + || else_branch + .as_ref() + .is_some_and(|branch| loop_has_prototype_surgery(branch)) + } + Stmt::While { condition, body } | Stmt::DoWhile { condition, body } => { + expr_has_prototype_surgery(condition) || loop_has_prototype_surgery(body) + } + Stmt::For { + init, + condition, + update, + body, + } => { + init.as_ref() + .is_some_and(|stmt| stmt_has_prototype_surgery(stmt)) + || condition.as_ref().is_some_and(expr_has_prototype_surgery) + || update.as_ref().is_some_and(expr_has_prototype_surgery) + || loop_has_prototype_surgery(body) + } + Stmt::Try { + body, + catch, + finally, + } => { + loop_has_prototype_surgery(body) + || catch + .as_ref() + .is_some_and(|catch| loop_has_prototype_surgery(&catch.body)) + || finally + .as_ref() + .is_some_and(|finally| loop_has_prototype_surgery(finally)) + } + Stmt::Switch { + discriminant, + cases, + } => { + expr_has_prototype_surgery(discriminant) + || cases.iter().any(|case| { + case.test.as_ref().is_some_and(expr_has_prototype_surgery) + || loop_has_prototype_surgery(&case.body) + }) + } + Stmt::Labeled { body, .. } => stmt_has_prototype_surgery(body), + Stmt::Let { init: None, .. } + | Stmt::Return(None) + | Stmt::Break + | Stmt::Continue + | Stmt::LabeledBreak(_) + | Stmt::LabeledContinue(_) + | Stmt::PreallocateBoxes(_) + | Stmt::PreallocateTdzBoxes(_) + | Stmt::ReleaseBoxes(_) => false, + } + } + + stmts.iter().any(stmt_has_prototype_surgery) +} + fn loop_invariant_seed_facts( outer: &ExactReceiverFacts, body: &[Stmt], extra_exprs: &[&Expr], ) -> ExactReceiverFacts { - if outer.is_empty() { + if outer.is_empty() + || loop_has_prototype_surgery(body) + || extra_exprs + .iter() + .any(|expr| loop_has_prototype_surgery(&[Stmt::Expr((*expr).clone())])) + { return ExactReceiverFacts::new(); } let mut mutated = std::collections::HashSet::new(); diff --git a/crates/perry/tests/issue_9131_prototype_method_replacement.rs b/crates/perry/tests/issue_9131_prototype_method_replacement.rs new file mode 100644 index 0000000000..240c6ddd60 --- /dev/null +++ b/crates/perry/tests/issue_9131_prototype_method_replacement.rs @@ -0,0 +1,103 @@ +//! Regression coverage for #9131: guarded direct method dispatch through a +//! typed-parameter receiver must observe later prototype method replacement +//! and per-instance prototype changes. Neither mutation changes the receiver's +//! class identity, so the fast path must also honor the prototype invalidation +//! state before calling the statically resolved method body. + +use std::path::PathBuf; +use std::process::Command; + +fn perry_bin() -> PathBuf { + PathBuf::from(env!("CARGO_BIN_EXE_perry")) +} + +fn compile_and_run(source: &str) -> String { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + std::fs::write(&entry, source).expect("write entry"); + + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .arg("--no-cache") + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output) + .current_dir(dir.path()) + .output() + .expect("run compiled binary"); + assert!( + run.status.success(), + "compiled binary failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + String::from_utf8_lossy(&run.stdout).into_owned() +} + +#[test] +fn typed_parameter_calls_observe_prototype_mutations() { + let stdout = compile_and_run( + r#" +class Replaced { + v = 1; + m() { return 1; } +} +function run(o: Replaced, n: number) { + let sum = 0; + for (let i = 0; i < n; i++) sum += (o as any).m(); + return sum; +} + +const replaced = new Replaced(); +const before = run(replaced, 3); +(Replaced.prototype as any).m = function () { return 100; }; +console.log("replace:", before, run(replaced, 3)); + +class MidLoop { + m() { return 1; } +} +const mid = new MidLoop(); +let midSum = 0; +for (let i = 0; i < 6; i++) { + midSum += mid.m(); + if (i === 2) (MidLoop.prototype as any).m = function () { return 50; }; +} +console.log("mid:", midSum); + +class Counter { + inc() { return 2; } +} +function readCounter(counter: Counter) { return (counter as any).inc(); } +const counter = new Counter(); +console.log("counter-before:", readCounter(counter)); +Object.setPrototypeOf(counter, { inc: () => 777 }); +console.log("counter-after:", readCounter(counter)); + +class A { m() { return "a"; } } +class B { m() { return "b"; } } +function readA(value: A) { return (value as any).m(); } +const a = new A(); +console.log("swap-before:", readA(a)); +Object.setPrototypeOf(a, B.prototype); +console.log("swap-after:", readA(a)); +"#, + ); + + assert_eq!( + stdout, + "replace: 3 300\nmid: 153\ncounter-before: 2\ncounter-after: 777\nswap-before: a\nswap-after: b\n" + ); +} From 0cc2c9eb6c173c92edce2bf630adb2c0ea5e8533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 02:43:05 +0200 Subject: [PATCH 2/3] fix(runtime): split the per-instance prototype-override lookup out for the 2000-line cap Both own-key misses in get_field_by_name_tail ask the same question, so it lives once in prototype_override.rs. Call-site comments that now duplicate the helper docs are reduced to pointers. --- .../perry-runtime/src/object/field_get_set.rs | 3 ++ .../field_get_set/get_field_by_name_tail.rs | 36 +++++-------------- .../field_get_set/prototype_override.rs | 33 +++++++++++++++++ 3 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 crates/perry-runtime/src/object/field_get_set/prototype_override.rs diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index 40f0ff4f3d..5f8dad3ab6 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -218,6 +218,9 @@ mod ic_miss; mod ic_miss_array_length_tests; mod map_set_receiver; mod probe_dispatch; +/// #9131: per-instance `[[Prototype]]` override lookup, split out of +/// `get_field_by_name_tail.rs` for the 2000-line cap. +mod prototype_override; /// Size of the direct-mapped `(keys_ptr, key_hash, field_index)` inline /// cache backing `js_object_get_field_by_name`'s slow tail. diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs index 66770d1b37..a3fa385b1c 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name_tail.rs @@ -889,12 +889,7 @@ pub(crate) fn get_field_by_name_object_tail( if key_bytes == b"length" { return JSValue::number(crate::array::js_array_length(arr) as f64); } - // #9192: `arr.__proto__` IS the array's `[[Prototype]]` (the - // spec models it as an `Object.prototype` accessor returning - // `[[GetPrototypeOf]](this)`) — the same shape the closure arm - // above resolves off the static-prototype side table. Without - // it a retargeted array reported the WRONG object here while - // `Object.getPrototypeOf(arr)` reported the right one. + // #9192; see `array_retargeted_proto::array_proto_slot`. if key_bytes == b"__proto__" { return super::array_retargeted_proto::array_proto_slot(obj); } @@ -915,10 +910,7 @@ pub(crate) fn get_field_by_name_object_tail( if let Some(v) = crate::array::array_named_property_get(arr, key) { return JSValue::from_bits(v.to_bits()); } - // A recorded custom `[[Prototype]]` replaces the whole - // implicit chain, so `constructor` must be resolved through - // it (a plain `{}` prototype answers `Object`, not `Array`) - // rather than short-circuiting to the global `Array`. #9192. + // #9192; see `array_retargeted_proto::array_constructor_slot`. if let Some(v) = super::array_retargeted_proto::array_constructor_slot(obj) { return v; } @@ -1357,16 +1349,9 @@ pub(crate) fn get_field_by_name_object_tail( let keys = crate::object::object_keys_array(obj); if keys.is_null() { - // An explicit per-instance [[Prototype]] replaces the class's - // declaration prototype; it is not an extra link in front of the - // original vtable. Walk that authoritative chain before exposing - // class getters or methods, and do not resurrect the old class - // surface when the custom chain misses. - if !key.is_null() - && super::super::prototype_chain::object_has_prototype_override(obj as usize) - { - return super::super::prototype_chain::resolve_inherited_field(obj as usize, key) - .unwrap_or_else(JSValue::undefined); + // #9131; see `prototype_override::inherited_field_if_overridden`. + if let Some(v) = super::prototype_override::inherited_field_if_overridden(obj, key) { + return v; } // #809: an object with no own keys (e.g. an `Object.create(proto)` // result, or a `Function.prototype = obj` instance) still has to @@ -1738,14 +1723,9 @@ pub(crate) fn get_field_by_name_object_tail( } } - // A shaped receiver's own-key scan has missed. As in the keyless arm - // above, a user-installed per-instance prototype is now authoritative - // and must win over class-vtable getters and methods. - if !key.is_null() - && super::super::prototype_chain::object_has_prototype_override(obj as usize) - { - return super::super::prototype_chain::resolve_inherited_field(obj as usize, key) - .unwrap_or_else(JSValue::undefined); + // Shaped-receiver own-key miss; same rule as the keyless arm above. + if let Some(v) = super::prototype_override::inherited_field_if_overridden(obj, key) { + return v; } // Key not found in the keys_array — fall back to the class diff --git a/crates/perry-runtime/src/object/field_get_set/prototype_override.rs b/crates/perry-runtime/src/object/field_get_set/prototype_override.rs new file mode 100644 index 0000000000..158096edac --- /dev/null +++ b/crates/perry-runtime/src/object/field_get_set/prototype_override.rs @@ -0,0 +1,33 @@ +//! #9131: a per-instance `[[Prototype]]` override wins over the class vtable. +//! +//! Split out of `get_field_by_name_tail.rs`, which is at the 2000-line cap. +//! Both of that file's own-key misses — the keyless arm and the shaped-receiver +//! arm — ask the same question, so it lives here once instead of twice. + +use crate::object::ObjectHeader; +use crate::value::JSValue; + +/// An explicit per-instance `[[Prototype]]` REPLACES the class's declaration +/// prototype; it is not an extra link in front of the original vtable. So when +/// the own-key scan misses, walk that authoritative chain before exposing class +/// getters or methods, and do not resurrect the old class surface when the +/// custom chain also misses — hence `Some(undefined)` rather than `None` once +/// an override is present. +/// +/// `None` means no override was installed and the caller keeps its existing +/// class-vtable fallback. +pub(super) fn inherited_field_if_overridden( + obj: *const ObjectHeader, + key: *const crate::string::StringHeader, +) -> Option { + if key.is_null() { + return None; + } + if !crate::object::prototype_chain::object_has_prototype_override(obj as usize) { + return None; + } + Some( + crate::object::prototype_chain::resolve_inherited_field(obj as usize, key) + .unwrap_or_else(JSValue::undefined), + ) +} From 3bfac814d1c575a3e43f02dd48315b1640a966b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 31 Aug 2026 05:00:33 +0200 Subject: [PATCH 3/3] fix(runtime): link built-in iterator prototypes as a class default, not a user override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit attach_iterator_prototype -> chain_to used object_set_static_prototype, the Object.setPrototypeOf variant, so OBJECT_META_FLAG_PROTO_OVERRIDE was set on every array/Map/Set/String iterator. A caller that treats an override as "resolve methods by ordinary inheriting lookup" then reached the %…IteratorPrototype% next THUNK, which resolves its receiver from js_implicit_this_get() rather than the bound this (#7576), throwing 'called on incompatible receiver'. The prototype is still recorded; only the flag and the plan-cache flush differ. --- .../src/object/iterator_prototypes.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/perry-runtime/src/object/iterator_prototypes.rs b/crates/perry-runtime/src/object/iterator_prototypes.rs index 902c131e6c..600c27e1dc 100644 --- a/crates/perry-runtime/src/object/iterator_prototypes.rs +++ b/crates/perry-runtime/src/object/iterator_prototypes.rs @@ -202,9 +202,22 @@ fn set_to_string_tag(obj: *mut ObjectHeader, tag: &str) { } /// Link `child`'s `[[Prototype]]` to `parent`. +/// +/// Uses the class-DEFAULT variant, not `object_set_static_prototype`. Attaching +/// `%ArrayIteratorPrototype%` to a fresh array iterator is exactly what that +/// function documents — a chain identical for every instance of the class — and +/// not a user `Object.setPrototypeOf`. The loud variant additionally sets +/// `OBJECT_META_FLAG_PROTO_OVERRIDE`, which made `object_has_prototype_override` +/// answer true for EVERY built-in iterator. Any caller that treats an override +/// as "the per-instance chain is authoritative, resolve methods by ordinary +/// inheriting lookup" then reached the `%…IteratorPrototype%` `next` THUNK, +/// which resolves its receiver from `js_implicit_this_get()` rather than the +/// bound `this` (#7576) — producing `Method %IteratorPrototype%.next called on +/// incompatible receiver`. The prototype itself is still recorded either way; +/// only the override flag and the plan-cache flush differ. fn chain_to(child: *mut ObjectHeader, parent: *mut ObjectHeader) { let parent_bits = crate::value::js_nanbox_pointer(parent as i64).to_bits(); - super::prototype_chain::object_set_static_prototype(child as usize, parent_bits); + super::prototype_chain::object_link_class_default_prototype(child as usize, parent_bits); } /// Build the shared `%IteratorPrototype%` and the four family prototypes,