Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 11 additions & 15 deletions crates/perry-codegen/src/expr/super_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,22 +72,18 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
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(&current_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(&current_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)),
Expand Down
44 changes: 44 additions & 0 deletions crates/perry-runtime/src/object/class_constructors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<f64> = 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
Expand Down
27 changes: 25 additions & 2 deletions crates/perry-runtime/src/object/class_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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 <key>" / "set <key>" 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),
);
Comment on lines +3856 to +3860

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the difference between set_property_attrs and set_builtin_property_attrs
echo "=== set_property_attrs signature/implementation ==="
rg -n -A10 'pub(\s+\(crate\))?\s+(unsafe\s+)?fn\s+set_property_attrs\s*\(' --type=rust

echo ""
echo "=== set_builtin_property_attrs signature/implementation ==="
rg -n -A10 'pub(\s+\(crate\))?\s+(unsafe\s+)?fn\s+set_builtin_property_attrs\s*\(' --type=rust

Repository: PerryTS/perry

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Simpler search for both functions
echo "=== Searching for set_property_attrs definition ==="
rg -n 'fn\s+set_property_attrs' --type=rust -A5

echo ""
echo "=== Searching for set_builtin_property_attrs definition ==="
rg -n 'fn\s+set_builtin_property_attrs' --type=rust -A5

echo ""
echo "=== Searching for where these functions are called ==="
rg -n 'set_property_attrs|set_builtin_property_attrs' --type=rust | head -20

Repository: PerryTS/perry

Length of output: 3174


🏁 Script executed:

#!/bin/bash
# Get full implementation of both functions to understand differences
echo "=== Full set_property_attrs implementation ==="
rg -n -A20 'pub\(crate\) fn set_property_attrs\(obj: usize' crates/perry-runtime/src/object/mod.rs

echo ""
echo "=== Full set_builtin_property_attrs implementation ==="
rg -n -A10 'pub\(crate\) fn set_builtin_property_attrs\(obj: usize' crates/perry-runtime/src/object/mod.rs

echo ""
echo "=== Find set_bound_native_closure_name context ==="
rg -n -B5 -A10 'set_property_attrs.*"name"' crates/perry-runtime/src/native_module.rs | head -40

Repository: PerryTS/perry

Length of output: 1831


🏁 Script executed:

#!/bin/bash
# Search more broadly for set_property_attrs usage with "name" key
echo "=== Uses of set_property_attrs with name key ==="
rg -n 'set_property_attrs.*"name"' --type=rust

echo ""
echo "=== Class registry file to see the accessor function code ==="
grep -n -A10 -B5 'set_builtin_property_attrs' crates/perry-runtime/src/object/class_registry.rs | head -50

echo ""
echo "=== Check if there's other similar name setting pattern in class_registry ==="
rg -n '\.name.*PropertyAttrs' crates/perry-runtime/src/object/class_registry.rs

echo ""
echo "=== Understand the purpose of PROPERTY_ATTRS_IN_USE and GLOBAL_DESCRIPTORS_IN_USE flags ==="
rg -n 'PROPERTY_ATTRS_IN_USE\|GLOBAL_DESCRIPTORS_IN_USE' crates/perry-runtime/src/object/mod.rs | head -20

Repository: PerryTS/perry

Length of output: 2567


🏁 Script executed:

#!/bin/bash
# Let's understand when each function should be used
echo "=== Context around set_property_attrs function definition ==="
rg -n -B10 'pub\(crate\) fn set_property_attrs' crates/perry-runtime/src/object/mod.rs | head -20

echo ""
echo "=== Context around set_builtin_property_attrs function definition ==="
rg -n -B10 'pub\(crate\) fn set_builtin_property_attrs' crates/perry-runtime/src/object/mod.rs | head -30

echo ""
echo "=== The only usage of set_property_attrs with 'name' key ==="
rg -n -B10 -A5 'set_property_attrs.*obj as usize.*"name"' crates/perry-runtime/src/child_process/mod.rs

Repository: PerryTS/perry

Length of output: 2533


Use set_property_attrs instead of set_builtin_property_attrs for the accessor function's .name property.

The code uses set_builtin_property_attrs, which is intended for built-in prototype methods (documented in mod.rs:911-920). It omits critical side-effect flags (PROPERTY_ATTRS_IN_USE, GLOBAL_DESCRIPTORS_IN_USE, disable_class_field_inline_guard()) that signal descriptor tracking is in use.

The accessor function's .name property is a dynamic property on a specific closure object, not a built-in prototype method. The only other usage of setting a "name" property descriptor in the codebase (child_process.rs:2024 for Error objects) correctly uses set_property_attrs. Use the same approach here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/class_registry.rs` around lines 3857 - 3861,
The code incorrectly uses set_builtin_property_attrs to set the "name" property
on a specific closure object, but this function is only intended for built-in
prototype methods and omits critical side-effect flags for descriptor tracking.
Replace the set_builtin_property_attrs call with set_property_attrs when setting
the "name" property on the accessor function's closure object, matching the same
pattern used elsewhere in the codebase for similar dynamic property descriptors.

crate::gc::runtime_write_barrier_root_heap_word(closure as u64);
crate::value::js_nanbox_pointer(closure as i64)
}
Expand Down
18 changes: 18 additions & 0 deletions crates/perry-runtime/src/object/delete_rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Comment on lines +188 to +198

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing configurability check before marking class-declared property as deleted.

The early deletion path for class-declaration prototype objects doesn't check whether the property has been redefined as non-configurable via Object.defineProperty. Other deletion branches in this function (lines 87-91, 106-110, 151-155, 169-173, 229-233) consistently check get_property_attrs before deleting.

If someone does Object.defineProperty(C.prototype, 'x', {configurable: false}) on an existing class accessor, the current code would incorrectly succeed in deletion because:

  1. No accessor descriptor is added to the side table (only property attrs)
  2. The check at line 168 returns None, skipping the configurability guard
  3. This early path marks the key deleted without further checks
🛡️ Proposed fix: add configurability check
 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))
         {
+            if let Some(attrs) = get_property_attrs(obj as usize, name) {
+                if !attrs.configurable() {
+                    return 0;
+                }
+            }
             super::class_registry::class_mark_key_deleted(cid, name);
             return 1;
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
}
}
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))
{
if let Some(attrs) = get_property_attrs(obj as usize, name) {
if !attrs.configurable() {
return 0;
}
}
super::class_registry::class_mark_key_deleted(cid, name);
return 1;
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/delete_rest.rs` around lines 188 - 198, The
early deletion path for class-declaration prototype objects in the condition
block starting with class_id_for_decl_prototype_object is missing a
configurability check before calling class_mark_key_deleted. Add a check using
get_property_attrs to verify the property is configurable, similar to the
configurability checks performed in other deletion branches at lines 87-91,
106-110, 151-155, 169-173, and 229-233. This ensures that properties redefined
as non-configurable via Object.defineProperty cannot be incorrectly deleted.

let keys = (*obj).keys_array;
if keys.is_null() {
// No keys array means no fields to delete, but delete "succeeds" vacuously
Expand Down
18 changes: 13 additions & 5 deletions crates/perry-runtime/src/object/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand Down Expand Up @@ -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,
);
Expand Down
30 changes: 27 additions & 3 deletions crates/perry-runtime/src/object/object_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

f64::from_bits(TAG_FALSE)
}
}

Expand Down
54 changes: 54 additions & 0 deletions crates/perry-runtime/src/object/property_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading