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
98 changes: 93 additions & 5 deletions crates/perry-runtime/src/object/array_object_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,51 @@ unsafe fn is_array_object(obj: *const ObjectHeader) -> bool {
(*gc_header).obj_type == crate::gc::GC_TYPE_ARRAY
}

/// Apply `Object.freeze` / `Object.seal` to an array's OWN index + named data
/// properties. The generic `mark_all_keys` walks `(*obj).keys_array`, but an
/// array's indices live in the dense element store and its named props in the
/// `ARRAY_NAMED_PROPS` side table — neither appears in `keys_array` — so
/// freeze/seal historically missed them, leaving a frozen array's elements
/// writable/configurable. Returns `true` when `obj` is an array (handled here),
/// `false` otherwise so the caller can fall back to the ordinary key walk.
pub(crate) unsafe fn mark_all_array_props(
obj: *mut ObjectHeader,
drop_writable: bool,
drop_configurable: bool,
) -> bool {
if !is_array_object(obj) {
return false;
}
// Any explicit per-index/named attribute override makes the raw numeric
// fast paths ineligible — gate them on the descriptor flag so the recorded
// non-writable/non-configurable attrs are actually honored on read/write.
{
let gc = gc_header_for(obj);
(*gc)._reserved |= crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS;
}
let arr = obj as *const crate::array::ArrayHeader;
let addr = obj as usize;
let mut apply = |key: String| {
let mut attrs =
super::get_property_attrs(addr, &key).unwrap_or(PropertyAttrs::new(true, true, true));
if drop_writable {
attrs.bits &= !PropertyAttrs::WRITABLE;
}
if drop_configurable {
attrs.bits &= !PropertyAttrs::CONFIGURABLE;
}
super::set_property_attrs(addr, key, attrs);
};
let len = (*arr).length;
for i in 0..len {
apply(i.to_string());
}
for name in crate::array::array_named_property_names(arr, false) {
apply(name);
}
true
}

pub(crate) unsafe fn array_property_is_enumerable(
obj: *mut ObjectHeader,
key_str: *const crate::StringHeader,
Expand Down Expand Up @@ -308,14 +353,29 @@ pub(crate) unsafe fn define_array_property(
let desc_has_get = super::desc_has_field(descriptor_value, b"get");
let desc_has_set = super::desc_has_field(descriptor_value, b"set");
if desc_has_get || desc_has_set {
// Non-configurable existing index can't switch to an accessor.
// ValidateAndApplyPropertyDescriptor for an existing non-configurable
// index: reject the data→accessor switch AND a change to a
// non-configurable accessor's `get`/`set` (or a forbidden
// enumerable/configurable change). The historical check only
// rejected the data→accessor case, so redefining a non-configurable
// accessor index with a different setter silently succeeded.
if exists {
let cur = super::get_property_attrs(obj as usize, key_name)
.unwrap_or_else(|| PropertyAttrs::new(true, true, true));
let already_accessor =
super::get_accessor_descriptor(obj as usize, key_name).is_some();
if !cur.configurable() && !already_accessor {
return Some(false);
if !cur.configurable() {
let cur_accessor = super::get_accessor_descriptor(obj as usize, key_name);
let cur_value = if cur_accessor.is_none() {
crate::array::js_array_get_f64(arr, index)
} else {
f64::from_bits(crate::value::TAG_UNDEFINED)
};
super::validate_nonconfigurable_redefine(
key_name,
cur,
cur_accessor,
cur_value,
descriptor_value,
);
}
}
let get_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, get_key);
Expand Down Expand Up @@ -502,6 +562,34 @@ pub(crate) unsafe fn define_array_property(
return Some(true);
}

// ValidateAndApplyPropertyDescriptor for an EXISTING non-configurable named
// (non-index) own property on an array. The index path above performs this
// check; the named path historically did not, so a redefine of a
// non-configurable `arr.prop` (data or accessor) silently succeeded instead
// of throwing a TypeError.
{
let cur_accessor = super::get_accessor_descriptor(obj as usize, key_name);
let cur_attrs = super::get_property_attrs(obj as usize, key_name);
if cur_attrs.is_some() || cur_accessor.is_some() {
let attrs = cur_attrs.unwrap_or_else(|| PropertyAttrs::new(true, true, true));
if !attrs.configurable() {
let cur_value = if cur_accessor.is_none() {
crate::array::array_named_property_get_by_name(arr, key_name)
.unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED))
} else {
f64::from_bits(crate::value::TAG_UNDEFINED)
};
super::validate_nonconfigurable_redefine(
key_name,
attrs,
cur_accessor,
cur_value,
descriptor_value,
);
}
}
}

// Named (non-index) accessor on an array target: store get/set in the
// side table, exactly like the index path above. Without this, a
// `defineProperty(arr, "prop", {get,set})` silently stored `undefined`
Expand Down
11 changes: 10 additions & 1 deletion crates/perry-runtime/src/object/exotic_expando.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,15 @@ pub(crate) fn exotic_has_own_property(kind: ExoticKind, addr: usize, name: &str)
|| (super::descriptors_in_use() && super::get_accessor_descriptor(addr, name).is_some())
}

/// Default enumerability for an exotic instance's own key when no explicit
/// attribute entry exists. A built-in slot that the spec defines as
/// non-enumerable (Error's `message`/`stack`) stays non-enumerable even after a
/// plain `err.message = x` write (which is a `[[Set]]` — it never changes
/// attributes); a user-added expando (`err.foo = 1`) defaults to enumerable.
pub(crate) fn exotic_default_enumerable(kind: ExoticKind, name: &str) -> bool {
!(kind == ExoticKind::Error && matches!(name, "message" | "stack"))
}

/// Own expando string keys: data props in insertion order, then
/// accessor-only keys. Optionally filtered to enumerable ones.
pub(crate) fn exotic_own_keys(kind: ExoticKind, addr: usize, enumerable_only: bool) -> Vec<String> {
Expand All @@ -320,7 +329,7 @@ pub(crate) fn exotic_own_keys(kind: ExoticKind, addr: usize, enumerable_only: bo
keys.retain(|k| {
super::get_property_attrs(addr, k)
.map(|a| a.enumerable())
.unwrap_or(true)
.unwrap_or_else(|| exotic_default_enumerable(kind, k))
});
}
keys
Expand Down
4 changes: 3 additions & 1 deletion crates/perry-runtime/src/object/object_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,7 +1034,9 @@ pub extern "C" fn js_object_property_is_enumerable(obj_value: f64, key_value: f6
}
let enumerable = super::get_property_attrs(addr, key_name)
.map(|a| a.enumerable())
.unwrap_or(true);
.unwrap_or_else(|| {
super::exotic_expando::exotic_default_enumerable(kind, key_name)
});
return f64::from_bits(if enumerable { TAG_TRUE } else { TAG_FALSE });
}

Expand Down
19 changes: 19 additions & 0 deletions crates/perry-runtime/src/object/object_ops_frozen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,16 @@ pub extern "C" fn js_object_freeze(obj_value: f64) -> f64 {
}
return obj_value;
}
// Arrays store indices densely and named props in a side table —
// neither is in `keys_array`, so handle them explicitly.
if super::mark_all_array_props(
obj, /*drop_writable=*/ true, /*drop_configurable=*/ true,
) {
mark_all_symbol_keys(
obj, /*drop_writable=*/ true, /*drop_configurable=*/ true,
);
return obj_value;
}
// Drop writable + configurable for every existing key.
mark_all_keys(
obj, /*drop_writable=*/ true, false, /*drop_configurable=*/ true,
Expand Down Expand Up @@ -277,6 +287,15 @@ pub extern "C" fn js_object_seal(obj_value: f64) -> f64 {
}
return obj_value;
}
// Arrays: indices + named props live outside `keys_array`.
if super::mark_all_array_props(
obj, /*drop_writable=*/ false, /*drop_configurable=*/ true,
) {
mark_all_symbol_keys(
obj, /*drop_writable=*/ false, /*drop_configurable=*/ true,
);
return obj_value;
}
// Drop configurable for every existing key (but leave writable intact).
mark_all_keys(
obj, /*drop_writable=*/ false, false, /*drop_configurable=*/ true,
Expand Down
17 changes: 17 additions & 0 deletions crates/perry-runtime/src/value/dynamic_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,23 @@ pub unsafe extern "C" fn js_dynamic_object_get_property(

// Handle Error objects specially
if object_type == crate::error::OBJECT_TYPE_ERROR {
// An own expando / accessor property (installed via defineProperty, or a
// reassigned `message`/`stack`) lives in the exotic side tables and wins
// over the builtin slot. The compiled member-get path consults these,
// but this lower-level dynamic getter — used by
// `Object.defineProperties` to read each descriptor off the properties
// bag — historically dropped straight to `undefined` for any key other
// than the five native slots, so an accessor/data expando on an Error
// read as `undefined` (and `defineProperties(obj, errObj)` then threw
// "Property description must be an object: undefined").
if let Some(v) = crate::object::exotic_expando::exotic_get_own_property(
ptr as usize,
crate::object::exotic_expando::ExoticKind::Error,
property_name,
obj_value,
) {
return v;
}
let error_ptr = ptr as *mut crate::error::ErrorHeader;
match property_name {
"message" => {
Expand Down
Loading