diff --git a/changelog.d/11651-class-static-accessor-properties.md b/changelog.d/11651-class-static-accessor-properties.md new file mode 100644 index 0000000000..6887d81c81 --- /dev/null +++ b/changelog.d/11651-class-static-accessor-properties.md @@ -0,0 +1,8 @@ +Class static accessors (`static get x()` / `static set x(v)`) are now real +accessor properties of the class's function object, so reflection, +`defineProperty`, `delete`, `Object.keys`, `propertyIsEnumerable`, `super.x` +and inherited reads all see one property with its attributes. A write to a +getter-only static is rejected (#11521): strict assignment throws the +TypeError node throws, and `Reflect.set` returns `false`, on the class and on +subclasses. A static walk that reaches a builtin parent (`class E extends +Error`) stops there instead of minting a class function object for it. diff --git a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv index a941ab3139..aceedca421 100644 --- a/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv +++ b/crates/perry-codegen/src/gc_effects/linux-x86_64.tsv @@ -2145,7 +2145,7 @@ js_object_get_symbol_property_ic_miss Reenters js_object_get_symbol_then_field_ic_miss Reenters js_object_group_by Reenters js_object_has_own Reenters -js_object_has_own_symbol Reenters +js_object_has_own_symbol AllocOnly js_object_has_property Reenters js_object_is Reenters js_object_is_extensible Reenters diff --git a/crates/perry-runtime/src/closure/props.rs b/crates/perry-runtime/src/closure/props.rs index 88eaacdcac..3840a4b096 100644 --- a/crates/perry-runtime/src/closure/props.rs +++ b/crates/perry-runtime/src/closure/props.rs @@ -36,7 +36,7 @@ pub(crate) unsafe fn bag_of(ptr: usize) -> *mut ObjectHeader { } /// Allocate the bag if absent and install it with the store barrier. -unsafe fn bag_ensure(ptr: usize) -> *mut ObjectHeader { +pub(crate) unsafe fn bag_ensure(ptr: usize) -> *mut ObjectHeader { let existing = bag_of(ptr); if !existing.is_null() { return existing; @@ -61,11 +61,14 @@ unsafe fn object_own_get(obj: *const ObjectHeader, key: &[u8]) -> Option { // come from the same descriptor. if let Some(d) = crate::object::shapes::object_shape_descriptor(obj) { if d.object_kind == crate::object::shapes::ShapeObjectKind::Ordinary && d.keys != 0 { - let slot = crate::object::keys_find_slot_by_bytes_resolved( - d.keys as usize as *const crate::array::ArrayHeader, - d.logical_key_count, - key, - )?; + let keys = d.keys as usize as *const crate::array::ArrayHeader; + let slot = + crate::object::keys_find_slot_by_bytes_resolved(keys, d.logical_key_count, key)?; + // An accessor key's slot holds its getter/setter pair, never a + // data value. + if crate::object::key_attrs::key_is_accessor_at(keys, slot as u32) { + return None; + } let value = crate::object::object_field_at_with_live(obj, slot, d.live_inline_slot_count); if value.bits() == crate::value::TAG_HOLE { @@ -80,6 +83,9 @@ unsafe fn object_own_get(obj: *const ObjectHeader, key: &[u8]) -> Option { return None; } let slot = crate::object::keys_find_slot_by_bytes_resolved(arr, keys.count(), key)?; + if crate::object::key_attrs::key_is_accessor_at(arr, slot as u32) { + return None; + } let live = crate::object::object_live_slot_count(obj); let value = crate::object::object_field_at_with_live(obj, slot, live); if value.bits() == crate::value::TAG_HOLE { @@ -122,7 +128,7 @@ pub(crate) unsafe fn bag_set(ptr: usize, key: &str, value: f64) { /// `ptr` is a proven, live closure cell. pub(crate) unsafe fn bag_remove(ptr: usize, key: &str) -> bool { let bag = bag_of(ptr); - if bag.is_null() || object_own_get(bag, key.as_bytes()).is_none() { + if bag.is_null() || !bag_has_own(ptr, key.as_bytes()) { return false; } let _no_move = crate::gc::GcSuppressScope::new(); @@ -131,6 +137,61 @@ pub(crate) unsafe fn bag_remove(ptr: usize, key: &str) -> bool { true } +/// Does the function own `key` — a data OR an accessor property? +/// +/// # Safety +/// `ptr` is a proven, live closure cell. +pub(crate) unsafe fn bag_has_own(ptr: usize, key: &[u8]) -> bool { + let bag = bag_of(ptr); + if bag.is_null() { + return false; + } + let keys = crate::object::object_keys(bag); + let arr = keys.arr(); + if arr.is_null() { + return false; + } + let Some(slot) = crate::object::keys_find_slot_by_bytes_resolved(arr, keys.count(), key) else { + return false; + }; + crate::object::key_attrs::key_is_accessor_at(arr, slot as u32) + || crate::object::object_field_at_with_live( + bag, + slot, + crate::object::object_live_slot_count(bag), + ) + .bits() + != crate::value::TAG_HOLE +} + +/// The function's own ACCESSOR property names, in creation order. +/// +/// # Safety +/// `ptr` is a proven, live closure cell. +pub(crate) unsafe fn bag_accessor_names(ptr: usize) -> Vec { + let bag = bag_of(ptr); + if bag.is_null() { + return Vec::new(); + } + let keys = crate::object::object_keys(bag); + let arr = keys.arr(); + if arr.is_null() { + return Vec::new(); + } + let mut out = Vec::new(); + for i in 0..keys.count() { + if !crate::object::key_attrs::key_is_accessor_at(arr, i) { + continue; + } + let key = JSValue::from_bits(crate::array::js_array_get_f64(arr, i).to_bits()); + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + if let Some(bytes) = crate::string::js_string_key_bytes(key, &mut scratch) { + out.push(String::from_utf8_lossy(bytes).into_owned()); + } + } + out +} + /// Every own data property in ECMA-262 own-key order: integer indices /// ascending, then other strings in creation order. /// @@ -151,7 +212,9 @@ pub(crate) unsafe fn bag_snapshot(ptr: usize) -> Vec<(String, f64)> { let mut strings: Vec<(String, f64)> = Vec::new(); for i in 0..keys.count() { let value = crate::object::object_field_at_with_live(bag, i, live); - if value.bits() == crate::value::TAG_HOLE { + if value.bits() == crate::value::TAG_HOLE + || crate::object::key_attrs::key_is_accessor_at(arr, i) + { continue; } let key = JSValue::from_bits(crate::array::js_array_get_f64(arr, i).to_bits()); diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 279fb64e26..dcbfb60604 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -68,16 +68,13 @@ mod prototype_methods; pub(crate) mod prototype_objects; mod registration; mod state; -mod static_accessor_attrs; pub(crate) mod verdict_classes; mod vm_brand; -// ── static_accessor_attrs.rs ──────────────────────────────────────────────── -pub(crate) use static_accessor_attrs::{ - set_static_accessor_attrs, static_accessor_attrs, static_accessor_attrs_in_use, - static_accessor_descriptor, static_declared_accessor_ptrs, static_enumerable_accessor_names, - CLASS_ACCESSOR_DEFAULT_ATTRS, -}; +// Static accessors are accessor properties of the class function object +// (`object::class_value`); the ClassBody defaults are shared with instance +// accessors. +pub(crate) use crate::object::class_value::CLASS_ACCESSOR_DEFAULT_ATTRS; // ── state.rs ──────────────────────────────────────────────────────────────── pub(crate) use state::async_resource_prototype_value; @@ -201,7 +198,7 @@ pub(crate) use gc_roots::{ // ── registration.rs ───────────────────────────────────────────────────────── pub(crate) use registration::{ class_accessor_function_value, class_accessor_source_func_ptr, class_own_accessor_ptrs, - class_own_static_accessor_ptrs, invalidate_class_string_member_order, + class_registered_static_accessor_ptrs, invalidate_class_string_member_order, }; pub use registration::{ is_class_id_registered, js_register_class_getter, js_register_class_method, diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index 47e1fc3b30..c9c75951d1 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -1136,117 +1136,6 @@ pub(crate) unsafe fn class_symbol_setter_apply( }) } -pub(crate) unsafe fn class_static_accessor_getter_value( - class_id: u32, - name: &str, - receiver: f64, -) -> Option { - let guard = CLASS_STATIC_ACCESSORS.read().ok(); - let map = guard.as_ref().and_then(|guard| guard.as_ref()); - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - // A descriptor installed by `defineProperty` replaces an existing - // class-body accessor at the same inheritance level. - if let Some(result) = class_dynamic_static_accessor_getter_value(cid, name, receiver) { - return Some(result); - } - if let Some(accessors) = map.and_then(|map| map.get(&cid)) { - if let Some(&(getter, _)) = accessors.get(name) { - if getter == 0 { - return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); - } - // #10911: when this getter was reached by walking the STATIC - // prototype chain -- a subclass reading an accessor declared - // on its parent class OBJECT -- `receiver` is that parent, the - // object the getter lives on. `resolve_proto_chain_field_inner` - // stashes the class the read actually started from, exactly as - // it does for instance getters (see `class_getter_this`), and - // spec OrdinaryGet threads that Receiver through. Bind `this` - // to it, or `Sub.accessor` runs with `this === Base`. - // - // Effect's `static get ast() { return getClassSchema(this).ast }` - // is this shape: the schema memoised against the base class, so - // decoded errors were built from the base and were not - // `instanceof` their own class (#10891). - // - // `this` and the capture/private OWNER are two different - // things here and must not be collapsed: `this` is the class - // the read started from, while the owner is the evaluation the - // getter was FOUND on -- the object whose `__perry_ctor_caps` - // hold its captured variables and whose brand gates `#x`. - // `js_class_capture_value_for_receiver` prefers the owner, so - // binding it to the subclass would lose every capture. - let owner = receiver; - let receiver = crate::object::field_get_set::accessor_receiver_override_take() - .unwrap_or(receiver); - // Static accessor bodies use the same receiver-resolving - // prologue as static methods. In particular, a fresh class - // expression must expose its per-evaluation class object as - // `this`, not the shared compile-time ClassRef. - crate::object::static_this_arm_if_unarmed(receiver); - crate::object::static_private_owner_push(owner); - let f: extern "C" fn() -> f64 = std::mem::transmute(getter); - let result = f(); - crate::object::static_private_owner_pop(); - crate::object::static_this_disarm(); - return Some(result); - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - None -} - -pub(crate) unsafe fn class_static_accessor_setter_apply( - class_id: u32, - name: &str, - receiver: f64, - value: f64, -) -> bool { - let guard = CLASS_STATIC_ACCESSORS.read().ok(); - let map = guard.as_ref().and_then(|guard| guard.as_ref()); - let mut cid = class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(applied) = - class_dynamic_static_accessor_setter_apply(cid, name, receiver, value) - { - return applied; - } - if let Some(accessors) = map.and_then(|map| map.get(&cid)) { - if let Some(&(_, setter)) = accessors.get(name) { - if setter != 0 { - // Mirror the getter path: the compiled static-accessor - // prologue consumes this override and binds `this` to the - // actual constructor value for this evaluation. - crate::object::static_this_arm_if_unarmed(receiver); - crate::object::static_private_owner_push(receiver); - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); - let _ = f(value); - crate::object::static_private_owner_pop(); - crate::object::static_this_disarm(); - } - return true; - } - } - match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, - } - } - false -} - /// Apply an instance `set name(v)` accessor from the class vtable chain, /// invoking it with the `(this, value)` calling convention class setters use. /// Returns `true` if a setter was found and called. Used when a write targets diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs index 33bb296b6e..3af1e17e3a 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs @@ -114,12 +114,36 @@ pub(crate) fn register_class_dynamic_static_accessor( if owner == 0 { return; } - let key = dynamic_static_accessor_storage_key(owner, name); - // A property is data OR accessor: redefining an own static data property - // as an accessor removes the data slot from the class function object. if !is_class_object_ptr(owner as *const u8) { - crate::object::class_value::class_static_remove(class_id, name); + // The class function object: an accessor property of its own + // property object (a data property of that name becomes it). An + // omitted half or attribute keeps the current one (a ClassBody half + // included); a new property defaults them to absent / false. + let existing = crate::object::class_value::class_static_own_accessor(class_id, name); + let have = existing.map(|(acc, _, _)| acc).unwrap_or_default(); + let acc = crate::object::accessor_pair::Accessor { + get: get_bits.map(|_| get.get_nanbox_u64()).unwrap_or(have.get), + set: set_bits.map(|_| set.get_nanbox_u64()).unwrap_or(have.set), + raw_get: if get_bits.is_some() { 0 } else { have.raw_get }, + raw_set: if set_bits.is_some() { 0 } else { have.raw_set }, + }; + let enumerable = enumerable + .or(existing.map(|(_, e, _)| e)) + .unwrap_or(false); + let configurable = configurable + .or(existing.map(|(_, _, c)| c)) + .unwrap_or(false); + crate::object::class_value::class_static_define_accessor( + class_id, + name, + acc, + enumerable, + configurable, + ); + crate::object::class_registry::class_static_alias_sync(class_id, name); + return; } + let key = dynamic_static_accessor_storage_key(owner, name); let existing = crate::object::get_accessor_descriptor(owner, &key).unwrap_or_default(); crate::object::set_accessor_descriptor( owner, @@ -133,28 +157,19 @@ pub(crate) fn register_class_dynamic_static_accessor( .unwrap_or(existing.set), }, ); - let existing_attrs = if is_class_object_ptr(owner as *const u8) { - crate::object::get_property_attrs(owner, &key) - .map(|attrs| (attrs.enumerable(), attrs.configurable())) - } else { - class_static_defined_attrs(class_id, name) - .map(|(_, enumerable, configurable)| (enumerable, configurable)) - }; + let existing_attrs = crate::object::get_property_attrs(owner, &key) + .map(|attrs| (attrs.enumerable(), attrs.configurable())); let enumerable = enumerable .or_else(|| existing_attrs.map(|attrs| attrs.0)) .unwrap_or(false); let configurable = configurable .or_else(|| existing_attrs.map(|attrs| attrs.1)) .unwrap_or(false); - if is_class_object_ptr(owner as *const u8) { - crate::object::set_property_attrs( - owner, - key, - crate::object::PropertyAttrs::new(false, enumerable, configurable), - ); - } else { - class_static_set_defined_attrs(class_id, name, false, enumerable, configurable); - } + crate::object::set_property_attrs( + owner, + key, + crate::object::PropertyAttrs::new(false, enumerable, configurable), + ); crate::object::class_registry::class_static_alias_sync(class_id, name); } @@ -172,16 +187,21 @@ pub(crate) fn class_dynamic_static_accessor_descriptor( if owner == 0 { return None; } + if !is_class_object_ptr(owner as *const u8) { + let (acc, enumerable, configurable) = + crate::object::class_value::class_static_own_accessor(class_id, name)?; + return Some(( + crate::object::AccessorDescriptor { + get: acc.get, + set: acc.set, + }, + crate::object::PropertyAttrs::new(false, enumerable, configurable), + )); + } let key = dynamic_static_accessor_storage_key(owner, name); let descriptor = crate::object::get_accessor_descriptor(owner, &key)?; - let attrs = if is_class_object_ptr(owner as *const u8) { - crate::object::get_property_attrs(owner, &key) - } else { - class_static_defined_attrs(class_id, name).map(|(_, enumerable, configurable)| { - crate::object::PropertyAttrs::new(false, enumerable, configurable) - }) - } - .unwrap_or(crate::object::PropertyAttrs::new(false, false, false)); + let attrs = crate::object::get_property_attrs(owner, &key) + .unwrap_or(crate::object::PropertyAttrs::new(false, false, false)); Some((descriptor, attrs)) } @@ -193,7 +213,9 @@ pub(crate) unsafe fn class_dynamic_static_accessor_getter_value( let scope = crate::gc::RuntimeHandleScope::new(); let receiver = scope.root_nanbox_f64(receiver); let owner = dynamic_static_accessor_owner(class_id, receiver.get_nanbox_f64()); - let descriptor = (owner != 0) + // The class function object's accessors are its own properties + // (`class_static_accessor_getter_value` reads them). + let descriptor = (owner != 0 && is_class_object_ptr(owner as *const u8)) .then(|| { crate::object::get_accessor_descriptor( owner, @@ -222,7 +244,7 @@ pub(crate) unsafe fn class_dynamic_static_accessor_setter_apply( let receiver = scope.root_nanbox_f64(receiver); let value = scope.root_nanbox_f64(value); let owner = dynamic_static_accessor_owner(class_id, receiver.get_nanbox_f64()); - let descriptor = (owner != 0) + let descriptor = (owner != 0 && is_class_object_ptr(owner as *const u8)) .then(|| { crate::object::get_accessor_descriptor( owner, diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs b/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs index 892c1b087c..d919359b38 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs @@ -5,11 +5,11 @@ pub(crate) fn static_accessor_in_chain(class_id: u32, name: &str) -> bool { let mut cid = class_id; let mut depth = 0usize; while cid != 0 && depth < 32 { - if class_own_static_accessor_ptrs(cid, name).is_some() { + if crate::object::class_value::class_static_has_own_accessor(cid, name) { return true; } match get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { + Some(p) if p != 0 && p != cid && crate::object::is_class_id_registered(p) => { cid = p; depth += 1; } @@ -108,3 +108,158 @@ pub(crate) unsafe fn try_static_accessor_value_call( crate::object::js_implicit_this_set(prev_this.get_nanbox_f64()); Some(result) } + +pub(crate) unsafe fn class_static_accessor_getter_value( + class_id: u32, + name: &str, + receiver: f64, +) -> Option { + if name.starts_with('#') { + return private_static_accessor_getter_value(class_id, name, receiver); + } + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + // A per-evaluation class object's own `defineProperty` accessor. + if let Some(result) = class_dynamic_static_accessor_getter_value(cid, name, receiver) { + return Some(result); + } + // The class function object's own accessor property (ClassBody or + // `defineProperty`). #10911: reached through the STATIC prototype + // chain, `receiver` is the class it was found on (the capture/private + // owner); `this` is the class the read started from, stashed as the + // accessor-receiver override (effect's `static get ast()`, #10891). + if let Some((acc, _, _)) = crate::object::class_value::class_static_own_accessor(cid, name) + { + return Some(crate::object::class_value::class_static_accessor_call_get( + acc, receiver, + )); + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid && crate::object::is_class_id_registered(p) => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} + +/// A private static accessor (`static get #x()`): not a property, so it is +/// read from the class's registration and never inherited through a public +/// lookup. +unsafe fn private_static_accessor_getter_value( + class_id: u32, + name: &str, + receiver: f64, +) -> Option { + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some((getter, _)) = class_registered_static_accessor_ptrs(cid, name) { + if getter == 0 { + return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); + } + let owner = receiver; + let receiver = + crate::object::field_get_set::accessor_receiver_override_take().unwrap_or(receiver); + crate::object::static_this_arm_if_unarmed(receiver); + crate::object::static_private_owner_push(owner); + let f: extern "C" fn() -> f64 = std::mem::transmute(getter); + let result = f(); + crate::object::static_private_owner_pop(); + crate::object::static_this_disarm(); + return Some(result); + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid && crate::object::is_class_id_registered(p) => { + cid = p; + depth += 1; + } + _ => break, + } + } + None +} + +/// `C.name = value` where the class (or an ancestor) has an accessor `name`: +/// its setter runs and `true` is returned. A getter-only accessor refuses the +/// write — strict-mode [[Set]] throws a TypeError (#11521); only a private +/// `#x` reports `true` without a setter, its caller decides. `false` when no +/// accessor of that name is on the chain. +pub(crate) unsafe fn class_static_accessor_setter_apply( + class_id: u32, + name: &str, + receiver: f64, + value: f64, +) -> bool { + if name.starts_with('#') { + return private_static_accessor_setter_apply(class_id, name, receiver, value); + } + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(applied) = + class_dynamic_static_accessor_setter_apply(cid, name, receiver, value) + { + if !applied { + throw_static_getter_only(class_id, name); + } + return true; + } + if let Some((acc, _, _)) = crate::object::class_value::class_static_own_accessor(cid, name) + { + if !crate::object::class_value::class_static_accessor_call_set(acc, receiver, value) { + throw_static_getter_only(class_id, name); + } + return true; + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid && crate::object::is_class_id_registered(p) => { + cid = p; + depth += 1; + } + _ => break, + } + } + false +} + +fn throw_static_getter_only(class_id: u32, name: &str) -> ! { + let class_name = class_name_for_id(class_id).unwrap_or_default(); + crate::collection_iter::throw_type_error(&format!( + "Cannot set property {name} of [class {class_name}] which has only a getter" + )) +} + +unsafe fn private_static_accessor_setter_apply( + class_id: u32, + name: &str, + receiver: f64, + value: f64, +) -> bool { + let mut cid = class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some((_, setter)) = class_registered_static_accessor_ptrs(cid, name) { + if setter != 0 { + crate::object::static_this_arm_if_unarmed(receiver); + crate::object::static_private_owner_push(receiver); + let f: extern "C" fn(f64) -> f64 = std::mem::transmute(setter); + let _ = f(value); + crate::object::static_private_owner_pop(); + crate::object::static_this_disarm(); + } + return true; + } + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid && crate::object::is_class_id_registered(p) => { + cid = p; + depth += 1; + } + _ => break, + } + } + false +} diff --git a/crates/perry-runtime/src/object/class_registry/registration.rs b/crates/perry-runtime/src/object/class_registry/registration.rs index a835e38fba..4ab6a4c3eb 100644 --- a/crates/perry-runtime/src/object/class_registry/registration.rs +++ b/crates/perry-runtime/src/object/class_registry/registration.rs @@ -190,9 +190,15 @@ pub(crate) fn class_own_accessor_ptrs(class_id: u32, name: &str) -> Option<(usiz (decl.get != 0 || decl.set != 0).then_some((decl.get, decl.set)) } -/// Own static accessor func_ptrs for the class *constructor*. Mirrors -/// `class_own_accessor_ptrs` against `CLASS_STATIC_ACCESSORS`. -pub(crate) fn class_own_static_accessor_ptrs(class_id: u32, name: &str) -> Option<(usize, usize)> { +/// The compiled entries of the ClassBody static accessor `name` of `class_id` +/// as REGISTERED (`CLASS_STATIC_ACCESSORS`): the input the class function +/// object's accessor property is built from, not the property itself — a +/// deleted or redefined accessor is still registered. Private (`#x`) static +/// accessors live only here. +pub(crate) fn class_registered_static_accessor_ptrs( + class_id: u32, + name: &str, +) -> Option<(usize, usize)> { let guard = CLASS_STATIC_ACCESSORS.read().ok()?; let reg = guard.as_ref()?; let pair = reg.get(&class_id)?.get(name).copied()?; diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index f55344c3a5..da95bbd52f 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -127,13 +127,7 @@ pub(crate) fn class_static_alias_sync(class_id: u32, name: &str) { }; let plain = !class_is_key_deleted(class_id, name) && class_static_defined_attrs(class_id, name).is_none_or(|(writable, _, _)| writable) - && class_own_static_accessor_ptrs(class_id, name).is_none() - && super::class_dynamic_static_accessor_descriptor( - class_id, - name, - crate::object::class_value::class_value(class_id), - ) - .is_none(); + && !crate::object::class_value::class_static_has_own_accessor(class_id, name); let value = plain .then(|| crate::object::class_value::class_static_get(class_id, name)) .flatten() diff --git a/crates/perry-runtime/src/object/class_registry/static_accessor_attrs.rs b/crates/perry-runtime/src/object/class_registry/static_accessor_attrs.rs deleted file mode 100644 index d32428edc5..0000000000 --- a/crates/perry-runtime/src/object/class_registry/static_accessor_attrs.rs +++ /dev/null @@ -1,199 +0,0 @@ -//! Reflective attributes of DECLARED STATIC class accessors (#10480). -//! -//! A ClassBody `static get x() {}` is an own property of the constructor `C`, -//! and a class constructor is a ClassRef value, not an object: its accessors -//! live in `CLASS_STATIC_ACCESSORS`, which records only the two function -//! pointers. This table holds what a generic descriptor -//! (`Object.defineProperty(C, "x", { enumerable: true })`) applied, keyed by -//! `(class_id, name)`; absence means the ClassBody defaults -//! (`enumerable: false`, `configurable: true`). -//! -//! Instance accessors are not here: they are real accessor properties of the -//! class's decl prototype (`decl_accessors.rs`), whose attributes live with -//! the prototype's keys like any other property's. -//! -//! [`static_accessor_attrs_in_use`] lets the enumeration paths skip the -//! lookup with one load. The values are booleans: nothing here is a GC root. - -use super::*; -use std::collections::HashMap; -use std::sync::atomic::{AtomicBool, Ordering}; - -crate::perry_thread_local! { - static STATIC_ACCESSOR_ATTRS: std::cell::RefCell> = - std::cell::RefCell::new(HashMap::new()); -} - -/// Sticky: set by the first [`set_static_accessor_attrs`]. Only a hint that -/// the table may be non-empty — never cleared, so a stale `true` merely costs -/// a lookup. -static STATIC_ACCESSOR_ATTRS_IN_USE: AtomicBool = AtomicBool::new(false); - -/// ClassBody defaults for an accessor: `(enumerable, configurable)`. -pub(crate) const CLASS_ACCESSOR_DEFAULT_ATTRS: (bool, bool) = (false, true); - -#[inline] -pub(crate) fn static_accessor_attrs_in_use() -> bool { - STATIC_ACCESSOR_ATTRS_IN_USE.load(Ordering::Relaxed) -} - -/// `(enumerable, configurable)` of the declared static accessor `name`. -pub(crate) fn static_accessor_attrs(class_id: u32, name: &str) -> (bool, bool) { - if !static_accessor_attrs_in_use() { - return CLASS_ACCESSOR_DEFAULT_ATTRS; - } - STATIC_ACCESSOR_ATTRS.with(|table| { - table - .borrow() - .get(&(class_id, name.to_string())) - .copied() - .unwrap_or(CLASS_ACCESSOR_DEFAULT_ATTRS) - }) -} - -pub(crate) fn set_static_accessor_attrs( - class_id: u32, - name: &str, - enumerable: bool, - configurable: bool, -) { - STATIC_ACCESSOR_ATTRS_IN_USE.store(true, Ordering::Relaxed); - STATIC_ACCESSOR_ATTRS.with(|table| { - table - .borrow_mut() - .insert((class_id, name.to_string()), (enumerable, configurable)); - }); -} - -/// Raw `(getter, setter)` func_ptrs of a live own declared static accessor — -/// `None` for a method, a field, an inherited accessor, or one `delete` -/// removed. -pub(crate) fn static_declared_accessor_ptrs(class_id: u32, name: &str) -> Option<(usize, usize)> { - if class_is_key_deleted(class_id, name) { - return None; - } - class_own_static_accessor_ptrs(class_id, name) -} - -/// `Object.getOwnPropertyDescriptor(C, name)` for a declared static accessor. -/// The getter value is rooted across the setter value's allocation. -pub(crate) unsafe fn static_accessor_descriptor( - class_id: u32, - name: &str, - getter: usize, - setter: usize, -) -> f64 { - let scope = crate::gc::RuntimeHandleScope::new(); - let get = scope.root_nanbox_f64(class_accessor_function_value(getter, false, name)); - let set = class_accessor_function_value(setter, true, name); - let (enumerable, configurable) = static_accessor_attrs(class_id, name); - crate::object::descriptors::build_accessor_descriptor( - get.get_nanbox_f64(), - set, - enumerable, - configurable, - ) -} - -/// The class's own declared static accessors that are currently enumerable, -/// in ClassBody order. Empty (without walking the class) unless some static -/// accessor of this class was made enumerable. -pub(crate) fn static_enumerable_accessor_names(class_id: u32) -> Vec { - if !static_accessor_attrs_in_use() { - return Vec::new(); - } - let any = STATIC_ACCESSOR_ATTRS.with(|table| { - table - .borrow() - .iter() - .any(|((cid, _), &(enumerable, _))| *cid == class_id && enumerable) - }); - if !any { - return Vec::new(); - } - class_own_string_member_names(class_id, true) - .into_iter() - .filter(|name| { - static_declared_accessor_ptrs(class_id, name).is_some() - && static_accessor_attrs(class_id, name).0 - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - extern "C" fn getter() -> f64 { - 0.0 - } - - extern "C" fn setter(_value: f64) -> f64 { - 0.0 - } - - unsafe fn register(class_id: u32, name: &str, with_setter: bool, order: i64) { - js_register_class_static_getter( - class_id as i64, - name.as_ptr(), - name.len() as i64, - getter as *const () as usize as i64, - ); - if with_setter { - js_register_class_static_setter( - class_id as i64, - name.as_ptr(), - name.len() as i64, - setter as *const () as usize as i64, - ); - } - js_register_class_string_member_order( - class_id as i64, - name.as_ptr(), - name.len() as i64, - 1, - order, - ); - } - - #[test] - fn unrecorded_static_accessor_keeps_classbody_defaults() { - assert_eq!(static_accessor_attrs(0x7c48_0001, "never"), (false, true)); - } - - #[test] - fn static_attrs_are_keyed_by_class_and_name() { - let cid = 0x7c48_0002; - set_static_accessor_attrs(cid, "x", true, false); - assert!(static_accessor_attrs_in_use()); - assert_eq!(static_accessor_attrs(cid, "x"), (true, false)); - assert_eq!(static_accessor_attrs(cid, "y"), (false, true)); - assert_eq!(static_accessor_attrs(cid + 1, "x"), (false, true)); - } - - /// Only live declared static accessors qualify: a deleted one, a name the - /// class never declared, and a non-enumerable one are all excluded, and - /// the survivors come back in ClassBody order rather than insertion order. - #[test] - fn static_enumerable_accessor_names_follow_classbody_order() { - let cid = 0x7c48_0003; - unsafe { - register(cid, "b", true, 10); - register(cid, "a", false, 20); - register(cid, "c", true, 30); - register(cid, "gone", true, 40); - } - set_static_accessor_attrs(cid, "a", true, true); - set_static_accessor_attrs(cid, "b", true, true); - set_static_accessor_attrs(cid, "c", false, true); - set_static_accessor_attrs(cid, "gone", true, true); - set_static_accessor_attrs(cid, "undeclared", true, true); - class_mark_key_deleted(cid, "gone"); - assert_eq!( - static_enumerable_accessor_names(cid), - vec!["b".to_string(), "a".to_string()] - ); - assert_eq!(static_declared_accessor_ptrs(cid, "gone"), None); - assert!(static_declared_accessor_ptrs(cid, "a").is_some_and(|(g, s)| g != 0 && s == 0)); - } -} diff --git a/crates/perry-runtime/src/object/class_value.rs b/crates/perry-runtime/src/object/class_value.rs index c7e2984549..1d6909fed8 100644 --- a/crates/perry-runtime/src/object/class_value.rs +++ b/crates/perry-runtime/src/object/class_value.rs @@ -225,6 +225,10 @@ fn class_value_slot(class_id: u32) -> *mut *mut ClosureHeader { #[cold] #[inline(never)] fn class_value_mint(class_id: u32) -> *mut ClosureHeader { + debug_assert!( + class_id != 0 && class_id < 0x7FFF_FF00, + "a class function object belongs to a compiled class id, never a builtin or synthetic band: {class_id:#x}" + ); let _no_collect = crate::gc::GcSuppressScope::new(); let payload = crate::closure::closure_payload_size(1); let ptr = crate::arena::arena_alloc_gc_old_born_tenured( @@ -257,6 +261,10 @@ fn class_value_mint(class_id: u32) -> *mut ClosureHeader { for key in INTRINSIC_OWN_DATA_KEYS { install_intrinsic_own_data(class_id, key); } + // ClassBody static accessors, in ClassBody order. + for key in super::class_registry::class_own_string_member_names(class_id, true) { + install_declared_static_accessor(class_id, &key); + } ptr } @@ -284,7 +292,7 @@ fn intrinsic_own_data_value(class_id: u32, key: &str) -> Option { /// not the intrinsic data property, is the class's own `key`. fn static_member_owns(class_id: u32, key: &str) -> bool { super::class_registry::class_has_own_static_method(class_id, key) - || super::class_registry::class_own_static_accessor_ptrs(class_id, key).is_some() + || super::class_registry::class_registered_static_accessor_ptrs(class_id, key).is_some() } /// Is own `key` of class `class_id` still the intrinsic data property (not @@ -336,10 +344,18 @@ pub(crate) fn note_static_field_defined(class_id: u32, key: &str) { /// property in line. A key the program already redefined or deleted is left /// alone. pub(crate) fn note_intrinsic_registration(class_id: u32, key: &str) { - if !INTRINSIC_OWN_DATA_KEYS.contains(&key) || class_value_cached(class_id).is_none() { + if class_value_cached(class_id).is_none() { return; } let _no_collect = crate::gc::GcSuppressScope::new(); + // A ClassBody static accessor registered after the object exists (a + // computed key registers when the class definition evaluates). + if super::class_registry::class_registered_static_accessor_ptrs(class_id, key).is_some() { + install_declared_static_accessor(class_id, key); + } + if !INTRINSIC_OWN_DATA_KEYS.contains(&key) { + return; + } if holds_intrinsic(class_id, key) { if static_member_owns(class_id, key) { class_static_remove(class_id, key); @@ -354,6 +370,15 @@ pub(crate) fn note_intrinsic_registration(class_id: u32, key: &str) { } } +/// The class function object for `class_id` if this agent has minted it. +/// A read that finds none has its answer without minting one: an object that +/// was never created owns no properties. (A builtin parent such as `Error` +/// never gets a class function object, so reads walking to it must use this.) +#[inline] +pub(crate) fn class_value_if_minted(class_id: u32) -> Option<*mut ClosureHeader> { + class_value_cached(class_id) +} + /// The class function object for `class_id` on this agent (minted on first /// use). `class_id` must be a registered class. #[inline] @@ -474,24 +499,16 @@ pub unsafe extern "C" fn js_class_static_field_put( crate::object::js_object_set_field_by_name(receiver, key, value); } -/// [[Get]] of `key` on class `class_id`'s [[Prototype]], `receiver` as the -/// receiver: the continuation of a read of a key the class does not own -/// (e.g. its own `name` was deleted — `Sub.name` then reads `Base.name`, a -/// base class reads `Function.prototype.name`). The [[Prototype]] is the +/// The address of class `class_id`'s [[Prototype]] (0 when null): the /// recorded one (`Object.setPrototypeOf(C, p)`), else the parent class's /// function object, else the parent function (`extends `), else /// %Function.prototype%. -pub(crate) fn class_prototype_get( - class_id: u32, - key: *const crate::StringHeader, - receiver: f64, -) -> crate::value::JSValue { - use crate::value::JSValue; +pub(crate) fn class_prototype_addr(class_id: u32) -> usize { if super::class_registry::class_static_prototype_is_nulled(class_id) { - return JSValue::undefined(); + return 0; } let proto = super::class_registry::class_static_prototype(class_id) as usize; - let proto = if proto != 0 { + if proto != 0 { proto } else if let Some(parent) = super::get_parent_class_id(class_id) .filter(|&p| p != 0 && p != class_id && super::is_class_id_registered(p)) @@ -502,7 +519,23 @@ pub(crate) fn class_prototype_get( } else { crate::closure::shape::FUNCTION_PROTOTYPE_PTR.load(std::sync::atomic::Ordering::Acquire) as usize - }; + } +} + +/// [[Get]] of `key` on class `class_id`'s [[Prototype]], `receiver` as the +/// receiver: the continuation of a read of a key the class does not own +/// (e.g. its own `name` was deleted — `Sub.name` then reads `Base.name`, a +/// base class reads `Function.prototype.name`). The [[Prototype]] is the +/// recorded one (`Object.setPrototypeOf(C, p)`), else the parent class's +/// function object, else the parent function (`extends `), else +/// %Function.prototype%. +pub(crate) fn class_prototype_get( + class_id: u32, + key: *const crate::StringHeader, + receiver: f64, +) -> crate::value::JSValue { + use crate::value::JSValue; + let proto = class_prototype_addr(class_id); if proto == 0 { return JSValue::undefined(); } @@ -512,6 +545,208 @@ pub(crate) fn class_prototype_get( value } +// --------------------------------------------------------------------------- +// Static accessors: accessor properties of the function object. +// --------------------------------------------------------------------------- + +/// ClassBody defaults for an accessor: `(enumerable, configurable)`. +pub(crate) const CLASS_ACCESSOR_DEFAULT_ATTRS: (bool, bool) = (false, true); + +/// Install — or refresh, when a half arrives later — the ClassBody static +/// accessor `name` of `class_id` as an accessor property of its function +/// object's own-property object: the pair holds the reflected closures and +/// the compiled static entries (`fn() -> value` / `fn(v)`, `this` armed by +/// the caller — NOT the instance `fn(this)` convention; only this module and +/// its callers read a class function object's pairs). A half whose compiled +/// entry is unchanged keeps its closure, so reflection hands out the same +/// function every time; attributes a `defineProperty` set are kept. +/// Private (`#x`) accessors are not properties and are never installed. +fn install_declared_static_accessor(class_id: u32, name: &str) { + if name.starts_with('#') { + return; + } + let Some((raw_get, raw_set)) = + super::class_registry::class_registered_static_accessor_ptrs(class_id, name) + else { + return; + }; + let _no_collect = crate::gc::GcSuppressScope::new(); + let existing = class_static_own_accessor(class_id, name); + let (have, enumerable, configurable) = match existing { + Some((acc, e, c)) => (acc, e, c), + None => ( + crate::object::accessor_pair::Accessor::default(), + CLASS_ACCESSOR_DEFAULT_ATTRS.0, + CLASS_ACCESSOR_DEFAULT_ATTRS.1, + ), + }; + let half = |raw: usize, have_raw: usize, have: u64, is_setter: bool| -> u64 { + if raw == 0 { + 0 + } else if raw == have_raw && have != 0 { + have + } else { + super::class_registry::class_accessor_function_value(raw, is_setter, name).to_bits() + } + }; + let get = half(raw_get, have.raw_get, have.get, false); + let set = half(raw_set, have.raw_set, have.set, true); + class_static_define_accessor( + class_id, + name, + crate::object::accessor_pair::Accessor { + get, + set, + raw_get, + raw_set, + }, + enumerable, + configurable, + ); +} + +/// Class `class_id`'s own accessor property `name` (ClassBody or +/// `defineProperty`), with `(enumerable, configurable)`. +pub(crate) fn class_static_own_accessor( + class_id: u32, + name: &str, +) -> Option<(crate::object::accessor_pair::Accessor, bool, bool)> { + use crate::object::key_attrs as ka; + let ptr = class_value_ptr(class_id) as usize; + // SAFETY: this agent's live class closure; its bag (if any) is a live + // ordinary object whose attributes live with its keys. Nothing allocates. + unsafe { + let bag = crate::closure::props::bag_of(ptr); + if bag.is_null() { + return None; + } + let entry = ka::object_key_entry(bag, name.as_bytes()); + if entry & ka::ENTRY_ACCESSOR == 0 { + return None; + } + let acc = crate::object::accessor_pair::own_accessor(bag as usize, name.as_bytes())?; + Some(( + acc, + entry & ka::ENTRY_NON_ENUMERABLE == 0, + entry & ka::ENTRY_NON_CONFIGURABLE == 0, + )) + } +} + +/// Does class `class_id` own an accessor property `name`? +pub(crate) fn class_static_has_own_accessor(class_id: u32, name: &str) -> bool { + class_static_own_accessor(class_id, name).is_some() +} + +/// Define (or replace) class `class_id`'s own accessor property `name`: a data +/// property of that name becomes this accessor. +pub(crate) fn class_static_define_accessor( + class_id: u32, + name: &str, + acc: crate::object::accessor_pair::Accessor, + enumerable: bool, + configurable: bool, +) { + let _no_collect = crate::gc::GcSuppressScope::new(); + let ptr = class_value_ptr(class_id) as usize; + // SAFETY: this agent's live class closure; no collection in this scope. + let bag = unsafe { crate::closure::props::bag_ensure(ptr) }; + crate::object::set_builtin_accessor_pair( + bag as usize, + name.to_string(), + acc, + crate::object::PropertyAttrs::new(false, enumerable, configurable), + ); +} + +/// Change the attributes of class `class_id`'s own accessor `name`. +pub(crate) fn class_static_set_accessor_attrs( + class_id: u32, + name: &str, + enumerable: bool, + configurable: bool, +) { + if let Some((acc, _, _)) = class_static_own_accessor(class_id, name) { + class_static_define_accessor(class_id, name, acc, enumerable, configurable); + } +} + +/// Class `class_id`'s own accessor property names, in creation order. +pub(crate) fn class_static_accessor_names(class_id: u32) -> Vec { + let ptr = class_value_ptr(class_id) as usize; + // SAFETY: this agent's live class closure. + unsafe { crate::closure::props::bag_accessor_names(ptr) } +} + +/// Run a class static accessor's getter for `receiver`. The compiled +/// ClassBody entry takes the static convention: `this` is armed (the class +/// the read started from — a stashed override — or `receiver`) and the +/// private/capture owner is `receiver`, the evaluation the getter was found +/// through (#10891/#10893). A `defineProperty` getter is an ordinary closure. +/// +/// # Safety +/// `acc` came from [`class_static_own_accessor`]. +pub(crate) unsafe fn class_static_accessor_call_get( + acc: crate::object::accessor_pair::Accessor, + receiver: f64, +) -> f64 { + let this = crate::object::field_get_set::accessor_receiver_override_take().unwrap_or(receiver); + if acc.raw_get != 0 { + crate::object::static_this_arm_if_unarmed(this); + crate::object::static_private_owner_push(receiver); + let f: extern "C" fn() -> f64 = std::mem::transmute(acc.raw_get); + let result = f(); + crate::object::static_private_owner_pop(); + crate::object::static_this_disarm(); + return result; + } + if acc.get != 0 { + return f64::from_bits(crate::object::invoke_accessor_getter(acc.get, this).bits()); + } + f64::from_bits(crate::value::TAG_UNDEFINED) +} + +/// Run a class static accessor's setter; `false` when the accessor has none. +/// +/// # Safety +/// As [`class_static_accessor_call_get`]. +pub(crate) unsafe fn class_static_accessor_call_set( + acc: crate::object::accessor_pair::Accessor, + receiver: f64, + value: f64, +) -> bool { + if acc.raw_set != 0 { + crate::object::static_this_arm_if_unarmed(receiver); + crate::object::static_private_owner_push(receiver); + let f: extern "C" fn(f64) -> f64 = std::mem::transmute(acc.raw_set); + let _ = f(value); + crate::object::static_private_owner_pop(); + crate::object::static_this_disarm(); + return true; + } + if acc.set != 0 { + crate::object::invoke_accessor_setter(acc.set, receiver, value); + return true; + } + false +} + +/// `Object.getOwnPropertyDescriptor(C, name)` for an own accessor of the class. +pub(crate) fn class_static_accessor_descriptor(class_id: u32, name: &str) -> Option { + let (acc, enumerable, configurable) = class_static_own_accessor(class_id, name)?; + let undef = crate::value::TAG_UNDEFINED; + // SAFETY: both halves are the property's own closure values (or + // undefined); the builder roots them across its allocation. + Some(unsafe { + crate::object::descriptors::build_accessor_descriptor( + f64::from_bits(if acc.get == 0 { undef } else { acc.get }), + f64::from_bits(if acc.set == 0 { undef } else { acc.set }), + enumerable, + configurable, + ) + }) +} + // --------------------------------------------------------------------------- // Statics: the class function object's OWN properties. // --------------------------------------------------------------------------- @@ -625,6 +860,44 @@ mod tests { assert_eq!(class_value_id(other), Some(0x6A02)); } + /// A static walk that reaches a BUILTIN parent (`class E extends Error`) + /// stops there: a builtin id has no class function object, so no reader + /// may mint one for it (minting `0xFFFF_0001` grew the class-value + /// directory to 16M pages, which every collection then scanned). + #[test] + fn a_builtin_parent_never_gets_a_class_function_object() { + let cid = 0x6D71; + register(cid); + crate::object::js_register_class_parent(cid, crate::error::CLASS_ID_ERROR); + let recv = class_value(cid); + let (_, pages_before) = CLASS_VALUES.with(std::cell::Cell::get); + let applied = unsafe { + crate::object::class_registry::class_static_accessor_setter_apply(cid, "zz", recv, 1.0) + }; + assert!(!applied, "no static accessor named zz on the chain"); + assert!(!crate::object::class_registry::static_accessor_in_chain( + cid, "zz" + )); + // A static symbol read up the chain (latch armed, as once any class + // has a static symbol member) reaches Error too and must not mint. + crate::symbol::CLASS_STATIC_SYMBOLS_LATCH.arm(); + let sym = unsafe { crate::symbol::js_symbol_new_empty() }; + assert!(crate::symbol::class_static_symbol_lookup_in_chain(cid, sym).is_none()); + assert!( + crate::symbol::class_static_symbol_keys_for_class(crate::error::CLASS_ID_ERROR) + .is_empty() + ); + assert!( + class_value_cached(crate::error::CLASS_ID_ERROR).is_none(), + "the builtin Error id must not get a class function object" + ); + let (_, pages_after) = CLASS_VALUES.with(std::cell::Cell::get); + assert_eq!( + pages_after, pages_before, + "the walks grew the class-value directory" + ); + } + /// The table is a root: the scan visits every minted class value. #[test] fn class_value_table_is_scanned() { @@ -775,6 +1048,50 @@ mod tests { ); } + /// A ClassBody static accessor is an accessor property of the class + /// function object: ClassBody attributes, one closure per half across + /// reads, attributes changed in place, and a delete removes it. + #[test] + fn static_accessors_are_accessor_properties_of_the_function_object() { + let cid = 0x6E01; + register(cid); + extern "C" fn getter() -> f64 { + 41.0 + } + unsafe { + crate::object::js_register_class_name(cid, b"Acc".as_ptr(), 3); + crate::object::class_registry::js_register_class_static_getter( + cid as i64, + b"g".as_ptr(), + 1, + getter as *const () as usize as i64, + ); + } + let (acc, enumerable, configurable) = + class_static_own_accessor(cid, "g").expect("an own accessor property"); + assert_ne!(acc.get, 0, "a reflected getter closure"); + assert_eq!(acc.set, 0); + assert_eq!((enumerable, configurable), CLASS_ACCESSOR_DEFAULT_ATTRS); + let again = class_static_own_accessor(cid, "g").unwrap().0; + assert_eq!(again.get, acc.get, "one closure per half"); + let ptr = class_value_ptr(cid) as usize; + assert_eq!( + unsafe { crate::closure::props::bag_get(ptr, b"g") }, + None, + "an accessor key has no data value" + ); + let got = unsafe { class_static_accessor_call_get(acc, class_value(cid)) }; + assert_eq!(got, 41.0); + class_static_set_accessor_attrs(cid, "g", true, false); + assert_eq!( + class_static_own_accessor(cid, "g").map(|(_, e, c)| (e, c)), + Some((true, false)) + ); + class_static_set_accessor_attrs(cid, "g", false, true); + assert!(class_static_remove(cid, "g"), "delete removes the property"); + assert!(class_static_own_accessor(cid, "g").is_none()); + } + /// Only the function object's own code pointer names a class. #[test] fn ordinary_closures_and_numbers_are_not_class_values() { diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index 58c073f261..dc24257bcf 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -69,18 +69,10 @@ pub extern "C" fn js_object_delete_field( unsafe { if let Some(name) = super::has_own_helpers::str_from_string_header(key) { let class_id = obj as usize as u32; - if super::class_registry::class_name_for_id(class_id).is_some() { - if super::class_registry::static_declared_accessor_ptrs(class_id, name) - .is_some() - && !super::class_registry::static_accessor_attrs(class_id, name).1 - { - return 0; - } - super::class_registry::class_delete_own_dynamic_prop(class_id, name); - super::class_registry::class_mark_key_deleted(class_id, name); - super::class_registry::invalidate_class_string_member_order( - class_id, name, true, - ); + if super::class_registry::class_name_for_id(class_id).is_some() + && class_delete_own_key(class_id, name) == 0 + { + return 0; } // #6363: a native HANDLE's own properties are its user expandos. // `delete` used to unconditionally report success while LEAVING @@ -204,6 +196,10 @@ pub extern "C" fn js_object_delete_field( // user-attached props are dropped from the dynamic-prop table outright. if crate::closure::is_closure_ptr(obj as usize) { if let Some(name) = super::has_own_helpers::str_from_string_header(key) { + // A class constructor: [[Delete]] on its own property. + if let Some(class_id) = crate::object::class_value::class_closure_id(obj as usize) { + return class_delete_own_key(class_id, name); + } // A plain (non-arrow, non-bound) function's `prototype` is a // non-configurable own property. `get_property_attrs` only knows // about it once #3655 has lazily registered a descriptor (on first @@ -793,6 +789,20 @@ fn delete_receiver_is_pointer(obj_value: f64) -> bool { crate::value::JSValue::from_bits(obj_value.to_bits()).is_pointer() } +/// `[[Delete]]` of class `class_id`'s own string key `name` (the class +/// constructor's own property): `0` when it is non-configurable. +fn class_delete_own_key(class_id: u32, name: &str) -> i32 { + if crate::object::class_value::class_static_own_accessor(class_id, name) + .is_some_and(|(_, _, configurable)| !configurable) + { + return 0; + } + super::class_registry::class_delete_own_dynamic_prop(class_id, name); + super::class_registry::class_mark_key_deleted(class_id, name); + super::class_registry::invalidate_class_string_member_order(class_id, name, true); + 1 +} + fn delete_class_prototype_key(class_id: u32, name: &str) -> i32 { if let Some(proto) = super::class_registry::decl_prototype_own_accessor(class_id, name) { // S2: the accessor is a real property of the declared prototype diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index d408b2f536..68409e3918 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -523,15 +523,13 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu { return js_object_get_own_property_descriptor(proto, key_value); } - } else if let Some((g, s)) = - super::class_registry::class_own_static_accessor_ptrs(class_id, &method_name) - { - return super::class_registry::static_accessor_descriptor( + } else if let Some(desc) = + crate::object::class_value::class_static_accessor_descriptor( class_id, &method_name, - g, - s, - ); + ) + { + return desc; } if super::class_prototype_ref_id(obj_value).is_some() && (method_name == "constructor" @@ -1189,6 +1187,10 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { for name in super::class_registry::class_own_dynamic_prop_names(class_id) { push_unique_name(&mut names, name); } + // Accessor properties a `defineProperty` added. + for name in crate::object::class_value::class_static_accessor_names(class_id) { + push_unique_name(&mut names, name); + } } names.retain(|n| { !super::field_get_set::is_internal_runtime_key(n) diff --git a/crates/perry-runtime/src/object/field_get_set/enumeration.rs b/crates/perry-runtime/src/object/field_get_set/enumeration.rs index dd3f783c53..0b6bf6e98f 100644 --- a/crates/perry-runtime/src/object/field_get_set/enumeration.rs +++ b/crates/perry-runtime/src/object/field_get_set/enumeration.rs @@ -171,8 +171,14 @@ pub extern "C" fn js_object_keys_value(value: f64) -> *mut ArrayHeader { if super::super::class_prototype_ref_id(value).is_none() { // Static accessors are defined before static fields, so an // enumerable one (#10480) precedes them. - let mut names = - super::super::class_registry::static_enumerable_accessor_names(class_id); + let mut names: Vec = + crate::object::class_value::class_static_accessor_names(class_id) + .into_iter() + .filter(|name| { + crate::object::class_value::class_static_own_accessor(class_id, name) + .is_some_and(|(_, enumerable, _)| enumerable) + }) + .collect(); names.extend(super::super::class_registry::class_own_enumerable_field_names(class_id)); super::super::descriptors::sort_property_names_ecma(&mut names); let arr = crate::array::js_array_alloc(names.len().max(1) as u32); diff --git a/crates/perry-runtime/src/object/field_get_set/has_property.rs b/crates/perry-runtime/src/object/field_get_set/has_property.rs index eb0e59a990..53b6638d3e 100644 --- a/crates/perry-runtime/src/object/field_get_set/has_property.rs +++ b/crates/perry-runtime/src/object/field_get_set/has_property.rs @@ -453,10 +453,9 @@ pub extern "C" fn js_object_has_property(obj: f64, key: f64) -> f64 { class_id, name, ) .is_some() - || super::super::class_registry::class_own_static_accessor_ptrs( + || super::super::class_registry::static_accessor_in_chain( class_id, name, ) - .is_some() || inherited_data)); if present { return nanbox_true; diff --git a/crates/perry-runtime/src/object/object_ops/define_class_accessor.rs b/crates/perry-runtime/src/object/object_ops/define_class_accessor.rs index 2f4c73251f..36f1058f38 100644 --- a/crates/perry-runtime/src/object/object_ops/define_class_accessor.rs +++ b/crates/perry-runtime/src/object/object_ops/define_class_accessor.rs @@ -3,9 +3,9 @@ //! //! An instance accessor is a real accessor property of the class's decl //! prototype, so a define through the prototype ref is the ordinary define on -//! that object. A static accessor is an own property of the constructor `C`, -//! which is a ClassRef value, not an object: its get/set live in -//! `CLASS_STATIC_ACCESSORS` and its attributes in `static_accessor_attrs.rs`. +//! that object. A static accessor is an accessor property of the class +//! function object's own-property object (`object::class_value`), attributes +//! with its key. use super::*; /// ValidateAndApplyPropertyDescriptor for the declared accessor `name` of @@ -43,32 +43,25 @@ pub(super) unsafe fn define_declared_class_accessor( super::js_object_define_property(proto.get_nanbox_f64(), key, desc.get_nanbox_f64()); return true; } - let Some((getter, setter)) = - super::super::class_registry::static_declared_accessor_ptrs(class_id, name) + let Some((acc, enumerable, configurable)) = + crate::object::class_value::class_static_own_accessor(class_id, name) else { return false; }; - let (enumerable, configurable) = - super::super::class_registry::static_accessor_attrs(class_id, name); // The per-field reads below allocate a field-name string (and may run a // user getter on a non-plain descriptor), so the descriptor is re-read from // its root at every use. let scope = crate::gc::RuntimeHandleScope::new(); let desc = scope.root_nanbox_f64(descriptor_value); if !configurable { - // The validator compares accessor halves by closure `func_ptr`, which a - // reflected class accessor value carries. Root the getter value across - // the setter value's allocation; the validator roots both on entry. - let get = scope.root_nanbox_f64( - super::super::class_registry::class_accessor_function_value(getter, false, name), - ); - let set = super::super::class_registry::class_accessor_function_value(setter, true, name); + // The validator compares accessor halves by closure identity: the + // property's own closures. validate_nonconfigurable_redefine( name, PropertyAttrs::new(false, enumerable, false), Some(AccessorDescriptor { - get: get.get_nanbox_u64(), - set: set.to_bits(), + get: acc.get, + set: acc.set, }), f64::from_bits(crate::value::TAG_UNDEFINED), desc.get_nanbox_f64(), @@ -102,7 +95,7 @@ pub(super) unsafe fn define_declared_class_accessor( }; let enumerable = flag(DESC_ENUMERABLE, b"enumerable").unwrap_or(enumerable); let configurable = flag(DESC_CONFIGURABLE, b"configurable").unwrap_or(configurable); - super::super::class_registry::set_static_accessor_attrs( + crate::object::class_value::class_static_set_accessor_attrs( class_id, name, enumerable, diff --git a/crates/perry-runtime/src/object/object_ops/has_own.rs b/crates/perry-runtime/src/object/object_ops/has_own.rs index 3d355e0f84..3aeed7440d 100644 --- a/crates/perry-runtime/src/object/object_ops/has_own.rs +++ b/crates/perry-runtime/src/object/object_ops/has_own.rs @@ -241,10 +241,9 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { class_id, key, ) .is_some() - || super::super::class_registry::class_own_static_accessor_ptrs( + || crate::object::class_value::class_static_has_own_accessor( class_id, key, - ) - .is_some())) + ))) } }) .unwrap_or(false); @@ -600,15 +599,8 @@ pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f6 // ClassBody default, but a generic descriptor can flip it // (Object.defineProperty(C, "x", { enumerable: true })). let is_enumerable_static_accessor = - super::super::class_registry::static_accessor_attrs_in_use() - && super::super::class_registry::static_declared_accessor_ptrs( - class_id, key_name, - ) - .is_some() - && super::super::class_registry::static_accessor_attrs( - class_id, key_name, - ) - .0; + crate::object::class_value::class_static_own_accessor(class_id, key_name) + .is_some_and(|(_, enumerable, _)| enumerable); return f64::from_bits(if is_static_field || is_enumerable_static_accessor { TAG_TRUE } else { diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index da68b8c1c3..63036a0e4a 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -362,32 +362,29 @@ pub unsafe extern "C" fn js_super_accessor_get( // class/super/in-static-{getter,methods,setter}. if super::class_ref_id(receiver).is_some() { if let Some(key_name) = key_name.as_ref() { - // (a) parent static getter, walking the class_id chain. - if let Ok(guard) = crate::object::CLASS_STATIC_ACCESSORS.read() { - if let Some(reg) = guard.as_ref() { - let mut cid = parent_class_id; - let mut depth = 0usize; - while cid != 0 && depth < 32 { - if let Some(getter_ptr) = - reg.get(&cid).and_then(|m| m.get(key_name)).map(|&(g, _)| g) + // (a) the parent's static accessor (an accessor property of its + // class function object), walking the class_id chain. + { + let mut cid = parent_class_id; + let mut depth = 0usize; + // Only a compiled class has a function object: a builtin parent + // (`extends Error`) ends the walk. + while cid != 0 && depth < 32 && crate::object::is_class_id_registered(cid) { + if let Some((acc, _, _)) = + crate::object::class_value::class_static_own_accessor(cid, key_name) + { + return crate::object::class_value::class_static_accessor_call_get( + acc, receiver, + ); + } + match crate::object::get_parent_class_id(cid) { + Some(p) + if p != 0 && p != cid && crate::object::is_class_id_registered(p) => { - if getter_ptr != 0 { - let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter_ptr); - let this_scope = crate::gc::RuntimeHandleScope::new(); // #9445 - let prev = this_scope - .root_nanbox_f64(crate::object::js_implicit_this_set(receiver)); - let r = f(receiver); - crate::object::js_implicit_this_set(prev.get_nanbox_f64()); - return r; - } - } - match crate::object::get_parent_class_id(cid) { - Some(p) if p != 0 && p != cid => { - cid = p; - depth += 1; - } - _ => break, + cid = p; + depth += 1; } + _ => break, } } } diff --git a/crates/perry-runtime/src/proxy.rs b/crates/perry-runtime/src/proxy.rs index 555e8c7f57..8ec725d66d 100644 --- a/crates/perry-runtime/src/proxy.rs +++ b/crates/perry-runtime/src/proxy.rs @@ -1585,6 +1585,25 @@ fn own_set_descriptor(target: f64, key: f64) -> Option { // allocation. Closures don't carry the flag, so keep consulting the side // tables for them (their `name`/`length` + user `defineProperty` descriptors // live there). + // A class function object's static accessor is an accessor property of + // its own-property object (#11521: a getter-only one refuses the write). + // Its own data properties live in the same object; anything else is not + // own, and the walk continues at the class's [[Prototype]]. + if let Some(class_id) = crate::object::class_value::class_closure_id(obj_ptr) { + if let Some((acc, _, _)) = + crate::object::class_value::class_static_own_accessor(class_id, &key_name) + { + return Some(OwnSetDescriptor::Accessor { + setter_bits: acc.set, + }); + } + if crate::object::class_value::class_static_get(class_id, &key_name).is_some() { + let writable = crate::object::class_static_defined_attrs(class_id, &key_name) + .is_none_or(|(writable, _, _)| writable); + return Some(OwnSetDescriptor::Data { writable }); + } + return None; + } if crate::object::object_has_descriptors(obj_ptr) || crate::closure::is_closure_ptr(obj_ptr) { if let Some(acc) = crate::object::get_accessor_descriptor(obj_ptr, &key_name) { return Some(OwnSetDescriptor::Accessor { @@ -1659,6 +1678,13 @@ fn prototype_of_for_set(value: f64) -> Option { // `is_valid_obj_ptr(obj)` -- a magnitude-only check whose own floor // is 0x1000 -- followed by an unconditional `(*obj).class_id` read, // so an admitted handle id reached that deref. + // A class function object is a closure, not an ObjectHeader: its + // [[Prototype]] is the class's (the parent class for `extends`). + if let Some(class_id) = crate::object::class_value::class_closure_id(raw) { + let proto = crate::object::class_value::class_prototype_addr(class_id); + return (proto != 0 && proto != raw) + .then(|| f64::from_bits(POINTER_TAG | proto as u64)); + } if crate::value::addr_class::is_above_handle_band(raw) { if let Some(proto_bits) = crate::object::prototype_chain::object_static_prototype(raw) { if proto_bits == TAG_NULL || proto_bits == TAG_UNDEFINED || proto_bits == bits { @@ -2286,7 +2312,14 @@ fn ordinary_set_with_receiver(target: f64, key: f64, value: f64, receiver: f64) legacy_dunder_proto_set(receiver, value); return true; } - if crate::closure::is_closure_ptr(extract_pointer(current.to_bits()) as usize) { + // A class function object is not a leaf of the walk: its [[Prototype]] + // (the parent class) may hold the accessor (#11521). + if crate::closure::is_closure_ptr(extract_pointer(current.to_bits()) as usize) + && crate::object::class_value::class_closure_id( + extract_pointer(current.to_bits()) as usize + ) + .is_none() + { // ECMAScript poison pill: `fn.caller = v` / `fn.arguments = v` on // a strict-mode function throws via %ThrowTypeError%. A plain // non-strict function instead rejects the inherited setter-less @@ -2370,6 +2403,11 @@ fn class_link_accessor_set(current: f64, key: f64, value: f64, receiver: f64) -> if link.is_null() || crate::object::js_object_get_class_id(recv) != 0 { return None; } + // A class function object is a closure (no ObjectHeader class id): its + // static accessors are own properties `own_set_descriptor` reports. + if crate::object::class_value::class_closure_id(link as usize).is_some() { + return None; + } let class_id = crate::object::js_object_get_class_id(link); if class_id == 0 || class_id == crate::object::NATIVE_MODULE_CLASS_ID diff --git a/crates/perry-runtime/src/symbol/properties.rs b/crates/perry-runtime/src/symbol/properties.rs index 5724c6a455..332e6edea4 100644 --- a/crates/perry-runtime/src/symbol/properties.rs +++ b/crates/perry-runtime/src/symbol/properties.rs @@ -633,7 +633,7 @@ fn class_static_symbol_lookup_slow(class_id: u32, sym_f64: f64) -> Option { if class_id == 0 || sym_key == 0 { return None; } - let owner = crate::object::class_value::class_value_ptr(class_id) as usize; + let owner = crate::object::class_value::class_value_if_minted(class_id)? as usize; symbol_property_root_bits(owner, sym_key) } @@ -664,8 +664,10 @@ pub(crate) fn class_static_symbol_keys_for_class(class_id: u32) -> Vec { if class_id == 0 { return Vec::new(); } - let owner = crate::object::class_value::class_value_ptr(class_id) as usize; - clone_symbol_entries_for_obj_ptr(owner) + let Some(owner) = crate::object::class_value::class_value_if_minted(class_id) else { + return Vec::new(); + }; + clone_symbol_entries_for_obj_ptr(owner as usize) .into_iter() .map(|(sym_key, _)| sym_key) .collect() diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 7ed081685d..ce44e6e3c9 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -872,12 +872,6 @@ "verdict": "test_only", "why": "#[cfg(test)] Cell counting the inherited-access chain-verdict hits served on the calling test thread, so a test can assert its own site fired while others run beside it. It stores only a count and is absent from shipped binaries." }, - { - "file": "crates/perry-runtime/src/object/class_registry/static_accessor_attrs.rs", - "name": "STATIC_ACCESSOR_ATTRS", - "verdict": "not_a_gc_pointer", - "why": "Reflective enumerable/configurable overrides applied by a generic Object.defineProperty(/ies) descriptor to a DECLARED STATIC class accessor (#10480), keyed by (class_id: u32, name: String) to (enumerable: bool, configurable: bool). Every field is a plain scalar or an owned String \u2014 no NaN-boxed JSValue, no heap ObjectHeader address, nothing for the collector to mark or rewrite. The static accessor's getter/setter function pointers stay in CLASS_STATIC_ACCESSORS; this table only remembers which two attribute bits a generic descriptor overrode. Instance accessors are real accessor properties of the class prototype and have no entry here." - }, { "file": "crates/perry-runtime/src/object/class_registry/state.rs", "name": "CLASS_DECLARED_STATIC_GLOBAL_SLOTS", diff --git a/scripts/registry_lifetime_allowlist.json b/scripts/registry_lifetime_allowlist.json index ad6345431b..459574f9ce 100644 --- a/scripts/registry_lifetime_allowlist.json +++ b/scripts/registry_lifetime_allowlist.json @@ -391,12 +391,6 @@ "verdict": "open_leak", "why": "Key includes the runtime SymbolHeader.id: a re-evaluated class expression with computed [Symbol()] members adds entries each time" }, - { - "file": "crates/perry-runtime/src/object/class_registry/static_accessor_attrs.rs", - "name": "STATIC_ACCESSOR_ATTRS", - "verdict": "bounded_by_program", - "why": "(class_id, declared static accessor name) -> attrs, set only on existing declared accessors" - }, { "file": "crates/perry-runtime/src/object/class_registry/verdict_classes.rs", "name": "VERDICT_CLASSES", diff --git a/test-files/test_gap_class_static_accessor_props.ts b/test-files/test_gap_class_static_accessor_props.ts new file mode 100644 index 0000000000..2a91c85775 --- /dev/null +++ b/test-files/test_gap_class_static_accessor_props.ts @@ -0,0 +1,44 @@ +// Static class accessors are accessor properties of the class constructor: +// reflection, attributes, delete, inheritance and [[Set]] all see one property. +class A { + static count = 0; + static get x() { return "x:" + (this as any).name; } + static set x(v: string) { A.count++; } + static get ro() { return 1; } +} +class B extends A {} +console.log(A.x, B.x, (B as any).ro); +const d = Object.getOwnPropertyDescriptor(A, "x")!; +console.log(typeof d.get, typeof d.set, d.enumerable, d.configurable); +console.log(d.get === Object.getOwnPropertyDescriptor(A, "x")!.get); +console.log(Object.getOwnPropertyNames(A).join(","), Object.keys(A).join(",")); +(A as any).x = "v"; +(B as any).x = "w"; +console.log(A.count); +// #11521: a getter-only static refuses the write. +try { + (A as any).ro = 5; + console.log("accepted", (A as any).ro); +} catch (e) { + console.log("TypeError", e instanceof TypeError, (A as any).ro); +} +try { + (B as any).ro = 5; + console.log("accepted", (B as any).ro); +} catch (e) { + console.log("TypeError", e instanceof TypeError); +} +Object.defineProperty(A, "x", { enumerable: true }); +console.log(Object.keys(A).join(","), A.propertyIsEnumerable("x"), A.x); +Object.defineProperty(A, "dyn", { get() { return "dyn"; }, set(v) {}, configurable: true, enumerable: false }); +console.log((A as any).dyn, (B as any).dyn, Object.getOwnPropertyNames(A).includes("dyn"), "dyn" in B); +console.log(delete (A as any).dyn, (A as any).dyn, "dyn" in A); +console.log(delete (A as any).x, A.x, B.x, "x" in A, Object.getOwnPropertyDescriptor(A, "x")); +class C { + static get v() { return 1; } +} +Object.defineProperty(C, "v", { configurable: false }); +console.log(Reflect.deleteProperty(C, "v"), C.v, Object.getOwnPropertyDescriptor(C, "v")!.configurable); +Object.defineProperty(C, "w", { value: 7, configurable: true }); +Object.defineProperty(C, "w", { get() { return 8; } }); +console.log((C as any).w, typeof Object.getOwnPropertyDescriptor(C, "w")!.get); diff --git a/test-files/test_gap_class_static_accessor_reflect.ts b/test-files/test_gap_class_static_accessor_reflect.ts new file mode 100644 index 0000000000..a1000c393e --- /dev/null +++ b/test-files/test_gap_class_static_accessor_reflect.ts @@ -0,0 +1,18 @@ +// Static class accessors are real accessor properties of the constructor; +// C.prototype's own names are its real own keys. +class C { static get a() { return 1; } static set a(v: number) {} static get k() { return 3; } } +console.log(Reflect.deleteProperty(C, "a"), "a" in C, Object.getOwnPropertyDescriptor(C, "a") === undefined); +Object.defineProperty(C, "b", { get() { return 2; }, configurable: false }); +console.log(Reflect.deleteProperty(C, "b"), (C as any).b, Object.getOwnPropertyDescriptor(C, "b")!.configurable); +Object.defineProperty(C, "k", { configurable: false }); +console.log(Reflect.deleteProperty(C, "k"), (C as any).k); +class D { m() {} get g() { return 1; } set g(v) {} static s() {} static get sg() { return 1; } static f = 1; x = 1; } +console.log(Object.getOwnPropertyNames(D.prototype).sort().join(",")); +delete (D.prototype as any).m; +console.log(Object.getOwnPropertyNames(D.prototype).sort().join(","), "m" in new D()); +(D.prototype as any).added = 1; +console.log(Object.getOwnPropertyNames(D.prototype).sort().join(",")); +delete (D.prototype as any).g; +console.log(Object.getOwnPropertyNames(D.prototype).sort().join(","), Reflect.ownKeys(D.prototype).length); +class E extends D { n() {} } +console.log(Object.getOwnPropertyNames(E.prototype).sort().join(","), Object.getOwnPropertyNames(Object.getPrototypeOf(E.prototype)).sort().join(",")); diff --git a/test-files/test_gap_class_static_getter_only_set.ts b/test-files/test_gap_class_static_getter_only_set.ts new file mode 100644 index 0000000000..83ae7e49b5 --- /dev/null +++ b/test-files/test_gap_class_static_getter_only_set.ts @@ -0,0 +1,31 @@ +// #11521: a write to a getter-only static accessor is rejected. Strict +// [[Set]] (module code) throws a TypeError; Reflect.set reports false. The +// value is unchanged either way, on the class and on a subclass. +class Cfg { + static get version() { return 3; } + static get label() { return "cfg:" + (this as any).name; } + static set label(_v: string) { Cfg.writes++; } + static writes = 0; +} +class Sub extends Cfg {} + +function attempt(what: string, fn: () => void) { + try { + fn(); + console.log(what, "accepted"); + } catch (e) { + console.log(what, (e as Error).constructor.name, e instanceof TypeError); + } +} + +attempt("Cfg.version", () => { (Cfg as any).version = 4; }); +attempt("Sub.version", () => { (Sub as any).version = 4; }); +attempt("Cfg[k]", () => { const k = "version"; (Cfg as any)[k] = 5; }); +console.log(Cfg.version, (Sub as any).version); +console.log(Reflect.set(Cfg, "version", 6), Reflect.set(Sub, "version", 6), Cfg.version); +// A setter half accepts the write (and runs), on the class and a subclass. +attempt("Cfg.label", () => { (Cfg as any).label = "x"; }); +attempt("Sub.label", () => { (Sub as any).label = "y"; }); +console.log(Reflect.set(Cfg, "label", "z"), Cfg.writes, Cfg.label, (Sub as any).label); +// The getter-only refusal does not create an own data property. +console.log(Object.getOwnPropertyNames(Sub).includes("version"), typeof Object.getOwnPropertyDescriptor(Cfg, "version")!.get);