Skip to content
Closed
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
38 changes: 38 additions & 0 deletions benchmarks/string_receiver_boxing.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// #9810: run with Node or compile with Perry. Arguments: receiver length, calls.
// .cjs keeps the receiver-binding controls in sloppy mode on both engines.
const n = Number(process.argv[3] || "20000");
const receiver = "x".repeat(Number(process.argv[2] || "200"));
function unused(value) { return value + 1; }
function observed(value) { return this.length + value; }
function capture() { return this; }
function strict(value) { "use strict"; return value + 1; }
String.prototype.bench9810 = unused;
const funcs = { unused, observed, strict, capture };
let sum = 0;
let start = Date.now();
for (let i = 0; i < n; i++) sum += receiver.bench9810(i);
console.log("method", receiver.length, Date.now() - start, sum);
sum = 0; start = Date.now();
for (let i = 0; i < n; i++) sum += funcs.unused.call(receiver, i);
console.log("call", receiver.length, Date.now() - start, sum);
sum = 0; start = Date.now();
for (let i = 0; i < n; i++) sum += funcs.observed.call(receiver, i);
console.log("call-this", receiver.length, Date.now() - start, sum);
sum = 0; start = Date.now();
for (let i = 0; i < n; i++) sum += funcs.unused.apply(receiver, [i]);
console.log("apply", receiver.length, Date.now() - start, sum);
sum = 0; start = Date.now();
for (let i = 0; i < n; i++) sum += funcs.strict.call(receiver, i);
console.log("strict", receiver.length, Date.now() - start, sum);
sum = 0; start = Date.now();
for (let i = 0; i < n; i++) sum += Object(receiver).length;
console.log("Object", receiver.length, Date.now() - start, sum);

const first = funcs.capture.call(receiver);
const second = funcs.capture.apply(receiver, []);
if (typeof first !== "object" || first === second || first.length !== receiver.length) {
throw new Error("sloppy calls must create distinct String wrappers");
}
first.extra = 7;
if (second.extra !== undefined) throw new Error("receiver state leaked");
delete String.prototype.bench9810;
1 change: 1 addition & 0 deletions changelog.d/9814-virtual-string-indices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix string-wrapper construction scaling with receiver length in non-strict method calls, `Function.prototype.call`/`apply`, and `Object(string)`. Character indices are now virtual properties, preserving UTF-16 indexing, reflection, and readonly descriptors without allocating a property and descriptor for every character (#9810).
73 changes: 27 additions & 46 deletions crates/perry-runtime/src/builtins/formatting/boxed_primitives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@ pub(super) unsafe fn boxed_primitive_base_for_object(
/// String, otherwise `None`.
///
/// The count is in UTF-16 code units, NOT Unicode scalar values: the index
/// properties are installed over `0..js_string_length` (`utf16_len`) by
/// `install_string_wrapper_indices`, so a non-BMP char (e.g. an emoji, two
/// UTF-16 units) occupies two indices. Counting `.chars()` would under-count
/// properties span `0..js_string_length` (`utf16_len`), so a non-BMP char
/// (e.g. an emoji, two UTF-16 units) occupies two indices. Counting `.chars()` would under-count
/// and leak a trailing index (e.g. `new String("a😀b")` → `{ 3: 'b' }`).
pub(super) unsafe fn boxed_string_char_index_count(
obj_ptr: *const crate::object::ObjectHeader,
Expand Down Expand Up @@ -139,6 +138,8 @@ fn attach_boxed_primitive_prototype(obj: *mut crate::object::ObjectHeader, class
if obj.is_null() {
return;
}
let scope = crate::gc::RuntimeHandleScope::new();
let obj_h = scope.root_raw_mut_ptr(obj);
let Some(name) = boxed_constructor_name(class_id) else {
return;
};
Expand All @@ -151,7 +152,12 @@ fn attach_boxed_primitive_prototype(obj: *mut crate::object::ObjectHeader, class
let proto = crate::closure::closure_get_dynamic_prop(ctor_ptr, "prototype");
let proto_value = crate::value::JSValue::from_bits(proto.to_bits());
if proto_value.is_pointer() {
crate::object::prototype_chain::object_set_static_prototype(obj as usize, proto.to_bits());
obj_h.with_mut_ptr(|obj: *mut crate::object::ObjectHeader| {
crate::object::prototype_chain::object_set_static_prototype(
obj as usize,
proto.to_bits(),
)
});
}
}

Expand All @@ -162,46 +168,18 @@ fn install_string_wrapper_length(
if obj.is_null() || string_ptr.is_null() {
return;
}
let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6);
let len = crate::string::js_string_length(string_ptr) as f64;
crate::object::js_object_set_field_by_name(obj, key, len);
crate::object::set_builtin_property_attrs(
obj as usize,
"length".to_string(),
crate::object::PropertyAttrs::new(false, false, false),
);
}

/// String exotic objects (ECMA-262 §10.4.3) expose each UTF-16 code unit as an
/// integer-indexed own property `"0".."len-1"` with the descriptor
/// `{ value: <char>, writable: false, enumerable: true, configurable: false }`.
/// `new String("abc")` therefore reports `getOwnPropertyDescriptor(s, "0")`,
/// `s.hasOwnProperty("0")`, and `Object.keys(s)`/enumeration over the indices.
/// Installed eagerly at construction (typical `new String` receivers are
/// short); the wrapper's `length` is installed separately and stays last.
fn install_string_wrapper_indices(
obj: *mut crate::object::ObjectHeader,
string_ptr: *const crate::string::StringHeader,
) {
if obj.is_null() || string_ptr.is_null() {
return;
}
let len = crate::string::js_string_length(string_ptr);
for i in 0..len {
let ch = crate::string::js_string_char_at(string_ptr, i as i32);
if ch.is_null() {
continue;
}
let name = i.to_string();
let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32);
let ch_value = f64::from_bits(crate::value::JSValue::string_ptr(ch).bits());
crate::object::js_object_set_field_by_name(obj, key, ch_value);
let scope = crate::gc::RuntimeHandleScope::new();
let obj_h = scope.root_raw_mut_ptr(obj);
let key = crate::string::js_string_from_bytes(b"length".as_ptr(), 6);
obj_h.with_mut_ptr(|obj| crate::object::js_object_set_field_by_name(obj, key, len));
obj_h.with_mut_ptr(|obj: *mut crate::object::ObjectHeader| {
crate::object::set_builtin_property_attrs(
obj as usize,
name,
crate::object::PropertyAttrs::new(false, true, false),
);
}
"length".to_string(),
crate::object::PropertyAttrs::new(false, false, false),
)
});
}

pub fn scan_boxed_primitive_payload_roots_mut(visitor: &mut crate::gc::RuntimeRootVisitor<'_>) {
Expand Down Expand Up @@ -327,8 +305,8 @@ pub extern "C" fn js_boxed_string_new(value: f64, has_arg: i32) -> f64 {
// empty-string case and `js_string_coerce` otherwise, the latter running a
// user `toString`/`valueOf` for a POINTER_TAG value — so either can collect
// and EVACUATE while `obj` sits in a raw Rust local. Every use below
// (`register_boxed_primitive_payload`, the two `install_string_wrapper_*`
// calls, `attach_boxed_primitive_prototype`, and the returned NaN-box)
// (`register_boxed_primitive_payload`, the `install_string_wrapper_length`
// call, `attach_boxed_primitive_prototype`, and the returned NaN-box)
// dereferences or keys on it.
let scope = crate::gc::RuntimeHandleScope::new();
let obj_handle = scope.root_raw_mut_ptr(obj);
Expand All @@ -348,10 +326,13 @@ pub extern "C" fn js_boxed_string_new(value: f64, has_arg: i32) -> f64 {
});
let boxed = f64::from_bits(crate::value::JSValue::string_ptr(ptr).bits());
register_boxed_primitive_payload(obj, boxed);
install_string_wrapper_indices(obj, ptr);
// #9810: character indices are virtual String exotic properties. Index
// keys and values are produced on demand; boxing never walks the string.
install_string_wrapper_length(obj, ptr);
attach_boxed_primitive_prototype(obj, CLASS_ID_BOXED_STRING);
crate::value::js_nanbox_pointer(obj as i64)
obj_handle.with_mut_ptr(|obj| attach_boxed_primitive_prototype(obj, CLASS_ID_BOXED_STRING));
obj_handle.with_mut_ptr(|obj: *mut crate::object::ObjectHeader| {
crate::value::js_nanbox_pointer(obj as i64)
})
}

#[no_mangle]
Expand Down
23 changes: 22 additions & 1 deletion crates/perry-runtime/src/object/alloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,16 @@ pub unsafe extern "C" fn js_object_clone_with_extra(
}

let src_ptr = src_raw as *const ObjectHeader;
if super::string_wrapper::length(src_raw).is_some() {
let scope = crate::gc::RuntimeHandleScope::new();
let src_h = scope.root_nanbox_f64(src_f64);
let target = js_object_alloc(0, 0);
let copied = js_object_assign_one(
crate::value::js_nanbox_pointer(target as i64),
src_h.get_nanbox_f64(),
);
return crate::value::js_nanbox_get_pointer(copied) as *mut ObjectHeader;
}
let src_field_count = crate::object::object_live_slot_count(src_ptr);

// Physical slot capacity: src_field_count + extra_count, but at least max(fc, 8) to match
Expand Down Expand Up @@ -1037,6 +1047,10 @@ pub unsafe extern "C" fn js_object_copy_own_fields(dst_i64: i64, src_f64: f64) {
_ => return,
}
let src = src_raw as *const ObjectHeader;
if super::string_wrapper::length(src_raw).is_some() {
js_object_assign_one(crate::value::js_nanbox_pointer(dst as i64), src_f64);
return;
}

// #6667: a native-module namespace (`{ ...require("crypto") }`) stores no
// real fields — only the internal `__module__` sentinel — so the raw
Expand Down Expand Up @@ -1811,7 +1825,14 @@ pub unsafe extern "C" fn js_object_assign_one(target_f64: f64, source_f64: f64)
);
}
} else if source_obj_type == crate::gc::GC_TYPE_OBJECT {
let src_keys = crate::object::object_keys_array(src);
let src_keys = if super::string_wrapper::length(src as usize).is_some() {
let names = src_h.with_const_ptr(|src: *const ObjectHeader| {
js_object_get_own_property_names(crate::value::js_nanbox_pointer(src as i64))
});
crate::value::js_nanbox_get_pointer(names) as *mut crate::ArrayHeader
} else {
crate::object::object_keys_array(src)
};
let keys_h = scope.root_raw_mut_ptr(src_keys);
if !src_keys.is_null() && (src_keys as usize) >= 0x10000 {
// Cap the key count at the keys array's capacity: a malformed keys
Expand Down
6 changes: 6 additions & 0 deletions crates/perry-runtime/src/object/delete_rest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ pub extern "C" fn js_object_delete_field(
}
return 1;
}
if super::string_wrapper::has_index_key(obj as usize, key) {
return 0;
}
// Once #9064's stable marker is installed, this receiver has already
// been proved to be an ordinary non-prototype object with no
// descriptors. Avoid decoding the same dynamic key and scanning the
Expand Down Expand Up @@ -1071,6 +1074,9 @@ pub extern "C" fn js_object_rest(
return js_object_alloc(0, 0);
}
unsafe {
if super::string_wrapper::length(src as usize).is_some() {
return super::string_wrapper::rest(src, exclude_keys);
}
let keys = crate::object::object_keys_array(src);
if keys.is_null() {
return js_object_alloc(0, 0);
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-runtime/src/object/descriptor_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,9 @@ pub(crate) fn note_descriptor_target(obj: usize) {
/// Look up the property descriptor for (obj, key). Returns None if no entry exists,
/// in which case the JS default `{ writable: true, enumerable: true, configurable: true }` applies.
pub(crate) fn get_property_attrs(obj: usize, key: &str) -> Option<PropertyAttrs> {
if super::string_wrapper::has_index(obj, key) {
return Some(PropertyAttrs::new(false, true, false));
}
// #6759 Phase C2: the meta-record summary proves most misses without
// the `String` build + table probe (and shields a fresh object at a
// recycled address from a dead owner's not-yet-pruned entries).
Expand Down
29 changes: 17 additions & 12 deletions crates/perry-runtime/src/object/descriptors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,18 @@ unsafe fn boxed_string_own_property_names(obj_value: f64, str_value: f64) -> f64
}

sort_property_names_ecma(&mut names);
let scope = crate::gc::RuntimeHandleScope::new();
let result = crate::array::js_array_alloc(names.len() as u32);
let result_h = scope.root_raw_mut_ptr(result);
for name in names {
let str_ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32);
crate::array::js_array_push(result, JSValue::string_ptr(str_ptr));
result_h.with_mut_ptr(|result| {
crate::array::js_array_push(result, JSValue::string_ptr(str_ptr))
});
}
f64::from_bits((result as u64) | 0x7FFD_0000_0000_0000)
result_h.with_mut_ptr(|result: *mut crate::ArrayHeader| {
f64::from_bits(JSValue::array_ptr(result).bits())
})
}

/// Object.getOwnPropertyDescriptor(obj, key) — returns a data descriptor
Expand Down Expand Up @@ -1214,16 +1220,15 @@ unsafe fn string_primitive_descriptor(str_value: f64, key_value: f64) -> f64 {

if let Some(index) = super::canonical_array_index(name) {
if index < utf16_len {
// Materialize the single UTF-16 unit at `index` as a 1-char string.
let bytes = std::slice::from_raw_parts(sptr, sblen as usize);
let s = std::str::from_utf8(bytes).unwrap_or("");
if let Some(ch) = s.chars().nth(index as usize) {
let mut buf = [0u8; 4];
let cs = ch.encode_utf8(&mut buf);
let cstr = crate::string::js_string_from_bytes(cs.as_ptr(), cs.len() as u32);
let char_val = f64::from_bits(JSValue::string_ptr(cstr).bits());
return build_data_descriptor(char_val, false, true, false);
}
// String exotic indices are UTF-16 code units, including lone
// surrogate halves. Use the same read path as s[index]; `.chars()`
// counts Unicode scalars and returned the wrong descriptors.
let string = crate::value::js_get_string_pointer_unified(f64::from_bits(
str_handle.get_heap_word_u64(),
)) as *const crate::StringHeader;
let cstr = crate::string::js_string_char_at(string, index as i32);
let char_val = f64::from_bits(JSValue::string_ptr(cstr).bits());
return build_data_descriptor(char_val, false, true, false);
}
}
f64::from_bits(crate::value::TAG_UNDEFINED)
Expand Down
60 changes: 18 additions & 42 deletions crates/perry-runtime/src/object/field_get_set/enumeration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,48 +151,6 @@ pub extern "C" fn js_object_keys_value(value: f64) -> *mut ArrayHeader {
}
return arr;
}
if crate::builtins::boxed_primitive_to_string_tag(value) == Some("String") {
if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(value) {
let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN];
let len = match crate::string::str_bytes_from_jsvalue(payload, &mut scratch) {
Some((ptr, blen)) if !ptr.is_null() => crate::string::compute_utf16_len(ptr, blen),
_ => 0,
};
let arr = crate::array::js_array_alloc(len.max(1));
for i in 0..len {
let s = i.to_string();
let k = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32);
crate::array::js_array_push(arr, JSValue::string_ptr(k));
}
if jv.is_pointer() {
let ptr = jv.as_pointer::<ObjectHeader>();
let own = js_object_keys(ptr);
let own_len = crate::array::js_array_length(own);
for i in 0..own_len {
let key_val = crate::array::js_array_get(own, i);
// The wrapper's character indices are installed as REAL
// own fields at construction (install_string_wrapper_
// indices), so they come back from `js_object_keys` too —
// skip them here or `Object.keys(Object("abc"))` lists
// every index twice. Only canonical indices below the
// string length are virtual; expando keys pass through.
let key_ptr =
(key_val.bits() & crate::value::POINTER_MASK) as *const crate::StringHeader;
if let Some(name) =
unsafe { super::super::has_own_helpers::str_from_string_header(key_ptr) }
{
if let Ok(idx) = name.parse::<u32>() {
if idx.to_string() == name && (idx as usize) < len as usize {
continue;
}
}
}
crate::array::js_array_push_f64(arr, f64::from_bits(key_val.bits()));
}
}
return arr;
}
}
if let Some(addr) = crate::typedarray_props::typed_array_addr_from_value(value) {
return unsafe {
crate::typedarray_props::typed_array_own_property_names(
Expand Down Expand Up @@ -1249,6 +1207,12 @@ fn js_object_keys_shape(obj: *const ObjectHeader) -> *mut ArrayHeader {
}
}
unsafe {
if let Some(result) = super::super::string_wrapper::enumerate(
obj,
super::super::string_wrapper::Enumeration::Keys,
) {
return result;
}
if (*obj).class_id == NATIVE_MODULE_CLASS_ID {
// Relocated to native_module.rs::vt_own_keys_array so the
// module key tables are reachable only through the vtable
Expand Down Expand Up @@ -1547,6 +1511,12 @@ fn js_object_values_shape(obj: *const ObjectHeader) -> *mut ArrayHeader {
return crate::array::js_array_alloc(0);
}
unsafe {
if let Some(result) = super::super::string_wrapper::enumerate(
obj,
super::super::string_wrapper::Enumeration::Values,
) {
return result;
}
if (*obj).class_id == NATIVE_MODULE_CLASS_ID {
if let Some(result) = native_module_enum(obj, MapSetEnum::Values) {
return result;
Expand Down Expand Up @@ -1758,6 +1728,12 @@ fn js_object_entries_shape(obj: *const ObjectHeader) -> *mut ArrayHeader {
return crate::array::js_array_alloc(0);
}
unsafe {
if let Some(result) = super::super::string_wrapper::enumerate(
obj,
super::super::string_wrapper::Enumeration::Entries,
) {
return result;
}
if (*obj).class_id == NATIVE_MODULE_CLASS_ID {
if let Some(result) = native_module_enum(obj, MapSetEnum::Entries) {
return result;
Expand Down
5 changes: 5 additions & 0 deletions crates/perry-runtime/src/object/field_set_by_name/tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ pub(crate) fn set_field_by_name_object_tail(
}
}

if super::super::string_wrapper::has_index_key(obj as usize, key) {
let name = key_to_str_for_diag(key);
crate::error::throw_immutable_write(0, &name);
}

// Resolve the interned key EARLY (hoisted from below the interception
// vet): the store-plan cache and the shape-transition cache both key
// on interned pointer identity. If the key is already interned
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/object/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pub(crate) mod has_own_helpers;
mod instanceof;
mod live_slots;
mod null_stub;
mod string_wrapper;
pub(crate) use live_slots::set_object_live_slot_count;
pub use live_slots::{
js_object_live_slot_count, object_live_slot_count, perry_object_header_abi_revision,
Expand Down
Loading
Loading