diff --git a/crates/perry-codegen/src/expr/super_method.rs b/crates/perry-codegen/src/expr/super_method.rs index 992879259..b634d39f3 100644 --- a/crates/perry-codegen/src/expr/super_method.rs +++ b/crates/perry-codegen/src/expr/super_method.rs @@ -72,22 +72,18 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { parent = ctx.classes.get(&p).and_then(|c| c.extends_name.clone()); } let Some(fn_name) = resolved_fn else { - // Static resolution failed. For a class with a DYNAMIC parent - // (`class X extends _mod.default` — the interop ESM - // default-export base, wall 38/42), `extends_name` is "default" - // and never resolves to a compile-time class, so the chain walk - // above finds nothing. Dispatch `super.method(...)` at runtime - // via the registered parent edge instead of returning the bogus - // numeric `0.0` (which made `super.getRequestHandler()` in - // Next.js's `NextNodeServer.makeRequestHandler` yield a number, - // and the handler it built threw "value is not a function"). - let has_dyn_parent = ctx - .classes - .get(¤t_class_name) - .map(|c| c.extends_expr.is_some()) - .unwrap_or(false); + // Compile-time resolution failed (the parent has no INSTANCE + // method of this name). This happens for (1) a DYNAMIC parent + // (`class X extends _mod.default` — the interop ESM default base, + // wall 38/42) whose `extends_name` never resolves to a known + // class, and (2) a `super.m()` inside a `static` method, where + // the target is the parent's STATIC method (not in the instance + // tables walked above). Both are handled by the runtime helper, + // which walks the registered parent edge and — when `this` is a + // ClassRef — resolves the parent's static method. Routing here + // beats the bogus numeric `0.0` ("value is not a function"). let cid = ctx.class_ids.get(¤t_class_name).copied().unwrap_or(0); - if has_dyn_parent && cid != 0 { + if cid != 0 { let this_box = match ctx.this_stack.last().cloned() { Some(slot) => ctx.block().load(DOUBLE, &slot), None => double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)), diff --git a/crates/perry-runtime/src/object/class_constructors.rs b/crates/perry-runtime/src/object/class_constructors.rs index 8d3db230f..21654ac71 100644 --- a/crates/perry-runtime/src/object/class_constructors.rs +++ b/crates/perry-runtime/src/object/class_constructors.rs @@ -262,6 +262,50 @@ pub unsafe extern "C" fn js_super_method_call_dynamic( Some(p) if p != 0 => p, _ => return undef, }; + // Static-context super call (`super.m()` inside a `static` method): the + // receiver is the class constructor (a ClassRef), so resolve the PARENT's + // STATIC method (not an instance/prototype method) and invoke it with + // `this` bound to the current class. Refs class/super/in-static-methods. + if super::class_ref_id(this_value).is_some() { + if let Some((func_ptr, param_count, has_rest)) = + super::class_registry::lookup_static_method_in_chain(parent_cid, name) + { + let prev_this = crate::object::js_implicit_this_set(this_value); + crate::object::static_this_arm_if_unarmed(this_value); + let result = if has_rest { + // Mirror `js_class_static_method_call`'s rest bundling: fixed + // positional args, then the remaining args as an array. + let fixed = (param_count as usize).saturating_sub(1); + let arr = crate::array::js_array_alloc(args_len.saturating_sub(fixed) as u32); + let mut i = fixed; + while i < args_len { + crate::array::js_array_push_f64(arr, *args_ptr.add(i)); + i += 1; + } + let rest_box = crate::value::js_nanbox_pointer(arr as i64); + let mut buf: Vec = Vec::with_capacity(param_count as usize); + for j in 0..fixed { + buf.push(if j < args_len { + *args_ptr.add(j) + } else { + f64::from_bits(crate::value::TAG_UNDEFINED) + }); + } + buf.push(rest_box); + super::class_registry::call_static_method( + func_ptr, + buf.as_ptr(), + buf.len(), + param_count, + ) + } else { + super::class_registry::call_static_method(func_ptr, args_ptr, args_len, param_count) + }; + crate::object::static_this_disarm(); + crate::object::js_implicit_this_set(prev_this); + return result; + } + } // `lookup_class_method_in_chain` resolves under the registry read lock and // DROPS it before returning — the invoked method body may take the registry // write lock (a lazy `require()` registering a module class), so we must not diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 5956736a6..56393f5e1 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -3815,8 +3815,15 @@ extern "C" fn class_accessor_setter_thunk( /// Wrap a raw class accessor func_ptr as a callable function VALUE for /// descriptor reflection (`Object.getOwnPropertyDescriptor(C.prototype, /// "x").get`). Built-in-shaped: `.length` 0/1, no `.prototype`, native -/// `toString` form. -pub(crate) fn class_accessor_function_value(raw_ptr: usize, is_setter: bool) -> f64 { +/// `toString` form. `prop_name` is the accessor's property key — the spec +/// `.name` of a `get`/`set` accessor is the key prefixed with `"get "`/`"set "` +/// (Function Definitions: SetFunctionName with the "get"/"set" prefix), e.g. +/// `Object.getOwnPropertyDescriptor(C.prototype, "x").get.name === "get x"`. +pub(crate) fn class_accessor_function_value( + raw_ptr: usize, + is_setter: bool, + prop_name: &str, +) -> f64 { if raw_ptr == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); } @@ -3835,6 +3842,22 @@ pub(crate) fn class_accessor_function_value(raw_ptr: usize, is_setter: bool) -> if is_setter { 1 } else { 0 }, ); super::native_module::set_builtin_closure_non_constructable(closure as usize); + // Spec `.name` = "get " / "set " with attributes + // { writable: false, enumerable: false, configurable: true } (mirrors the + // `Function.prototype.bind` name path). Without this the reflected accessor + // value's `.name` defaulted to "" — refs class/.../fn-name-accessor-{get,set}. + let prefix = if is_setter { "set " } else { "get " }; + let fn_name = format!("{prefix}{prop_name}"); + let name_ptr = crate::string::js_string_from_bytes(fn_name.as_ptr(), fn_name.len() as u32); + let name_value = f64::from_bits(crate::value::JSValue::string_ptr(name_ptr).bits()); + unsafe { + crate::closure::closure_set_dynamic_prop(closure as usize, "name", name_value); + } + crate::object::set_builtin_property_attrs( + closure as usize, + "name".to_string(), + crate::object::PropertyAttrs::new(false, false, true), + ); crate::gc::runtime_write_barrier_root_heap_word(closure as u64); crate::value::js_nanbox_pointer(closure as i64) } diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index be1983b9c..9613b259f 100644 --- a/crates/perry-runtime/src/object/delete_rest.rs +++ b/crates/perry-runtime/src/object/delete_rest.rs @@ -178,6 +178,24 @@ pub extern "C" fn js_object_delete_field( // below so hasOwnProperty / Object.keys stop seeing it. } } + // A class-declaration prototype object: instance accessors (`get x()`) + // and methods live in the class vtable, not the keys_array, so the scan + // below would "succeed vacuously" while the member stayed visible to + // hasOwnProperty / getOwnPropertyDescriptor. Record the key as deleted + // so those reflective paths agree it is gone (test262 verifyProperty's + // `configurable` check: `delete obj[name]` then assert the key absent — + // class/definition/{getters,setters}-prop-desc). + if let Some(cid) = super::class_registry::class_id_for_decl_prototype_object(obj as usize) { + if let Some(name) = super::has_own_helpers::str_from_string_header(key) { + if name != "constructor" + && (super::class_registry::class_own_accessor_ptrs(cid, name).is_some() + || super::native_module::class_has_own_method(cid, name)) + { + super::class_registry::class_mark_key_deleted(cid, name); + return 1; + } + } + } let keys = (*obj).keys_array; if keys.is_null() { // No keys array means no fields to delete, but delete "succeeds" vacuously diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index 0d5381bc8..64427ca8f 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -323,8 +323,12 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu }; if let Some((g, s)) = accessor { return build_accessor_descriptor( - super::class_registry::class_accessor_function_value(g, false), - super::class_registry::class_accessor_function_value(s, true), + super::class_registry::class_accessor_function_value( + g, + false, + &method_name, + ), + super::class_registry::class_accessor_function_value(s, true, &method_name), false, true, ); @@ -728,10 +732,14 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu // fields, but they ARE own properties of the prototype. if let Some(cid) = super::class_registry::class_id_for_decl_prototype_object(obj as usize) { if let Some(ref name) = key_rust { - if let Some((g, s)) = super::class_registry::class_own_accessor_ptrs(cid, name) { + if super::class_registry::class_is_key_deleted(cid, name) { + // `delete C.prototype.x` recorded the accessor as removed. + } else if let Some((g, s)) = + super::class_registry::class_own_accessor_ptrs(cid, name) + { return build_accessor_descriptor( - super::class_registry::class_accessor_function_value(g, false), - super::class_registry::class_accessor_function_value(s, true), + super::class_registry::class_accessor_function_value(g, false, name), + super::class_registry::class_accessor_function_value(s, true, name), false, true, ); diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index c74e7c91c..b6a155d14 100644 --- a/crates/perry-runtime/src/object/object_ops.rs +++ b/crates/perry-runtime/src/object/object_ops.rs @@ -801,6 +801,12 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { .is_some_and(|props| props.contains_key(key)) }) || super::class_registry::lookup_static_method_in_chain(class_id, key) .is_some() + // A static accessor (`static get x()`) is an own + // property of the constructor — own-only, mirroring + // getOwnPropertyDescriptor (class/definition/ + // {getters,setters}-prop-desc `staticX`). + || super::class_registry::class_own_static_accessor_ptrs(class_id, key) + .is_some() } }) .unwrap_or(false); @@ -934,10 +940,28 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { } if own_key_present(obj, key_str) { - f64::from_bits(TAG_TRUE) - } else { - f64::from_bits(TAG_FALSE) + return f64::from_bits(TAG_TRUE); + } + + // A class-declaration prototype object: instance accessors (`get x()`) + // and methods live in the class vtable, not the object's keys_array, yet + // they ARE own properties of `C.prototype` — `getOwnPropertyDescriptor` + // already reflects them, so `hasOwnProperty` must agree (test262 + // class/definition/{getters,setters}-prop-desc, which assert via + // `verifyProperty` → `hasOwnProperty`). + if let Some(cid) = super::class_registry::class_id_for_decl_prototype_object(obj as usize) { + if let Some(key) = super::has_own_helpers::str_from_string_header(key_str) { + if !super::class_registry::class_is_key_deleted(cid, key) + && (key == "constructor" + || super::class_registry::class_own_accessor_ptrs(cid, key).is_some() + || super::native_module::class_has_own_method(cid, key)) + { + return f64::from_bits(TAG_TRUE); + } + } } + + f64::from_bits(TAG_FALSE) } } diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index 79b3903e9..08185a061 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -158,6 +158,60 @@ pub unsafe extern "C" fn js_super_accessor_get( .ok() .map(|s| s.to_string()) }; + // Static-context super (`super.x` inside a `static` method/getter): the + // receiver is the class constructor (a ClassRef), so resolve against the + // PARENT's static side — a static getter, then a static data field — + // rather than the parent prototype/instance vtable below. Refs + // 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) + { + if getter_ptr != 0 { + let f: extern "C" fn(f64) -> f64 = std::mem::transmute(getter_ptr); + let prev = crate::object::js_implicit_this_set(receiver); + let r = f(receiver); + crate::object::js_implicit_this_set(prev); + return r; + } + } + match crate::object::get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + } + } + // (b) parent static data field (CLASS_DYNAMIC_PROPS), same walk. + let mut cid = parent_class_id; + let mut depth = 0usize; + while cid != 0 && depth < 32 { + if let Some(v) = crate::object::CLASS_DYNAMIC_PROPS + .with(|m| m.borrow().get(&cid).and_then(|f| f.get(key_name)).copied()) + { + return v; + } + match crate::object::get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + } + } + return f64::from_bits(crate::value::TAG_UNDEFINED); + } if let Some(key_name) = key_name { if let Ok(registry) = crate::object::CLASS_VTABLE_REGISTRY.read() { if let Some(reg) = registry.as_ref() {