diff --git a/changelog.d/8780-class-instance-object-prototype-read.md b/changelog.d/8780-class-instance-object-prototype-read.md new file mode 100644 index 0000000000..87a3f5c314 --- /dev/null +++ b/changelog.d/8780-class-instance-object-prototype-read.md @@ -0,0 +1,4 @@ +Fixed inherited `Object.prototype` property reads on declared class instances. +Classes without their own `toString` or `valueOf` now resolve the default +methods instead of reporting the properties as present while reading them as +`undefined`, restoring ordinary string coercion such as `String(new C())`. diff --git a/crates/perry-runtime/src/object/field_get_set/accessors.rs b/crates/perry-runtime/src/object/field_get_set/accessors.rs index de75ce90db..5f5059da14 100644 --- a/crates/perry-runtime/src/object/field_get_set/accessors.rs +++ b/crates/perry-runtime/src/object/field_get_set/accessors.rs @@ -240,7 +240,16 @@ pub(crate) unsafe fn ordinary_object_prototype_property_value( return None; } let class_id = (*obj).class_id; - if class_id != 0 && !is_anon_shape_class_id(class_id) { + // Declared ES class instances have a registered class id, but still end + // their implicit prototype chain at Object.prototype. Their class + // methods/accessors have already been consulted before this fallback, so + // a miss must remain eligible for Object.prototype (including user-added + // properties). Keep excluding unregistered native/synthetic class ids: + // those object kinds resolve their own intrinsic prototype chains. + if class_id != 0 + && !is_anon_shape_class_id(class_id) + && !super::super::class_registry::is_class_id_registered(class_id) + { return None; } default_object_prototype_property_value(obj as usize, key)