From f539c31236af51bc205a365fb01a0dffa662c6a1 Mon Sep 17 00:00:00 2001 From: Ralph Date: Thu, 18 Jun 2026 02:38:16 -0700 Subject: [PATCH 1/4] fix(class): super-in-static resolution + accessor .name reflection (#5345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two ECMAScript class-semantics fixes for the test262 class tail. 1. super in a STATIC method (super/in-static-{getter,methods,setter}, +3): `super.x` and `super.m()` inside a `static` method must resolve against the PARENT's static side (its static getter / static method on the constructor), not the parent prototype/instance vtable. Perry returned `undefined` for `super.x` and threw "value is not a function" for `super.m()`. The receiver in a static method is the class constructor (a ClassRef), so the runtime helpers now branch on that: - js_super_accessor_get: when the receiver is a ClassRef, walk the parent class_id chain for a static getter (CLASS_STATIC_ACCESSORS), then a static data field (CLASS_DYNAMIC_PROPS). - js_super_method_call_dynamic: when `this` is a ClassRef, resolve the parent's static method (lookup_static_method_in_chain) and invoke it with `this` bound to the current class. - codegen: route `super.m()` to the dynamic helper whenever compile-time INSTANCE resolution fails (previously only for dynamic-parent classes), so the static case reaches the runtime resolver instead of a bogus 0.0. 2. accessor .name reflection (fn-name-accessor-{get,set} correctness): `Object.getOwnPropertyDescriptor(C.prototype, "x").get.name` must be "get x" / "set x" with { writable:false, enumerable:false, configurable:true }. The reflected accessor function value left `.name` as "". class_accessor_function_value now sets the prefixed name with the spec attributes (mirroring the Function.prototype.bind name path). test262 language/statements/class/super: 3 pass → 6 pass (+3: in-static-getter, in-static-methods, in-static-setter), 0 regressions. No regressions across subclass/definition/super. (The bundled fn-name-accessor test262 cases still need symbol-keyed accessor reflection, a separate gap, but string-keyed accessor .name is now spec-correct.) Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/perry-codegen/src/expr/super_method.rs | 26 ++++----- .../src/object/class_constructors.rs | 44 +++++++++++++++ .../src/object/class_registry.rs | 28 +++++++++- .../perry-runtime/src/object/descriptors.rs | 8 +-- .../perry-runtime/src/object/property_key.rs | 55 +++++++++++++++++++ 5 files changed, 140 insertions(+), 21 deletions(-) diff --git a/crates/perry-codegen/src/expr/super_method.rs b/crates/perry-codegen/src/expr/super_method.rs index 9928792592..b634d39f34 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 8d3db230ff..21654ac717 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 5956736a65..090b6ec997 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,23 @@ 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/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index 0d5381bc83..030b1ff05c 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -323,8 +323,8 @@ 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, ); @@ -730,8 +730,8 @@ pub extern "C" fn js_object_get_own_property_descriptor(obj_value: f64, key_valu if let Some(ref name) = key_rust { 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/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index 79b3903e9c..34eae50888 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -158,6 +158,61 @@ 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() { From db9cce8b4c17664c118113effcec2296fd6b3124 Mon Sep 17 00:00:00 2001 From: Ralph Date: Thu, 18 Jun 2026 02:48:36 -0700 Subject: [PATCH 2/4] fix(runtime): hasOwnProperty recognizes class accessors/methods on the prototype (#5345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Object.prototype.hasOwnProperty.call(C.prototype, "x")` returned `false` for a class instance getter/setter/method, even though `Object.getOwnPropertyDescriptor(C.prototype, "x")` correctly returns the descriptor — the two disagreed. Class instance accessors and methods live in the class vtable, not the prototype object's keys_array, so `js_object_has_own` missed them. `js_object_has_own` now, for a class-declaration prototype object, also recognizes `constructor`, own instance accessors (`class_own_accessor_ptrs`), and own instance methods (`class_has_own_method`) — mirroring the `getOwnPropertyDescriptor` path. Refs class/definition/{getters,setters}-prop-desc (those tests also exercise accessor deletion for the `configurable` check, a separate gap, so they remain red — but hasOwnProperty is now spec-consistent). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/perry-runtime/src/object/object_ops.rs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index c74e7c91c3..f1af55c1b9 100644 --- a/crates/perry-runtime/src/object/object_ops.rs +++ b/crates/perry-runtime/src/object/object_ops.rs @@ -934,10 +934,27 @@ 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 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) } } From dae833bc36706940596bc69f00c3d76fc42b244d Mon Sep 17 00:00:00 2001 From: Ralph Date: Thu, 18 Jun 2026 03:02:13 -0700 Subject: [PATCH 3/4] fix(runtime): class accessor deletion + static-accessor hasOwnProperty (#5345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the class-accessor reflection fixes so `verifyProperty` passes for class getters/setters (test262 class/definition/{getters,setters}-prop-desc, +2): - `delete C.prototype.x` for a class instance accessor/method now records the key in CLASS_DELETED_KEYS (it lives in the class vtable, not the keys_array, so the prior keys-scan delete was a vacuous no-op). hasOwnProperty and getOwnPropertyDescriptor honor the deleted mark, so verifyProperty's `configurable` probe (`delete` then assert-absent) now observes the removal. - hasOwnProperty on a class constructor (ClassRef) now recognizes a static accessor (`static get x()`) as an own property (`class_own_static_accessor_ptrs`, own-only), mirroring getOwnPropertyDescriptor — previously it returned false ("staticX should be an own property"). test262 language/statements/class/definition: 49 → 51 pass (+2: getters-prop-desc, setters-prop-desc), 0 regressions. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/perry-runtime/src/object/delete_rest.rs | 18 ++++++++++++++++++ crates/perry-runtime/src/object/descriptors.rs | 6 +++++- crates/perry-runtime/src/object/object_ops.rs | 13 ++++++++++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/perry-runtime/src/object/delete_rest.rs b/crates/perry-runtime/src/object/delete_rest.rs index be1983b9c4..9613b259f1 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 030b1ff05c..0bb5edae88 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -728,7 +728,11 @@ 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, name), super::class_registry::class_accessor_function_value(s, true, name), diff --git a/crates/perry-runtime/src/object/object_ops.rs b/crates/perry-runtime/src/object/object_ops.rs index f1af55c1b9..b6a155d140 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); @@ -945,9 +951,10 @@ pub extern "C" fn js_object_has_own(obj_value: f64, key_value: f64) -> f64 { // `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 key == "constructor" - || super::class_registry::class_own_accessor_ptrs(cid, key).is_some() - || super::native_module::class_has_own_method(cid, key) + 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); } From 43448d0c736bfbeca45c6d364693b4b67132ba62 Mon Sep 17 00:00:00 2001 From: Ralph Date: Thu, 18 Jun 2026 03:08:56 -0700 Subject: [PATCH 4/4] style: rustfmt the class-reflection fixes (#5345) Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/perry-runtime/src/object/class_registry.rs | 3 +-- crates/perry-runtime/src/object/descriptors.rs | 6 +++++- crates/perry-runtime/src/object/property_key.rs | 3 +-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 090b6ec997..56393f5e11 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -3848,8 +3848,7 @@ pub(crate) fn class_accessor_function_value( // 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_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); diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index 0bb5edae88..64427ca8f5 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -323,7 +323,11 @@ 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, &method_name), + super::class_registry::class_accessor_function_value( + g, + false, + &method_name, + ), super::class_registry::class_accessor_function_value(s, true, &method_name), false, true, diff --git a/crates/perry-runtime/src/object/property_key.rs b/crates/perry-runtime/src/object/property_key.rs index 34eae50888..08185a061d 100644 --- a/crates/perry-runtime/src/object/property_key.rs +++ b/crates/perry-runtime/src/object/property_key.rs @@ -175,8 +175,7 @@ pub unsafe extern "C" fn js_super_accessor_get( 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 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);