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
30 changes: 30 additions & 0 deletions crates/perry-codegen/src/expr/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,36 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&[(I64, &ctor_handle), (I64, &key_raw)],
));
}
// Object statics read as VALUES (`var f = Object.seal`,
// `typeof Object.defineProperties`, `Object.is.length`).
// The receiver name is collapsed to GlobalGet(0), so route by
// property name — but ONLY names unique to `Object` among the
// builtin globals: the Reflect-overlapping ones
// (defineProperty / getOwnPropertyDescriptor / getPrototypeOf /
// setPrototypeOf / isExtensible / preventExtensions) and
// Map-overlapping `groupBy` must keep their current behavior.
// Resolves the reified ctor closure installed by
// `install_builtin_constructor_statics`.
if matches!(
property.as_str(),
"keys"
| "values"
| "entries"
| "fromEntries"
| "assign"
| "create"
| "seal"
| "freeze"
| "isFrozen"
| "isSealed"
| "is"
| "getOwnPropertyNames"
| "getOwnPropertySymbols"
| "getOwnPropertyDescriptors"
| "defineProperties"
) {
return Ok(lower_global_builtin_static_value(ctx, "Object", property));
}
// #3527: `Object.hasOwn` read as a VALUE (not a direct call) —
// e.g. iconv-lite's merge-exports does
// `var hasOwn = typeof Object.hasOwn === "undefined" ? … :
Expand Down
9 changes: 9 additions & 0 deletions crates/perry-runtime/src/array/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,11 @@ pub(crate) unsafe fn array_numeric_raw_f64_get(arr: *mut ArrayHeader, index: u32
if arr.is_null() {
return None;
}
// An index converted to an accessor (or given custom attrs) via
// `Object.defineProperty` must dispatch through the slow path.
if array_object_flags(arr) & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 {
return None;
}
if index >= (*arr).length {
return None;
}
Expand All @@ -972,6 +977,10 @@ pub(crate) unsafe fn array_numeric_raw_f64_set_inbounds(
if arr.is_null() || index >= (*arr).length {
return false;
}
// Accessor setters / non-writable attrs on indices need the slow path.
if array_object_flags(arr) & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 {
return false;
}
let original_bits = value.to_bits();
let value_bits = canonicalize_array_numeric_store_bits(arr, original_bits);
let value = f64::from_bits(value_bits);
Expand Down
58 changes: 58 additions & 0 deletions crates/perry-runtime/src/array/indexing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ pub extern "C" fn js_array_get_f64_unchecked(arr: *const ArrayHeader, index: u32
if arr.is_null() {
return f64::NAN;
}
// Index accessors / custom attrs installed via `Object.defineProperty`
// need the descriptor-aware getter.
if array_object_flags(arr) & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 {
return js_array_get_f64(arr, index);
}
const TAG_UNDEFINED_F64: f64 = f64::from_bits(0x7FFC_0000_0000_0001u64);
unsafe {
let length = (*arr).length;
Expand Down Expand Up @@ -368,6 +373,11 @@ pub extern "C" fn js_array_set_f64_unchecked(arr: *mut ArrayHeader, index: u32,
if array_is_frozen(arr) {
return;
}
// Index accessors / non-writable attrs need the descriptor-aware setter.
if array_object_flags(arr) & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 {
js_array_set_f64_extend(arr, index, value);
return;
}
unsafe {
let length = (*arr).length;
if index >= length {
Expand Down Expand Up @@ -499,6 +509,38 @@ pub extern "C" fn js_array_set_f64_extend(
return arr;
}

// Index properties customized via `Object.defineProperty`: dispatch
// accessor setters and honor non-writable data attributes before the
// dense-element store. Gated on the per-array descriptor flag so the
// common fast path pays one header-flag test.
if flags & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0 {
let key = index.to_string();
if let Some(acc) = crate::object::get_accessor_descriptor(arr as usize, &key) {
if acc.set != 0 {
crate::object::invoke_accessor_setter(
acc.set,
crate::value::js_nanbox_pointer(arr as i64),
value_handle.get_nanbox_f64(),
);
}
return arr;
}
if let Some(attrs) = crate::object::get_property_attrs(arr as usize, &key) {
if !attrs.writable() {
return arr;
}
}
// Extending past `length` requires a writable `length`.
if index >= length {
let len_writable = crate::object::get_property_attrs(arr as usize, "length")
.map(|a| a.writable())
.unwrap_or(true);
if !len_writable {
return arr;
}
}
}

// If index is within bounds, just set it
if index < length {
if is_frozen {
Expand Down Expand Up @@ -656,6 +698,22 @@ pub extern "C" fn js_array_set_string_key(
if !existing && array_is_sealed_or_no_extend(arr) {
return arr;
}
// Named accessor installed via `Object.defineProperty(arr, "prop",
// {get,set})`: dispatch the setter instead of the expando store.
if crate::object::descriptors_in_use() {
if let Some(acc) = crate::object::get_accessor_descriptor(arr as usize, key_str) {
if acc.set != 0 {
unsafe {
crate::object::invoke_accessor_setter(
acc.set,
crate::value::js_nanbox_pointer(arr as i64),
value,
);
}
}
return arr;
}
}
if let Some(attrs) = crate::object::get_property_attrs(arr as usize, key_str) {
if !attrs.writable() {
return arr;
Expand Down
11 changes: 11 additions & 0 deletions crates/perry-runtime/src/array/push_pop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,17 @@ pub extern "C" fn js_array_set_length(arr: *mut ArrayHeader, new_length: f64) {
if flags & (crate::gc::OBJ_FLAG_SEALED | crate::gc::OBJ_FLAG_NO_EXTEND) != 0 && n != cur {
return;
}
// `defineProperty(arr, "length", {writable:false})` records the flag
// in the attrs side table; an ordinary `arr.length = n` write must
// then no-op (strict-mode throw is handled by the caller's PutValue).
if n != cur
&& flags & crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS != 0
&& crate::object::get_property_attrs(arr as usize, "length")
.map(|a| !a.writable())
.unwrap_or(false)
{
return;
}
if n < cur {
// Truncate: clear elements at indices [n..cur) to TAG_HOLE so
// any code that resurrects the slot via `arr[i]` reads `undefined`,
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-runtime/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ pub fn alloc_date_cell(ts: f64) -> f64 {
crate::gc::GC_TYPE_DATE_CELL,
) as *mut DateCell;
(*ptr).ts = ts;
// A previous (collected) Date at this address may have left expando
// properties in the side table; a fresh Date must start clean.
crate::object::exotic_expando::expando_clear_on_alloc(ptr as usize);
f64::from_bits(crate::value::JSValue::pointer(ptr as *const u8).bits())
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ pub fn gc_init() {
gc_register_mutable_root_scanner(async_hooks_mutable_root_scanner);
gc_register_mutable_root_scanner(shape_cache_mutable_root_scanner);
gc_register_mutable_root_scanner(crate::regex::scan_last_exec_groups_root_mut);
gc_register_mutable_root_scanner(crate::object::scan_exotic_expando_roots_mut);
gc_register_mutable_root_scanner(crate::array::scan_template_raw_roots_mut);
gc_register_mutable_root_scanner(crate::perf_hooks::scan_perf_entries_roots_mut);
gc_register_mutable_root_scanner(crate::v8::scan_v8_promise_hook_roots_mut);
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-runtime/src/gc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,14 @@ pub const OBJ_FLAG_NO_EXTEND: u16 = 0x04;
// (`GC_COPY_SURVIVAL_AGE_MASK = 0x0038`) and bits 14..15 the layout state,
// so 0x08 would be clobbered on every minor GC. Bits 6..13 are free.
pub const OBJ_FLAG_NULL_PROTO: u16 = 0x40;
// Array carries per-index property descriptors (accessors or custom attrs
// installed via `Object.defineProperty`, or a non-writable `length`). The
// raw-f64 numeric fast paths must decline and route through the
// descriptor-aware element get/set. Bit 10 — bits 7/8/9 are taken by
// `GC_ARRAY_RAW_F64_LAYOUT` (0x80), `OBJ_FLAG_TYPED_ARRAY_PROTO` (0x100),
// and `GC_ARRAY_ARGUMENTS_OBJECT` (0x200). Only meaningful for
// `GC_TYPE_ARRAY`.
pub const OBJ_FLAG_ARRAY_DESCRIPTORS: u16 = 0x400;
// #2145: this object is a per-kind `<TypedArrayCtor>.prototype` whose
// `[[Prototype]]` is the shared `%TypedArray%.prototype` intrinsic.
// `Object.getPrototypeOf(Int8Array.prototype)` returns the cached
Expand Down
15 changes: 15 additions & 0 deletions crates/perry-runtime/src/node_submodules/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,21 @@ pub fn error_user_prop(error_ptr: usize, key: &str) -> Option<f64> {
})
}

/// Remove a user-assigned own property from an Error object. Returns true
/// when the property existed (used by `delete err.prop` and data↔accessor
/// descriptor conversions).
pub fn remove_error_user_prop(error_ptr: usize, key: &str) -> bool {
if error_ptr == 0 {
return false;
}
ERROR_USER_PROPS.with(|m| {
m.borrow_mut()
.get_mut(&error_ptr)
.map(|props| props.remove(key).is_some())
.unwrap_or(false)
})
}

/// Return user-assigned own properties on an Error object as materialized JS
/// values so util.inspect/console formatting can show them.
pub fn error_user_props(error_ptr: usize) -> Vec<(String, f64)> {
Expand Down
119 changes: 109 additions & 10 deletions crates/perry-runtime/src/object/array_object_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ pub(crate) unsafe fn array_set_length_from_descriptor(
if desc_ptr.is_null() {
return true;
}
// A customized `length` (e.g. writable:false) gates the raw numeric
// fast paths — see OBJ_FLAG_ARRAY_DESCRIPTORS in define_array_property.
// Set here too so the `Reflect.defineProperty` entry point is covered.
{
let gc = gc_header_for(obj);
(*gc)._reserved |= crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS;
}
let arr = obj as *mut crate::array::ArrayHeader;

let read_present = |name: &[u8]| -> bool {
Expand Down Expand Up @@ -238,6 +245,15 @@ pub(crate) unsafe fn define_array_property(
return Some(true);
};

// Any explicit per-index/named/length descriptor makes the raw numeric
// fast paths ineligible for this array — they can't see accessors or
// attribute overrides, so they must decline to the descriptor-aware
// element get/set (OBJ_FLAG_ARRAY_DESCRIPTORS gates them).
{
let gc = gc_header_for(obj);
(*gc)._reserved |= crate::gc::OBJ_FLAG_ARRAY_DESCRIPTORS;
}

if key_name == "length" {
return Some(array_set_length_from_descriptor(obj, descriptor_value));
}
Expand Down Expand Up @@ -324,6 +340,16 @@ pub(crate) unsafe fn define_array_property(
} else {
prior.map(|a| a.set).unwrap_or(0)
};
// Materialize BEFORE storing the accessor — the extend helper
// dispatches accessor setters, so installing the accessor first
// would turn this internal materialization into a setter call.
if !exists {
crate::array::js_array_set_f64_extend(
arr,
index,
f64::from_bits(crate::value::TAG_UNDEFINED),
);
}
set_accessor_descriptor(
obj as usize,
key_name.to_string(),
Expand All @@ -332,18 +358,15 @@ pub(crate) unsafe fn define_array_property(
set: set_bits,
},
);
// Ensure the index counts as an own key for reflection.
if !exists {
crate::array::js_array_set_f64_extend(
arr,
index,
f64::from_bits(crate::value::TAG_UNDEFINED),
);
}
// Retain existing attrs the descriptor omits when redefining; new
// accessor defaults to non-enumerable / non-configurable.
// accessor defaults to non-enumerable / non-configurable. An
// existing dense element with no side-table entry has default
// all-true attributes (so data→accessor keeps enumerable:true).
let cur = if exists {
super::get_property_attrs(obj as usize, key_name)
Some(
super::get_property_attrs(obj as usize, key_name)
.unwrap_or_else(|| PropertyAttrs::new(true, true, true)),
)
} else {
None
};
Expand Down Expand Up @@ -400,11 +423,35 @@ pub(crate) unsafe fn define_array_property(
}
}

// A GENERIC descriptor (attrs only, no value/writable/get/set) on an
// existing ACCESSOR property just updates the attributes — it must
// NOT convert the accessor back to data (spec ValidateAndApply step:
// IsGenericDescriptor → no [[Get]]/[[Set]]/[[Value]] changes).
if !has_value
&& !super::desc_has_field(descriptor_value, b"writable")
&& super::get_accessor_descriptor(obj as usize, key_name).is_some()
{
let cur = cur_attrs.unwrap_or(PropertyAttrs::new(false, false, false));
let enumerable = read_bool(b"enumerable").unwrap_or_else(|| cur.enumerable());
let configurable = read_bool(b"configurable").unwrap_or_else(|| cur.configurable());
set_property_attrs(
obj as usize,
key_name.to_string(),
PropertyAttrs::new(false, enumerable, configurable),
);
return Some(true);
}

// Redefining an index that was previously an accessor back to a data
// property: drop the stale accessor entry.
ACCESSOR_DESCRIPTORS.with(|m| {
m.borrow_mut().remove(&(obj as usize, key_name.to_string()));
});
// [[DefineOwnProperty]] writes the slot directly — clear any stale
// attrs first so the extend helper's [[Set]]-side writability check
// (added for ordinary `arr[i] = v` writes) can't reject this store.
// The final attributes are recorded below after the write.
super::clear_property_attrs(obj as usize, key_name);

if has_value {
crate::array::js_array_set_f64_extend(arr, index, value);
Expand Down Expand Up @@ -438,6 +485,58 @@ pub(crate) unsafe fn define_array_property(
return Some(true);
}

// 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`
// as a data property and dropped the accessors.
{
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 {
let get_key = crate::string::js_string_from_bytes(b"get".as_ptr(), 3);
let set_key = crate::string::js_string_from_bytes(b"set".as_ptr(), 3);
let get_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, get_key);
let set_field = js_object_get_field_by_name(desc_ptr as *const ObjectHeader, set_key);
let recv = crate::value::js_nanbox_pointer(obj as i64);
let prior = super::get_accessor_descriptor(obj as usize, key_name);
let get_bits = if desc_has_get {
if get_field.is_undefined() {
0
} else {
crate::closure::clone_closure_rebind_this(get_field.bits(), recv)
}
} else {
prior.map(|a| a.get).unwrap_or(0)
};
let set_bits = if desc_has_set {
if set_field.is_undefined() {
0
} else {
crate::closure::clone_closure_rebind_this(set_field.bits(), recv)
}
} else {
prior.map(|a| a.set).unwrap_or(0)
};
set_accessor_descriptor(
obj as usize,
key_name.to_string(),
AccessorDescriptor {
get: get_bits,
set: set_bits,
},
);
let enumerable = read_bool(b"enumerable").unwrap_or(false);
let configurable = read_bool(b"configurable").unwrap_or(false);
set_property_attrs(
obj as usize,
key_name.to_string(),
PropertyAttrs::new(false, enumerable, configurable),
);
let _ = obj_value;
return Some(true);
}
}

crate::array::array_named_property_set(arr, key_str, value);

let writable = read_bool(b"writable").unwrap_or(false);
Expand Down
Loading