From e395d657d7587c2a0f119b3262b7dbbcc42ff007 Mon Sep 17 00:00:00 2001 From: jdalton Date: Tue, 13 Oct 2026 10:26:34 -0400 Subject: [PATCH] Fix WTF-8 class key forwarding --- changelog.d/12471-wtf8-class-keys.md | 1 + .../src/closure/dynamic_props.rs | 13 +- crates/perry-runtime/src/closure/mod.rs | 6 +- crates/perry-runtime/src/closure/props.rs | 140 ++-- .../perry-runtime/src/object/accessor_pair.rs | 4 + .../perry-runtime/src/object/class_image.rs | 15 +- .../src/object/class_registry.rs | 65 +- .../object/class_registry/decl_accessors.rs | 77 +- .../class_registry/decl_prototype_birth.rs | 24 +- .../src/object/class_registry/declarations.rs | 33 +- .../class_registry/declarations_tests.rs | 55 +- .../object/class_registry/parent_static.rs | 113 +-- .../parent_static/private_and_dynamic.rs | 42 +- .../parent_static/static_accessor_call.rs | 57 +- .../class_registry/prototype_methods.rs | 7 + .../src/object/class_registry/registration.rs | 128 ++-- .../src/object/class_registry/state.rs | 156 ++-- .../perry-runtime/src/object/class_value.rs | 691 ++++-------------- .../src/object/class_value/tests.rs | 472 ++++++++++++ .../src/object/descriptor_state.rs | 108 ++- .../perry-runtime/src/object/descriptors.rs | 205 ++++-- .../field_get_set/class_object_props.rs | 73 +- .../field_get_set/class_object_template.rs | 60 +- .../object/field_get_set/get_field_by_name.rs | 196 ++++- .../src/object/field_set_by_name.rs | 64 +- .../src/object/field_set_by_name/tail.rs | 99 ++- crates/perry-runtime/src/object/mod.rs | 5 +- .../src/object/native_call_method.rs | 74 ++ .../invalid_utf8_method_tests.rs | 108 +++ .../object/native_module/class_method_bind.rs | 44 +- .../native_module/class_method_values.rs | 71 +- .../object/native_module/class_ref_values.rs | 121 ++- scripts/string_payload_access_baseline.txt | 2 +- scripts/string_validator_allowlist.txt | 3 +- .../test_issue_12202_wtf8_class_keys.ts | 170 +++++ 35 files changed, 2289 insertions(+), 1213 deletions(-) create mode 100644 changelog.d/12471-wtf8-class-keys.md create mode 100644 crates/perry-runtime/src/object/class_value/tests.rs create mode 100644 crates/perry-runtime/src/object/native_call_method/invalid_utf8_method_tests.rs create mode 100644 test-files/test_issue_12202_wtf8_class_keys.ts diff --git a/changelog.d/12471-wtf8-class-keys.md b/changelog.d/12471-wtf8-class-keys.md new file mode 100644 index 0000000000..bbdec9125f --- /dev/null +++ b/changelog.d/12471-wtf8-class-keys.md @@ -0,0 +1 @@ +Computed class member names now retain their exact UTF-16 identity, including lone surrogates, through registration, lookup, reflection, invocation, and instance accessor writes. Symbol-key behavior and the runtime ABI are unchanged. diff --git a/crates/perry-runtime/src/closure/dynamic_props.rs b/crates/perry-runtime/src/closure/dynamic_props.rs index 02ae39f845..b675960991 100644 --- a/crates/perry-runtime/src/closure/dynamic_props.rs +++ b/crates/perry-runtime/src/closure/dynamic_props.rs @@ -558,14 +558,23 @@ pub(crate) fn closure_define_data_with_attrs( prop: &str, value: f64, attrs: crate::object::PropertyAttrs, +) { + closure_define_data_with_attrs_bytes(ptr, prop.as_bytes(), value, attrs) +} + +pub(crate) fn closure_define_data_with_attrs_bytes( + ptr: usize, + prop: &[u8], + value: f64, + attrs: crate::object::PropertyAttrs, ) { if !is_closure_ptr(ptr) { return; } crate::object::prop_plan::prop_plan_epoch_bump_for_owner(ptr); unsafe { - super::props::bag_define_data_with_attrs(ptr, prop, value, attrs.bits); - super::props::state_clear_deleted(ptr, prop); + super::props::bag_define_data_with_attrs_bytes(ptr, prop, value, attrs.bits); + super::props::state_clear_deleted_bytes(ptr, prop); } super::shape::refresh_closure_shape(ptr); } diff --git a/crates/perry-runtime/src/closure/mod.rs b/crates/perry-runtime/src/closure/mod.rs index e9b95f3866..3a74da8162 100644 --- a/crates/perry-runtime/src/closure/mod.rs +++ b/crates/perry-runtime/src/closure/mod.rs @@ -91,7 +91,8 @@ pub(crate) use dynamic_props::{ function_prototype_inherited_get, }; pub(crate) use dynamic_props::{ - closure_define_data_with_attrs, closure_define_first_props_with_attrs, + closure_define_data_with_attrs, closure_define_data_with_attrs_bytes, + closure_define_first_props_with_attrs, }; pub use dynamic_props::{ closure_define_dynamic_prop, closure_define_first_props, closure_delete_own_dynamic_prop, @@ -100,8 +101,7 @@ pub use dynamic_props::{ closure_set_dynamic_prop, closure_set_static_prototype, closure_static_prototype, is_closure_bits, is_closure_ptr, }; -#[cfg(feature = "wasm-host")] -pub(crate) use wasm_funcref::register_wasm_funcref_external; +pub(crate) use props::bag_own_key_names_bytes; // v8_stubs re-exports the AOT stubs + non-macOS Rust V8-interop stubs. #[cfg(not(target_os = "macos"))] diff --git a/crates/perry-runtime/src/closure/props.rs b/crates/perry-runtime/src/closure/props.rs index 792bab6956..208c5d961b 100644 --- a/crates/perry-runtime/src/closure/props.rs +++ b/crates/perry-runtime/src/closure/props.rs @@ -226,6 +226,10 @@ pub(crate) unsafe fn bag_get(ptr: usize, key: &[u8]) -> Option { } unsafe fn object_own_set(obj: *mut ObjectHeader, key: &str, value: f64) { + object_own_set_bytes(obj, key.as_bytes(), value); +} + +unsafe fn object_own_set_bytes(obj: *mut ObjectHeader, key: &[u8], value: f64) { let key = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); // These are ordinary properties of the bag, even when their spelling // matches an internal private storage key. Do not reinterpret the string @@ -239,18 +243,20 @@ unsafe fn object_own_set(obj: *mut ObjectHeader, key: &str, value: f64) { /// # Safety /// `ptr` is a proven, live closure cell. pub(crate) unsafe fn bag_set(ptr: usize, key: &str, value: f64) { + bag_set_bytes(ptr, key.as_bytes(), value); +} + +pub(crate) unsafe fn bag_set_bytes(ptr: usize, key: &[u8], value: f64) { let _no_move = crate::gc::GcSuppressScope::new(); - let declared = crate::object::class_value::holds_declared_static_method(ptr, key); + let declared = crate::object::class_value::holds_declared_static_method_bytes(ptr, key); let bag = bag_ensure(ptr); - object_own_set(bag, key, value); - declared_value_replaced(ptr, key, declared); + object_own_set_bytes(bag, key, value); + declared_value_replaced_bytes(ptr, key, declared); } -/// After a write to own `key`: when it held the declaration (`declared`) and -/// no longer does, the shape transitions. -unsafe fn declared_value_replaced(ptr: usize, key: &str, declared: Option) { +unsafe fn declared_value_replaced_bytes(ptr: usize, key: &[u8], declared: Option) { let Some(old) = declared else { return }; - if bag_get(ptr, key.as_bytes()).is_some_and(|v| v.to_bits() == old.to_bits()) { + if bag_get(ptr, key).is_some_and(|v| v.to_bits() == old.to_bits()) { return; } crate::object::shapes::transition_object_shape_semantics(bag_of(ptr)); @@ -265,25 +271,30 @@ unsafe fn declared_value_replaced(ptr: usize, key: &str, declared: Option) /// # Safety /// `ptr` is a proven, live closure cell. pub(crate) unsafe fn bag_define_value(ptr: usize, key: &str, value: f64) { + bag_define_value_bytes(ptr, key.as_bytes(), value); +} + +pub(crate) unsafe fn bag_define_value_bytes(ptr: usize, key: &[u8], value: f64) { let _no_move = crate::gc::GcSuppressScope::new(); - let declared = crate::object::class_value::holds_declared_static_method(ptr, key); - if bag_of(ptr).is_null() && bag_born_with_attrs(ptr, &[(key, value)], &[]) { - declared_value_replaced(ptr, key, declared); - return; + if let Ok(key) = std::str::from_utf8(key) { + if bag_of(ptr).is_null() && bag_born_with_attrs(ptr, &[(key, value)], &[]) { + return; + } } + let declared = crate::object::class_value::holds_declared_static_method_bytes(ptr, key); let bag = bag_ensure(ptr); - if !bag_has_own(ptr, key.as_bytes()) { + if !bag_has_own(ptr, key) { // A NEW property — including one `delete` removed earlier: it is // appended as any new key is, so it enumerates last. The in-place // store below would find the deleted key's tombstoned entry and // bring the property back at its old position. - object_own_set(bag, key, value); - declared_value_replaced(ptr, key, declared); + object_own_set_bytes(bag, key, value); + declared_value_replaced_bytes(ptr, key, declared); return; } let key_hdr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); crate::object::object_ops::define_property_force_store_value(bag, key_hdr, value); - declared_value_replaced(ptr, key, declared); + declared_value_replaced_bytes(ptr, key, declared); } /// [[DefineOwnProperty]] of own data property `key` with its value AND its @@ -295,16 +306,29 @@ pub(crate) unsafe fn bag_define_value(ptr: usize, key: &str, value: f64) { /// /// # Safety /// `ptr` is a proven, live closure cell. -pub(crate) unsafe fn bag_define_data_with_attrs(ptr: usize, key: &str, value: f64, attrs: u8) { +pub(crate) unsafe fn bag_define_data_with_attrs_bytes( + ptr: usize, + key: &[u8], + value: f64, + attrs: u8, +) { let _no_move = crate::gc::GcSuppressScope::new(); - let entry = crate::object::key_attrs::attr_bits_to_entry(attrs); - if bag_of(ptr).is_null() && bag_born_with_attrs(ptr, &[(key, value)], &[entry]) { - return; + if let Ok(key) = std::str::from_utf8(key) { + if bag_of(ptr).is_null() + && bag_born_with_attrs( + ptr, + &[(key, value)], + &[crate::object::key_attrs::attr_bits_to_entry(attrs)], + ) + { + return; + } } - let declared = crate::object::class_value::holds_declared_static_method(ptr, key); + let entry = crate::object::key_attrs::attr_bits_to_entry(attrs); + let declared = crate::object::class_value::holds_declared_static_method_bytes(ptr, key); let bag = bag_ensure(ptr); let key_hdr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); - let present = bag_has_own(ptr, key.as_bytes()); + let present = bag_has_own(ptr, key); if !present && entry != 0 && crate::object::shapes::object_shape_descriptor(bag).is_some_and(|d| d.hole_count == 0) @@ -320,19 +344,16 @@ pub(crate) unsafe fn bag_define_data_with_attrs(ptr: usize, key: &str, value: f6 crate::object::object_ops::define_property_force_store_value(bag, key_hdr, value); } else { // New (possibly after a delete): appended, so it enumerates last. - object_own_set(bag, key, value); + object_own_set_bytes(bag, key, value); } - if crate::object::key_attrs::object_key_entry(bag, key.as_bytes()) != entry { + if crate::object::key_attrs::object_key_entry(bag, key) != entry { crate::object::descriptor_state::note_descriptor_target_edits( bag as usize, - &[crate::object::key_attrs::AttrsEdit::Data( - key.as_bytes(), - attrs, - )], + &[crate::object::key_attrs::AttrsEdit::Data(key, attrs)], ); } } - declared_value_replaced(ptr, key, declared); + declared_value_replaced_bytes(ptr, key, declared); } /// Remove the function's own data property `key`; true when it existed. @@ -340,17 +361,21 @@ pub(crate) unsafe fn bag_define_data_with_attrs(ptr: usize, key: &str, value: f6 /// # Safety /// `ptr` is a proven, live closure cell. pub(crate) unsafe fn bag_remove(ptr: usize, key: &str) -> bool { + bag_remove_bytes(ptr, key.as_bytes()) +} + +pub(crate) unsafe fn bag_remove_bytes(ptr: usize, key: &[u8]) -> bool { let bag = bag_of(ptr); - if bag.is_null() || !bag_has_own(ptr, key.as_bytes()) { + if bag.is_null() || !bag_has_own(ptr, key) { return false; } let _no_move = crate::gc::GcSuppressScope::new(); - let declared = crate::object::class_value::holds_declared_static_method(ptr, key); + let declared = crate::object::class_value::holds_declared_static_method_bytes(ptr, key); let key_hdr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); crate::object::js_object_delete_field(bag, key_hdr); // Deleting the declaration must not let a re-add reach the shape that // proved it (a removed last key re-added lands on the same key list). - declared_value_replaced(ptr, key, declared); + declared_value_replaced_bytes(ptr, key, declared); true } @@ -386,6 +411,13 @@ pub(crate) unsafe fn bag_has_own(ptr: usize, key: &[u8]) -> bool { /// # Safety /// `ptr` is a proven, live closure cell. pub(crate) unsafe fn bag_accessor_names(ptr: usize) -> Vec { + bag_accessor_names_bytes(ptr) + .into_iter() + .map(|name| String::from_utf8_lossy(&name).into_owned()) + .collect() +} + +pub(crate) unsafe fn bag_accessor_names_bytes(ptr: usize) -> Vec> { let bag = bag_of(ptr); if bag.is_null() { return Vec::new(); @@ -406,7 +438,7 @@ pub(crate) unsafe fn bag_accessor_names(ptr: usize) -> Vec { if crate::object::field_get_set::own_key_hidden_bytes(bag, bytes) { continue; } - out.push(String::from_utf8_lossy(bytes).into_owned()); + out.push(bytes.to_vec()); } } out @@ -419,6 +451,13 @@ pub(crate) unsafe fn bag_accessor_names(ptr: usize) -> Vec { /// # Safety /// `ptr` is a proven, live closure cell. pub(crate) unsafe fn bag_own_key_names(ptr: usize) -> Vec { + bag_own_key_names_bytes(ptr) + .into_iter() + .map(|name| String::from_utf8_lossy(&name).into_owned()) + .collect() +} + +pub(crate) unsafe fn bag_own_key_names_bytes(ptr: usize) -> Vec> { let bag = bag_of(ptr); if bag.is_null() { return Vec::new(); @@ -443,7 +482,7 @@ pub(crate) unsafe fn bag_own_key_names(ptr: usize) -> Vec { if crate::object::field_get_set::own_key_hidden_bytes(bag, bytes) { continue; } - out.push(String::from_utf8_lossy(bytes).into_owned()); + out.push(bytes.to_vec()); } } out @@ -537,39 +576,52 @@ unsafe fn state_ensure(ptr: usize) -> Option<*mut ObjectHeader> { /// # Safety /// `ptr` is a proven, live closure cell. pub(crate) unsafe fn state_is_deleted(ptr: usize, key: &str) -> bool { + state_is_deleted_bytes(ptr, key.as_bytes()) +} + +pub(crate) unsafe fn state_is_deleted_bytes(ptr: usize, key: &[u8]) -> bool { let state = state_of(ptr); if state.is_null() { return false; } - let mut marker = String::with_capacity(DELETED_PREFIX.len() + key.len()); - marker.push_str(DELETED_PREFIX); - marker.push_str(key); - object_own_get(state, marker.as_bytes()).is_some() + let mut marker = Vec::with_capacity(DELETED_PREFIX.len() + key.len()); + marker.extend_from_slice(DELETED_PREFIX.as_bytes()); + marker.extend_from_slice(key); + object_own_get(state, &marker).is_some() } /// # Safety /// `ptr` is a proven, live closure cell. pub(crate) unsafe fn state_mark_deleted(ptr: usize, key: &str) { + state_mark_deleted_bytes(ptr, key.as_bytes()); +} + +pub(crate) unsafe fn state_mark_deleted_bytes(ptr: usize, key: &[u8]) { let _no_move = crate::gc::GcSuppressScope::new(); let Some(state) = state_ensure(ptr) else { return; }; - object_own_set( - state, - &format!("{DELETED_PREFIX}{key}"), - f64::from_bits(crate::value::TAG_TRUE), - ); + let mut marker = Vec::with_capacity(DELETED_PREFIX.len() + key.len()); + marker.extend_from_slice(DELETED_PREFIX.as_bytes()); + marker.extend_from_slice(key); + object_own_set_bytes(state, &marker, f64::from_bits(crate::value::TAG_TRUE)); } /// # Safety /// `ptr` is a proven, live closure cell. pub(crate) unsafe fn state_clear_deleted(ptr: usize, key: &str) { - if !state_is_deleted(ptr, key) { + state_clear_deleted_bytes(ptr, key.as_bytes()); +} + +pub(crate) unsafe fn state_clear_deleted_bytes(ptr: usize, key: &[u8]) { + if !state_is_deleted_bytes(ptr, key) { return; } let _no_move = crate::gc::GcSuppressScope::new(); let state = state_of(ptr); - let marker = format!("{DELETED_PREFIX}{key}"); + let mut marker = Vec::with_capacity(DELETED_PREFIX.len() + key.len()); + marker.extend_from_slice(DELETED_PREFIX.as_bytes()); + marker.extend_from_slice(key); let key_hdr = crate::string::js_string_from_bytes(marker.as_ptr(), marker.len() as u32); crate::object::js_object_delete_field(state, key_hdr); } diff --git a/crates/perry-runtime/src/object/accessor_pair.rs b/crates/perry-runtime/src/object/accessor_pair.rs index a01d62bc78..492be1ca0b 100644 --- a/crates/perry-runtime/src/object/accessor_pair.rs +++ b/crates/perry-runtime/src/object/accessor_pair.rs @@ -336,6 +336,10 @@ pub(crate) unsafe fn own_accessor(obj: usize, key: &[u8]) -> Option { /// `obj` is a live object whose attributes live with its keys, and `key` is /// one of its own keys. pub(crate) unsafe fn store_own_accessor(obj: usize, key: &str, acc: Option) { + store_own_accessor_bytes(obj, key.as_bytes(), acc); +} + +pub(crate) unsafe fn store_own_accessor_bytes(obj: usize, key: &[u8], acc: Option) { let _no_move = crate::gc::GcSuppressScope::new(); let value = match acc { Some(acc) => crate::value::js_nanbox_pointer(pair_new(acc) as i64), diff --git a/crates/perry-runtime/src/object/class_image.rs b/crates/perry-runtime/src/object/class_image.rs index 21cb44e727..5bd385f301 100644 --- a/crates/perry-runtime/src/object/class_image.rs +++ b/crates/perry-runtime/src/object/class_image.rs @@ -97,12 +97,12 @@ pub type StaticAccessorTable = PtrHashMap = HashMap; +pub type StaticNameMap = HashMap, V, ahash::RandomState>; /// `(class_id, is_static, property_name) -> source-order token` for declared /// string-keyed methods and accessors. The token is the member function's HIR /// id, which is allocated while walking the class body and therefore orders /// entries across the otherwise separate method/getter/setter registries. -pub type StringMemberOrderTable = HashMap<(u32, bool, String), u32>; +pub type StringMemberOrderTable = HashMap<(u32, bool, Vec), u32>; /// class_id -> (ctor func_ptr, total param count, signature capture count). pub type ConstructorTable = PtrHashMap; /// class_id -> (has_synthetic_arguments, has_rest, parameters before the first @@ -126,17 +126,18 @@ pub type ConstructorFlagTable = PtrHashMap)>; /// Iteration order is not observable for any of these: nothing in the tree /// iterates them (only `get` / `insert` / `contains`). The `String`-keyed /// INNER maps of [`StaticMethodTable`] / [`StaticAccessorTable`] keep a -/// randomly keyed hasher ([`StaticNameMap`]), and the `(u32, String)`-keyed -/// `method_bind_lengths` pair below stays on SipHash: their keys are -/// JS-supplied member names. The inner maps are enumerated on paths that +/// randomly keyed hasher ([`StaticNameMap`]), and the `(u32, Vec)`-keyed +/// `method_bind_lengths` pair below keeps the default randomized hasher: their +/// keys are JS-supplied member names. The inner maps are enumerated on paths +/// that /// reach user-visible output, so no caller may rely on their order. pub struct ClassImageTables { pub(crate) declarations: RwLock, pub(crate) static_methods: RwLock>, pub(crate) static_accessors: RwLock>, pub(crate) string_member_orders: RwLock>, - pub(crate) method_bind_lengths: RwLock>>, - pub(crate) static_method_bind_lengths: RwLock>>, + pub(crate) method_bind_lengths: RwLock), u32>>>, + pub(crate) static_method_bind_lengths: RwLock), u32>>>, pub(crate) registered_class_ids: RwLock>>, pub(crate) parents: RwLock>>, /// `parent + 1` for every registered edge whose child id is diff --git a/crates/perry-runtime/src/object/class_registry.rs b/crates/perry-runtime/src/object/class_registry.rs index 7070ed976f..11050285bb 100644 --- a/crates/perry-runtime/src/object/class_registry.rs +++ b/crates/perry-runtime/src/object/class_registry.rs @@ -45,8 +45,10 @@ pub(crate) use construct::{ordinary_compiled_function_has_instance, OrdinaryInst mod decl_accessors; pub(crate) mod declarations; pub(crate) use decl_accessors::{ - class_chain_getter_value, class_chain_setter_apply, decl_prototype_own_accessor, - install_decl_prototype_accessor, instance_chain_getter_value, instance_chain_setter_apply, + class_chain_getter_value, class_chain_getter_value_bytes, class_chain_setter_apply, + decl_prototype_own_accessor, decl_prototype_own_accessor_bytes, + install_decl_prototype_accessor_bytes, instance_chain_getter_value, + instance_chain_setter_apply, instance_chain_setter_apply_bytes, }; pub use declarations::{ js_register_class_declaration, ClassDeclaration, ClassMemberDecl, CLASS_MEMBER_GETTER, @@ -92,21 +94,23 @@ pub(crate) use state::stashed_dynamic_parent_value; pub(crate) use state::{ builtin_parent_ctor_in_chain, class_decl_prototype_object, class_decl_prototype_value, class_decl_prototype_value_for_instance_class, class_decl_prototype_value_selected, - class_declaration_value, class_declaration_value_store, class_delete_own_dynamic_prop, + class_declaration_value, class_declaration_value_bytes, class_declaration_value_store, + class_declaration_value_store_bytes, class_delete_own_dynamic_prop, class_dynamic_prop_root_store, class_has_own_dynamic_prop, class_id_for_decl_prototype_object, - class_method_entry, class_object_value_for_cid, class_object_value_root_store, + class_method_entry_bytes, class_object_value_for_cid, class_object_value_root_store, class_own_dynamic_prop_names, class_own_enumerable_field_names, class_own_static_field_value, - class_own_string_member_names, class_parent_closure, class_parent_closure_root_store, - class_proto_key_deleted, class_prototype_member_names, - class_prototype_object_addr_index_contains, class_prototype_object_addr_index_rekey, - class_prototype_object_root_store, class_ref_dynamic_prop_root_store, - class_register_declared_static_global_slot, class_static_alias_sync, - class_static_clear_defined_attrs, class_static_defined_attrs, class_static_key_deleted, + class_own_static_field_value_bytes, class_own_string_member_names, class_parent_closure, + class_parent_closure_root_store, class_proto_key_deleted, class_proto_key_deleted_bytes, + class_prototype_member_names, class_prototype_object_addr_index_contains, + class_prototype_object_addr_index_rekey, class_prototype_object_root_store, + class_ref_dynamic_prop_root_store, class_register_declared_static_global_slot, + class_static_alias_sync, class_static_clear_defined_attrs, class_static_defined_attrs, + class_static_defined_attrs_bytes, class_static_key_deleted, class_static_key_deleted_bytes, class_static_prototype, class_static_prototype_is_nulled, class_static_prototype_root_clear, - class_static_prototype_root_store, class_static_set_defined_attrs, decl_prototype_identity_id, - global_object_prototype_bits, is_bound_native_constructor_closure_value, - is_non_constructable_builtin_function_value, parent_closure_in_chain, - proto_member_has_no_string_key, template_has_class_objects, + class_static_prototype_root_store, class_static_set_defined_attrs, + class_static_set_defined_attrs_bytes, decl_prototype_identity_id, global_object_prototype_bits, + is_bound_native_constructor_closure_value, is_non_constructable_builtin_function_value, + parent_closure_in_chain, proto_member_has_no_string_key_bytes, template_has_class_objects, throw_non_constructable_builtin_function, ClassDeclarationValueKind, CLASS_OBJECT_EVER, }; pub use state::{ @@ -150,7 +154,9 @@ pub use class_meta::{ CLASS_NAMES, }; // ── prototype_methods.rs ──────────────────────────────────────────────────── +#[cfg(test)] pub(crate) use prototype_methods::class_prototype_set; +pub(crate) use prototype_methods::class_prototype_set_bytes; pub use prototype_methods::{ js_class_register_static_field, js_get_function_prototype_method, js_register_function_prototype_method, js_register_prototype_method, @@ -190,8 +196,9 @@ pub(crate) use gc_roots::{ // ── registration.rs ───────────────────────────────────────────────────────── pub(crate) use registration::{ - class_accessor_function_value, class_accessor_source_func_ptr, class_own_accessor_ptrs, - class_own_setter_length, class_registered_static_accessor_ptrs, + class_accessor_function_value, class_accessor_function_value_bytes, + class_accessor_source_func_ptr, class_own_accessor_ptrs_bytes, class_own_setter_length_bytes, + class_registered_static_accessor_ptrs, class_registered_static_accessor_ptrs_bytes, invalidate_class_string_member_order, }; pub use registration::{ @@ -215,17 +222,21 @@ pub(crate) use dispatch::{ // ── parent_static.rs ──────────────────────────────────────────────────────── pub(crate) use parent_static::{ call_private_static_method_for_owner, call_registered_static_method, - class_dynamic_static_accessor_descriptor, class_has_own_static_method, - class_has_own_symbol_member, class_has_symbol_member_in_chain, class_method_bind_length, - class_method_slot_owner, class_method_slot_target, class_method_slot_value, - class_object_own_field_bytes, class_object_owns_key_bytes, class_object_pinned_parent, - class_own_static_method_code, class_own_static_method_entry, class_own_symbol_accessor_ptrs, - class_own_symbol_member_keys, class_own_symbol_method, class_private_instance_getter_value, - class_private_instance_setter_apply, class_static_accessor_getter_value, - class_static_accessor_setter_apply, class_symbol_getter_value, class_symbol_setter_apply, - dynamic_value_class_id, get_parent_class_id, lookup_class_symbol_method_in_chain, - lookup_static_method_in_chain, lookup_static_method_owner, register_class, - register_class_dynamic_static_accessor, static_accessor_in_chain, + class_dynamic_static_accessor_descriptor, class_dynamic_static_accessor_descriptor_bytes, + class_has_own_static_method, class_has_own_symbol_member, class_has_symbol_member_in_chain, + class_method_bind_length_bytes, class_method_slot_owner, class_method_slot_target, + class_method_slot_value, class_method_slot_value_bytes, class_object_own_field_bytes, + class_object_owns_key_bytes, class_object_pinned_parent, class_own_static_method_code, + class_own_static_method_code_bytes, class_own_static_method_entry, + class_own_symbol_accessor_ptrs, class_own_symbol_member_keys, class_own_symbol_method, + class_private_instance_getter_value, class_private_instance_setter_apply, + class_static_accessor_getter_value, class_static_accessor_getter_value_bytes, + class_static_accessor_setter_apply, class_static_accessor_setter_apply_bytes, + class_symbol_getter_value, class_symbol_setter_apply, dynamic_value_class_id, + get_parent_class_id, lookup_class_symbol_method_in_chain, lookup_static_method_in_chain, + lookup_static_method_in_chain_bytes, lookup_static_method_owner, + lookup_static_method_owner_bytes, register_class, register_class_dynamic_static_accessor, + static_accessor_in_chain, }; pub use parent_static::{ is_class_object_ptr, is_class_object_value, is_registered_class_prototype_object, diff --git a/crates/perry-runtime/src/object/class_registry/decl_accessors.rs b/crates/perry-runtime/src/object/class_registry/decl_accessors.rs index 7766cc1405..7c27a0d1a5 100644 --- a/crates/perry-runtime/src/object/class_registry/decl_accessors.rs +++ b/crates/perry-runtime/src/object/class_registry/decl_accessors.rs @@ -22,17 +22,25 @@ use crate::object::accessor_pair::{own_accessor, Accessor}; /// A half whose compiled entry is unchanged keeps its closure, so reflection /// hands out the same function object every time. pub(crate) fn install_decl_prototype_accessor(proto: *mut ObjectHeader, class_id: u32, name: &str) { + install_decl_prototype_accessor_bytes(proto, class_id, name.as_bytes()) +} + +pub(crate) fn install_decl_prototype_accessor_bytes( + proto: *mut ObjectHeader, + class_id: u32, + name: &[u8], +) { if proto.is_null() { return; } - let Some((raw_get, raw_set)) = class_own_accessor_ptrs(class_id, name) else { + let Some((raw_get, raw_set)) = class_own_accessor_ptrs_bytes(class_id, name) else { return; }; // A half arriving later keeps whatever attributes the accessor has now // (a `defineProperty` may have changed them); a fresh one takes the // ClassBody defaults. use crate::object::key_attrs as ka; - let entry = unsafe { ka::object_key_entry(proto, name.as_bytes()) }; + let entry = unsafe { ka::object_key_entry(proto, name) }; let (enumerable, configurable) = if entry & ka::ENTRY_ACCESSOR != 0 { ( entry & ka::ENTRY_NON_ENUMERABLE == 0, @@ -44,7 +52,7 @@ pub(crate) fn install_decl_prototype_accessor(proto: *mut ObjectHeader, class_id let scope = crate::gc::RuntimeHandleScope::new(); let proto_h = scope.root_raw_mut_ptr(proto); let existing = proto_h - .with_mut_ptr(|p: *mut ObjectHeader| unsafe { own_accessor(p as usize, name.as_bytes()) }) + .with_mut_ptr(|p: *mut ObjectHeader| unsafe { own_accessor(p as usize, name) }) .unwrap_or_default(); let half = |raw: usize, have_raw: usize, have: u64, is_setter: bool| -> u64 { if raw == 0 { @@ -53,11 +61,11 @@ pub(crate) fn install_decl_prototype_accessor(proto: *mut ObjectHeader, class_id have } else { let set_length = if is_setter { - class_own_setter_length(class_id, name, false) + class_own_setter_length_bytes(class_id, name, false) } else { None }; - class_accessor_function_value(raw, is_setter, false, name, set_length).to_bits() + class_accessor_function_value_bytes(raw, is_setter, false, name, set_length).to_bits() } }; let get = scope.root_nanbox_u64(half(raw_get, existing.raw_get, existing.get, false)); @@ -71,9 +79,9 @@ pub(crate) fn install_decl_prototype_accessor(proto: *mut ObjectHeader, class_id static_set: 0, }; proto_h.with_mut_ptr(|p: *mut ObjectHeader| { - crate::object::set_builtin_accessor_pair( + crate::object::descriptor_state::set_builtin_accessor_pair_bytes( p as usize, - name.to_string(), + name, pair, PropertyAttrs::new(true, enumerable, configurable), ) @@ -102,6 +110,10 @@ pub(crate) fn note_instance_accessor_registered(class_id: u32, name: &str, newly /// a `defineProperty` or `delete` has made of it — so the ref and the object /// never disagree. `None` for every other key. pub(crate) fn decl_prototype_own_accessor(class_id: u32, name: &str) -> Option { + decl_prototype_own_accessor_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn decl_prototype_own_accessor_bytes(class_id: u32, name: &[u8]) -> Option { let proto = class_decl_prototype_value(class_id); let js = crate::JSValue::from_bits(proto.to_bits()); if !js.is_pointer() { @@ -111,8 +123,8 @@ pub(crate) fn decl_prototype_own_accessor(class_id: u32, name: &str) -> Option Option Option<(usize, Accessor)> { + class_proto_accessor_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_proto_accessor_bytes(class_id: u32, name: &[u8]) -> Option<(usize, Accessor)> { let _no_move = crate::gc::GcSuppressScope::new(); prototype_accessor(class_decl_prototype_value(class_id), name) } @@ -133,6 +149,15 @@ pub(crate) fn class_proto_accessor(class_id: u32, name: &str) -> Option<(usize, pub(crate) unsafe fn instance_proto_accessor( obj: *const ObjectHeader, name: &str, +) -> Option<(usize, Accessor)> { + instance_proto_accessor_bytes(obj, name.as_bytes()) +} + +/// Byte-preserving form of [`instance_proto_accessor`] for class member keys +/// that may contain WTF-8 encodings of lone surrogates. +pub(crate) unsafe fn instance_proto_accessor_bytes( + obj: *const ObjectHeader, + name: &[u8], ) -> Option<(usize, Accessor)> { // The walk runs no user code. Materialization is rare and cannot move // these raw pointers; an ordinary miss needs no handle-scope allocation. @@ -175,7 +200,7 @@ unsafe fn accessor_prototype(obj: *const ObjectHeader) -> Option { } } -fn prototype_accessor(start: f64, name: &str) -> Option<(usize, Accessor)> { +fn prototype_accessor(start: f64, name: &[u8]) -> Option<(usize, Accessor)> { use crate::object::key_attrs as ka; let mut cur = start; for _ in 0..10_000 { @@ -198,11 +223,7 @@ fn prototype_accessor(start: f64, name: &str) -> Option<(usize, Accessor)> { // these skipped data holders shadows it. let slot = (shape.summary() & ka::SUMMARY_ACCESSOR != 0) .then(|| unsafe { - ka::keys_find_accessor_slot_resolved( - keys, - shape.logical_key_count(), - name.as_bytes(), - ) + ka::keys_find_accessor_slot_resolved(keys, shape.logical_key_count(), name) }) .flatten(); if let Some(slot) = slot { @@ -223,7 +244,7 @@ fn prototype_accessor(start: f64, name: &str) -> Option<(usize, Accessor)> { /// Called only after an accessor was found. No user code ran in either walk; /// the same shape links lead to its holder. Uncertainty declines the answer. -unsafe fn accessor_is_shadowed(start: f64, holder: *const ObjectHeader, name: &str) -> bool { +unsafe fn accessor_is_shadowed(start: f64, holder: *const ObjectHeader, name: &[u8]) -> bool { let mut cur = start; for _ in 0..10_000 { let value = crate::JSValue::from_bits(cur.to_bits()); @@ -240,7 +261,7 @@ unsafe fn accessor_is_shadowed(start: f64, holder: *const ObjectHeader, name: &s if crate::object::keys_find_slot_by_bytes_resolved( shape.keys() as usize as *const crate::array::ArrayHeader, shape.logical_key_count(), - name.as_bytes(), + name, ) .is_some() { @@ -273,6 +294,15 @@ pub(crate) unsafe fn class_chain_getter_value( Some(invoke_instance_getter(acc, this_of())) } +pub(crate) unsafe fn class_chain_getter_value_bytes( + class_id: u32, + name: &[u8], + this_of: impl FnOnce() -> f64, +) -> Option<(crate::JSValue, usize)> { + let acc = class_proto_accessor_bytes(class_id, name)?.1; + Some(invoke_instance_getter(acc, this_of())) +} + /// Generic instance read, resolved through the receiver's current chain. /// The receiver override is consumed only when an accessor actually answers. pub(crate) unsafe fn instance_chain_getter_value( @@ -341,7 +371,18 @@ pub(crate) unsafe fn instance_chain_setter_apply( this: f64, value: f64, ) -> Option { - let acc = instance_proto_accessor(obj, name)?.1; + instance_chain_setter_apply_bytes(obj, name.as_bytes(), this, value) +} + +/// Byte-preserving form of [`instance_chain_setter_apply`] for class member +/// keys that may contain WTF-8 encodings of lone surrogates. +pub(crate) unsafe fn instance_chain_setter_apply_bytes( + obj: *const ObjectHeader, + name: &[u8], + this: f64, + value: f64, +) -> Option { + let acc = instance_proto_accessor_bytes(obj, name)?.1; let scope = crate::gc::RuntimeHandleScope::new(); let this_h = scope.root_nanbox_f64(this); let value_h = scope.root_nanbox_f64(value); diff --git a/crates/perry-runtime/src/object/class_registry/decl_prototype_birth.rs b/crates/perry-runtime/src/object/class_registry/decl_prototype_birth.rs index 7a6ef65d9c..2968981796 100644 --- a/crates/perry-runtime/src/object/class_registry/decl_prototype_birth.rs +++ b/crates/perry-runtime/src/object/class_registry/decl_prototype_birth.rs @@ -45,7 +45,7 @@ pub(super) fn decl_prototype_born_final(class_id: u32, parent_bits: u64) -> Opti let requested = Some(super::declarations::class_birth_shape(class_id)).filter(|&id| id != 0); let members = class_prototype_member_names(class_id); if members.iter().any(|(name, accessor)| { - name == b"constructor" || (!*accessor && class_method_entry(class_id, name).is_none()) + name == b"constructor" || (!*accessor && class_method_entry_bytes(class_id, name).is_none()) }) || !class_own_symbol_member_keys(class_id, false).is_empty() { return None; @@ -84,21 +84,16 @@ pub(super) fn decl_prototype_born_final(class_id: u32, parent_bits: u64) -> Opti if *accessor { use crate::object::accessor_pair::{pair_new, Accessor}; use crate::object::key_attrs as ka; - let (raw_get, raw_set) = - class_own_accessor_ptrs(class_id, std::str::from_utf8(name).ok()?)?; - let get = scope.root_nanbox_f64(class_accessor_function_value( - raw_get, - false, - false, - std::str::from_utf8(name).ok()?, - None, + let (raw_get, raw_set) = class_own_accessor_ptrs_bytes(class_id, name)?; + let get = scope.root_nanbox_f64(class_accessor_function_value_bytes( + raw_get, false, false, name, None, )); - let set = scope.root_nanbox_f64(class_accessor_function_value( + let set = scope.root_nanbox_f64(class_accessor_function_value_bytes( raw_set, true, false, - std::str::from_utf8(name).ok()?, - class_own_setter_length(class_id, std::str::from_utf8(name).ok()?, false), + name, + class_own_setter_length_bytes(class_id, name, false), )); let pair = unsafe { pair_new(Accessor { @@ -119,8 +114,7 @@ pub(super) fn decl_prototype_born_final(class_id: u32, parent_bits: u64) -> Opti ); continue; } - let f = - crate::object::native_module::class_prototype_method_value_for_bytes(class_id, name); + let f = class_prototype_method_value_for_name_bytes(class_id, name); if unsafe { crate::object::field_rep_store::constfn_store_info(f.to_bits()) }.is_none() { return None; } @@ -132,7 +126,7 @@ pub(super) fn decl_prototype_born_final(class_id: u32, parent_bits: u64) -> Opti let proof = crate::object::canonical_keys::SharedLayout::shape_cache_entry(); let list = scope.root_raw_mut_ptr::(std::ptr::null_mut()); let mut count = 0u32; - for (name, entry) in std::iter::once(&b"constructor"[..]) + for (name, entry) in std::iter::once(b"constructor".as_slice()) .chain(members.iter().map(|(n, _)| n.as_slice())) .zip(entries) { diff --git a/crates/perry-runtime/src/object/class_registry/declarations.rs b/crates/perry-runtime/src/object/class_registry/declarations.rs index 588b582b18..dc44e9a77d 100644 --- a/crates/perry-runtime/src/object/class_registry/declarations.rs +++ b/crates/perry-runtime/src/object/class_registry/declarations.rs @@ -598,17 +598,6 @@ fn member_is_named(class_id: u32, member: &ClassMemberDecl, name: &[u8]) -> bool } } -/// The index (in declaration order) of the method `name` of class -/// `class_id` as last declared, and the declaration's member count. -#[inline] -pub(crate) fn class_method_decl_index( - class_id: u32, - name: impl AsRef<[u8]>, -) -> Option<(usize, usize)> { - class_method_decl_index_bytes(class_id, name.as_ref()) -} - -#[inline(never)] pub(crate) fn class_method_decl_index_bytes(class_id: u32, name: &[u8]) -> Option<(usize, usize)> { let decl = class_declaration(class_id)?; let members = decl.members(); @@ -619,15 +608,11 @@ pub(crate) fn class_method_decl_index_bytes(class_id: u32, name: &[u8]) -> Optio } /// The method `name` of class `class_id` as last declared. -#[inline] -pub(crate) fn class_method_decl( - class_id: u32, - name: impl AsRef<[u8]>, -) -> Option<&'static ClassMemberDecl> { - class_method_decl_bytes(class_id, name.as_ref()) +#[cfg(test)] +pub(crate) fn class_method_decl(class_id: u32, name: &str) -> Option<&'static ClassMemberDecl> { + class_method_decl_bytes(class_id, name.as_bytes()) } -#[inline(never)] pub(crate) fn class_method_decl_bytes( class_id: u32, name: &[u8], @@ -641,12 +626,10 @@ pub(crate) fn class_method_decl_bytes( /// The accessor `name` of class `class_id` as declared: each half from the /// last member declaring it, and the setter's spec `.length`. -#[inline] -pub(crate) fn class_accessor_decl(class_id: u32, name: impl AsRef<[u8]>) -> Option { - class_accessor_decl_bytes(class_id, name.as_ref()) +pub(crate) fn class_accessor_decl(class_id: u32, name: &str) -> Option { + class_accessor_decl_bytes(class_id, name.as_bytes()) } -#[inline(never)] pub(crate) fn class_accessor_decl_bytes(class_id: u32, name: &[u8]) -> Option { let decl = class_declaration(class_id)?; if !decl.has_accessors() { @@ -860,10 +843,8 @@ fn name_computed_member_by_key(class_id: u32, definition_order: u32, value: f64) let accessor = member.is_accessor(); proto_h.with_mut_ptr(|proto: *mut ObjectHeader| { if accessor { - super::decl_accessors::install_decl_prototype_accessor( - proto, - class_id, - std::str::from_utf8(&name).expect("accessor name"), + super::decl_accessors::install_decl_prototype_accessor_bytes( + proto, class_id, &name, ); } else { super::state::install_class_decl_prototype_method_field(proto, class_id, &name); diff --git a/crates/perry-runtime/src/object/class_registry/declarations_tests.rs b/crates/perry-runtime/src/object/class_registry/declarations_tests.rs index 77d23e9760..64dfe26236 100644 --- a/crates/perry-runtime/src/object/class_registry/declarations_tests.rs +++ b/crates/perry-runtime/src/object/class_registry/declarations_tests.rs @@ -45,6 +45,41 @@ fn own_names(proto: f64) -> Vec { .collect() } +#[test] +fn computed_member_name_round_trips_wtf8_bytes_without_replacement() { + let _no_move = crate::gc::GcSuppressScope::new(); + let bytes = [0xed, 0xa0, 0x80]; // WTF-8 encoding of one lone high surrogate. + let value = crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32); + let value = f64::from_bits(crate::JSValue::string_ptr(value).bits()); + assert_eq!( + member_name_of_value(value, true), + MemberName::Str(bytes.to_vec()) + ); + let round_trip = member_name_value(&MemberName::Str(bytes.to_vec())); + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + assert_eq!( + unsafe { + crate::string::js_string_key_bytes( + crate::JSValue::from_bits(round_trip.to_bits()), + &mut scratch, + ) + }, + Some(bytes.as_slice()) + ); +} + +#[test] +fn declaration_method_lookup_matches_exact_wtf8_bytes() { + let _no_move = crate::gc::GcSuppressScope::new(); + let cid = 0x6F00; + register(cid, b"Wtf8Lookup"); + let key = [0xed, 0xa0, 0x80]; + test_declare_member(cid, &key, method()); + + assert!(class_method_decl_bytes(cid, &key).is_some()); + assert!(class_method_decl_bytes(cid, b"\xef\xbf\xbd").is_none()); +} + /// A birth before the definition names a computed member stops there; /// naming it installs it and the literal members after it, so the prototype /// keeps ClassBody order. @@ -59,7 +94,7 @@ fn computed_member_keeps_classbody_order_across_an_early_birth() { for (name, order) in [("a", 10u32), ("b", 12)] { super::super::registration::record_class_string_member_order( cid, - name.to_string(), + name.as_bytes().to_vec(), false, order, ); @@ -67,8 +102,8 @@ fn computed_member_keeps_classbody_order_across_an_early_birth() { // The prototype is born before the definition evaluates the key. let proto = crate::object::class_decl_prototype_value(cid); assert_eq!(own_names(proto), ["constructor", "a"]); - super::super::registration::record_class_string_member_order(cid, "k".to_string(), false, 11); - name_computed_member(cid, 11, MemberName::Str("k".as_bytes().to_vec())); + super::super::registration::record_class_string_member_order(cid, b"k".to_vec(), false, 11); + name_computed_member(cid, 11, MemberName::Str(b"k".to_vec())); assert_eq!(own_names(proto), ["constructor", "a", "k", "b"]); assert!(crate::object::class_method_slot_target(cid, "b").is_some()); assert!(crate::object::class_method_slot_target(cid, "k").is_some()); @@ -100,7 +135,7 @@ fn reevaluated_computed_member_keeps_the_holder_bounded() { let cid = 0x6F03; register(cid, b"Renamed"); test_declare_computed_member(cid, 30, method()); - name_computed_member(cid, 30, MemberName::Str("k0".as_bytes().to_vec())); + name_computed_member(cid, 30, MemberName::Str(b"k0".to_vec())); let holder = crate::object::class_value::class_value_ptr(cid) as usize; let mut previous = crate::object::class_prototype_method_value_for_name(cid, "k0").to_bits(); let baseline = unsafe { crate::closure::props::state_key_count(holder) }; @@ -145,7 +180,7 @@ fn holder_namespace_separates_names_values_and_private_statics() { // a private static key. literal(cid, "\u{1}k:40", 41); literal(cid, "#x", 42); - name_computed_member(cid, 40, MemberName::Str("real".as_bytes().to_vec())); + name_computed_member(cid, 40, MemberName::Str(b"real".to_vec())); let is_function = |v: f64| { let js = crate::JSValue::from_bits(v.to_bits()); js.is_pointer() && crate::closure::is_closure_ptr(js.as_pointer::() as usize) @@ -165,7 +200,7 @@ fn holder_namespace_separates_names_values_and_private_statics() { let computed = decl.members().iter().find(|m| m.name.is_null()).unwrap(); assert_eq!( class_member_name(cid, computed), - MemberName::Str("real".as_bytes().to_vec()) + MemberName::Str(b"real".to_vec()) ); // Each value answers again with the same object. assert_eq!( @@ -210,7 +245,7 @@ fn class_expression_evaluations_keep_their_own_computed_names() { register(cid, b"Expr"); literal(cid, "x", 60); test_declare_computed_member(cid, 61, method()); - super::super::registration::record_class_string_member_order(cid, "x".to_string(), false, 60); + super::super::registration::record_class_string_member_order(cid, b"x".to_vec(), false, 60); let scope = crate::gc::RuntimeHandleScope::new(); let mut classes = Vec::new(); for key in ["p", "q"] { @@ -222,7 +257,7 @@ fn class_expression_evaluations_keep_their_own_computed_names() { class.with_mut_ptr::(|c| crate::value::js_nanbox_pointer(c as i64)); super::super::registration::record_class_string_member_order( cid, - key.to_string(), + key.as_bytes().to_vec(), false, 61, ); @@ -260,8 +295,8 @@ fn computed_method_names_preserve_surrogate_bytes() { test_declare_computed_member(cid, 90, method()); let bytes = vec![0xED, 0xA0, 0x80]; name_computed_member(cid, 90, MemberName::Str(bytes.clone())); - assert!(class_method_decl(cid, &bytes).is_some()); - assert!(class_method_decl(cid, b"").is_none()); + assert!(class_method_decl_bytes(cid, &bytes).is_some()); + assert!(class_method_decl_bytes(cid, b"").is_none()); assert!(class_method_decl(cid, "\u{FFFD}").is_none()); let prototype = crate::object::class_decl_prototype_value(cid); let object = crate::JSValue::from_bits(prototype.to_bits()).as_pointer::(); diff --git a/crates/perry-runtime/src/object/class_registry/parent_static.rs b/crates/perry-runtime/src/object/class_registry/parent_static.rs index 0b1c459ec9..2eba135332 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static.rs @@ -639,10 +639,7 @@ pub unsafe extern "C" fn js_register_class_static_method( if class_id == 0 || name_ptr.is_null() || name_len <= 0 { return; } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - }; + let name = std::slice::from_raw_parts(name_ptr, name_len as usize).to_vec(); { let mut guard = CLASS_STATIC_METHODS.write().unwrap(); if guard.is_none() { @@ -659,7 +656,7 @@ pub unsafe extern "C" fn js_register_class_static_method( }) .or_insert((func_ptr as usize, param_count as u32, has_rest != 0, 0)); } - crate::object::class_value::note_intrinsic_registration(class_id as u32, &name); + crate::object::class_value::note_intrinsic_registration_bytes(class_id as u32, &name); } /// Record the `JsFunctionInfo` of the closure-convention entry @@ -678,10 +675,7 @@ pub unsafe extern "C" fn js_register_class_static_method_entry( if class_id == 0 || name_ptr.is_null() || name_len <= 0 || entry == 0 { return; } - let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) - else { - return; - }; + let name = std::slice::from_raw_parts(name_ptr, name_len as usize); { let mut guard = CLASS_STATIC_METHODS.write().unwrap(); let Some(record) = guard @@ -693,7 +687,7 @@ pub unsafe extern "C" fn js_register_class_static_method_entry( }; record.3 = entry as usize; } - crate::object::class_value::note_intrinsic_registration(class_id as u32, name); + crate::object::class_value::note_intrinsic_registration_bytes(class_id as u32, name); } /// A class definition evaluated an instance computed member's key: a class @@ -727,20 +721,17 @@ fn name_computed_instance_member( } } -fn property_key_string(key: f64) -> Option> { - let property_key = unsafe { crate::object::js_to_property_key(key) }; +fn property_key_bytes(property_key: f64) -> Option> { if unsafe { crate::symbol::js_is_symbol(property_key) } != 0 { return None; } - let str_ptr = crate::value::js_jsvalue_to_string(property_key); - if str_ptr.is_null() { - return Some(Vec::new()); - } + let mut short = [0u8; crate::value::SHORT_STRING_MAX_LEN]; unsafe { - let len = (*str_ptr).byte_len as usize; - let data = (str_ptr as *const u8).add(std::mem::size_of::()); - let bytes = std::slice::from_raw_parts(data, len); - Some(bytes.to_vec()) + crate::string::js_string_key_bytes( + crate::JSValue::from_bits(property_key.to_bits()), + &mut short, + ) + .map(<[u8]>::to_vec) } } @@ -829,18 +820,16 @@ pub unsafe extern "C" fn js_register_class_computed_method( } return; } - let name = match property_key_string(property_key) { + let name = match property_key_bytes(property_key) { Some(name) => name, None => return, }; - if let Ok(utf8) = std::str::from_utf8(&name) { - super::registration::record_class_string_member_order( - class_id, - utf8.to_owned(), - is_static != 0, - definition_order as u32, - ); - } + super::registration::record_class_string_member_order( + class_id, + name.clone(), + is_static != 0, + definition_order as u32, + ); if is_static != 0 && name == b"prototype" { throw_object_type_error(b"Classes may not have a static property named 'prototype'"); } @@ -890,6 +879,7 @@ pub unsafe extern "C" fn js_register_class_computed_method( if crate::closure::is_closure_ptr(addr) { crate::closure::shape::refresh_closure_shape(addr); } + crate::object::class_value::note_intrinsic_registration_bytes(class_id, &name); } else { // The member's declaration carries its body; this evaluation names // it, which also installs it when the decl prototype is waiting for @@ -964,17 +954,14 @@ pub unsafe extern "C" fn js_register_class_computed_accessor( } return; } - if let Some(bytes) = property_key_string(property_key) { - let Ok(name) = String::from_utf8(bytes) else { - return; - }; + if let Some(name) = property_key_bytes(property_key) { super::registration::record_class_string_member_order( class_id, name.clone(), is_static != 0, definition_order as u32, ); - if is_static != 0 && name == "prototype" { + if is_static != 0 && name == b"prototype" { throw_object_type_error(b"Classes may not have a static property named 'prototype'"); } if is_static == 0 { @@ -984,7 +971,7 @@ pub unsafe extern "C" fn js_register_class_computed_accessor( owner, class_id, definition_order, - super::declarations::MemberName::Str(name.into_bytes()), + super::declarations::MemberName::Str(name), property_key, ); } else { @@ -1007,7 +994,7 @@ pub unsafe extern "C" fn js_register_class_computed_accessor( entry.set = setter_ptr as usize; } } - crate::object::class_value::note_intrinsic_registration(class_id, &name); + crate::object::class_value::note_intrinsic_registration_bytes(class_id, &name); } } } @@ -1018,6 +1005,10 @@ pub unsafe extern "C" fn js_register_class_computed_accessor( /// `getOwnPropertyDescriptor(C, name)`, where inherited statics must NOT be /// reported as own properties of `C`. pub(crate) fn class_has_own_static_method(class_id: u32, name: &str) -> bool { + class_has_own_static_method_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_has_own_static_method_bytes(class_id: u32, name: &[u8]) -> bool { CLASS_STATIC_METHODS .read() .ok() @@ -1033,6 +1024,13 @@ pub(crate) fn class_has_own_static_method(class_id: u32, name: &str) -> bool { pub(crate) fn class_own_static_method_entry( class_id: u32, name: &str, +) -> Option<(usize, u32, bool)> { + class_own_static_method_entry_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_own_static_method_entry_bytes( + class_id: u32, + name: &[u8], ) -> Option<(usize, u32, bool)> { let guard = CLASS_STATIC_METHODS.read().ok()?; let e = guard.as_ref()?.get(&class_id)?.get(name).copied()?; @@ -1042,6 +1040,10 @@ pub(crate) fn class_own_static_method_entry( /// The closure-convention entry of ClassBody static method `name` declared by /// class `class_id` itself: the code of its own function object. pub(crate) fn class_own_static_method_code(class_id: u32, name: &str) -> Option { + class_own_static_method_code_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_own_static_method_code_bytes(class_id: u32, name: &[u8]) -> Option { let guard = CLASS_STATIC_METHODS.read().ok()?; let e = guard.as_ref()?.get(&class_id)?.get(name).copied()?; (e.3 != 0).then_some(e.3) @@ -1055,7 +1057,14 @@ pub(crate) fn lookup_static_method_in_chain( class_id: u32, name: &str, ) -> Option<(usize, u32, bool)> { - lookup_static_method_owner(class_id, name).map(|(_, e)| e) + lookup_static_method_in_chain_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn lookup_static_method_in_chain_bytes( + class_id: u32, + name: &[u8], +) -> Option<(usize, u32, bool)> { + lookup_static_method_owner_bytes(class_id, name).map(|(_, e)| e) } /// [`lookup_static_method_in_chain`] plus the class whose declaration runs. @@ -1066,6 +1075,13 @@ pub(crate) fn lookup_static_method_in_chain( pub(crate) fn lookup_static_method_owner( class_id: u32, name: &str, +) -> Option<(u32, (usize, u32, bool))> { + lookup_static_method_owner_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn lookup_static_method_owner_bytes( + class_id: u32, + name: &[u8], ) -> Option<(u32, (usize, u32, bool))> { use crate::object::class_value::StaticMethodProperty; let mut cid = class_id; @@ -1075,7 +1091,11 @@ pub(crate) fn lookup_static_method_owner( let guard = CLASS_STATIC_METHODS.read().ok()?; guard.as_ref()?.get(&cid).and_then(|m| m.get(name)).copied() }; - match crate::object::class_value::static_method_property(cid, name, entry.map(|e| e.3)) { + match crate::object::class_value::static_method_property_bytes( + cid, + name, + entry.map(|e| e.3), + ) { StaticMethodProperty::Live => { return entry.map(|e| (cid, (e.0, e.1, e.2))); } @@ -1102,7 +1122,7 @@ pub(crate) fn lookup_static_method_owner( /// Note: does not subtract for default-valued params (the registry doesn't /// record the first-default position); methods with defaults already reported /// the wrong length, so this is a strict improvement, never a regression. -pub(crate) fn class_method_bind_length(class_id: u32, name: &str) -> Option { +pub(crate) fn class_method_bind_length_bytes(class_id: u32, name: &[u8]) -> Option { // Exact spec length (default-aware) when codegen recorded it; walk the // parent chain so an inherited method's `.length` resolves too. if let Ok(guard) = CLASS_METHOD_BIND_LENGTHS.read() { @@ -1110,7 +1130,7 @@ pub(crate) fn class_method_bind_length(class_id: u32, name: &str) -> Option let mut cid = class_id; let mut depth = 0usize; while cid != 0 && depth < 32 { - if let Some(&len) = map.get(&(cid, name.to_string())) { + if let Some(&len) = map.get(&(cid, name.to_vec())) { return Some(len); } match get_parent_class_id(cid) { @@ -1127,7 +1147,8 @@ pub(crate) fn class_method_bind_length(class_id: u32, name: &str) -> Option // member while its evaluation prototype is being filled. Consume the // declaration's function value rather than building a canonical holder, // which would evaluate the superclass's prototype a second time. - let declaration = crate::object::class_prototype_method_value_for_name(class_id, name); + let declaration = + crate::object::native_module::class_prototype_method_value_for_name_bytes(class_id, name); if let Some((_, params, synthetic, rest)) = unsafe { crate::object::native_module::class_method_value_target(declaration.to_bits()) } { @@ -1141,7 +1162,7 @@ pub(crate) fn class_method_bind_length(class_id: u32, name: &str) -> Option let mut cid = class_id; let mut depth = 0usize; while cid != 0 && depth < 32 { - if let Some(&len) = map.get(&(cid, name.to_string())) { + if let Some(&len) = map.get(&(cid, name.to_vec())) { return Some(len); } match get_parent_class_id(cid) { @@ -1155,7 +1176,7 @@ pub(crate) fn class_method_bind_length(class_id: u32, name: &str) -> Option } } // CLASS_STATIC_METHODS stores (func_ptr, param_count, has_rest). - if let Some((_, param_count, has_rest)) = lookup_static_method_in_chain(class_id, name) { + if let Some((_, param_count, has_rest)) = lookup_static_method_in_chain_bytes(class_id, name) { let mut len = param_count; if has_rest { len = len.saturating_sub(1); @@ -1667,6 +1688,10 @@ pub(crate) use crate::object::class_meta_registry::get_parent_class_id; /// Materialization is a no-move window: legacy callers can hold raw /// receiver/argument pointers while asking this question. pub(crate) fn class_method_slot_value(class_id: u32, name: &str) -> Option { + class_method_slot_value_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_method_slot_value_bytes(class_id: u32, name: &[u8]) -> Option { let _no_move = crate::gc::GcSuppressScope::new(); let selected = decl_prototype_identity_id(class_id); let word = crate::object::class_holder_word_selected(selected); @@ -1680,7 +1705,7 @@ pub(crate) fn class_method_slot_value(class_id: u32, name: &str) -> Option } else { existing.as_pointer::() as *const ObjectHeader }; - let key = crate::object::native_call_method::class_holder::MethodKey::bytes(name.as_bytes()); + let key = crate::object::native_call_method::class_holder::MethodKey::bytes(name); match unsafe { crate::object::native_call_method::class_holder::chain_method(start, &key) } { crate::object::native_call_method::class_holder::ChainMethod::Data { value, .. } => { Some(value) diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs index 1a374d2d8a..6ae27533a2 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/private_and_dynamic.rs @@ -188,6 +188,17 @@ pub(crate) fn class_dynamic_static_accessor_descriptor( ) -> Option<( crate::object::AccessorDescriptor, crate::object::PropertyAttrs, +)> { + class_dynamic_static_accessor_descriptor_bytes(class_id, name.as_bytes(), receiver) +} + +pub(crate) fn class_dynamic_static_accessor_descriptor_bytes( + class_id: u32, + name: &[u8], + receiver: f64, +) -> Option<( + crate::object::AccessorDescriptor, + crate::object::PropertyAttrs, )> { let scope = crate::gc::RuntimeHandleScope::new(); let receiver = scope.root_nanbox_f64(receiver); @@ -197,7 +208,7 @@ pub(crate) fn class_dynamic_static_accessor_descriptor( } if !is_class_object_ptr(owner as *const u8) { let (acc, enumerable, configurable) = - crate::object::class_value::class_static_own_accessor(class_id, name)?; + crate::object::class_value::class_static_own_accessor_bytes(class_id, name)?; return Some(( crate::object::AccessorDescriptor { get: acc.get, @@ -206,16 +217,15 @@ pub(crate) fn class_dynamic_static_accessor_descriptor( crate::object::PropertyAttrs::new(false, enumerable, configurable), )); } - let key = dynamic_static_accessor_storage_key(owner, name); - let descriptor = crate::object::get_accessor_descriptor(owner, &key)?; - let attrs = crate::object::get_property_attrs(owner, &key) + let descriptor = crate::object::descriptor_state::get_accessor_descriptor_bytes(owner, name)?; + let attrs = crate::object::descriptor_state::get_property_attrs_bytes(owner, name) .unwrap_or(crate::object::PropertyAttrs::new(false, false, false)); Some((descriptor, attrs)) } -pub(crate) unsafe fn class_dynamic_static_accessor_getter_value( +pub(crate) unsafe fn class_dynamic_static_accessor_getter_value_bytes( class_id: u32, - name: &str, + name: &[u8], receiver: f64, ) -> Option { let scope = crate::gc::RuntimeHandleScope::new(); @@ -224,12 +234,7 @@ pub(crate) unsafe fn class_dynamic_static_accessor_getter_value( // The class function object's accessors are its own properties // (`class_static_accessor_getter_value` reads them). let descriptor = (owner != 0 && is_class_object_ptr(owner as *const u8)) - .then(|| { - crate::object::get_accessor_descriptor( - owner, - &dynamic_static_accessor_storage_key(owner, name), - ) - }) + .then(|| crate::object::descriptor_state::get_accessor_descriptor_bytes(owner, name)) .flatten()?; if descriptor.get == 0 { return Some(f64::from_bits(crate::value::TAG_UNDEFINED)); @@ -242,9 +247,9 @@ pub(crate) unsafe fn class_dynamic_static_accessor_getter_value( /// `Some(true)` means a setter was invoked, `Some(false)` means an accessor /// exists but has no setter, and `None` means this class has no such dynamic /// accessor. -pub(crate) unsafe fn class_dynamic_static_accessor_setter_apply( +pub(crate) unsafe fn class_dynamic_static_accessor_setter_apply_bytes( class_id: u32, - name: &str, + name: &[u8], receiver: f64, value: f64, ) -> Option { @@ -253,12 +258,7 @@ pub(crate) unsafe fn class_dynamic_static_accessor_setter_apply( let value = scope.root_nanbox_f64(value); let owner = dynamic_static_accessor_owner(class_id, receiver.get_nanbox_f64()); let descriptor = (owner != 0 && is_class_object_ptr(owner as *const u8)) - .then(|| { - crate::object::get_accessor_descriptor( - owner, - &dynamic_static_accessor_storage_key(owner, name), - ) - }) + .then(|| crate::object::descriptor_state::get_accessor_descriptor_bytes(owner, name)) .flatten()?; if descriptor.set == 0 { return Some(false); @@ -339,7 +339,7 @@ pub(crate) unsafe fn call_private_static_method_for_owner( .ok()? .as_ref()? .get(&owner_class_id)? - .get(name) + .get(name.as_bytes()) .copied()?; let scope = crate::gc::RuntimeHandleScope::new(); let this_value = scope.root_nanbox_f64(this_value); diff --git a/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs b/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs index 37600b1a2c..965cf841f2 100644 --- a/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs +++ b/crates/perry-runtime/src/object/class_registry/parent_static/static_accessor_call.rs @@ -111,14 +111,25 @@ pub(crate) unsafe fn class_static_accessor_getter_value( name: &str, receiver: f64, ) -> Option { - if name.starts_with('#') { - return private_static_accessor_getter_value(class_id, name, receiver); + class_static_accessor_getter_value_bytes(class_id, name.as_bytes(), receiver) +} + +pub(crate) unsafe fn class_static_accessor_getter_value_bytes( + class_id: u32, + name: &[u8], + receiver: f64, +) -> Option { + if name.starts_with(b"#") { + return std::str::from_utf8(name) + .ok() + .and_then(|name| private_static_accessor_getter_value(class_id, name, receiver)); } let mut cid = class_id; let mut depth = 0usize; while cid != 0 && depth < 32 { // A per-evaluation class object's own `defineProperty` accessor. - if let Some(result) = class_dynamic_static_accessor_getter_value(cid, name, receiver) { + if let Some(result) = class_dynamic_static_accessor_getter_value_bytes(cid, name, receiver) + { return Some(result); } // The class function object's own accessor property (ClassBody or @@ -126,7 +137,8 @@ pub(crate) unsafe fn class_static_accessor_getter_value( // chain, `receiver` is the class it was found on (the capture/private // owner); `this` is the class the read started from, stashed as the // accessor-receiver override (effect's `static get ast()`, #10891). - if let Some((acc, _, _)) = crate::object::class_value::class_static_own_accessor(cid, name) + if let Some((acc, _, _)) = + crate::object::class_value::class_static_own_accessor_bytes(cid, name) { return Some(crate::object::class_value::class_static_accessor_call_get( acc, receiver, @@ -191,24 +203,39 @@ pub(crate) unsafe fn class_static_accessor_setter_apply( receiver: f64, value: f64, ) -> bool { - if name.starts_with('#') { - return private_static_accessor_setter_apply(class_id, name, receiver, value); + class_static_accessor_setter_apply_bytes(class_id, name.as_bytes(), receiver, value) +} + +/// Byte-preserving form of static accessor assignment. Class keys can contain +/// WTF-8 that is not valid Rust UTF-8, so assignment must retain the original +/// key bytes through descriptor lookup and invocation. +pub(crate) unsafe fn class_static_accessor_setter_apply_bytes( + class_id: u32, + name: &[u8], + receiver: f64, + value: f64, +) -> bool { + if let Ok(name_str) = std::str::from_utf8(name) { + if name_str.starts_with('#') { + return private_static_accessor_setter_apply(class_id, name_str, receiver, value); + } } let mut cid = class_id; let mut depth = 0usize; while cid != 0 && depth < 32 { if let Some(applied) = - class_dynamic_static_accessor_setter_apply(cid, name, receiver, value) + class_dynamic_static_accessor_setter_apply_bytes(cid, name, receiver, value) { if !applied { - throw_static_getter_only(class_id, name); + throw_static_getter_only_bytes(class_id, name); } return true; } - if let Some((acc, _, _)) = crate::object::class_value::class_static_own_accessor(cid, name) + if let Some((acc, _, _)) = + crate::object::class_value::class_static_own_accessor_bytes(cid, name) { if !crate::object::class_value::class_static_accessor_call_set(acc, receiver, value) { - throw_static_getter_only(class_id, name); + throw_static_getter_only_bytes(class_id, name); } return true; } @@ -230,6 +257,16 @@ fn throw_static_getter_only(class_id: u32, name: &str) -> ! { )) } +fn throw_static_getter_only_bytes(class_id: u32, name: &[u8]) -> ! { + if let Ok(name) = std::str::from_utf8(name) { + throw_static_getter_only(class_id, name); + } + let class_name = class_name_for_id(class_id).unwrap_or_default(); + crate::collection_iter::throw_type_error(&format!( + "Cannot set property of [class {class_name}] which has only a getter" + )) +} + unsafe fn private_static_accessor_setter_apply( class_id: u32, name: &str, diff --git a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs index 4672603805..a26855db3b 100644 --- a/crates/perry-runtime/src/object/class_registry/prototype_methods.rs +++ b/crates/perry-runtime/src/object/class_registry/prototype_methods.rs @@ -123,6 +123,13 @@ pub unsafe extern "C" fn js_class_computed_field_key( /// Compatibility entry points for HIR assembled by embedders. The prototype /// is read before the value is stored; its ordinary [[Set]] owns the mutation. pub(crate) fn class_prototype_set(class_id: u32, name: String, value_bits: u64) { + class_prototype_set_bytes(class_id, name.as_bytes(), value_bits); +} + +pub(crate) fn class_prototype_set_bytes(class_id: u32, name: &[u8], value_bits: u64) { + // Prototype materialization below can allocate and move the source JS + // string, so retain an owned exact-byte key across those safepoints. + let name = name.to_vec(); let scope = crate::gc::RuntimeHandleScope::new(); let value = scope.root_nanbox_u64(value_bits); // The holder an instance read walks (declared, else synthetic); a class diff --git a/crates/perry-runtime/src/object/class_registry/registration.rs b/crates/perry-runtime/src/object/class_registry/registration.rs index 3b31e2a6c1..800a31752f 100644 --- a/crates/perry-runtime/src/object/class_registry/registration.rs +++ b/crates/perry-runtime/src/object/class_registry/registration.rs @@ -28,7 +28,7 @@ pub(super) fn publish_unbuilt_holder(class_id: u32) { pub(crate) fn record_class_string_member_order( class_id: u32, - name: String, + name: Vec, is_static: bool, definition_order: u32, ) { @@ -61,6 +61,14 @@ pub(crate) fn record_class_string_member_order( /// dispatch registry entry for fast calls, but retire its declaration-order /// position until a future class evaluation explicitly registers it again. pub(crate) fn invalidate_class_string_member_order(class_id: u32, name: &str, is_static: bool) { + invalidate_class_string_member_order_bytes(class_id, name.as_bytes(), is_static); +} + +pub(crate) fn invalidate_class_string_member_order_bytes( + class_id: u32, + name: &[u8], + is_static: bool, +) { if class_id == 0 { return; } @@ -74,7 +82,7 @@ pub(crate) fn invalidate_class_string_member_order(class_id: u32, name: &str, is guard .as_mut() .unwrap() - .insert((class_id, is_static, name.to_string()), u32::MAX); + .insert((class_id, is_static, name.to_vec()), u32::MAX); drop(guard); if !is_static { super::class_meta::refresh_anon_declaration_role(class_id); @@ -116,13 +124,10 @@ pub unsafe extern "C" fn js_register_class_string_member_order( if class_id <= 0 || name_ptr.is_null() || name_len < 0 { return; } - let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) - else { - return; - }; + let name = std::slice::from_raw_parts(name_ptr, name_len as usize).to_vec(); record_class_string_member_order( class_id as u32, - name.to_string(), + name, is_static != 0, definition_order as u32, ); @@ -217,16 +222,8 @@ pub extern "C" fn js_register_class_prototype_shape(class_id: u32, shape_id: u32 super::declarations::test_declare_birth_shape(class_id, shape_id); } -/// The ClassBody's own public instance accessor declaration for `class_id` + -/// `name`: `(getter_ptr, setter_ptr)`, each 0 when that half is absent. -/// -/// Class metadata: read by the decl-prototype installer (`decl_accessors.rs`) -/// and by "does this class declare an accessor `name`" filters. A property -/// read, write or reflection must not answer from it — the prototype's real -/// accessor property is the truth, and `defineProperty` / `delete` may have -/// changed it. -pub(crate) fn class_own_accessor_ptrs(class_id: u32, name: &str) -> Option<(usize, usize)> { - let decl = super::declarations::class_accessor_decl(class_id, name)?; +pub(crate) fn class_own_accessor_ptrs_bytes(class_id: u32, name: &[u8]) -> Option<(usize, usize)> { + let decl = super::declarations::class_accessor_decl_bytes(class_id, name)?; (decl.get != 0 || decl.set != 0).then_some((decl.get, decl.set)) } @@ -238,6 +235,13 @@ pub(crate) fn class_own_accessor_ptrs(class_id: u32, name: &str) -> Option<(usiz pub(crate) fn class_registered_static_accessor_ptrs( class_id: u32, name: &str, +) -> Option<(usize, usize)> { + class_registered_static_accessor_ptrs_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_registered_static_accessor_ptrs_bytes( + class_id: u32, + name: &[u8], ) -> Option<(usize, usize)> { let guard = CLASS_STATIC_ACCESSORS.read().ok()?; let reg = guard.as_ref()?; @@ -245,14 +249,16 @@ pub(crate) fn class_registered_static_accessor_ptrs( (decl.get != 0 || decl.set != 0).then_some((decl.get, decl.set)) } -/// The recorded spec `.length` of `class_id`'s own setter `name` (static when -/// `is_static`), for [`class_accessor_function_value`]. -pub(crate) fn class_own_setter_length(class_id: u32, name: &str, is_static: bool) -> Option { +pub(crate) fn class_own_setter_length_bytes( + class_id: u32, + name: &[u8], + is_static: bool, +) -> Option { if is_static { let guard = CLASS_STATIC_ACCESSORS.read().ok()?; guard.as_ref()?.get(&class_id)?.get(name)?.set_length } else { - super::declarations::class_accessor_decl(class_id, name)?.set_length + super::declarations::class_accessor_decl_bytes(class_id, name)?.set_length } } @@ -388,6 +394,22 @@ pub(crate) fn class_accessor_function_value( is_static: bool, prop_name: &str, setter_length: Option, +) -> f64 { + class_accessor_function_value_bytes( + raw_ptr, + is_setter, + is_static, + prop_name.as_bytes(), + setter_length, + ) +} + +pub(crate) fn class_accessor_function_value_bytes( + raw_ptr: usize, + is_setter: bool, + is_static: bool, + prop_name: &[u8], + setter_length: Option, ) -> f64 { if raw_ptr == 0 { return f64::from_bits(crate::value::TAG_UNDEFINED); @@ -404,7 +426,11 @@ pub(crate) fn class_accessor_function_value( if closure.is_null() { return f64::from_bits(crate::value::TAG_UNDEFINED); } - crate::closure::js_closure_set_capture_ptr(closure, 0, raw_ptr as i64); + let scope = crate::gc::RuntimeHandleScope::new(); + let closure = scope.root_raw_mut_ptr(closure); + closure.with_mut_ptr(|closure| { + crate::closure::js_closure_set_capture_ptr(closure, 0, raw_ptr as i64) + }); // Spec `.length`: params before the first default/rest. A getter takes no // params (0); a setter takes exactly one formal param — but `set m(x = 42)` // has `.length === 0` (defaults don't count). Codegen records the setter's @@ -416,24 +442,39 @@ pub(crate) fn class_accessor_function_value( } else { 0 }; - super::super::native_module::set_builtin_closure_length(closure as usize, spec_length); - super::super::native_module::set_builtin_closure_non_constructable(closure as usize); + closure.with_mut_ptr::(|closure| { + super::super::native_module::set_builtin_closure_length(closure as usize, spec_length); + super::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 prefix = if is_setter { + b"set ".as_slice() + } else { + b"get ".as_slice() + }; + let mut fn_name = Vec::with_capacity(prefix.len() + prop_name.len()); + fn_name.extend_from_slice(prefix); + fn_name.extend_from_slice(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()); - crate::closure::closure_define_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_raw_ptr(closure as *const u8); - crate::value::js_nanbox_pointer(closure as i64) + let name = scope.root_string_ptr(name_ptr); + closure.with_mut_ptr::(|closure| { + let name_value = name.with_const_ptr::(|name| { + f64::from_bits( + crate::value::JSValue::string_ptr(name as *mut crate::StringHeader).bits(), + ) + }); + crate::closure::closure_define_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_raw_ptr(closure as *const u8); + crate::value::js_nanbox_pointer(closure as i64) + }) } /// Unit tests declare a class's accessor halves one at a time (codegen emits @@ -538,10 +579,7 @@ pub unsafe extern "C" fn js_register_class_method_bind_length( if name_ptr.is_null() || name_len < 0 { return; } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - }; + let name = std::slice::from_raw_parts(name_ptr, name_len as usize).to_vec(); let mut guard = match CLASS_METHOD_BIND_LENGTHS.write() { Ok(g) => g, Err(_) => return, @@ -572,10 +610,7 @@ pub unsafe extern "C" fn js_register_class_static_method_bind_length( if name_ptr.is_null() || name_len < 0 { return; } - let name = match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - }; + let name = std::slice::from_raw_parts(name_ptr, name_len as usize).to_vec(); let mut guard = match CLASS_STATIC_METHOD_BIND_LENGTHS.write() { Ok(g) => g, Err(_) => return, @@ -607,10 +642,7 @@ unsafe fn register_class_static_accessor_half( let name = if name_ptr.is_null() || name_len < 0 { return; } else { - match std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len as usize)) { - Ok(s) => s.to_string(), - Err(_) => return, - } + std::slice::from_raw_parts(name_ptr, name_len as usize).to_vec() }; { let mut guard = CLASS_STATIC_ACCESSORS.write().unwrap(); @@ -631,5 +663,5 @@ unsafe fn register_class_static_accessor_half( entry.set_length = set_length; } } - crate::object::class_value::note_intrinsic_registration(class_id as u32, &name); + crate::object::class_value::note_intrinsic_registration_bytes(class_id as u32, &name); } diff --git a/crates/perry-runtime/src/object/class_registry/state.rs b/crates/perry-runtime/src/object/class_registry/state.rs index c2c3e2bb35..f551b2230f 100644 --- a/crates/perry-runtime/src/object/class_registry/state.rs +++ b/crates/perry-runtime/src/object/class_registry/state.rs @@ -88,6 +88,10 @@ pub(crate) fn throw_non_constructable_builtin_function() -> ! { /// that owns the member: it was declared, the class's decl prototype exists, /// and that object no longer holds the key. pub(crate) fn class_proto_key_deleted(class_id: u32, name: &str) -> bool { + class_proto_key_deleted_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_proto_key_deleted_bytes(class_id: u32, name: &[u8]) -> bool { if class_id == 0 { return false; } @@ -101,7 +105,7 @@ pub(crate) fn class_proto_key_deleted(class_id: u32, name: &str) -> bool { if unsafe { crate::object::shapes::pristine_declaration_holder(proto) } { return false; } - if proto_member_has_no_string_key(class_id, name) { + if proto_member_has_no_string_key_bytes(class_id, name) { return false; } // SAFETY: `proto` is this realm's live decl prototype; nothing below @@ -110,8 +114,7 @@ pub(crate) fn class_proto_key_deleted(class_id: u32, name: &str) -> bool { let keys = crate::object::object_keys(proto); let arr = keys.arr(); !arr.is_null() - && crate::object::keys_find_slot_by_bytes_resolved(arr, keys.count(), name.as_bytes()) - .is_some() + && crate::object::keys_find_slot_by_bytes_resolved(arr, keys.count(), name).is_some() } } @@ -122,8 +125,8 @@ pub(crate) fn class_proto_key_deleted(class_id: u32, name: &str) -> bool { /// well-known-symbol method (`@@iterator` stands in for `[Symbol.iterator]`, /// which lives under its symbol key). A source method literally named /// `"@@iterator"` has a string-member order registration and is a real key. -pub(crate) fn proto_member_has_no_string_key(class_id: u32, name: &str) -> bool { - if name.starts_with('#') { +pub(crate) fn proto_member_has_no_string_key_bytes(class_id: u32, name: &[u8]) -> bool { + if name.starts_with(b"#") { return true; } internal_symbol_dispatch_alias(name) @@ -133,7 +136,7 @@ pub(crate) fn proto_member_has_no_string_key(class_id: u32, name: &str) -> bool .and_then(|guard| { guard .as_ref() - .map(|map| map.contains_key(&(class_id, false, name.to_string()))) + .map(|map| map.contains_key(&(class_id, false, name.to_vec()))) }) .unwrap_or(false) } @@ -145,6 +148,10 @@ pub(crate) fn class_static_key_deleted(class_id: u32, name: &str) -> bool { crate::object::class_value::class_static_key_deleted(class_id, name) } +pub(crate) fn class_static_key_deleted_bytes(class_id: u32, name: &[u8]) -> bool { + crate::object::class_value::class_static_key_deleted_bytes(class_id, name) +} + /// Record `C. = value` in the class-ref side table that dynamic reads /// (`const K: any = C; K.name`, `Object.keys(C)`, `getOwnPropertyDescriptor`) /// consult, and shade the stored value for the incremental marker. @@ -232,7 +239,11 @@ pub(crate) fn class_ref_dynamic_prop_root_store(class_id: u32, name: &str, value /// constructor ref so `verifyProperty(C, "field", …)` sees a real data /// descriptor (test262 class/elements static-field-declaration & friends). pub(crate) fn class_own_static_field_value(class_id: u32, name: &str) -> Option { - crate::object::class_value::class_static_get(class_id, name) + class_own_static_field_value_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_own_static_field_value_bytes(class_id: u32, name: &[u8]) -> Option { + crate::object::class_value::class_static_get_bytes(class_id, name) } /// Enumerable own string keys of a class constructor: the static fields (and @@ -270,6 +281,22 @@ pub(crate) fn class_static_set_defined_attrs( writable: bool, enumerable: bool, configurable: bool, +) { + class_static_set_defined_attrs_bytes( + class_id, + name.as_bytes(), + writable, + enumerable, + configurable, + ) +} + +pub(crate) fn class_static_set_defined_attrs_bytes( + class_id: u32, + name: &[u8], + writable: bool, + enumerable: bool, + configurable: bool, ) { { let _no_collect = crate::gc::GcSuppressScope::new(); @@ -277,13 +304,15 @@ pub(crate) fn class_static_set_defined_attrs( // SAFETY: this agent's live class function object; no collection in // this scope. let bag = unsafe { crate::closure::props::bag_ensure(ptr) }; - crate::object::set_builtin_property_attrs( + crate::object::descriptor_state::set_builtin_property_attrs_bytes( bag as usize, - name.to_string(), + name, crate::object::PropertyAttrs::new(writable, enumerable, configurable), ); } - class_static_alias_sync(class_id, name); + if let Ok(name) = std::str::from_utf8(name) { + class_static_alias_sync(class_id, name); + } } /// Static `name` becomes an ordinary writable, enumerable, configurable data @@ -308,16 +337,23 @@ pub(crate) fn class_static_clear_defined_attrs(class_id: u32, name: &str) { /// the key of the function object's own-property object and never mints the /// function object (one never created owns no properties). pub(crate) fn class_static_defined_attrs(class_id: u32, name: &str) -> Option<(bool, bool, bool)> { + class_static_defined_attrs_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_static_defined_attrs_bytes( + class_id: u32, + name: &[u8], +) -> Option<(bool, bool, bool)> { let ptr = crate::object::class_value::class_value_if_minted(class_id)? as usize; // SAFETY: this agent's live class function object. unsafe { - if crate::object::is_internal_runtime_key(name) - || crate::closure::props::bag_get(ptr, name.as_bytes()).is_none() + if std::str::from_utf8(name).is_ok_and(crate::object::is_internal_runtime_key) + || crate::closure::props::bag_get(ptr, name).is_none() { return None; } let bag = crate::closure::props::bag_of(ptr); - let attrs = crate::object::get_property_attrs(bag as usize, name) + let attrs = crate::object::descriptor_state::get_property_attrs_bytes(bag as usize, name) .unwrap_or(crate::object::PropertyAttrs::new(true, true, true)); Some((attrs.writable(), attrs.enumerable(), attrs.configurable())) } @@ -359,18 +395,16 @@ fn method_values(holder: usize) -> Option<*mut crate::ArrayHeader> { /// Does (`kind`, `name`) name a method of the declaration, and is that the /// kind it is kept as? A name has one kind: `proto_member_has_no_string_key`. -fn declared_method_index( +fn declared_method_index_bytes( class_id: u32, kind: ClassDeclarationValueKind, - name: impl AsRef<[u8]>, + name: &[u8], ) -> Option<(usize, usize)> { - let name = name.as_ref(); - let non_property = - std::str::from_utf8(name).is_ok_and(|name| proto_member_has_no_string_key(class_id, name)); + let non_property = proto_member_has_no_string_key_bytes(class_id, name); if non_property != (kind == ClassDeclarationValueKind::NonPropertyMethod) { return None; } - super::declarations::class_method_decl_index(class_id, name) + super::declarations::class_method_decl_index_bytes(class_id, name) } /// Class `class_id`'s declaration value (`kind`, `name`), if its holder @@ -398,7 +432,7 @@ pub(crate) fn class_declaration_value_bytes( } .map(f64::to_bits); } - let (index, _) = declared_method_index(class_id, kind, name)?; + let (index, _) = declared_method_index_bytes(class_id, kind, name)?; let values = method_values(holder)?; let bits = crate::array::js_array_get_f64(values, index as u32).to_bits(); (bits != crate::value::TAG_UNDEFINED).then_some(bits) @@ -432,10 +466,10 @@ pub(crate) fn class_declaration_value_store_bytes( }; let holder = holder as usize; if kind == ClassDeclarationValueKind::PrivateStaticMethod { - let key = class_declaration_value_key( - kind, - std::str::from_utf8(name).expect("private identifier"), - ); + let Ok(name) = std::str::from_utf8(name) else { + return; + }; + let key = class_declaration_value_key(kind, name); // SAFETY: this agent's live, pinned class function object; the // internal write allocates under its own GcSuppressScope and stores // through the state record's barriered slot. @@ -444,7 +478,7 @@ pub(crate) fn class_declaration_value_store_bytes( }; return; } - let Some((index, count)) = declared_method_index(class_id, kind, name) else { + let Some((index, count)) = declared_method_index_bytes(class_id, kind, name) else { return; }; let scope = crate::gc::RuntimeHandleScope::new(); @@ -563,13 +597,13 @@ pub static CLASS_STRING_MEMBER_ORDERS: ImageTable>>> = +pub static CLASS_METHOD_BIND_LENGTHS: ImageTable), u32>>>> = ImageTable::new(|image| &image.method_bind_lengths); /// The closure-convention entry declared for method `name` of class /// `class_id`, if any. -pub(crate) fn class_method_entry(class_id: u32, name: impl AsRef<[u8]>) -> Option { - let entry = super::declarations::class_method_decl(class_id, name)?.entry as usize; +pub(crate) fn class_method_entry_bytes(class_id: u32, name: &[u8]) -> Option { + let entry = super::declarations::class_method_decl_bytes(class_id, name)?.entry as usize; (entry != 0).then_some(entry) } @@ -579,7 +613,7 @@ pub(crate) fn class_method_entry(class_id: u32, name: impl AsRef<[u8]>) -> Optio /// colliding on the (class_id, name) key. (Test262 *-method-static /// dflt-params-trailing-comma.) pub static CLASS_STATIC_METHOD_BIND_LENGTHS: ImageTable< - RwLock>>, + RwLock), u32>>>, > = ImageTable::new(|image| &image.static_method_bind_lengths); crate::perry_thread_local! { @@ -1069,43 +1103,34 @@ pub(crate) fn class_decl_prototype_object(class_id: u32) -> *mut ObjectHeader { crate::object::class_value::class_decl_prototype_link(decl_prototype_identity_id(class_id)) } -fn internal_symbol_dispatch_alias(name: &str) -> bool { +fn internal_symbol_dispatch_alias(name: &[u8]) -> bool { matches!( name, - "@@iterator" - | "@@asyncIterator" - | "@@toPrimitive" - | "__perry_dispose__" - | "__perry_async_dispose__" - | "__perry_inspect_custom__" + b"@@iterator" + | b"@@asyncIterator" + | b"@@toPrimitive" + | b"__perry_dispose__" + | b"__perry_async_dispose__" + | b"__perry_inspect_custom__" ) } -fn order_class_string_member_names + Ord>( - class_id: u32, - is_static: bool, - names: &mut Vec, -) { +fn order_class_string_member_names(class_id: u32, is_static: bool, names: &mut Vec>) { + names.sort(); + names.dedup(); let orders = CLASS_STRING_MEMBER_ORDERS.read().ok(); let order_for = |name: &[u8]| { - std::str::from_utf8(name) - .ok() - .and_then(|name| { - orders - .as_ref() - .and_then(|guard| guard.as_ref()) - .and_then(|map| map.get(&(class_id, is_static, name.to_string())).copied()) - }) - .or_else(|| { - super::declarations::class_method_decl(class_id, name).map(|m| m.definition_order) - }) + orders + .as_ref() + .and_then(|guard| guard.as_ref()) + .and_then(|map| map.get(&(class_id, is_static, name.to_vec())).copied()) }; names.retain(|name| { let key = name.as_ref(); let order = order_for(key); order != Some(u32::MAX) && (order.is_some() - || !std::str::from_utf8(key).is_ok_and(internal_symbol_dispatch_alias)) + || !internal_symbol_dispatch_alias(key)) }); crate::cold_sort::sort_by(names, |left, right| { match (order_for(left.as_ref()), order_for(right.as_ref())) { @@ -1124,7 +1149,7 @@ fn order_class_string_member_names + Ord>( /// Own string-keyed methods and accessors in ClassBody definition order. /// The dispatch registries intentionally remain hash maps; reflection is the /// only consumer that needs their cross-kind ordering. -pub(crate) fn class_own_string_member_names(class_id: u32, is_static: bool) -> Vec { +pub(crate) fn class_own_string_member_names(class_id: u32, is_static: bool) -> Vec> { let mut names = Vec::new(); if is_static { if let Ok(methods) = CLASS_STATIC_METHODS.read() { @@ -1157,15 +1182,13 @@ pub(crate) fn class_own_string_member_names(class_id: u32, is_static: bool) -> V for i in 0..keys.count() { let value = crate::array::js_array_get(keys.arr(), i); if let Some(bytes) = crate::string::js_string_key_bytes(value, &mut short) { - if let Ok(name) = std::str::from_utf8(bytes) { - names.push(name.to_string()); - } + names.push(bytes.to_vec()); } } } return names; } - names.retain(|name| !name.starts_with('#')); + names.retain(|name| !name.starts_with(b"#")); order_class_string_member_names(class_id, is_static, &mut names); names } @@ -1185,7 +1208,7 @@ pub(super) fn install_class_decl_prototype_method_field( // name and make the installation write the class constructor straight // back. The canonical vtable value is the property value we need. let method_handle = scope.root_nanbox_f64( - crate::object::native_module::class_prototype_method_value_for_bytes(class_id, name), + crate::object::native_module::class_prototype_method_value_for_name_bytes(class_id, name), ); let key_handle = scope.root_string_ptr(crate::string::js_string_from_bytes( name.as_ptr(), @@ -1201,10 +1224,11 @@ pub(super) fn install_class_decl_prototype_method_field( } }) }); - proto_handle.with_mut_ptr::(|proto| unsafe { - crate::object::key_attrs::apply_edits( - proto, - &[crate::object::key_attrs::AttrsEdit::Data(name, 5)], + proto_handle.with_mut_ptr::(|proto| { + crate::object::descriptor_state::set_builtin_property_attrs_bytes( + proto as usize, + name, + PropertyAttrs::new(true, false, true), ) }); } @@ -1350,10 +1374,8 @@ fn install_class_decl_prototype_method_fields(proto: *mut ObjectHeader, class_id // Both installers root the prototype across their own allocations. proto_h.with_mut_ptr(|proto: *mut ObjectHeader| { if is_accessor { - super::decl_accessors::install_decl_prototype_accessor( - proto, - class_id, - std::str::from_utf8(&name).expect("accessor name"), + super::decl_accessors::install_decl_prototype_accessor_bytes( + proto, class_id, &name, ); } else { install_class_decl_prototype_method_field(proto, class_id, &name); diff --git a/crates/perry-runtime/src/object/class_value.rs b/crates/perry-runtime/src/object/class_value.rs index f0b104327a..57334a262e 100644 --- a/crates/perry-runtime/src/object/class_value.rs +++ b/crates/perry-runtime/src/object/class_value.rs @@ -445,8 +445,8 @@ fn class_value_mint(class_id: u32) -> *mut ClosureHeader { } // ClassBody static methods and accessors, in ClassBody order. for key in super::class_registry::class_own_string_member_names(class_id, true) { - install_declared_static_accessor(class_id, &key); - install_declared_static_method(class_id, &key); + install_declared_static_accessor_bytes(class_id, &key); + install_declared_static_method_bytes(class_id, &key); } ptr } @@ -474,8 +474,13 @@ pub(crate) fn intrinsic_own_data_value(class_id: u32, key: &str) -> Option /// Does a static method or accessor of class `class_id` own `key`? Then it, /// not the intrinsic data property, is the class's own `key`. pub(crate) fn static_member_owns(class_id: u32, key: &str) -> bool { - super::class_registry::class_has_own_static_method(class_id, key) - || super::class_registry::class_registered_static_accessor_ptrs(class_id, key).is_some() + static_member_owns_bytes(class_id, key.as_bytes()) +} + +pub(crate) fn static_member_owns_bytes(class_id: u32, key: &[u8]) -> bool { + super::class_registry::parent_static::class_has_own_static_method_bytes(class_id, key) + || super::class_registry::class_registered_static_accessor_ptrs_bytes(class_id, key) + .is_some() } /// Is own `key` of class `class_id` still the intrinsic data property (not @@ -527,19 +532,25 @@ pub(crate) fn note_static_field_defined(class_id: u32, key: &str) { /// property in line. A key the program already redefined or deleted is left /// alone. pub(crate) fn note_intrinsic_registration(class_id: u32, key: &str) { + note_intrinsic_registration_bytes(class_id, key.as_bytes()); +} + +pub(crate) fn note_intrinsic_registration_bytes(class_id: u32, key: &[u8]) { if class_value_cached(class_id).is_none() { return; } let _no_collect = crate::gc::GcSuppressScope::new(); // A ClassBody static accessor registered after the object exists (a // computed key registers when the class definition evaluates). - if super::class_registry::class_registered_static_accessor_ptrs(class_id, key).is_some() { - install_declared_static_accessor(class_id, key); + if super::class_registry::class_registered_static_accessor_ptrs_bytes(class_id, key).is_some() { + install_declared_static_accessor_bytes(class_id, key); } - if INTRINSIC_OWN_DATA_KEYS.contains(&key) { - note_intrinsic_key_registration(class_id, key); + if let Ok(key) = std::str::from_utf8(key) { + if INTRINSIC_OWN_DATA_KEYS.contains(&key) { + note_intrinsic_key_registration(class_id, key); + } } - install_declared_static_method(class_id, key); + install_declared_static_method_bytes(class_id, key); } fn note_intrinsic_key_registration(class_id: u32, key: &str) { @@ -552,7 +563,12 @@ fn note_intrinsic_key_registration(class_id: u32, key: &str) { } } else if class_static_get(class_id, key).is_none() // SAFETY: minted above (`class_value_cached`). - && !unsafe { crate::closure::props::state_is_deleted(class_value_ptr(class_id) as usize, key) } + && !unsafe { + crate::closure::props::state_is_deleted_bytes( + class_value_ptr(class_id) as usize, + key.as_bytes(), + ) + } { install_intrinsic_own_data(class_id, key); } @@ -575,17 +591,22 @@ pub(crate) fn note_static_key_deleted(class_id: u32, name: &str) { /// Derived from the function object: the member was declared and the object /// no longer has the key. A never-minted object has deleted nothing. pub(crate) fn class_static_key_deleted(class_id: u32, name: &str) -> bool { - if name.starts_with('#') || is_internal_static_key(name) { + class_static_key_deleted_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_static_key_deleted_bytes(class_id: u32, name: &[u8]) -> bool { + if name.starts_with(b"#") || is_internal_static_key_bytes(name) { return false; } let Some(ptr) = class_value_if_minted(class_id) else { return false; }; - let declared = static_member_owns(class_id, name) - || (INTRINSIC_OWN_DATA_KEYS.contains(&name) - && intrinsic_own_data_registered(class_id, name)); + let declared = static_member_owns_bytes(class_id, name) + || std::str::from_utf8(name).ok().is_some_and(|name| { + INTRINSIC_OWN_DATA_KEYS.contains(&name) && intrinsic_own_data_registered(class_id, name) + }); // SAFETY: this agent's live class closure; the lookup does not allocate. - declared && !unsafe { crate::closure::props::bag_has_own(ptr as usize, name.as_bytes()) } + declared && !unsafe { crate::closure::props::bag_has_own(ptr as usize, name) } } fn intrinsic_own_data_registered(class_id: u32, key: &str) -> bool { @@ -612,9 +633,9 @@ pub(crate) enum StaticMethodProperty { /// `declared` is its own ClassBody declaration of `name` (the declaration's /// closure-convention entry, 0 when it has none), `None` when it declares no /// such method. -pub(crate) fn static_method_property( +pub(crate) fn static_method_property_bytes( class_id: u32, - name: &str, + name: &[u8], declared: Option, ) -> StaticMethodProperty { let live_or_next = if declared.is_some() { @@ -622,7 +643,7 @@ pub(crate) fn static_method_property( } else { StaticMethodProperty::Deleted }; - if name.starts_with('#') || is_internal_static_key(name) { + if name.starts_with(b"#") || is_internal_static_key_bytes(name) { return live_or_next; } // A template evaluated to class objects keeps its statics on those objects @@ -641,12 +662,12 @@ pub(crate) fn static_method_property( let code = declared.unwrap_or(0); // SAFETY: this agent's live class closure; nothing below allocates. unsafe { - match crate::closure::props::bag_get(ptr as usize, name.as_bytes()) { + match crate::closure::props::bag_get(ptr as usize, name) { Some(v) if code != 0 && static_method_code_of(v) == Some(code) => { StaticMethodProperty::Live } Some(_) => StaticMethodProperty::Replaced, - None if crate::closure::props::bag_has_own(ptr as usize, name.as_bytes()) => { + None if crate::closure::props::bag_has_own(ptr as usize, name) => { StaticMethodProperty::Replaced } // A declaration without an entry is never installed as a property. @@ -675,10 +696,14 @@ unsafe fn static_method_code_of(value: f64) -> Option { /// function object: its value, minting the owner's function object (so every /// subclass reads the one own property, `Q.a === P.a`). pub(crate) fn inherited_static_method_value(class_id: u32, name: &str) -> Option { - let (owner, _) = super::class_registry::lookup_static_method_owner(class_id, name)?; - super::class_registry::class_own_static_method_code(owner, name)?; + inherited_static_method_value_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn inherited_static_method_value_bytes(class_id: u32, name: &[u8]) -> Option { + let (owner, _) = super::class_registry::lookup_static_method_owner_bytes(class_id, name)?; + super::class_registry::class_own_static_method_code_bytes(owner, name)?; class_value(owner); - class_static_get(owner, name) + class_static_get_bytes(owner, name) } /// Does own `key` of the class function object `ptr` hold its ClassBody @@ -686,9 +711,13 @@ pub(crate) fn inherited_static_method_value(class_id: u32, name: &str) -> Option /// part of the object's shape: a write that ends it transitions the shape /// (`object::class_value::js_class_static_call_guard`). pub(crate) unsafe fn holds_declared_static_method(ptr: usize, key: &str) -> Option { + holds_declared_static_method_bytes(ptr, key.as_bytes()) +} + +pub(crate) unsafe fn holds_declared_static_method_bytes(ptr: usize, key: &[u8]) -> Option { let cid = class_closure_id_unchecked(ptr as *const ClosureHeader)?; - let code = super::class_registry::class_own_static_method_code(cid, key)?; - let v = crate::closure::props::bag_get(ptr, key.as_bytes())?; + let code = super::class_registry::parent_static::class_own_static_method_code_bytes(cid, key)?; + let v = crate::closure::props::bag_get(ptr, key)?; let js = crate::JSValue::from_bits(v.to_bits()); if !js.is_pointer() { return None; @@ -880,24 +909,26 @@ pub(crate) fn class_static_owns_method(class_id: u32, name: &str) -> bool { /// declaration's code (a resolved entry, never a by-name dispatch). A /// property the program redefined is left alone; a re-registered entry /// (a class expression evaluated again) refreshes the function. -fn install_declared_static_method(class_id: u32, name: &str) { - if name.starts_with('#') || is_internal_static_key(name) { +fn install_declared_static_method_bytes(class_id: u32, name: &[u8]) { + if name.starts_with(b"#") || is_internal_static_key_bytes(name) { return; } - let Some(code) = super::class_registry::class_own_static_method_code(class_id, name) else { + let Some(code) = + super::class_registry::parent_static::class_own_static_method_code_bytes(class_id, name) + else { return; }; let ptr = class_value_ptr(class_id) as usize; // SAFETY: this agent's live class closure. unsafe { - match crate::closure::props::bag_get(ptr, name.as_bytes()) { + match crate::closure::props::bag_get(ptr, name) { // Already this declaration's function, or a value the program // put in its place: leave it. Some(_) => return, - None if crate::closure::props::bag_has_own(ptr, name.as_bytes()) - || crate::closure::props::state_is_deleted(ptr, name) => + None if crate::closure::props::bag_has_own(ptr, name) + || crate::closure::props::state_is_deleted_bytes(ptr, name) => { - return + return; } None => {} } @@ -907,14 +938,12 @@ fn install_declared_static_method(class_id: u32, name: &str) { if f.is_null() { return; } - unsafe { - crate::closure::closure_install_boxed_captures( - f, - &[crate::value::js_nanbox_pointer(ptr as i64).to_bits()], - ); - } - class_static_set(class_id, name, crate::value::js_nanbox_pointer(f as i64)); - super::class_registry::class_static_set_defined_attrs(class_id, name, true, false, true); + crate::object::native_module::set_function_name_bytes( + crate::value::js_nanbox_pointer(f as i64), + name, + ); + class_static_set_bytes(class_id, name, crate::value::js_nanbox_pointer(f as i64)); + super::class_registry::class_static_set_defined_attrs_bytes(class_id, name, true, false, true); // The object's shape now carries "own `name` is this declaration": a // process-unique successor, so no object that got `name` any other way // (and no other agent's object) shares it (`js_class_static_call_guard`). @@ -1181,17 +1210,17 @@ pub(crate) const CLASS_ACCESSOR_DEFAULT_ATTRS: (bool, bool) = (false, true); /// entry is unchanged keeps its closure, so reflection hands out the same /// function every time; attributes a `defineProperty` set are kept. /// Private (`#x`) accessors are not properties and are never installed. -fn install_declared_static_accessor(class_id: u32, name: &str) { - if name.starts_with('#') { +fn install_declared_static_accessor_bytes(class_id: u32, name: &[u8]) { + if name.starts_with(b"#") { return; } let Some((raw_get, raw_set)) = - super::class_registry::class_registered_static_accessor_ptrs(class_id, name) + super::class_registry::class_registered_static_accessor_ptrs_bytes(class_id, name) else { return; }; let _no_collect = crate::gc::GcSuppressScope::new(); - let existing = class_static_own_accessor(class_id, name); + let existing = class_static_own_accessor_bytes(class_id, name); let (have, enumerable, configurable) = match existing { Some((acc, e, c)) => (acc, e, c), None => ( @@ -1207,11 +1236,11 @@ fn install_declared_static_accessor(class_id: u32, name: &str) { have } else { let setter_length = if is_setter { - super::class_registry::class_own_setter_length(class_id, name, true) + super::class_registry::class_own_setter_length_bytes(class_id, name, true) } else { None }; - super::class_registry::class_accessor_function_value( + super::class_registry::class_accessor_function_value_bytes( raw, is_setter, true, @@ -1223,7 +1252,7 @@ fn install_declared_static_accessor(class_id: u32, name: &str) { }; let get = half(raw_get, have.static_get, have.get, false); let set = half(raw_set, have.static_set, have.set, true); - class_static_define_accessor( + class_static_define_accessor_bytes( class_id, name, crate::object::accessor_pair::Accessor { @@ -1244,6 +1273,13 @@ fn install_declared_static_accessor(class_id: u32, name: &str) { pub(crate) fn class_static_own_accessor( class_id: u32, name: &str, +) -> Option<(crate::object::accessor_pair::Accessor, bool, bool)> { + class_static_own_accessor_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_static_own_accessor_bytes( + class_id: u32, + name: &[u8], ) -> Option<(crate::object::accessor_pair::Accessor, bool, bool)> { // Static parent walks can reach registered builtin/synthetic ids. Those // have their own dispatch and no compiled-class function object to query. @@ -1260,11 +1296,11 @@ pub(crate) fn class_static_own_accessor( if bag.is_null() { return None; } - let entry = ka::object_key_entry(bag, name.as_bytes()); + let entry = ka::object_key_entry(bag, name); if entry & ka::ENTRY_ACCESSOR == 0 { return None; } - let acc = crate::object::accessor_pair::own_accessor(bag as usize, name.as_bytes())?; + let acc = crate::object::accessor_pair::own_accessor(bag as usize, name)?; Some(( acc, entry & ka::ENTRY_NON_ENUMERABLE == 0, @@ -1286,14 +1322,24 @@ pub(crate) fn class_static_define_accessor( acc: crate::object::accessor_pair::Accessor, enumerable: bool, configurable: bool, +) { + class_static_define_accessor_bytes(class_id, name.as_bytes(), acc, enumerable, configurable) +} + +pub(crate) fn class_static_define_accessor_bytes( + class_id: u32, + name: &[u8], + acc: crate::object::accessor_pair::Accessor, + enumerable: bool, + configurable: bool, ) { let _no_collect = crate::gc::GcSuppressScope::new(); let ptr = class_value_ptr(class_id) as usize; // SAFETY: this agent's live class closure; no collection in this scope. let bag = unsafe { crate::closure::props::bag_ensure(ptr) }; - crate::object::set_builtin_accessor_pair( + crate::object::descriptor_state::set_builtin_accessor_pair_bytes( bag as usize, - name.to_string(), + name, acc, crate::object::PropertyAttrs::new(false, enumerable, configurable), ); @@ -1318,6 +1364,12 @@ pub(crate) fn class_static_accessor_names(class_id: u32) -> Vec { unsafe { crate::closure::props::bag_accessor_names(ptr) } } +pub(crate) fn class_static_accessor_names_bytes(class_id: u32) -> Vec> { + let ptr = class_value_ptr(class_id) as usize; + // SAFETY: this agent's live class closure. + unsafe { crate::closure::props::bag_accessor_names_bytes(ptr) } +} + /// Run a class static accessor's getter for `receiver`. The compiled /// ClassBody entry takes the static convention: `this` is armed (the class /// the read started from — a stashed override — or `receiver`) and the @@ -1373,7 +1425,11 @@ pub(crate) unsafe fn class_static_accessor_call_set( /// `Object.getOwnPropertyDescriptor(C, name)` for an own accessor of the class. pub(crate) fn class_static_accessor_descriptor(class_id: u32, name: &str) -> Option { - let (acc, enumerable, configurable) = class_static_own_accessor(class_id, name)?; + class_static_accessor_descriptor_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_static_accessor_descriptor_bytes(class_id: u32, name: &[u8]) -> Option { + let (acc, enumerable, configurable) = class_static_own_accessor_bytes(class_id, name)?; let undef = crate::value::TAG_UNDEFINED; // SAFETY: both halves are the property's own closure values (or // undefined); the builder roots them across its allocation. @@ -1395,14 +1451,18 @@ pub(crate) fn class_static_accessor_descriptor(class_id: u32, name: &str) -> Opt /// class captures): stored in the function object's internal state record, /// never as a property. #[inline] -fn is_internal_static_key(name: &str) -> bool { - crate::object::is_internal_runtime_key(name) +fn is_internal_static_key_bytes(name: &[u8]) -> bool { + std::str::from_utf8(name).is_ok_and(crate::object::is_internal_runtime_key) } /// Class `class_id`'s own static data property `name` (a declared static /// field or a runtime `C.x = v`): a slot of its function object's own-property /// bag. pub(crate) fn class_static_get(class_id: u32, name: &str) -> Option { + class_static_get_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_static_get_bytes(class_id: u32, name: &[u8]) -> Option { // A registered builtin parent has no compiled-class function object. // Let the caller continue to its builtin static dispatch on a miss. if class_id == 0 || class_id >= 0x7FFF_FF00 { @@ -1411,10 +1471,12 @@ pub(crate) fn class_static_get(class_id: u32, name: &str) -> Option { let ptr = class_value_ptr(class_id) as usize; // SAFETY: `class_value_ptr` returns this agent's live class closure. unsafe { - if is_internal_static_key(name) { - crate::closure::props::state_internal_get(ptr, name) + if is_internal_static_key_bytes(name) { + std::str::from_utf8(name) + .ok() + .and_then(|name| crate::closure::props::state_internal_get(ptr, name)) } else { - crate::closure::props::bag_get(ptr, name.as_bytes()) + crate::closure::props::bag_get(ptr, name) } } } @@ -1423,13 +1485,19 @@ pub(crate) fn class_static_get(class_id: u32, name: &str) -> Option { /// value only, the key keeps its attributes. Callers performing a [[Set]] /// have checked `writable` (the attributes live with the key). pub(crate) fn class_static_set(class_id: u32, name: &str, value: f64) { + class_static_set_bytes(class_id, name.as_bytes(), value) +} + +pub(crate) fn class_static_set_bytes(class_id: u32, name: &[u8], value: f64) { let ptr = class_value_ptr(class_id) as usize; // SAFETY: as above; the bag writers run under a GcSuppressScope. unsafe { - if is_internal_static_key(name) { - crate::closure::props::state_internal_set(ptr, name, value); + if is_internal_static_key_bytes(name) { + if let Ok(name) = std::str::from_utf8(name) { + crate::closure::props::state_internal_set(ptr, name, value); + } } else { - crate::closure::props::bag_define_value(ptr, name, value); + crate::closure::props::bag_define_value_bytes(ptr, name, value); } } } @@ -1437,13 +1505,19 @@ pub(crate) fn class_static_set(class_id: u32, name: &str, value: f64) { /// Remove class `class_id`'s own static data property `name`; true when it /// existed. pub(crate) fn class_static_remove(class_id: u32, name: &str) -> bool { + class_static_remove_bytes(class_id, name.as_bytes()) +} + +pub(crate) fn class_static_remove_bytes(class_id: u32, name: &[u8]) -> bool { let ptr = class_value_ptr(class_id) as usize; // SAFETY: as above. unsafe { - if is_internal_static_key(name) { - crate::closure::props::state_internal_remove(ptr, name) + if is_internal_static_key_bytes(name) { + std::str::from_utf8(name) + .ok() + .is_some_and(|name| crate::closure::props::state_internal_remove(ptr, name)) } else { - crate::closure::props::bag_remove(ptr, name) + crate::closure::props::bag_remove_bytes(ptr, name) } } } @@ -1500,495 +1574,8 @@ pub(crate) fn class_static_entries(class_id: u32) -> Vec<(String, f64)> { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn static_property_lookups_do_not_mint_builtin_class_values() { - for cid in [0, 0x7FFF_FF00, 0xFFFF_0024, u32::MAX] { - assert!(class_static_get(cid, "from").is_none()); - assert!(class_static_own_accessor(cid, "from").is_none()); - assert!(crate::object::class_registry::class_static_prototype(cid).is_null()); - assert!(!crate::object::class_registry::class_static_prototype_is_nulled(cid)); - assert!(class_value_cached(cid).is_none()); - } - } - - fn register(cid: u32) { - let mut guard = crate::object::REGISTERED_CLASS_IDS.write().unwrap(); - guard - .get_or_insert_with(crate::fast_hash::new_ptr_hash_set) - .insert(cid); - } - - /// #11414: a class value is ONE function object per class — never an - /// INT32 word a number can equal — pinned and old, so its address is its - /// identity across collections. - #[test] - fn class_value_is_one_pinned_function_object_per_class() { - let cid = 0x6A01; - register(cid); - let a = class_value(cid); - let b = class_value(cid); - assert_eq!(a.to_bits(), b.to_bits(), "one function object per class"); - let v = crate::value::JSValue::from_bits(a.to_bits()); - assert!( - !v.is_int32() && !v.is_number(), - "a class value is not a number" - ); - assert!(v.is_pointer()); - let ptr = (a.to_bits() & crate::value::POINTER_MASK) as usize; - assert!( - crate::closure::is_closure_ptr(ptr), - "a GC_TYPE_CLOSURE cell" - ); - assert_eq!(class_value_id(a), Some(cid)); - assert_eq!(class_closure_id(ptr), Some(cid)); - // The number equal to the class id is not the class. - assert_ne!( - f64::from_bits(crate::value::INT32_TAG | cid as u64).to_bits(), - a.to_bits() - ); - let header = unsafe { crate::value::addr_class::try_read_gc_header(ptr) }.expect("header"); - assert_ne!(header.gc_flags & crate::gc::GC_FLAG_PINNED, 0, "pinned"); - assert_ne!(header.gc_flags & crate::gc::GC_FLAG_TENURED, 0, "born old"); - crate::gc::js_gc_collect(); - assert_eq!(class_value(cid).to_bits(), a.to_bits(), "never moves"); - assert_eq!(class_value_id(a), Some(cid), "survives a full collection"); - let other = class_value(0x6A02); - assert_ne!(other.to_bits(), a.to_bits()); - assert_eq!(class_value_id(other), Some(0x6A02)); - } - - /// The class-value table is sized by how many ids of a band exist, never - /// by an id's magnitude: one class object for the highest id of every - /// band (fs.Stats' reserved `0xFFFF_0070` among them, which once grew the - /// directory to 16M pages — 128 MiB zero-filled — on a single - /// `stats.mtime` read) leaves every directory at most 256 pages. - #[test] - fn class_value_directories_are_sized_by_band_offset_not_by_id() { - let ids = [ - 0x7FFF_FF00, - 0x7FFF_FFFF, - SYNTHETIC_CLASS_ID_BASE, - 0xFFFF_0070, // fs.Stats (`fs::stats::STATS_REGULAR_CLASS_ID`) - 0xFFFF_0071, // bigint fs.Stats - crate::native_class_ids::CRYPTO_HASH, - u32::MAX, - ]; - // Not registered: a registered id is also an INT32 class-ref word - // (#11414), and `u32::MAX` would turn every `int32(-1)` into a class. - for cid in ids { - let v = class_value(cid); - assert_eq!(class_value_id(v), Some(cid), "{cid:#x} round-trips"); - assert_eq!(class_value(cid).to_bits(), v.to_bits(), "one object per id"); - } - for (band, (_, len)) in class_value_dirs().into_iter().enumerate() { - assert!( - len <= CLASS_VALUE_PAGE_LEN, - "band {band}: directory of {len} pages (sized by a raw class id)" - ); - } - // Distinct ids in distinct bands never share a slot. - let objs: Vec = ids.iter().map(|&c| class_value(c).to_bits()).collect(); - let mut unique = objs.clone(); - unique.sort_unstable(); - unique.dedup(); - assert_eq!(unique.len(), objs.len(), "every band id has its own object"); - // Non-class words have no slot and are never minted. - assert_eq!(class_value_band(0), None); - assert_eq!(class_value_band(0x8000_0000), None); - assert!(class_value_cached(0x8000_0000).is_none()); - } - - /// A static walk that reaches a BUILTIN parent (`class E extends Error`) - /// stops there: a builtin id has no class function object, so no reader - /// may mint one for it (minting `0xFFFF_0001` grew the class-value - /// directory to 16M pages, which every collection then scanned). - #[test] - fn a_builtin_parent_never_gets_a_class_function_object() { - let cid = 0x6D71; - register(cid); - crate::object::js_register_class_parent(cid, crate::error::CLASS_ID_ERROR); - let recv = class_value(cid); - let pages_before = class_value_dirs().map(|(_, len)| len); - let applied = unsafe { - crate::object::class_registry::class_static_accessor_setter_apply(cid, "zz", recv, 1.0) - }; - assert!(!applied, "no static accessor named zz on the chain"); - assert!(!crate::object::class_registry::static_accessor_in_chain( - cid, "zz" - )); - // A static symbol read up the chain (latch armed, as once any class - // has a static symbol member) reaches Error too and must not mint. - crate::symbol::CLASS_STATIC_SYMBOLS_LATCH.arm(); - let sym = unsafe { crate::symbol::js_symbol_new_empty() }; - assert!(crate::symbol::class_static_symbol_lookup_in_chain(cid, sym).is_none()); - assert!( - crate::symbol::class_static_symbol_keys_for_class(crate::error::CLASS_ID_ERROR) - .is_empty() - ); - // Object.getPrototypeOf(C), the static `super` parent value and a - // static `super[k] = v` all step to the parent id; none may mint. - let _ = crate::object::js_object_get_prototype_of(recv); - let _ = crate::object::class_registry::parent_static::template_dynamic_parent_value(cid); - let _ = crate::proxy::js_super_put_value_set( - crate::error::CLASS_ID_ERROR, - crate::value::js_nanbox_string( - crate::string::js_string_from_bytes(b"zz".as_ptr(), 2) as i64 - ), - 1.0, - recv, - 0, - ); - assert!( - class_value_cached(crate::error::CLASS_ID_ERROR).is_none(), - "the builtin Error id must not get a class function object" - ); - let pages_after = class_value_dirs().map(|(_, len)| len); - assert_eq!( - pages_after, pages_before, - "the walks grew the class-value directory" - ); - } - - /// A class function object is traced like any other pinned object: a full - /// collection reaches its own-property bag (the statics) through the - /// pinned-object root scan, with no class-value scanner. - #[test] - fn a_full_collection_keeps_the_class_statics_bag() { - let cid = 0x6B02; - register(cid); - // A unit-test thread may not have run `gc_init`'s scanner list. - crate::gc::gc_register_mutable_root_scanner(crate::gc::scan_pinned_object_roots_mut); - let text = "static-payload-11609"; - let s = crate::string::js_string_from_bytes(text.as_ptr(), text.len() as u32); - class_static_set( - cid, - "k11609", - f64::from_bits(crate::value::JSValue::string_ptr(s).bits()), - ); - crate::gc::js_gc_collect(); - crate::gc::js_gc_collect(); - let got = class_static_get(cid, "k11609").expect("the static survives a full collection"); - let got = crate::value::JSValue::from_bits(got.to_bits()); - let hdr = got.as_string_ptr(); - assert!(!hdr.is_null()); - let bytes = unsafe { crate::string::OwnedStringBytes::copy_from_header(hdr) }; - assert_eq!(bytes.as_bytes(), text.as_bytes()); - let keys: Vec = class_static_entries(cid) - .into_iter() - .map(|(k, _)| k) - .collect(); - assert!(keys.iter().any(|k| k == "k11609"), "own keys: {keys:?}"); - } - - /// The prototype link (capture 2) and the statics bag of an OLD, pinned - /// class hold young objects written after its birth: the slot barrier - /// remembers them, so a moving minor keeps them alive and rewrites both - /// edges. Dropping the barrier in `class_decl_prototype_link_store` makes - /// the link read back a stale address. - #[test] - fn young_values_in_an_old_class_survive_moving_minors() { - let _copying_nursery = crate::gc::CopyingNurseryTestGuard::new(0); - let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - let _force_evacuation = crate::gc::knob_overrides::ForcedEvacuationTestGuard::on(); - crate::gc::register_runtime_handle_root_scanner_for_tests(); - let cid = 0x6B04; - register(cid); - let _ = class_value_ptr(cid); - // The class is old and pinned from birth; everything below is young. - let proto = crate::object::js_object_alloc(0x6B05, 1); - crate::object::js_object_set_field( - proto, - 0, - crate::value::JSValue::from_bits(crate::value::INT32_TAG | 4242), - ); - let before = proto as usize; - class_decl_prototype_link_store(cid, proto); - let text = "young-static-6b04"; - let s = crate::string::js_string_from_bytes(text.as_ptr(), text.len() as u32); - class_static_set( - cid, - "k6b04", - f64::from_bits(crate::value::JSValue::string_ptr(s).bits()), - ); - for _ in 0..3 { - let _ = crate::gc::gc_collect_minor(); - } - let linked = class_decl_prototype_link(cid); - assert!(!linked.is_null()); - assert_ne!( - linked as usize, before, - "forced evacuation must have moved the young prototype" - ); - let field = crate::object::js_object_get_field(linked, 0); - assert_eq!(field.bits(), crate::value::INT32_TAG | 4242); - let got = class_static_get(cid, "k6b04").expect("the static survives minors"); - let hdr = crate::value::JSValue::from_bits(got.to_bits()).as_string_ptr(); - assert!(!hdr.is_null()); - let bytes = unsafe { crate::string::OwnedStringBytes::copy_from_header(hdr) }; - assert_eq!(bytes.as_bytes(), text.as_bytes()); - } - - /// Write once, then survive several minors with NO further writes. With - /// the promotion age pinned at 4 the young prototype stays young across - /// minors 1..=3, so the class (old, pinned) must stay in the minor scan - /// set the whole time: an entry is re-armed by every scan that still finds - /// a young referent, not consumed after one minor. Returns false (without - /// reading the possibly stale link) the first time the entry is missing. - fn write_once_then_minors(cid: u32, sabotage_drop_entry: bool) -> bool { - let _copying_nursery = crate::gc::CopyingNurseryTestGuard::new(0); - let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); - let _force_evacuation = crate::gc::knob_overrides::ForcedEvacuationTestGuard::on(); - let _age = crate::gc::pin_tenuring_survivals_for_test(4); - crate::gc::register_runtime_handle_root_scanner_for_tests(); - crate::gc::register_realm_prototype_scanners_for_tests(); - register(cid); - let closure = class_value_ptr(cid); - // The witness below is page-granular. Prototypes born old (I1, - // `gc::never_move`) can share the class's old page and keep it dirty - // through their own young members; age those members out first so - // the page's remembered state is this slot's alone. - for _ in 0..5 { - let _ = crate::gc::gc_collect_minor(); - } - assert!( - !crate::gc::old_slot_page_is_remembered_for_test( - unsafe { prototype_link_slot(closure) } as usize - ), - "premise: the class's page owes the remembered set nothing yet" - ); - let proto = crate::object::js_object_alloc(cid + 1, 1); - crate::object::js_object_set_field( - proto, - 0, - crate::value::JSValue::from_bits(crate::value::INT32_TAG | 777), - ); - class_decl_prototype_link_store(cid, proto); - let slot = unsafe { prototype_link_slot(closure) } as usize; - assert!( - crate::gc::old_slot_page_is_remembered_for_test(slot), - "the write must have remembered the class" - ); - let _sabotage = sabotage_drop_entry.then(|| { - crate::gc::copy_decode_sabotage::Guard::arm(crate::gc::copy_decode_sabotage::CHILD) - }); - let mut moved_from = proto as usize; - for minor in 1..=3 { - let _ = crate::gc::gc_collect_minor(); - // No write since the first store. The referent must still be young - // (age < 4), so the entry must still be present. - if !crate::gc::old_slot_page_is_remembered_for_test(slot) { - return false; - } - let linked = class_decl_prototype_link(cid); - assert!(!linked.is_null()); - assert!( - matches!( - crate::arena::classify_heap_generation(linked as usize), - crate::arena::HeapGeneration::Nursery - ), - "minor {minor}: the referent must still be young" - ); - assert_ne!( - linked as usize, moved_from, - "minor {minor}: it must have moved" - ); - moved_from = linked as usize; - let field = crate::object::js_object_get_field(linked, 0); - assert_eq!(field.bits(), crate::value::INT32_TAG | 777, "minor {minor}"); - } - // The fourth survival promotes it; the value is intact afterwards too. - let _ = crate::gc::gc_collect_minor(); - let linked = class_decl_prototype_link(cid); - let field = crate::object::js_object_get_field(linked, 0); - assert_eq!( - field.bits(), - crate::value::INT32_TAG | 777, - "after promotion" - ); - true - } - - #[test] - fn a_class_stays_remembered_while_its_referent_is_young() { - assert!(write_once_then_minors(0x6B10, false)); - } - - /// Sabotage: the scan loses the child, so the entry is dropped after the - /// first minor. The run must go red: either the collector's own - /// post-cycle coverage check fires (the expected panic) or the witness - /// above sees the entry missing and returns false (the `assert!` panic). - #[test] - #[should_panic] - fn dropping_the_entry_after_one_minor_is_caught() { - assert!(write_once_then_minors(0x6B20, true)); - } - - /// The kind is a shape fact: class function objects carry their own - /// ShapeId, distinct from FunctionDictionary (shapes are canonical per - /// facts — without its marker fact the class shape WOULD be the dictionary - /// id and every dictionary function would route as a class), and it is - /// sticky across own-property installs. - #[test] - fn class_function_objects_have_their_own_sticky_shape() { - let cid = 0x6C01; - register(cid); - let class_shape = crate::closure::shape::function_class_shape(); - assert_ne!( - class_shape, - crate::closure::shape::function_dictionary_shape() - ); - let ptr = class_value_ptr(cid); - assert_eq!(unsafe { (*ptr).shape_id }, class_shape); - class_static_set(cid, "s", 1.0); - crate::closure::shape::note_function_own_state_changed(ptr as usize); - assert_eq!(unsafe { (*ptr).shape_id }, class_shape, "sticky"); - extern "C" fn body(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { - 0.0 - } - let f = crate::closure::js_closure_alloc(crate::fn_info!(body, 0), 0); - crate::closure::shape::note_function_own_state_changed(f as usize); - assert_ne!( - unsafe { (*f).shape_id }, - class_shape, - "a dictionary function is not a class" - ); - } - - /// A class constructor's `length` and `name` are own data properties of - /// its function object (in its own-property object, intrinsic - /// attributes); a static method of that name owns the key instead. - #[test] - fn name_and_length_are_own_data_of_the_function_object() { - let cid = 0x6D01; - register(cid); - unsafe { crate::object::js_register_class_name(cid, b"Zed".as_ptr(), 3) }; - crate::object::js_register_class_length(cid, 2); - let ptr = class_value_ptr(cid) as usize; - assert_eq!( - unsafe { crate::closure::props::bag_get(ptr, b"length") }, - Some(2.0), - "own length" - ); - let name = unsafe { crate::closure::props::bag_get(ptr, b"name") }.expect("own name"); - let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; - // SAFETY: a live string value just read from the object. - let bytes = unsafe { - crate::string::js_string_key_bytes( - crate::value::JSValue::from_bits(name.to_bits()), - &mut scratch, - ) - } - .expect("a string"); - assert_eq!(bytes, b"Zed"); - for key in ["length", "name"] { - assert_eq!( - crate::object::class_registry::class_static_defined_attrs(cid, key), - Some(INTRINSIC_ATTRS), - "{key}: non-writable, non-enumerable, configurable" - ); - } - extern "C" fn static_name() -> f64 { - 0.0 - } - unsafe { - crate::object::class_registry::js_register_class_static_method( - cid as i64, - b"name".as_ptr(), - 4, - static_name as *const () as usize as i64, - 0, - 0, - ) - }; - extern "C" fn static_name_entry(_closure: i64) -> f64 { - 0.0 - } - unsafe { - crate::object::class_registry::parent_static::js_register_class_static_method_entry( - cid as i64, - b"name".as_ptr(), - 4, - static_name_entry as *const () as usize as i64, - ) - }; - // node: `class Zed { static name() {} }` -> Zed.name is the method, an - // own data property { writable, !enumerable, configurable }. - let method = unsafe { crate::closure::props::bag_get(ptr, b"name") } - .expect("a static method named `name` is the class's own `name`"); - assert_eq!( - unsafe { static_method_code_of(method) }, - Some(static_name_entry as *const () as usize), - "its value is the method's function object" - ); - assert_eq!( - crate::object::class_registry::class_static_defined_attrs(cid, "name"), - Some((true, false, true)), - "method attributes, not the intrinsic's" - ); - } - - /// A ClassBody static accessor is an accessor property of the class - /// function object: ClassBody attributes, one closure per half across - /// reads, attributes changed in place, and a delete removes it. - #[test] - fn static_accessors_are_accessor_properties_of_the_function_object() { - let cid = 0x6E01; - register(cid); - extern "C" fn getter() -> f64 { - 41.0 - } - unsafe { - crate::object::js_register_class_name(cid, b"Acc".as_ptr(), 3); - crate::object::class_registry::js_register_class_static_getter( - cid as i64, - b"g".as_ptr(), - 1, - getter as *const () as usize as i64, - ); - } - let (acc, enumerable, configurable) = - class_static_own_accessor(cid, "g").expect("an own accessor property"); - assert_ne!(acc.get, 0, "a reflected getter closure"); - assert_eq!(acc.set, 0); - assert_eq!((enumerable, configurable), CLASS_ACCESSOR_DEFAULT_ATTRS); - let again = class_static_own_accessor(cid, "g").unwrap().0; - assert_eq!(again.get, acc.get, "one closure per half"); - let ptr = class_value_ptr(cid) as usize; - assert_eq!( - unsafe { crate::closure::props::bag_get(ptr, b"g") }, - None, - "an accessor key has no data value" - ); - let got = unsafe { class_static_accessor_call_get(acc, class_value(cid)) }; - assert_eq!(got, 41.0); - class_static_set_accessor_attrs(cid, "g", true, false); - assert_eq!( - class_static_own_accessor(cid, "g").map(|(_, e, c)| (e, c)), - Some((true, false)) - ); - class_static_set_accessor_attrs(cid, "g", false, true); - assert!(class_static_remove(cid, "g"), "delete removes the property"); - assert!(class_static_own_accessor(cid, "g").is_none()); - } - - /// Only the function object's own code pointer names a class. - #[test] - fn ordinary_closures_and_numbers_are_not_class_values() { - extern "C" fn body(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { - 0.0 - } - let c = crate::closure::js_closure_alloc(crate::fn_info!(body, 0), 0); - assert_eq!(class_closure_id(c as usize), None); - assert_eq!(class_value_id(42.0), None); - assert_eq!( - class_value_id(f64::from_bits(crate::value::TAG_UNDEFINED)), - None - ); - } -} +#[path = "class_value/tests.rs"] +mod tests; /// Expose the existing class function-object directory, without another registry. #[no_mangle] diff --git a/crates/perry-runtime/src/object/class_value/tests.rs b/crates/perry-runtime/src/object/class_value/tests.rs new file mode 100644 index 0000000000..f4283f24e1 --- /dev/null +++ b/crates/perry-runtime/src/object/class_value/tests.rs @@ -0,0 +1,472 @@ +use super::*; + +#[test] +fn static_property_lookups_do_not_mint_builtin_class_values() { + for cid in [0, 0x7FFF_FF00, 0xFFFF_0024, u32::MAX] { + assert!(class_static_get(cid, "from").is_none()); + assert!(class_static_own_accessor(cid, "from").is_none()); + assert!(crate::object::class_registry::class_static_prototype(cid).is_null()); + assert!(!crate::object::class_registry::class_static_prototype_is_nulled(cid)); + assert!(class_value_cached(cid).is_none()); + } +} + +fn register(cid: u32) { + let mut guard = crate::object::REGISTERED_CLASS_IDS.write().unwrap(); + guard + .get_or_insert_with(crate::fast_hash::new_ptr_hash_set) + .insert(cid); +} + +/// #11414: a class value is ONE function object per class — never an +/// INT32 word a number can equal — pinned and old, so its address is its +/// identity across collections. +#[test] +fn class_value_is_one_pinned_function_object_per_class() { + let cid = 0x6A01; + register(cid); + let a = class_value(cid); + let b = class_value(cid); + assert_eq!(a.to_bits(), b.to_bits(), "one function object per class"); + let v = crate::value::JSValue::from_bits(a.to_bits()); + assert!( + !v.is_int32() && !v.is_number(), + "a class value is not a number" + ); + assert!(v.is_pointer()); + let ptr = (a.to_bits() & crate::value::POINTER_MASK) as usize; + assert!( + crate::closure::is_closure_ptr(ptr), + "a GC_TYPE_CLOSURE cell" + ); + assert_eq!(class_value_id(a), Some(cid)); + assert_eq!(class_closure_id(ptr), Some(cid)); + // The number equal to the class id is not the class. + assert_ne!( + f64::from_bits(crate::value::INT32_TAG | cid as u64).to_bits(), + a.to_bits() + ); + let header = unsafe { crate::value::addr_class::try_read_gc_header(ptr) }.expect("header"); + assert_ne!(header.gc_flags & crate::gc::GC_FLAG_PINNED, 0, "pinned"); + assert_ne!(header.gc_flags & crate::gc::GC_FLAG_TENURED, 0, "born old"); + crate::gc::js_gc_collect(); + assert_eq!(class_value(cid).to_bits(), a.to_bits(), "never moves"); + assert_eq!(class_value_id(a), Some(cid), "survives a full collection"); + let other = class_value(0x6A02); + assert_ne!(other.to_bits(), a.to_bits()); + assert_eq!(class_value_id(other), Some(0x6A02)); +} + +/// The class-value table is sized by how many ids of a band exist, never +/// by an id's magnitude: one class object for the highest id of every +/// band (fs.Stats' reserved `0xFFFF_0070` among them, which once grew the +/// directory to 16M pages — 128 MiB zero-filled — on a single +/// `stats.mtime` read) leaves every directory at most 256 pages. +#[test] +fn class_value_directories_are_sized_by_band_offset_not_by_id() { + let ids = [ + 0x7FFF_FF00, + 0x7FFF_FFFF, + SYNTHETIC_CLASS_ID_BASE, + 0xFFFF_0070, // fs.Stats (`fs::stats::STATS_REGULAR_CLASS_ID`) + 0xFFFF_0071, // bigint fs.Stats + crate::native_class_ids::CRYPTO_HASH, + u32::MAX, + ]; + // Not registered: a registered id is also an INT32 class-ref word + // (#11414), and `u32::MAX` would turn every `int32(-1)` into a class. + for cid in ids { + let v = class_value(cid); + assert_eq!(class_value_id(v), Some(cid), "{cid:#x} round-trips"); + assert_eq!(class_value(cid).to_bits(), v.to_bits(), "one object per id"); + } + for (band, (_, len)) in class_value_dirs().into_iter().enumerate() { + assert!( + len <= CLASS_VALUE_PAGE_LEN, + "band {band}: directory of {len} pages (sized by a raw class id)" + ); + } + // Distinct ids in distinct bands never share a slot. + let objs: Vec = ids.iter().map(|&c| class_value(c).to_bits()).collect(); + let mut unique = objs.clone(); + unique.sort_unstable(); + unique.dedup(); + assert_eq!(unique.len(), objs.len(), "every band id has its own object"); + // Non-class words have no slot and are never minted. + assert_eq!(class_value_band(0), None); + assert_eq!(class_value_band(0x8000_0000), None); + assert!(class_value_cached(0x8000_0000).is_none()); +} + +/// A static walk that reaches a BUILTIN parent (`class E extends Error`) +/// stops there: a builtin id has no class function object, so no reader +/// may mint one for it (minting `0xFFFF_0001` grew the class-value +/// directory to 16M pages, which every collection then scanned). +#[test] +fn a_builtin_parent_never_gets_a_class_function_object() { + let cid = 0x6D71; + register(cid); + crate::object::js_register_class_parent(cid, crate::error::CLASS_ID_ERROR); + let recv = class_value(cid); + let pages_before = class_value_dirs().map(|(_, len)| len); + let applied = unsafe { + crate::object::class_registry::class_static_accessor_setter_apply(cid, "zz", recv, 1.0) + }; + assert!(!applied, "no static accessor named zz on the chain"); + assert!(!crate::object::class_registry::static_accessor_in_chain( + cid, "zz" + )); + // A static symbol read up the chain (latch armed, as once any class + // has a static symbol member) reaches Error too and must not mint. + crate::symbol::CLASS_STATIC_SYMBOLS_LATCH.arm(); + let sym = unsafe { crate::symbol::js_symbol_new_empty() }; + assert!(crate::symbol::class_static_symbol_lookup_in_chain(cid, sym).is_none()); + assert!( + crate::symbol::class_static_symbol_keys_for_class(crate::error::CLASS_ID_ERROR).is_empty() + ); + // Object.getPrototypeOf(C), the static `super` parent value and a + // static `super[k] = v` all step to the parent id; none may mint. + let _ = crate::object::js_object_get_prototype_of(recv); + let _ = crate::object::class_registry::parent_static::template_dynamic_parent_value(cid); + let _ = crate::proxy::js_super_put_value_set( + crate::error::CLASS_ID_ERROR, + crate::value::js_nanbox_string( + crate::string::js_string_from_bytes(b"zz".as_ptr(), 2) as i64 + ), + 1.0, + recv, + 0, + ); + assert!( + class_value_cached(crate::error::CLASS_ID_ERROR).is_none(), + "the builtin Error id must not get a class function object" + ); + let pages_after = class_value_dirs().map(|(_, len)| len); + assert_eq!( + pages_after, pages_before, + "the walks grew the class-value directory" + ); +} + +/// A class function object is traced like any other pinned object: a full +/// collection reaches its own-property bag (the statics) through the +/// pinned-object root scan, with no class-value scanner. +#[test] +fn a_full_collection_keeps_the_class_statics_bag() { + let cid = 0x6B02; + register(cid); + // A unit-test thread may not have run `gc_init`'s scanner list. + crate::gc::gc_register_mutable_root_scanner(crate::gc::scan_pinned_object_roots_mut); + let text = "static-payload-11609"; + let s = crate::string::js_string_from_bytes(text.as_ptr(), text.len() as u32); + class_static_set( + cid, + "k11609", + f64::from_bits(crate::value::JSValue::string_ptr(s).bits()), + ); + crate::gc::js_gc_collect(); + crate::gc::js_gc_collect(); + let got = class_static_get(cid, "k11609").expect("the static survives a full collection"); + let got = crate::value::JSValue::from_bits(got.to_bits()); + let hdr = got.as_string_ptr(); + assert!(!hdr.is_null()); + let bytes = unsafe { crate::string::OwnedStringBytes::copy_from_header(hdr) }; + assert_eq!(bytes.as_bytes(), text.as_bytes()); + let keys: Vec = class_static_entries(cid) + .into_iter() + .map(|(k, _)| k) + .collect(); + assert!(keys.iter().any(|k| k == "k11609"), "own keys: {keys:?}"); +} + +/// The prototype link (capture 2) and the statics bag of an OLD, pinned +/// class hold young objects written after its birth: the slot barrier +/// remembers them, so a moving minor keeps them alive and rewrites both +/// edges. Dropping the barrier in `class_decl_prototype_link_store` makes +/// the link read back a stale address. +#[test] +fn young_values_in_an_old_class_survive_moving_minors() { + let _copying_nursery = crate::gc::CopyingNurseryTestGuard::new(0); + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force_evacuation = crate::gc::knob_overrides::ForcedEvacuationTestGuard::on(); + crate::gc::register_runtime_handle_root_scanner_for_tests(); + let cid = 0x6B04; + register(cid); + let _ = class_value_ptr(cid); + // The class is old and pinned from birth; everything below is young. + let proto = crate::object::js_object_alloc(0x6B05, 1); + crate::object::js_object_set_field( + proto, + 0, + crate::value::JSValue::from_bits(crate::value::INT32_TAG | 4242), + ); + let before = proto as usize; + class_decl_prototype_link_store(cid, proto); + let text = "young-static-6b04"; + let s = crate::string::js_string_from_bytes(text.as_ptr(), text.len() as u32); + class_static_set( + cid, + "k6b04", + f64::from_bits(crate::value::JSValue::string_ptr(s).bits()), + ); + for _ in 0..3 { + let _ = crate::gc::gc_collect_minor(); + } + let linked = class_decl_prototype_link(cid); + assert!(!linked.is_null()); + assert_ne!( + linked as usize, before, + "forced evacuation must have moved the young prototype" + ); + let field = crate::object::js_object_get_field(linked, 0); + assert_eq!(field.bits(), crate::value::INT32_TAG | 4242); + let got = class_static_get(cid, "k6b04").expect("the static survives minors"); + let hdr = crate::value::JSValue::from_bits(got.to_bits()).as_string_ptr(); + assert!(!hdr.is_null()); + let bytes = unsafe { crate::string::OwnedStringBytes::copy_from_header(hdr) }; + assert_eq!(bytes.as_bytes(), text.as_bytes()); +} + +/// Write once, then survive several minors with NO further writes. With +/// the promotion age pinned at 4 the young prototype stays young across +/// minors 1..=3, so the class (old, pinned) must stay in the minor scan +/// set the whole time: an entry is re-armed by every scan that still finds +/// a young referent, not consumed after one minor. Returns false (without +/// reading the possibly stale link) the first time the entry is missing. +fn write_once_then_minors(cid: u32, sabotage_drop_entry: bool) -> bool { + let _copying_nursery = crate::gc::CopyingNurseryTestGuard::new(0); + let _triggers = crate::gc::GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force_evacuation = crate::gc::knob_overrides::ForcedEvacuationTestGuard::on(); + let _age = crate::gc::pin_tenuring_survivals_for_test(4); + crate::gc::register_runtime_handle_root_scanner_for_tests(); + register(cid); + let closure = class_value_ptr(cid); + let proto = crate::object::js_object_alloc(cid + 1, 1); + crate::object::js_object_set_field( + proto, + 0, + crate::value::JSValue::from_bits(crate::value::INT32_TAG | 777), + ); + class_decl_prototype_link_store(cid, proto); + let slot = unsafe { prototype_link_slot(closure) } as usize; + assert!( + crate::gc::old_slot_page_is_remembered_for_test(slot), + "the write must have remembered the class" + ); + let _sabotage = sabotage_drop_entry.then(|| { + crate::gc::copy_decode_sabotage::Guard::arm(crate::gc::copy_decode_sabotage::CHILD) + }); + let mut moved_from = proto as usize; + for minor in 1..=3 { + let _ = crate::gc::gc_collect_minor(); + // No write since the first store. The referent must still be young + // (age < 4), so the entry must still be present. + if !crate::gc::old_slot_page_is_remembered_for_test(slot) { + return false; + } + let linked = class_decl_prototype_link(cid); + assert!(!linked.is_null()); + assert!( + matches!( + crate::arena::classify_heap_generation(linked as usize), + crate::arena::HeapGeneration::Nursery + ), + "minor {minor}: the referent must still be young" + ); + assert_ne!( + linked as usize, moved_from, + "minor {minor}: it must have moved" + ); + moved_from = linked as usize; + let field = crate::object::js_object_get_field(linked, 0); + assert_eq!(field.bits(), crate::value::INT32_TAG | 777, "minor {minor}"); + } + // The fourth survival promotes it; the value is intact afterwards too. + let _ = crate::gc::gc_collect_minor(); + let linked = class_decl_prototype_link(cid); + let field = crate::object::js_object_get_field(linked, 0); + assert_eq!( + field.bits(), + crate::value::INT32_TAG | 777, + "after promotion" + ); + true +} + +#[test] +fn a_class_stays_remembered_while_its_referent_is_young() { + assert!(write_once_then_minors(0x6B10, false)); +} + +/// Sabotage: the scan loses the child, so the entry is dropped after the +/// first minor. The run must go red: either the collector's own +/// post-cycle coverage check fires (the expected panic) or the witness +/// above sees the entry missing and returns false (the `assert!` panic). +#[test] +#[should_panic] +fn dropping_the_entry_after_one_minor_is_caught() { + assert!(write_once_then_minors(0x6B20, true)); +} + +/// The kind is a shape fact: class function objects carry their own +/// ShapeId, distinct from FunctionDictionary (shapes are canonical per +/// facts — without its marker fact the class shape WOULD be the dictionary +/// id and every dictionary function would route as a class), and it is +/// sticky across own-property installs. +#[test] +fn class_function_objects_have_their_own_sticky_shape() { + let cid = 0x6C01; + register(cid); + let class_shape = crate::closure::shape::function_class_shape(); + assert_ne!( + class_shape, + crate::closure::shape::function_dictionary_shape() + ); + let ptr = class_value_ptr(cid); + assert_eq!(unsafe { (*ptr).shape_id }, class_shape); + class_static_set(cid, "s", 1.0); + crate::closure::shape::note_function_own_state_changed(ptr as usize); + assert_eq!(unsafe { (*ptr).shape_id }, class_shape, "sticky"); + extern "C" fn body(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { + 0.0 + } + let f = crate::closure::js_closure_alloc(crate::fn_info!(body, 0), 0); + crate::closure::shape::note_function_own_state_changed(f as usize); + assert_ne!( + unsafe { (*f).shape_id }, + class_shape, + "a dictionary function is not a class" + ); +} + +/// A class constructor's `length` and `name` are own data properties of +/// its function object (in its own-property object, intrinsic +/// attributes); a static method of that name owns the key instead. +#[test] +fn name_and_length_are_own_data_of_the_function_object() { + let cid = 0x6D01; + register(cid); + unsafe { crate::object::js_register_class_name(cid, b"Zed".as_ptr(), 3) }; + crate::object::js_register_class_length(cid, 2); + let ptr = class_value_ptr(cid) as usize; + assert_eq!( + unsafe { crate::closure::props::bag_get(ptr, b"length") }, + Some(2.0), + "own length" + ); + let name = unsafe { crate::closure::props::bag_get(ptr, b"name") }.expect("own name"); + let mut scratch = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + // SAFETY: a live string value just read from the object. + let bytes = unsafe { + crate::string::js_string_key_bytes( + crate::value::JSValue::from_bits(name.to_bits()), + &mut scratch, + ) + } + .expect("a string"); + assert_eq!(bytes, b"Zed"); + for key in ["length", "name"] { + assert_eq!( + crate::object::class_registry::class_static_defined_attrs(cid, key), + Some(INTRINSIC_ATTRS), + "{key}: non-writable, non-enumerable, configurable" + ); + } + extern "C" fn static_name() -> f64 { + 0.0 + } + unsafe { + crate::object::class_registry::js_register_class_static_method( + cid as i64, + b"name".as_ptr(), + 4, + static_name as *const () as usize as i64, + 0, + 0, + ) + }; + extern "C" fn static_name_entry(_closure: i64) -> f64 { + 0.0 + } + unsafe { + crate::object::class_registry::parent_static::js_register_class_static_method_entry( + cid as i64, + b"name".as_ptr(), + 4, + static_name_entry as *const () as usize as i64, + ) + }; + // node: `class Zed { static name() {} }` -> Zed.name is the method, an + // own data property { writable, !enumerable, configurable }. + let method = unsafe { crate::closure::props::bag_get(ptr, b"name") } + .expect("a static method named `name` is the class's own `name`"); + assert_eq!( + unsafe { static_method_code_of(method) }, + Some(static_name_entry as *const () as usize), + "its value is the method's function object" + ); + assert_eq!( + crate::object::class_registry::class_static_defined_attrs(cid, "name"), + Some((true, false, true)), + "method attributes, not the intrinsic's" + ); +} + +/// A ClassBody static accessor is an accessor property of the class +/// function object: ClassBody attributes, one closure per half across +/// reads, attributes changed in place, and a delete removes it. +#[test] +fn static_accessors_are_accessor_properties_of_the_function_object() { + let cid = 0x6E01; + register(cid); + extern "C" fn getter() -> f64 { + 41.0 + } + unsafe { + crate::object::js_register_class_name(cid, b"Acc".as_ptr(), 3); + crate::object::class_registry::js_register_class_static_getter( + cid as i64, + b"g".as_ptr(), + 1, + getter as *const () as usize as i64, + ); + } + let (acc, enumerable, configurable) = + class_static_own_accessor(cid, "g").expect("an own accessor property"); + assert_ne!(acc.get, 0, "a reflected getter closure"); + assert_eq!(acc.set, 0); + assert_eq!((enumerable, configurable), CLASS_ACCESSOR_DEFAULT_ATTRS); + let again = class_static_own_accessor(cid, "g").unwrap().0; + assert_eq!(again.get, acc.get, "one closure per half"); + let ptr = class_value_ptr(cid) as usize; + assert_eq!( + unsafe { crate::closure::props::bag_get(ptr, b"g") }, + None, + "an accessor key has no data value" + ); + let got = unsafe { class_static_accessor_call_get(acc, class_value(cid)) }; + assert_eq!(got, 41.0); + class_static_set_accessor_attrs(cid, "g", true, false); + assert_eq!( + class_static_own_accessor(cid, "g").map(|(_, e, c)| (e, c)), + Some((true, false)) + ); + class_static_set_accessor_attrs(cid, "g", false, true); + assert!(class_static_remove(cid, "g"), "delete removes the property"); + assert!(class_static_own_accessor(cid, "g").is_none()); +} + +/// Only the function object's own code pointer names a class. +#[test] +fn ordinary_closures_and_numbers_are_not_class_values() { + extern "C" fn body(_: *const ClosureHeader, _this: crate::closure::JsThis) -> f64 { + 0.0 + } + let c = crate::closure::js_closure_alloc(crate::fn_info!(body, 0), 0); + assert_eq!(class_closure_id(c as usize), None); + assert_eq!(class_value_id(42.0), None); + assert_eq!( + class_value_id(f64::from_bits(crate::value::TAG_UNDEFINED)), + None + ); +} diff --git a/crates/perry-runtime/src/object/descriptor_state.rs b/crates/perry-runtime/src/object/descriptor_state.rs index f7e73e85f4..79651befab 100644 --- a/crates/perry-runtime/src/object/descriptor_state.rs +++ b/crates/perry-runtime/src/object/descriptor_state.rs @@ -202,14 +202,15 @@ pub(crate) fn note_data_descriptor_target(obj: usize, key: &str, attrs: Property /// present, never the getter/setter identities, which differ per receiver /// (zod binds a fresh closure per schema) and live with the receiver. pub(crate) fn note_accessor_descriptor_target(obj: usize, key: &str, acc: &AccessorDescriptor) { - note_descriptor_target_edits( - obj, - &[AttrsEdit::Accessor( - key.as_bytes(), - acc.get != 0, - acc.set != 0, - )], - ); + note_accessor_descriptor_target_bytes(obj, key.as_bytes(), acc); +} + +pub(crate) fn note_accessor_descriptor_target_bytes( + obj: usize, + key: &[u8], + acc: &AccessorDescriptor, +) { + note_descriptor_target_edits(obj, &[AttrsEdit::Accessor(key, acc.get != 0, acc.set != 0)]); } /// Apply descriptor facts to the ordinary holder shape. Arrays, functions, @@ -248,20 +249,20 @@ pub(crate) fn note_attrs_born_with_keys(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 { + get_property_attrs_bytes(obj, key.as_bytes()) +} + +pub(crate) fn get_property_attrs_bytes(obj: usize, key: &[u8]) -> Option { unsafe { let route = descriptor_route(obj); let bag = route.bag; if !bag.is_null() { - let entry = super::key_attrs::object_key_entry(bag, key.as_bytes()); + let entry = super::key_attrs::object_key_entry(bag, key); let keys = super::object_keys(bag); if entry != 0 || (bag as usize != obj - && super::keys_find_property_slot_by_bytes( - keys.arr(), - keys.count(), - key.as_bytes(), - ) - .is_some()) + && super::keys_find_property_slot_by_bytes(keys.arr(), keys.count(), key) + .is_some()) { return Some(PropertyAttrs { bits: super::key_attrs::entry_to_attr_bits(entry), @@ -269,10 +270,14 @@ pub(crate) fn get_property_attrs(obj: usize, key: &str) -> Option } } if !route.integrity_array.is_null() { - return array_integrity_attrs(route.integrity_array, key); + return std::str::from_utf8(key) + .ok() + .and_then(|key| array_integrity_attrs(route.integrity_array, key)); } } - string_wrapper_index_attrs(obj, key) + std::str::from_utf8(key) + .ok() + .and_then(|key| string_wrapper_index_attrs(obj, key)) } /// Default dense/length attributes come from the array's resolved header. @@ -635,12 +640,16 @@ pub(crate) fn clear_property_attrs(obj: usize, key: &str) { /// Look up the accessor descriptor (get/set) for (obj, key). pub(crate) fn get_accessor_descriptor(obj: usize, key: &str) -> Option { + get_accessor_descriptor_bytes(obj, key.as_bytes()) +} + +pub(crate) fn get_accessor_descriptor_bytes(obj: usize, key: &[u8]) -> Option { unsafe { let bag = descriptor_route(obj).bag; if bag.is_null() { return None; } - own_accessor(bag as usize, key.as_bytes()).map(descriptor_from) + own_accessor(bag as usize, key).map(descriptor_from) } } @@ -855,6 +864,15 @@ fn note_accessor_function_replaced( key: &str, previous: Option, now: super::accessor_pair::Accessor, +) { + note_accessor_function_replaced_bytes(obj, key.as_bytes(), previous, now); +} + +fn note_accessor_function_replaced_bytes( + obj: usize, + key: &[u8], + previous: Option, + now: super::accessor_pair::Accessor, ) { let Some(previous) = previous else { return; @@ -871,7 +889,7 @@ fn note_accessor_function_replaced( let _no_move = crate::gc::GcSuppressScope::new(); crate::object::shapes::transition_object_shape_accessor_replaced( obj as *mut ObjectHeader, - key.as_bytes(), + key, ); } } @@ -973,6 +991,15 @@ pub(crate) fn set_builtin_accessor_pair( key: String, pair: super::accessor_pair::Accessor, attrs: PropertyAttrs, +) { + set_builtin_accessor_pair_bytes(obj, key.as_bytes(), pair, attrs) +} + +pub(crate) fn set_builtin_accessor_pair_bytes( + obj: usize, + key: &[u8], + pair: super::accessor_pair::Accessor, + attrs: PropertyAttrs, ) { crate::closure::shape::note_function_own_state_changed(obj); let function = HolderEdit::new(obj); @@ -981,34 +1008,38 @@ pub(crate) fn set_builtin_accessor_pair( super::prop_plan::prop_plan_epoch_bump_for_owner(obj); let in_keys = unsafe { super::key_attrs::attrs_live_in_keys_for_install(obj) }; let previous = if in_keys { - unsafe { own_accessor(obj, key.as_bytes()) } + unsafe { own_accessor(obj, key) } } else { None }; note_descriptor_target_edits( obj, &[ - AttrsEdit::Accessor(key.as_bytes(), acc.get != 0, acc.set != 0), - AttrsEdit::Data(key.as_bytes(), attrs.bits), + AttrsEdit::Accessor(key, acc.get != 0, acc.set != 0), + AttrsEdit::Data(key, attrs.bits), ], ); if in_keys { // Charter step 3: the pair lives in the key's slot. - unsafe { store_own_accessor(obj, &key, Some(pair)) }; - note_accessor_function_replaced(obj, &key, previous, pair); + unsafe { super::accessor_pair::store_own_accessor_bytes(obj, key, Some(pair)) }; + note_accessor_function_replaced_bytes(obj, key, previous, pair); return; } } /// Record a built-in data property's attributes in its holder shape. pub(crate) fn set_builtin_property_attrs(obj: usize, key: String, attrs: PropertyAttrs) { + set_builtin_property_attrs_bytes(obj, key.as_bytes(), attrs) +} + +pub(crate) fn set_builtin_property_attrs_bytes(obj: usize, key: &[u8], attrs: PropertyAttrs) { // A function born with this key's attributes (its bag's key entry // already says so): nothing changes. - if unsafe { function_key_has_attrs(obj, &key, attrs) } { + if unsafe { function_key_has_attrs_bytes(obj, key, attrs) } { return; } super::prop_plan::prop_plan_epoch_bump_for_owner(obj); - note_descriptor_target_edits(obj, &[AttrsEdit::Data(key.as_bytes(), attrs.bits)]); + note_descriptor_target_edits(obj, &[AttrsEdit::Data(key, attrs.bits)]); if unsafe { crate::closure::is_closure_ptr(obj) || super::key_attrs::attrs_live_in_keys_for_install(obj) } { @@ -1021,15 +1052,15 @@ pub(crate) fn set_builtin_property_attrs(obj: usize, key: String, attrs: Propert /// /// # Safety /// `obj` is any address; only a proven closure's bag is read. -unsafe fn function_key_has_attrs(obj: usize, key: &str, attrs: PropertyAttrs) -> bool { +unsafe fn function_key_has_attrs_bytes(obj: usize, key: &[u8], attrs: PropertyAttrs) -> bool { let entry = super::key_attrs::attr_bits_to_entry(attrs.bits); if entry == 0 || !crate::closure::is_closure_ptr(obj) { return false; } let bag = crate::closure::props::bag_of(obj); !bag.is_null() - && super::key_attrs::object_key_entry(bag, key.as_bytes()) == entry - && crate::closure::props::bag_has_own(obj, key.as_bytes()) + && super::key_attrs::object_key_entry(bag, key) == entry + && crate::closure::props::bag_has_own(obj, key) } /// Install a built-in data property WITH its attributes: `obj[key] = value` @@ -1050,12 +1081,27 @@ pub(crate) fn define_builtin_data_property( value: f64, name: String, attrs: PropertyAttrs, +) { + define_builtin_data_property_bytes(obj, key, value, name.as_bytes(), attrs) +} + +pub(crate) fn define_builtin_data_property_bytes( + obj: *mut ObjectHeader, + key: *const crate::StringHeader, + value: f64, + name: &[u8], + attrs: PropertyAttrs, ) { unsafe { if crate::closure::is_closure_ptr(obj as usize) { super::own_override::as_builtin_definition(|| { super::own_override::note_exotic_named_prop_install(obj as usize); - crate::closure::closure_define_data_with_attrs(obj as usize, &name, value, attrs); + crate::closure::closure_define_data_with_attrs_bytes( + obj as usize, + name, + value, + attrs, + ); }); return; } else if super::key_attrs::attrs_live_in_keys_for_install(obj as usize) { @@ -1071,7 +1117,7 @@ pub(crate) fn define_builtin_data_property( }); } } - set_builtin_property_attrs(obj as usize, name, attrs); + set_builtin_property_attrs_bytes(obj as usize, name, attrs); } mod integrity; diff --git a/crates/perry-runtime/src/object/descriptors.rs b/crates/perry-runtime/src/object/descriptors.rs index 1beb16ea4f..ec1cc164d5 100644 --- a/crates/perry-runtime/src/object/descriptors.rs +++ b/crates/perry-runtime/src/object/descriptors.rs @@ -9,6 +9,79 @@ use super::*; pub(crate) mod own_keys; pub(crate) use own_keys::{OwnNamesSnapshot, OwnProperty}; +unsafe fn class_ref_wtf8_own_property( + obj_value: f64, + class_id: u32, + key_value: f64, + name: &[u8], +) -> Option { + let is_prototype = super::class_prototype_ref_id(obj_value).is_some(); + if is_prototype { + if super::class_registry::class_proto_key_deleted_bytes(class_id, name) { + return None; + } + if let Some(proto) = + super::class_registry::decl_prototype_own_accessor_bytes(class_id, name) + { + return own_property(proto, key_value); + } + if super::native_module::class_has_own_method_bytes(class_id, name) { + if let Some(value) = + super::class_registry::class_method_slot_value_bytes(class_id, name) + { + return OwnProperty::data(f64::from_bits(value), true, false, true); + } + } + return None; + } + + if super::class_registry::class_static_key_deleted_bytes(class_id, name) { + return None; + } + if let Some((accessor, attrs)) = + super::class_registry::class_dynamic_static_accessor_descriptor_bytes( + class_id, name, obj_value, + ) + { + let undefined = crate::value::TAG_UNDEFINED; + return OwnProperty::accessor( + f64::from_bits(if accessor.get == 0 { + undefined + } else { + accessor.get + }), + f64::from_bits(if accessor.set == 0 { + undefined + } else { + accessor.set + }), + attrs.enumerable(), + attrs.configurable(), + ); + } + if let Some(descriptor) = + crate::object::class_value::class_static_accessor_descriptor_bytes(class_id, name) + { + return OwnProperty::public(descriptor); + } + if let Some(value) = super::class_registry::class_own_static_field_value_bytes(class_id, name) { + let (writable, enumerable, configurable) = + super::class_registry::class_static_defined_attrs_bytes(class_id, name) + .unwrap_or((true, true, true)); + return OwnProperty::data(value, writable, enumerable, configurable); + } + if super::class_registry::lookup_static_method_owner_bytes(class_id, name) + .is_some_and(|(owner, _)| owner == class_id) + { + if let Some(value) = + crate::object::class_value::inherited_static_method_value_bytes(class_id, name) + { + return OwnProperty::data(value, true, false, true); + } + } + None +} + fn property_name_array_index(name: &str) -> Option { if name.is_empty() || (name.len() > 1 && name.as_bytes()[0] == b'0') { return None; @@ -35,6 +108,30 @@ pub(crate) fn sort_property_names_ecma(names: &mut Vec) { names.extend(rest); } +fn sort_property_name_bytes_ecma(names: &mut Vec>) { + let mut indexed = Vec::new(); + let mut rest = Vec::new(); + for name in names.drain(..) { + let index = std::str::from_utf8(&name) + .ok() + .and_then(property_name_array_index); + if let Some(index) = index { + indexed.push((index, name)); + } else { + rest.push(name); + } + } + indexed.sort_by_key(|(index, _)| *index); + names.extend(indexed.into_iter().map(|(_, name)| name)); + names.extend(rest); +} + +fn push_unique_name_bytes(names: &mut Vec>, name: Vec) { + if !names.iter().any(|existing| existing == &name) { + names.push(name); + } +} + fn push_unique_name(names: &mut Vec, name: String) { if !names.iter().any(|existing| existing == &name) { names.push(name); @@ -244,15 +341,13 @@ pub(crate) unsafe fn own_property(obj_value: f64, key_value: f64) -> Option Option Option Option name, + Err(_) => { + return class_ref_wtf8_own_property( + obj_value, + class_id, + key_value, + &method_name_bytes, + ) + } + }; if if class_prototype_ref_id(obj_value).is_some() { super::class_registry::class_proto_key_deleted(class_id, &method_name) } else { @@ -1161,28 +1275,32 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { if let Some(fo) = crate::object::class_value::class_value_if_minted(class_id) { let ptr = fo as usize; // This agent's live class closure (the enclosing `unsafe`). - let names = crate::closure::props::bag_own_key_names(ptr); - let mut out: Vec = Vec::new(); - for intrinsic in ["length", "name", "prototype"] { + let names = crate::closure::bag_own_key_names_bytes(ptr); + let mut out: Vec> = Vec::new(); + for intrinsic in [b"length".as_slice(), b"name", b"prototype"] { if !names.iter().any(|n| n == intrinsic) - && !crate::closure::closure_is_key_deleted(ptr, intrinsic) - && (intrinsic == "prototype" + && !crate::closure::closure_is_key_deleted( + ptr, + std::str::from_utf8(intrinsic).unwrap(), + ) + && (intrinsic == b"prototype" || !super::class_registry::class_static_key_deleted( - class_id, intrinsic, + class_id, + std::str::from_utf8(intrinsic).unwrap(), )) { - out.push(intrinsic.to_string()); + out.push(intrinsic.to_vec()); } } for name in names { - if name.starts_with('#') - || super::field_get_set::is_internal_runtime_key(&name) + if name.starts_with(b"#") + || super::field_get_set::is_internal_runtime_key_bytes(&name) { continue; } - push_unique_name(&mut out, name); + push_unique_name_bytes(&mut out, name); } - sort_property_names_ecma(&mut out); + sort_property_name_bytes_ecma(&mut out); let result = crate::array::js_array_alloc(out.len() as u32); for name in out { let str_ptr = @@ -1192,44 +1310,41 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { return f64::from_bits((result as u64) | 0x7FFD_0000_0000_0000); } } - let mut names: Vec = if is_prototype_ref { - vec!["constructor".to_string()] + let mut names: Vec> = if is_prototype_ref { + vec![b"constructor".to_vec()] } else { - vec![ - "length".to_string(), - "name".to_string(), - "prototype".to_string(), - ] + vec![b"length".to_vec(), b"name".to_vec(), b"prototype".to_vec()] }; for name in super::class_registry::class_own_string_member_names(class_id, !is_prototype_ref) { let deleted = if is_prototype_ref { - super::class_registry::class_proto_key_deleted(class_id, &name) + super::class_registry::class_proto_key_deleted_bytes(class_id, &name) } else { - super::class_registry::class_static_key_deleted(class_id, &name) + super::class_registry::class_static_key_deleted_bytes(class_id, &name) }; if !deleted { - push_unique_name(&mut names, name); + push_unique_name_bytes(&mut names, name); } } if !is_prototype_ref { for name in super::class_registry::class_own_dynamic_prop_names(class_id) { - push_unique_name(&mut names, name); + push_unique_name_bytes(&mut names, name.into_bytes()); } // Accessor properties a `defineProperty` added. - for name in crate::object::class_value::class_static_accessor_names(class_id) { - push_unique_name(&mut names, name); + for name in crate::object::class_value::class_static_accessor_names_bytes(class_id) + { + push_unique_name_bytes(&mut names, name); } } names.retain(|n| { - !super::field_get_set::is_internal_runtime_key(n) + !super::field_get_set::is_internal_runtime_key_bytes(n) // A deleted `length` / `name` is no longer own. && !(!is_prototype_ref - && matches!(n.as_str(), "length" | "name") - && super::class_registry::class_static_key_deleted(class_id, n)) + && (n.as_slice() == b"length" || n.as_slice() == b"name") + && super::class_registry::class_static_key_deleted_bytes(class_id, n)) }); - sort_property_names_ecma(&mut names); + sort_property_name_bytes_ecma(&mut names); let result = crate::array::js_array_alloc(names.len() as u32); for name in names { let str_ptr = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); @@ -1410,7 +1525,7 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { if let Some(class_id) = super::class_registry::class_id_for_decl_prototype_object(obj as usize) { - let mut physical = Vec::new(); + let mut physical: Vec> = Vec::new(); let mut sso_buf = [0u8; crate::value::SHORT_STRING_MAX_LEN]; for i in 0..len { let key_val = crate::array::js_array_get(keys, pos(i)); @@ -1432,27 +1547,25 @@ fn js_object_get_own_property_names_shape(obj_value: f64) -> f64 { { continue; } - if let Ok(name) = std::str::from_utf8(bytes) { - push_unique_name(&mut physical, name.to_string()); - } + push_unique_name_bytes(&mut physical, bytes.to_vec()); } - let mut names = Vec::new(); - if physical.iter().any(|name| name == "constructor") { - names.push("constructor".to_string()); + let mut names: Vec> = Vec::new(); + if physical.iter().any(|name| name == b"constructor") { + names.push(b"constructor".to_vec()); } for name in super::class_registry::class_own_string_member_names(class_id, false) { - if super::class_registry::class_proto_key_deleted(class_id, &name) { + if super::class_registry::class_proto_key_deleted_bytes(class_id, &name) { continue; } if physical.contains(&name) { - push_unique_name(&mut names, name); + push_unique_name_bytes(&mut names, name); } } for name in physical { - push_unique_name(&mut names, name); + push_unique_name_bytes(&mut names, name); } - sort_property_names_ecma(&mut names); + sort_property_name_bytes_ecma(&mut names); let result = crate::array::js_array_alloc(names.len() as u32); for name in names { let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs index 5a7596776f..5bf597b246 100644 --- a/crates/perry-runtime/src/object/field_get_set/class_object_props.rs +++ b/crates/perry-runtime/src/object/field_get_set/class_object_props.rs @@ -233,10 +233,8 @@ pub(crate) unsafe fn finish_class_evaluation_prototype( if is_accessor { // S2: an accessor is a real accessor property of this prototype. proto.with_mut_ptr::(|proto| { - super::super::class_registry::install_decl_prototype_accessor( - proto, - class_id, - std::str::from_utf8(&name).expect("accessor name"), + super::super::class_registry::install_decl_prototype_accessor_bytes( + proto, class_id, &name, ) }); continue; @@ -248,22 +246,27 @@ pub(crate) unsafe fn finish_class_evaluation_prototype( // The evaluation's own function object for the method: its entry runs // the method at home in this evaluation (`__eclo`). A template // compiled without entries keeps the by-name method value. - let method = - super::class_object_template::evaluation_method_value(class_id, &name, class_value) - .unwrap_or_else(|| { - super::super::native_module::class_evaluation_method_value_for_name( - class_id, - std::str::from_utf8(&name).expect("method without entry"), - class_value, - ) - }); + let method = super::class_object_template::evaluation_method_value_bytes( + class_id, + &name, + class_value, + ) + .unwrap_or_else(|| { + super::super::native_module::class_evaluation_method_value_for_name_bytes( + class_id, + &name, + class_value, + ) + }); proto.with_mut_ptr::(|proto| { - key.with_const_ptr::(|key| unsafe { - crate::object::object_ops::define_property_force_store_value(proto, key, method); - crate::object::key_attrs::apply_edits( + key.with_const_ptr::(|key| { + crate::object::descriptor_state::define_builtin_data_property_bytes( proto, - &[crate::object::key_attrs::AttrsEdit::Data(&name, 5)], - ); + key, + method, + &name, + PropertyAttrs::new(true, false, true), + ) }); }); } @@ -336,22 +339,23 @@ pub(crate) unsafe fn define_class_object_own_properties( } let scope = crate::gc::RuntimeHandleScope::new(); let class = scope.root_raw_mut_ptr(obj); - let define = |name: &str, value: f64, attrs: (bool, bool, bool)| { + let define_bytes = |name: &[u8], value: f64, attrs: (bool, bool, bool)| { let value = scope.root_nanbox_f64(value); let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); let key = scope.root_string_ptr(key); class.with_mut_ptr::(|class| { key.with_const_ptr::(|key| { - define_builtin_data_property( + crate::object::descriptor_state::define_builtin_data_property_bytes( class, key, value.get_nanbox_f64(), - name.to_string(), + name, PropertyAttrs::new(attrs.0, attrs.1, attrs.2), ) }) }); }; + let define = |name: &str, value: f64, attrs| define_bytes(name.as_bytes(), value, attrs); for (bit, key) in [(1, "length"), (2, "name")] { // A static method of this name replaces the value (and the attributes) // in the same position, below; a static accessor leaves nothing here. @@ -382,7 +386,9 @@ pub(crate) unsafe fn define_class_object_own_properties( // it is called (`js_static_method_entry_enter_home`). An accessor has // no entry. let Some(code) = - super::super::class_registry::class_own_static_method_code(class_id, &name) + super::super::class_registry::parent_static::class_own_static_method_code_bytes( + class_id, &name, + ) else { continue; }; @@ -390,15 +396,24 @@ pub(crate) unsafe fn define_class_object_own_properties( if f.is_null() { continue; } - class.with_mut_ptr::(|class| { - super::class_object_template::set_static_method_home( - f, - crate::value::js_nanbox_pointer(class as i64), - ) + let f = scope.root_raw_mut_ptr(f); + let f_value = f.with_const_ptr::(|f| { + crate::value::js_nanbox_pointer(f as i64) + }); + crate::object::native_module::set_function_name_bytes(f_value, &name); + f.with_mut_ptr::(|f| { + class.with_mut_ptr::(|class| { + super::class_object_template::set_static_method_home( + f, + crate::value::js_nanbox_pointer(class as i64), + ) + }) }); - define( + define_bytes( &name, - crate::value::js_nanbox_pointer(f as i64), + f.with_const_ptr::(|f| { + crate::value::js_nanbox_pointer(f as i64) + }), (true, false, true), ); } diff --git a/crates/perry-runtime/src/object/field_get_set/class_object_template.rs b/crates/perry-runtime/src/object/field_get_set/class_object_template.rs index 473b6ab84e..5148f019b5 100644 --- a/crates/perry-runtime/src/object/field_get_set/class_object_template.rs +++ b/crates/perry-runtime/src/object/field_get_set/class_object_template.rs @@ -633,10 +633,34 @@ unsafe fn class_object_in_template_shape( .map_or(crate::value::TAG_UNDEFINED, f64::to_bits), Fill::Method(code) => { let f = static_method_value(code); - class.with_mut_ptr::(|class| { - set_static_method_home(f, crate::value::js_nanbox_pointer(class as i64)) - }); - crate::value::js_nanbox_pointer(f as i64).to_bits() + if f.is_null() { + crate::value::TAG_UNDEFINED + } else { + let f = scope.root_raw_mut_ptr(f); + let name = class.with_const_ptr::(|class| { + let stored = crate::object::object_keys(class as *const ObjectHeader) + .get(slot as u32); + let mut short = [0u8; crate::value::SHORT_STRING_MAX_LEN]; + unsafe { + crate::string::js_string_key_bytes(stored, &mut short) + .map(|bytes| bytes.to_vec()) + } + }); + if let Some(name) = name { + let f_value = f.with_const_ptr::(|f| { + crate::value::js_nanbox_pointer(f as i64) + }); + crate::object::native_module::set_function_name_bytes(f_value, &name); + } + f.with_mut_ptr::(|f| { + class.with_mut_ptr::(|class| { + set_static_method_home(f, crate::value::js_nanbox_pointer(class as i64)) + }) + }); + f.with_const_ptr::(|f| { + crate::value::js_nanbox_pointer(f as i64).to_bits() + }) + } } Fill::Parent => { // `js_class_evaluation_object`'s ordering rule: arm before @@ -708,11 +732,13 @@ unsafe fn record_class_object_template( { saw_parent = true; Fill::Parent - } else if let Some(name) = statics.iter().find(|name| is(name.as_bytes())) { + } else if let Some(name) = statics.iter().find(|name| is(name)) { // A static method's own function object (a static accessor of // this name leaves an accessor, which no template records). let Some(code) = - super::super::class_registry::class_own_static_method_code(class_id, name) + super::super::class_registry::parent_static::class_own_static_method_code_bytes( + class_id, name, + ) else { return; }; @@ -892,29 +918,20 @@ enum ProtoFill { /// template `class_id`: it runs the method's closure-convention entry, and its /// home capture names class object `class`. `None` when the template /// registered no entry for `name`. -#[inline] -pub(crate) unsafe fn evaluation_method_value( - class_id: u32, - name: impl AsRef<[u8]>, - class: f64, -) -> Option { - evaluation_method_value_bytes(class_id, name.as_ref(), class) -} - -#[inline(never)] pub(crate) unsafe fn evaluation_method_value_bytes( class_id: u32, name: &[u8], class: f64, ) -> Option { - let Some(code) = super::super::class_registry::class_method_entry(class_id, name) else { + let Some(code) = super::super::class_registry::class_method_entry_bytes(class_id, name) else { // A computed member is its body: the function object captures it // (at home in, and branded by, this evaluation), never a name to // look up again after a later evaluation renamed the member. - let decl = super::super::class_registry::declarations::class_method_decl(class_id, name) - .filter(|decl| decl.name.is_null())?; + let decl = + super::super::class_registry::declarations::class_method_decl_bytes(class_id, name) + .filter(|decl| decl.name.is_null())?; return Some( - crate::object::native_module::class_method_declaration_value( + crate::object::native_module::class_method_declaration_value_bytes( class, name, decl.code as usize, @@ -1062,8 +1079,7 @@ pub(crate) unsafe fn record_prototype_template( let fill = if slot == 0 && is(b"constructor") && bits == owner { ProtoFill::Constructor } else if let Some((name, false)) = members.iter().find(|(name, _)| is(name)) { - let name = name.as_slice(); - let Some(code) = super::super::class_registry::class_method_entry(class_id, name) + let Some(code) = super::super::class_registry::class_method_entry_bytes(class_id, name) else { return; }; diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index 62ff258f2a..40fa033194 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -97,6 +97,88 @@ pub extern "C" fn js_object_get_field_by_name( /// out so a class function object is routed here BEFORE the closure arms of /// the generic read (`get_field_by_name_after_site_miss`), which it /// would otherwise walk end to end first. +unsafe fn class_value_get_wtf8_field( + class_value: f64, + class_id: u32, + is_prototype_ref: bool, + key: *const crate::StringHeader, + name: &[u8], +) -> JSValue { + let scope = crate::gc::RuntimeHandleScope::new(); + let class_value = scope.root_nanbox_f64(class_value); + let key = scope.root_string_ptr(key); + if is_prototype_ref { + if super::super::native_module::class_has_own_method_bytes(class_id, name) { + if let Some(value) = + super::super::class_registry::class_method_slot_value_bytes(class_id, name) + { + return JSValue::from_bits(value); + } + } + if let Some((value, _)) = + super::super::class_registry::class_chain_getter_value_bytes(class_id, name, || { + class_value.get_nanbox_f64() + }) + { + return value; + } + return key.with_const_ptr::(|key| { + crate::object::class_value::class_prototype_get( + class_id, + key, + class_value.get_nanbox_f64(), + ) + }); + } + + // A capture-carrying class declaration may be referenced through its + // template ClassRef from an earlier-created helper closure. Runtime-added + // static properties live on the current evaluation's heap class object, + // so give that object first chance to answer the exact WTF-8 key too. + if let Some(class_object) = super::super::class_registry::class_object_value_for_cid(class_id) { + let class_object = JSValue::from_bits(class_object.to_bits()); + if class_object.is_pointer() { + let class_object = class_object.as_pointer::(); + if !class_object.is_null() { + let value = key.with_const_ptr::(|key| { + js_object_get_field_by_name(class_object, key) + }); + if !value.is_undefined() { + return value; + } + } + } + } + + if super::super::class_registry::class_static_key_deleted_bytes(class_id, name) { + return key.with_const_ptr::(|key| { + crate::object::class_value::class_prototype_get( + class_id, + key, + class_value.get_nanbox_f64(), + ) + }); + } + if let Some(value) = crate::object::class_value::class_static_get_bytes(class_id, name) { + return JSValue::from_bits(value.to_bits()); + } + if let Some(value) = + crate::object::class_value::inherited_static_method_value_bytes(class_id, name) + { + return JSValue::from_bits(value.to_bits()); + } + if let Some(value) = super::super::class_registry::class_static_accessor_getter_value_bytes( + class_id, + name, + class_value.get_nanbox_f64(), + ) { + return JSValue::from_bits(value.to_bits()); + } + key.with_const_ptr::(|key| { + crate::object::class_value::class_prototype_get(class_id, key, class_value.get_nanbox_f64()) + }) +} + #[inline(never)] pub(crate) fn class_value_get_field( obj: *const ObjectHeader, @@ -130,8 +212,18 @@ pub(crate) fn class_value_get_field( unsafe { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; - let name = - std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)).unwrap_or(""); + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + let name = std::str::from_utf8(name_bytes).unwrap_or(""); + if name.is_empty() && !name_bytes.is_empty() { + let name_bytes = name_bytes.to_vec(); + return class_value_get_wtf8_field( + class_value, + class_id, + is_prototype_ref, + key, + &name_bytes, + ); + } // v0.5.752: class_ref.constructor synthesizes back to the // same class ref so drizzle's // `Object.getPrototypeOf(value).constructor === Class` chain @@ -1093,7 +1185,7 @@ fn get_field_by_name_past_data_probe( b"byteOffset" => { return JSValue::number( crate::typedarray_view::js_typed_array_byte_offset(ta) as f64, - ) + ); } b"BYTES_PER_ELEMENT" => return JSValue::number(elem_size as f64), // `(new Int8Array(…)).constructor === Int8Array`. The @@ -1365,10 +1457,12 @@ fn get_field_by_name_past_data_probe( if class_id != 0 { let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; - let name = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) - .unwrap_or(""); - if !name.is_empty() - && !super::super::class_registry::class_static_key_deleted(class_id, name) + let name_bytes = std::slice::from_raw_parts(name_ptr, name_len); + let name = std::str::from_utf8(name_bytes).ok(); + if !name_bytes.is_empty() + && !super::super::class_registry::class_static_key_deleted_bytes( + class_id, name_bytes, + ) { // A class object owns every static method its template // declares (`define_class_object_own_properties`), and so @@ -1377,15 +1471,16 @@ fn get_field_by_name_past_data_probe( // DELETED from its object: it must not come back. Only a // method of a shared (never-evaluated-to-an-object) class // is served from the registry. - if super::super::class_registry::lookup_static_method_owner(class_id, name) - .is_some_and(|(owner, _)| { - let object_owned = - super::super::class_registry::class_object_value_for_cid(owner) - .is_some() - || owner == class_id; - !object_owned - }) - { + if super::super::class_registry::lookup_static_method_owner_bytes( + class_id, name_bytes, + ) + .is_some_and(|(owner, _)| { + let object_owned = + super::super::class_registry::class_object_value_for_cid(owner) + .is_some() + || owner == class_id; + !object_owned + }) { let heap_name = { let layout = std::alloc::Layout::from_size_align(name_len.max(1), 1).unwrap(); @@ -1397,9 +1492,9 @@ fn get_field_by_name_past_data_probe( return JSValue::from_bits(result.to_bits()); } if let Some(v) = - super::super::class_registry::class_static_accessor_getter_value( + super::super::class_registry::class_static_accessor_getter_value_bytes( class_id, - name, + name_bytes, class_value, ) { @@ -1407,13 +1502,15 @@ fn get_field_by_name_past_data_probe( } // Fresh nested Promise subclasses inherit the same reified // statics as ClassRef receivers (`P.all`, `P.resolve`, ...). - if super::super::promise_parent_in_chain(class_id) - && super::super::promise_static_function_spec(name).is_some() - { - let value = - super::super::js_promise_static_function_value(name_ptr, name_len); - if value.to_bits() != crate::value::TAG_UNDEFINED { - return JSValue::from_bits(value.to_bits()); + if let Some(name) = name.filter(|name| !name.is_empty()) { + if super::super::promise_parent_in_chain(class_id) + && super::super::promise_static_function_spec(name).is_some() + { + let value = + super::super::js_promise_static_function_value(name_ptr, name_len); + if value.to_bits() != crate::value::TAG_UNDEFINED { + return JSValue::from_bits(value.to_bits()); + } } } } @@ -1885,6 +1982,55 @@ mod null_key_guard_5972 { } } +#[cfg(test)] +mod wtf8_class_object_forwarding_tests { + use super::*; + + #[test] + fn template_class_ref_forwards_runtime_wtf8_static_properties() { + let _global = crate::gc::global_side_table_test_lock(); + unsafe { + const CID: u32 = 62_447; + crate::object::js_register_class_id(CID); + crate::object::js_register_class_name(CID, b"LateStatic".as_ptr(), 10); + + let scope = crate::gc::RuntimeHandleScope::new(); + let template = + scope.root_nanbox_f64(crate::object::class_value::js_class_value(CID as i32)); + let class = scope.root_raw_mut_ptr(crate::object::js_object_alloc(CID, 0)); + class + .with_mut_ptr::(|p| crate::object::js_object_mark_class(p as i64)); + + for bytes in [&b"ascii"[..], &b"\xed\xa0\x80"[..], &b"\xed\xb0\x80"[..]] { + let key = scope.root_string_ptr(crate::string::js_string_from_bytes( + bytes.as_ptr(), + bytes.len() as u32, + )); + key.with_const_ptr::(|key| { + class.with_mut_ptr::(|p| { + crate::object::js_object_set_field_by_name(p, key, 42.0) + }); + class.with_const_ptr::(|p| { + assert_eq!( + js_object_get_field_by_name(p, key).as_number(), + 42.0, + "heap class own property {bytes:?}" + ); + }); + let template_ptr = + crate::value::js_nanbox_get_pointer(template.get_nanbox_f64()) + as *const ObjectHeader; + assert_eq!( + js_object_get_field_by_name(template_ptr, key).as_number(), + 42.0, + "template class ref must forward key {bytes:?}" + ); + }); + } + } + } +} + /// Record `(shape token, key content) -> slot` for the megamorphic read stub. /// /// Only called from inside the fast lane, i.e. once the receiver has already diff --git a/crates/perry-runtime/src/object/field_set_by_name.rs b/crates/perry-runtime/src/object/field_set_by_name.rs index f7f16a248f..9031447541 100644 --- a/crates/perry-runtime/src/object/field_set_by_name.rs +++ b/crates/perry-runtime/src/object/field_set_by_name.rs @@ -450,30 +450,40 @@ pub extern "C" fn js_object_set_field_by_name( let name_ptr = (key as *const u8).add(std::mem::size_of::()); let name_len = (*key).byte_len as usize; - let name = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) - .unwrap_or("") - .to_string(); + // Keep the key alive independently of the StringHeader: + // class accessor lookup and prototype materialization can + // allocate and move the original JS string. + let name_storage = std::slice::from_raw_parts(name_ptr, name_len).to_vec(); + let name = name_storage.as_slice(); + let name_str = std::str::from_utf8(name).ok(); let recv = crate::object::class_value::boxed_class_word(bits); let is_prototype_ref = super::class_prototype_ref_id(recv).is_some(); if is_prototype_ref { - super::class_registry::class_prototype_set(class_id, name, value.to_bits()); + super::class_registry::class_prototype_set_bytes( + class_id, + name, + value.to_bits(), + ); return; } if !is_prototype_ref - && name == "name" - && !super::class_registry::class_static_key_deleted(class_id, &name) - && super::class_registry::lookup_static_method_in_chain(class_id, &name) - .is_none() - && super::class_registry::class_static_defined_attrs(class_id, &name) + && name_str == Some("name") + && !super::class_registry::class_static_key_deleted_bytes(class_id, name) + && super::class_registry::lookup_static_method_in_chain_bytes( + class_id, name, + ) + .is_none() + && super::class_registry::class_static_defined_attrs_bytes(class_id, name) .is_none_or(|(writable, _, _)| !writable) { return; } let has_own_data = - crate::object::class_value::class_static_get(class_id, &name).is_some(); + crate::object::class_value::class_static_get_bytes(class_id, name) + .is_some(); if !has_own_data - && super::class_registry::class_static_accessor_setter_apply( - class_id, &name, recv, value, + && super::class_registry::class_static_accessor_setter_apply_bytes( + class_id, name, recv, value, ) { return; @@ -483,7 +493,7 @@ pub extern "C" fn js_object_set_field_by_name( // from Function.prototype. Prototype refs are plain objects. if !is_prototype_ref && !has_own_data - && matches!(name.as_str(), "caller" | "arguments") + && matches!(name_str, Some("caller" | "arguments")) { crate::fs::validate::throw_type_error_with_code( "Restricted function property access", @@ -494,13 +504,21 @@ pub extern "C" fn js_object_set_field_by_name( // A read-only own static (defineProperty writable:false): // strict-mode [[Set]] throws; the value stays. if has_own_data - && super::class_registry::class_static_defined_attrs(class_id, &name) - .is_some_and(|(writable, _, _)| !writable) + && super::class_registry::class_static_defined_attrs_bytes( + class_id, name, + ) + .is_some_and(|(writable, _, _)| !writable) { - let message = format!( - "Cannot assign to read only property '{name}' of function '{}'", - super::class_registry::class_ref_to_string(class_id) - ); + let class_name = super::class_registry::class_ref_to_string(class_id); + let message = if let Some(name) = name_str { + format!( + "Cannot assign to read only property '{name}' of function '{class_name}'" + ) + } else { + format!( + "Cannot assign to read only property of function '{class_name}'" + ) + }; crate::node_submodules::diagnostics::throw_type_error_no_code( message.as_bytes(), ); @@ -509,7 +527,13 @@ pub extern "C" fn js_object_set_field_by_name( // property dispatch and the LLVM global used by direct // `C.name` reads. Keep both coherent for every runtime // spelling, including a genuinely dynamic `C[key]`. - class_ref_dynamic_prop_root_store(class_id, &name, value); + if let Some(name) = name_str { + class_ref_dynamic_prop_root_store(class_id, name, value); + } else { + crate::object::class_value::class_static_set_bytes( + class_id, name, value, + ); + } } } return; diff --git a/crates/perry-runtime/src/object/field_set_by_name/tail.rs b/crates/perry-runtime/src/object/field_set_by_name/tail.rs index eefe9a9e19..288c7f61ab 100644 --- a/crates/perry-runtime/src/object/field_set_by_name/tail.rs +++ b/crates/perry-runtime/src/object/field_set_by_name/tail.rs @@ -302,17 +302,15 @@ pub(crate) fn set_field_by_name_object_tail( { let name_ptr = crate::string::string_data(key); let name_len = (*key).byte_len as usize; - if let Ok(name) = std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) { - let receiver = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - if super::class_registry::class_static_accessor_setter_apply( - obj_class_id, - name, - receiver, - value, - ) { - return; - } + let name = std::slice::from_raw_parts(name_ptr, name_len); + let receiver = f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + if super::class_registry::class_static_accessor_setter_apply_bytes( + obj_class_id, + name, + receiver, + value, + ) { + return; } } @@ -343,32 +341,33 @@ pub(crate) fn set_field_by_name_object_tail( let key_bytes = { let name_ptr = (key as *const u8).add(std::mem::size_of::()); - std::slice::from_raw_parts(name_ptr, (*key).byte_len as usize) + std::slice::from_raw_parts(name_ptr, (*key).byte_len as usize).to_vec() }; - if let Ok(name) = std::str::from_utf8(key_bytes) { - // Class accessors are properties of the class prototype - // chain (charter step 3). A getter-only accessor refuses - // the write: no data property is created. - let this_f64: f64 = - f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); - let throw_getter_only = || { - let class_name = super::class_registry::class_name_for_id(class_id) - .unwrap_or_else(|| "Object".to_string()); - crate::collection_iter::throw_type_error(&format!( - "Cannot set property {name} of #<{class_name}> which has only a getter" - )); - }; - // #11499: the receiver carrying a `class_id` may be the - // CLASS OBJECT itself, not an instance of that class. The - // vtable accessors below live on `C.prototype`, which is - // NOT on the constructor's own prototype chain - // (`VR8 -> XX -> Error -> Function.prototype`), so a - // static write must resolve against the STATIC accessor - // chain. cc 2.1.112 hits this with - // `class XX extends Error { get errorCode(){…} }` plus - // `VR8.errorCode = "invalid_request"`: the instance getter - // was found for a static write and refused it. - if crate::object::is_class_object_ptr(obj.cast()) { + // Class accessors are properties of the class prototype chain + // (charter step 3). Keep the owned bytes across prototype + // materialization and possible user code in the setter. + let name = std::str::from_utf8(&key_bytes).ok(); + let this_f64: f64 = + f64::from_bits(crate::value::js_nanbox_pointer(obj as i64).to_bits()); + let throw_getter_only = || { + let class_name = super::class_registry::class_name_for_id(class_id) + .unwrap_or_else(|| "Object".to_string()); + let display_name = String::from_utf8_lossy(&key_bytes); + crate::collection_iter::throw_type_error(&format!( + "Cannot set property {display_name} of #<{class_name}> which has only a getter" + )); + }; + // #11499: the receiver carrying a `class_id` may be the + // CLASS OBJECT itself, not an instance of that class. The + // vtable accessors below live on `C.prototype`, which is + // NOT on the constructor's own prototype chain + // (`VR8 -> XX -> Error -> Function.prototype`), so a + // static write must resolve against the STATIC accessor + // chain. The byte-aware class-object path above handles + // non-UTF8 static keys; retain this string path for private + // names and existing diagnostic behavior. + if crate::object::is_class_object_ptr(obj.cast()) { + if let Some(name) = name { if super::class_registry::static_accessor_in_chain(class_id, name) { // A real `static set name(v)` (own or inherited // through `extends`) must still fire. @@ -381,20 +380,20 @@ pub(crate) fn set_field_by_name_object_tail( // refusal the instance side gives. throw_getter_only(); } - // No static accessor of this name: fall through to the - // ordinary own-property store below. - } else { - match super::class_registry::instance_chain_setter_apply( - obj, name, this_f64, value, - ) { - Some(true) => return, - // This entry point is the strict one (issue #615: - // strict is the TS default; sloppy writes reach - // `js_put_value_set` with `strict = 0`, whose - // OrdinarySet walk refuses the same write silently). - Some(false) => throw_getter_only(), - None => {} - } + } + // No static accessor of this name: fall through to the + // ordinary own-property store below. + } else { + match super::class_registry::instance_chain_setter_apply_bytes( + obj, &key_bytes, this_f64, value, + ) { + Some(true) => return, + // This entry point is the strict one (issue #615: + // strict is the TS default; sloppy writes reach + // `js_put_value_set` with `strict = 0`, whose + // OrdinarySet walk refuses the same write silently). + Some(false) => throw_getter_only(), + None => {} } } } diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index d127c6b249..e4f87335c7 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -386,6 +386,8 @@ pub use class_meta_registry::{ js_register_class_has_instance, js_register_class_to_string_tag, }; #[cfg(test)] +pub(crate) use descriptor_state::set_builtin_accessor_pair; +#[cfg(test)] pub(crate) use descriptor_state::test_may_have_descriptor_entry; pub(crate) use descriptor_state::{ accessor_descriptor_keys_for_obj, class_instance_set_may_intercept, clear_accessor_descriptor, @@ -395,8 +397,7 @@ pub(crate) use descriptor_state::{ owner_has_property_descriptors, owner_may_have_descriptor_entries, plain_custom_prototype_may_intercept, plain_data_write_may_intercept, reflect_getter_closure_bits, set_accessor_descriptor, set_builtin_accessor_descriptor, - set_builtin_accessor_pair, set_builtin_property_attrs, set_property_attrs, AccessorDescriptor, - PropertyAttrs, + set_builtin_property_attrs, set_property_attrs, AccessorDescriptor, PropertyAttrs, }; pub(crate) use field_get_set::FieldLookupCaches; pub(crate) use field_get_set::{ diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index 25bdb7c472..981e13e71e 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -33,6 +33,8 @@ mod code_point_at_dispatch_tests; #[cfg(test)] mod dispatch_arg_coercion_tests; #[cfg(test)] +mod invalid_utf8_method_tests; +#[cfg(test)] mod probe_dispatch_tests; #[cfg(test)] /// #8139: `toLocaleString` on an array / typed-array / buffer receiver. @@ -1255,6 +1257,11 @@ pub(crate) unsafe fn native_call_method_tower( args_len, ); } + if let Some(result) = + call_invalid_utf8_method_exact(object, method_name_ptr, method_name_len, args_ptr, args_len) + { + return result; + } if !method_name_ptr.is_null() && method_name_len > 0 { let method_name_bytes = std::slice::from_raw_parts(method_name_ptr as *const u8, method_name_len); @@ -2619,6 +2626,73 @@ pub(crate) unsafe fn native_call_method_tower( ); } +/// Preserve the exact property key for method calls whose UTF-8 byte spelling +/// contains a WTF-8 surrogate. The general dispatch tower below is optimized +/// around Rust `str`; converting such a key lossily changes it to U+FFFD before +/// the ordinary class/prototype lookup can answer it. +/// +/// Valid UTF-8 names keep using the existing specialized tower. For a pointer +/// receiver and invalid UTF-8 key, do the equivalent `Get(receiver, key)` and +/// call the resolved value with the original receiver as `this`. +/// +/// # Safety +/// The method-name and argument slices are readable for their supplied +/// lengths. +unsafe fn call_invalid_utf8_method_exact( + receiver: f64, + method_name_ptr: *const i8, + method_name_len: usize, + args_ptr: *const f64, + args_len: usize, +) -> Option { + if method_name_ptr.is_null() || method_name_len == 0 { + return None; + } + let name_bytes = std::slice::from_raw_parts(method_name_ptr.cast::(), method_name_len); + if std::str::from_utf8(name_bytes).is_ok() { + return None; + } + // Keep a stable copy before rooting/allocating anything; the source pointer + // can refer to storage owned by a moving JS string. + let name_bytes = name_bytes.to_vec(); + let receiver = canonicalize_bare_gc_receiver(receiver); + let args = if args_len != 0 && !args_ptr.is_null() { + std::slice::from_raw_parts(args_ptr, args_len).to_vec() + } else { + Vec::new() + }; + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(receiver); + let args = scope.root_nanbox_f64_slice(&args); + let key = scope.root_string_ptr(crate::string::js_string_from_bytes( + name_bytes.as_ptr(), + name_bytes.len() as u32, + )); + let key_value = scope.root_nanbox_f64( + key.with_const_ptr::(crate::value::nanbox_string_key), + ); + let value = crate::object::js_object_get_index_polymorphic( + receiver.get_nanbox_u64() as i64, + key_value.get_nanbox_f64(), + ); + let value = scope.root_nanbox_u64(value.to_bits()); + let rebound = crate::closure::clone_closure_rebind_this( + value.get_nanbox_u64(), + receiver.get_nanbox_f64(), + ); + let rebound = scope.root_nanbox_u64(rebound); + // Rebinding can allocate. Refresh both the receiver and arguments only + // after the callee has been rooted, immediately before entering user code. + let this_value = receiver.get_nanbox_f64(); + let args = crate::gc::RuntimeHandleScope::refreshed_nanbox_f64_slice(&args); + Some(crate::closure::native_call_value_this( + f64::from_bits(rebound.get_nanbox_u64()), + crate::closure::JsThis::from_f64(this_value), + args.as_ptr(), + args.len(), + )) +} + #[cfg(test)] mod undefined_fallback_tests { //! Regression: a method call that falls through to a "no dispatcher → diff --git a/crates/perry-runtime/src/object/native_call_method/invalid_utf8_method_tests.rs b/crates/perry-runtime/src/object/native_call_method/invalid_utf8_method_tests.rs new file mode 100644 index 0000000000..58239c7520 --- /dev/null +++ b/crates/perry-runtime/src/object/native_call_method/invalid_utf8_method_tests.rs @@ -0,0 +1,108 @@ +//! Invalid UTF-8 (WTF-8 surrogate) method keys must survive method-call +//! dispatch, while ordinary UTF-8 names continue through the existing tower. + +use super::*; + +extern "C" fn return_this( + _closure: *const crate::closure::ClosureHeader, + this: crate::closure::JsThis, +) -> f64 { + this.as_f64() +} + +fn string_key(bytes: &[u8]) -> *mut crate::StringHeader { + crate::string::js_string_from_bytes(bytes.as_ptr(), bytes.len() as u32) +} + +fn assert_method_call_binds_receiver(name: &[u8]) { + let _lock = crate::gc::global_side_table_test_lock(); + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(crate::value::js_nanbox_pointer( + crate::object::js_object_alloc(0, 1) as i64, + )); + let key = scope.root_string_ptr(string_key(name)); + let method = scope.root_nanbox_f64(crate::value::js_nanbox_pointer( + crate::closure::js_closure_alloc(crate::fn_info!(return_this, 0; with_declared(0)), 0) + as i64, + )); + key.with_const_ptr::(|key| { + let object = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) + as *mut crate::object::ObjectHeader; + crate::object::js_object_set_field_by_name(object, key, method.get_nanbox_f64()); + }); + + let result = unsafe { + super::js_native_call_method( + receiver.get_nanbox_f64(), + name.as_ptr().cast(), + name.len(), + std::ptr::null(), + 0, + ) + }; + assert_eq!( + result.to_bits(), + receiver.get_nanbox_u64(), + "method call for key {name:?} must preserve its receiver" + ); +} + +fn runtime_throw(f: impl FnOnce()) -> bool { + let env = crate::exception::js_try_push(); + let jumped = unsafe { crate::ffi::setjmp::setjmp(env as *mut std::os::raw::c_int) }; + if jumped == 0 { + f(); + crate::exception::js_try_end(); + false + } else { + crate::exception::js_try_end(); + crate::exception::js_clear_exception(); + true + } +} + +#[test] +fn wtf8_method_call_preserves_exact_key_and_receiver() { + // U+D800 encoded as WTF-8, which is intentionally invalid standard UTF-8. + assert_method_call_binds_receiver(b"\xED\xA0\x80"); +} + +#[test] +fn utf8_method_call_stays_on_the_existing_dispatch_path() { + assert_method_call_binds_receiver(b"ascii"); +} + +#[test] +fn wtf8_non_callable_does_not_fall_through_to_replacement_character_key() { + let _lock = crate::gc::global_side_table_test_lock(); + let scope = crate::gc::RuntimeHandleScope::new(); + let receiver = scope.root_nanbox_f64(crate::value::js_nanbox_pointer( + crate::object::js_object_alloc(0, 2) as i64, + )); + let exact_key = scope.root_string_ptr(string_key(b"\xED\xA0\x80")); + let replacement_key = scope.root_string_ptr(string_key("\u{FFFD}".as_bytes())); + let replacement = scope.root_nanbox_f64(crate::value::js_nanbox_pointer( + crate::closure::js_closure_alloc(crate::fn_info!(return_this, 0; with_declared(0)), 0) + as i64, + )); + exact_key.with_const_ptr::(|key| { + let object = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) + as *mut crate::object::ObjectHeader; + crate::object::js_object_set_field_by_name(object, key, 17.0); + }); + replacement_key.with_const_ptr::(|key| { + let object = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) + as *mut crate::object::ObjectHeader; + crate::object::js_object_set_field_by_name(object, key, replacement.get_nanbox_f64()); + }); + + assert!(runtime_throw(|| unsafe { + super::js_native_call_method( + receiver.get_nanbox_f64(), + b"\xED\xA0\x80".as_ptr().cast(), + 3, + std::ptr::null(), + 0, + ); + })); +} diff --git a/crates/perry-runtime/src/object/native_module/class_method_bind.rs b/crates/perry-runtime/src/object/native_module/class_method_bind.rs index 6a9fdd9ed6..052490013e 100644 --- a/crates/perry-runtime/src/object/native_module/class_method_bind.rs +++ b/crates/perry-runtime/src/object/native_module/class_method_bind.rs @@ -307,30 +307,38 @@ pub(super) fn build_bound_method_closure_with_private_brand( } }); if !method_name_ptr.is_null() && method_name_len > 0 { - if let Ok(name) = unsafe { - std::str::from_utf8(std::slice::from_raw_parts(method_name_ptr, method_name_len)) - } { + let name_bytes = unsafe { std::slice::from_raw_parts(method_name_ptr, method_name_len) }; + let native_length = if let Ok(name) = std::str::from_utf8(name_bytes) { closure_handle.with_mut_ptr::(|closure| { set_bound_native_closure_name(closure, name) }); - if let Some(length) = bound_native_method_length(name) { + bound_native_method_length(name) + } else { + super::set_function_name_bytes( + closure_handle.with_const_ptr::(|closure| { + crate::value::js_nanbox_pointer(closure as i64) + }), + name_bytes, + ); + None + }; + if let Some(length) = native_length { + closure_handle.with_mut_ptr::(|closure| { + set_builtin_closure_length(closure as usize, length) + }); + } else if let Some(class_id) = + class_id_from_method_receiver(instance_handle.get_nanbox_f64()) + { + // User class method bound as a value (`C.prototype.m`, `c.m`): + // stamp its spec `.length` from the registered param count so + // `C.prototype.m.length` reflects the declared arity instead of + // the closure's capture count (Test262 method `.length` tests). + if let Some(length) = + super::class_registry::class_method_bind_length_bytes(class_id, name_bytes) + { closure_handle.with_mut_ptr::(|closure| { set_builtin_closure_length(closure as usize, length) }); - } else if let Some(class_id) = - class_id_from_method_receiver(instance_handle.get_nanbox_f64()) - { - // User class method bound as a value (`C.prototype.m`, `c.m`): - // stamp its spec `.length` from the registered param count so - // `C.prototype.m.length` reflects the declared arity instead of - // the closure's capture count (Test262 method `.length` tests). - if let Some(length) = - super::class_registry::class_method_bind_length(class_id, name) - { - closure_handle.with_mut_ptr::(|closure| { - set_builtin_closure_length(closure as usize, length) - }); - } } } } diff --git a/crates/perry-runtime/src/object/native_module/class_method_values.rs b/crates/perry-runtime/src/object/native_module/class_method_values.rs index d304d9c8ea..a0473f53f8 100644 --- a/crates/perry-runtime/src/object/native_module/class_method_values.rs +++ b/crates/perry-runtime/src/object/native_module/class_method_values.rs @@ -2,16 +2,30 @@ pub(crate) fn class_evaluation_method_value_for_name( owner_class_id: u32, method_name: &str, evaluation_brand: f64, +) -> f64 { + class_evaluation_method_value_for_name_bytes( + owner_class_id, + method_name.as_bytes(), + evaluation_brand, + ) +} + +pub(crate) fn class_evaluation_method_value_for_name_bytes( + owner_class_id: u32, + method_name: &[u8], + evaluation_brand: f64, ) -> f64 { // A template compiled with method entries gives each evaluation's // prototype its own function object per method (`__eclo`): that // is the evaluation's method value while the prototype still holds it. if let Some(value) = - evaluation_prototype_method_value(owner_class_id, method_name, evaluation_brand) + evaluation_prototype_method_value_bytes(owner_class_id, method_name, evaluation_brand) { return value; } - let cache_key = format!("#"); + let mut cache_key = format!("#'); let cached = crate::object::js_object_get_own_field_or_undef( evaluation_brand, cache_key.as_ptr(), @@ -23,14 +37,15 @@ pub(crate) fn class_evaluation_method_value_for_name( let scope = crate::gc::RuntimeHandleScope::new(); let brand = scope.root_nanbox_f64(evaluation_brand); - let leaked = intern_class_method_name(owner_class_id, method_name); + let name = intern_class_method_name_bytes(owner_class_id, method_name); let method = build_bound_method_closure_with_private_brand( class_prototype_ref_value(owner_class_id), - leaked.as_ptr(), - leaked.len(), + name.as_ptr(), + name.len(), Some(brand.get_nanbox_f64()), ); let method = scope.root_nanbox_f64(method); + set_function_name_bytes(method.get_nanbox_f64(), method_name); let key = crate::string::js_string_from_bytes(cache_key.as_ptr(), cache_key.len() as u32); let key = scope.root_string_ptr(key); let class_obj = JSValue::from_bits(brand.get_nanbox_f64().to_bits()) @@ -38,11 +53,11 @@ pub(crate) fn class_evaluation_method_value_for_name( let class_obj = scope.root_raw_mut_ptr(class_obj); class_obj.with_mut_ptr::(|class_obj| { key.with_const_ptr::(|key| { - crate::object::define_builtin_data_property( + crate::object::descriptor_state::define_builtin_data_property_bytes( class_obj, key, method.get_nanbox_f64(), - cache_key.clone(), + &cache_key, crate::object::PropertyAttrs::new(true, true, true), ); }); @@ -50,6 +65,30 @@ pub(crate) fn class_evaluation_method_value_for_name( method.get_nanbox_f64() } +pub(crate) fn set_function_name_bytes(function: f64, name: &[u8]) { + let value = crate::JSValue::from_bits(function.to_bits()); + if !value.is_pointer() { + return; + } + let scope = crate::gc::RuntimeHandleScope::new(); + let function = scope + .root_raw_mut_ptr(value.as_pointer::() + as *mut crate::closure::ClosureHeader); + let name = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let name = scope.root_string_ptr(name); + function.with_mut_ptr::(|function| { + let value = name.with_const_ptr::(|name| { + crate::value::js_nanbox_string(name as i64) + }); + crate::closure::closure_define_data_with_attrs( + function as usize, + "name", + value, + crate::object::PropertyAttrs::new(false, false, true), + ); + }); +} + pub(crate) fn class_private_static_method_value_for_name( owner_class_id: u32, method_name: &str, @@ -112,16 +151,20 @@ pub(crate) fn class_private_static_method_value_for_name( class_declaration_value_store(owner_class_id, kind, method_name, method.to_bits()); method } -static CLASS_METHOD_NAME_INTERNER: OnceLock>> = +static CLASS_METHOD_NAME_INTERNER: OnceLock), &'static [u8]>>> = OnceLock::new(); /// Stable storage for the method-name pointer captured by bound-method /// closures. The key set is bounded by the program's declared class methods, /// even when one class expression is evaluated arbitrarily many times. pub(super) fn intern_class_method_name(class_id: u32, method_name: &str) -> &'static [u8] { + intern_class_method_name_bytes(class_id, method_name.as_bytes()) +} + +pub(super) fn intern_class_method_name_bytes(class_id: u32, method_name: &[u8]) -> &'static [u8] { let interner = crate::once_init::get_or_init(&CLASS_METHOD_NAME_INTERNER, || RwLock::new(HashMap::new())); - let key = (class_id, method_name.to_string()); + let key = (class_id, method_name.to_vec()); if let Ok(guard) = interner.read() { if let Some(bytes) = guard.get(&key).copied() { return bytes; @@ -133,7 +176,7 @@ pub(super) fn intern_class_method_name(class_id: u32, method_name: &str) -> &'st if let Some(bytes) = guard.get(&key).copied() { return bytes; } - let bytes: &'static [u8] = method_name.as_bytes().to_vec().leak(); + let bytes: &'static [u8] = method_name.to_vec().leak(); guard.insert(key, bytes); bytes } @@ -152,7 +195,11 @@ pub(crate) fn build_bound_method_closure( /// `owner_class_id`) holds for method `name` in its prototype, while that /// prototype's own `name` is still the declaration's entry-backed function at /// home in `brand`. `None` for a template without method entries. -fn evaluation_prototype_method_value(owner_class_id: u32, name: &str, brand: f64) -> Option { +fn evaluation_prototype_method_value_bytes( + owner_class_id: u32, + name: &[u8], + brand: f64, +) -> Option { if !class_registry::is_class_object_value(brand) { return None; } @@ -164,7 +211,7 @@ fn evaluation_prototype_method_value(owner_class_id: u32, name: &str, brand: f64 // an existing holder only; asking to build it here would recurse for a // declaration without a closure-convention entry. let proto = crate::object::field_get_set::class_object_materialized_prototype(class)?; - let value = class_registry::class_object_own_field_bytes(proto, name.as_bytes())?; + let value = class_registry::class_object_own_field_bytes(proto, name)?; let closure = JSValue::from_bits(value.to_bits()); if !closure.is_pointer() { return None; diff --git a/crates/perry-runtime/src/object/native_module/class_ref_values.rs b/crates/perry-runtime/src/object/native_module/class_ref_values.rs index 6844461081..9c81b04b5a 100644 --- a/crates/perry-runtime/src/object/native_module/class_ref_values.rs +++ b/crates/perry-runtime/src/object/native_module/class_ref_values.rs @@ -60,24 +60,32 @@ pub(crate) fn class_ref_id(value: f64) -> Option { } pub(crate) unsafe fn metadata_key_to_string(value: f64) -> Option { + let bytes = metadata_key_to_bytes(value)?; + std::str::from_utf8(&bytes).ok().map(str::to_owned) +} + +pub(crate) unsafe fn metadata_key_to_bytes(value: f64) -> Option> { let key_str = crate::builtins::js_string_coerce(value); if key_str.is_null() { return None; } let name_ptr = (key_str as *const u8).add(std::mem::size_of::()); let name_len = (*key_str).byte_len as usize; - std::str::from_utf8(std::slice::from_raw_parts(name_ptr, name_len)) - .ok() - .map(|s| s.to_string()) + Some(std::slice::from_raw_parts(name_ptr, name_len).to_vec()) } pub(crate) fn class_has_own_method(class_id: u32, method_name: &str) -> bool { + class_has_own_method_bytes(class_id, method_name.as_bytes()) +} + +pub(crate) fn class_has_own_method_bytes(class_id: u32, method_name: &[u8]) -> bool { let _no_move = crate::gc::GcSuppressScope::new(); - if crate::object::native_call_method::class_holder::name_is_not_a_prototype_method( - method_name.as_bytes(), - ) { - if class_non_property_method_value(class_id, method_name).is_some() { - return true; + if crate::object::native_call_method::class_holder::name_is_not_a_prototype_method(method_name) + { + if let Ok(method_name) = std::str::from_utf8(method_name) { + if class_non_property_method_value(class_id, method_name).is_some() { + return true; + } } } let selected = crate::object::class_registry::decl_prototype_identity_id(class_id); @@ -94,8 +102,7 @@ pub(crate) fn class_has_own_method(class_id: u32, method_name: &str) -> bool { } else { existing.as_pointer::() as *const ObjectHeader }; - let key = - crate::object::native_call_method::class_holder::MethodKey::bytes(method_name.as_bytes()); + let key = crate::object::native_call_method::class_holder::MethodKey::bytes(method_name); matches!(unsafe { crate::object::native_call_method::class_holder::chain_method(start, &key) }, crate::object::native_call_method::class_holder::ChainMethod::Data { holder, .. } if holder == start) } @@ -122,12 +129,15 @@ pub(crate) fn class_instance_has_method(class_id: u32, name: &str) -> bool { ) } -pub fn class_prototype_method_value_for_name(class_id: u32, name: &str) -> f64 { - class_prototype_method_value_for_bytes(class_id, name.as_bytes()) +pub fn class_prototype_method_value_for_name(class_id: u32, method_name: &str) -> f64 { + class_prototype_method_value_for_name_bytes(class_id, method_name.as_bytes()) } -pub fn class_prototype_method_value_for_bytes(class_id: u32, method_name: &[u8]) -> f64 { - if let Some(bits) = class_kept_method_value(class_id, method_name) { +pub(crate) fn class_prototype_method_value_for_name_bytes( + class_id: u32, + method_name: &[u8], +) -> f64 { + if let Some(bits) = class_kept_method_value_bytes(class_id, method_name) { return f64::from_bits(bits); } // The class's holder keeps its method values, so a declared class gets @@ -141,7 +151,7 @@ pub fn class_prototype_method_value_for_bytes(class_id: u32, method_name: &[u8]) && !crate::object::class_registry::template_has_class_objects(class_id) { crate::object::class_value::class_value_ptr(class_id); - if let Some(bits) = class_kept_method_value(class_id, method_name) { + if let Some(bits) = class_kept_method_value_bytes(class_id, method_name) { return f64::from_bits(bits); } } @@ -149,8 +159,8 @@ pub fn class_prototype_method_value_for_bytes(class_id: u32, method_name: &[u8]) // The declaration is read only to materialize its function object. // Calls and probes subsequently read the holder's slot. let declaration = - crate::object::class_registry::declarations::class_method_decl(class_id, method_name).map( - |m| { + crate::object::class_registry::declarations::class_method_decl_bytes(class_id, method_name) + .map(|m| { ( m.code as usize, m.param_count, @@ -159,8 +169,7 @@ pub fn class_prototype_method_value_for_bytes(class_id: u32, method_name: &[u8]) m.entry as usize, m.name.is_null(), ) - }, - ); + }); let Some((body, params, synthetic, rest, entry, computed_name)) = declaration else { if crate::object::native_call_method::class_holder::name_is_not_a_prototype_method( method_name, @@ -173,7 +182,7 @@ pub fn class_prototype_method_value_for_bytes(class_id: u32, method_name: &[u8]) .map(f64::from_bits) .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)); }; - let value = class_method_declaration_value( + let value = class_method_declaration_value_bytes( class_prototype_ref_value(class_id), method_name, body, @@ -186,14 +195,15 @@ pub fn class_prototype_method_value_for_bytes(class_id: u32, method_name: &[u8]) // The immutable declaration input distinguishes a literal alias-looking // string key from a private or symbol member at materialization. Readers // use this typed function-value entry, never reclassify a deleted key. - let kind = if std::str::from_utf8(method_name).is_ok_and(|name| { - crate::object::class_registry::proto_member_has_no_string_key(class_id, name) - }) { + let kind = if crate::object::class_registry::proto_member_has_no_string_key_bytes( + class_id, + method_name, + ) { ClassDeclarationValueKind::NonPropertyMethod } else { ClassDeclarationValueKind::Method }; - crate::object::class_registry::class_declaration_value_store( + crate::object::class_registry::class_declaration_value_store_bytes( class_id, kind, method_name, @@ -205,17 +215,15 @@ pub fn class_prototype_method_value_for_bytes(class_id: u32, method_name: &[u8]) /// The method value class `class_id`'s holder keeps for `name`: one name /// has one kind (`proto_member_has_no_string_key` decides it, as the store /// below does). -fn class_kept_method_value(class_id: u32, name: impl AsRef<[u8]>) -> Option { - let name = name.as_ref(); - use crate::object::class_registry::{class_declaration_value, ClassDeclarationValueKind}; - let kind = if std::str::from_utf8(name).is_ok_and(|name| { - crate::object::class_registry::proto_member_has_no_string_key(class_id, name) - }) { - ClassDeclarationValueKind::NonPropertyMethod - } else { - ClassDeclarationValueKind::Method - }; - class_declaration_value(class_id, kind, name) +fn class_kept_method_value_bytes(class_id: u32, name: &[u8]) -> Option { + use crate::object::class_registry::{class_declaration_value_bytes, ClassDeclarationValueKind}; + let kind = + if crate::object::class_registry::proto_member_has_no_string_key_bytes(class_id, name) { + ClassDeclarationValueKind::NonPropertyMethod + } else { + ClassDeclarationValueKind::Method + }; + class_declaration_value_bytes(class_id, kind, name) } /// A materialized lexical/symbol member, separate from public properties. @@ -317,23 +325,19 @@ pub(crate) fn class_method_declaration_value_bytes( crate::object::set_builtin_closure_non_constructable(ptr as usize); }); } - // Literal compiled members retain their immutable image metadata and - // birth shape. A computed key supplies an evaluation-specific own name. - if entry == 0 || computed_name { - let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); - let key = scope.root_string_ptr(key); - f.with_mut_ptr::(|ptr| { - let value = key.with_const_ptr::(|key| { - crate::value::js_nanbox_string(key as i64) - }); - crate::closure::closure_define_data_with_attrs( - ptr as usize, - "name", - value, - crate::object::PropertyAttrs::new(false, false, true), - ); + let name_value = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let name_value = scope.root_string_ptr(name_value); + f.with_mut_ptr::(|ptr| { + let value = name_value.with_const_ptr::(|key| { + crate::value::js_nanbox_string(key as i64) }); - } + crate::closure::closure_define_data_with_attrs( + ptr as usize, + "name", + value, + crate::object::PropertyAttrs::new(false, false, true), + ); + }); crate::value::js_nanbox_pointer(f.get_raw_mut_ptr::() as i64) } @@ -353,15 +357,8 @@ pub(crate) fn class_method_entry_declaration_value(class_id: u32, entry: usize, let Some((name, body, params, synthetic, rest, computed_name)) = declaration else { return f64::from_bits(crate::value::TAG_UNDEFINED); }; - class_method_declaration_value( - home, - &name, - body, - params, - synthetic, - rest, - entry, - computed_name, + class_method_declaration_value_bytes( + home, &name, body, params, synthetic, rest, entry, computed_name, ) } @@ -447,11 +444,11 @@ pub extern "C" fn js_class_prototype_method_value(class_ref: f64, method_key: f6 let Some(class_id) = class_ref_id(class_ref) else { return f64::from_bits(crate::value::TAG_UNDEFINED); }; - let method_name = unsafe { metadata_key_to_string(method_key) }; + let method_name = unsafe { metadata_key_to_bytes(method_key) }; let Some(method_name) = method_name else { return f64::from_bits(crate::value::TAG_UNDEFINED); }; - crate::object::class_method_slot_value(class_id, &method_name) + crate::object::class_method_slot_value_bytes(class_id, &method_name) .map(f64::from_bits) .unwrap_or_else(|| f64::from_bits(crate::value::TAG_UNDEFINED)) } diff --git a/scripts/string_payload_access_baseline.txt b/scripts/string_payload_access_baseline.txt index 03f9af3fbf..5d95d3b678 100644 --- a/scripts/string_payload_access_baseline.txt +++ b/scripts/string_payload_access_baseline.txt @@ -9,7 +9,7 @@ inline-offset | perry-ext-net | 1 inline-offset | perry-ext-nodemailer | 1 inline-offset | perry-ext-zlib | 3 inline-offset | perry-ffi | 3 -inline-offset | perry-runtime | 331 +inline-offset | perry-runtime | 329 inline-offset | perry-stdlib | 26 inline-offset | perry-updater | 5 reader-helper | perry-ext-ethers | 1 diff --git a/scripts/string_validator_allowlist.txt b/scripts/string_validator_allowlist.txt index 5211628094..35a831f37d 100644 --- a/scripts/string_validator_allowlist.txt +++ b/scripts/string_validator_allowlist.txt @@ -16,7 +16,8 @@ crates/perry-runtime/src/array/push_pop.rs | js_array_grow | classify_heap_gener crates/perry-runtime/src/json/mod.rs | scan_parse_roots_mut | classify_heap_generation | 1 | GC parse-root scanner generation admission, not string validation. crates/perry-runtime/src/json/parser_scan_tests.rs | large_unescaped_parsed_strings_are_individually_tracked_leaves | gc_malloc_header_is_tracked | 1 | Test of parser malloc-leaf ownership. crates/perry-runtime/src/object/field_set_by_name.rs | js_object_set_field_by_name | classify_heap_generation | 1 | Array forwarding before a property write; no string admission. -crates/perry-runtime/src/object/class_value.rs | write_once_then_minors | classify_heap_generation | 1 | Test of prototype-barrier generation preservation. +crates/perry-runtime/src/object/class_value/tests.rs | write_once_then_minors | classify_heap_generation | 1 | Test of prototype-barrier generation preservation. +crates/perry-runtime/src/object/native_get.rs | try_data_lookup_key | classify_heap_generation | 1 | Ordinary object data-slot proof; no string admission. crates/perry-runtime/src/object/method_site.rs | address_is_prime_stable | classify_heap_generation | 1 | Object method memo publication requires a stable heap generation. crates/perry-runtime/src/promise/then_probe.rs | prove_no_then | classify_heap_generation | 1 | Ordinary-object thenable shape proof; no string admission. crates/perry-runtime/src/object/class_registry/construct.rs | drop | classify_heap_generation | 1 | Test fixture teardown of constructor-owned objects. diff --git a/test-files/test_issue_12202_wtf8_class_keys.ts b/test-files/test_issue_12202_wtf8_class_keys.ts new file mode 100644 index 0000000000..c094ddcd74 --- /dev/null +++ b/test-files/test_issue_12202_wtf8_class_keys.ts @@ -0,0 +1,170 @@ +// #12202: computed class keys retain their exact UTF-16 identity, including +// lone surrogates, through registration, lookup, reflection and function names. +const high = String.fromCharCode(0xd800); +const low = String.fromCharCode(0xdc00); +const replacement = "\ufffd"; +const escapedText = "\\uD800"; +const staticAccess = high + "s"; +const formalKey = high + "d"; +const restKey = high + "r"; +const getterOnlyKey = high + "ro"; +const shadowedSetterKey = high + "shadow"; +const symbolKey = Symbol.for("issue-12202-symbol-control"); + +function units(value) { + const out = []; + for (let i = 0; i < value.length; i++) out.push(value.charCodeAt(i)); + return out; +} + +let coercions = 0; +const coercible = { + [Symbol.toPrimitive]() { + coercions++; + return high; + }, +}; + +class Keys { + [coercible](value) { return `high:${value}`; } + [low](value) { return `low:${value}`; } + [replacement](value) { return `replacement:${value}`; } + [escapedText](value) { return `escaped:${value}`; } + [""](value) { return `empty:${value}`; } + ["ascii"](value) { return `ascii:${value}`; } + ["é"](value) { return `bmp:${value}`; } + ["𝄞"](value) { return `astral:${value}`; } + get [high + "g"]() { return 17; } + set [high + "g"](value) { this.saved = value; } + [formalKey](value) { return value; } + [restKey](...values) { return values.length; } + [symbolKey]() { return "symbol"; } + static [high](value) { return `static:${value}`; } + static get [staticAccess]() { return this.savedStatic; } + static set [staticAccess](value) { this.savedStatic = value; } +} +class GetterOnlyKey { + get [getterOnlyKey]() { return 41; } +} +class ShadowedSetterKey { + set [shadowedSetterKey](value) { this.setterRan = true; } +} + +const instance = new Keys(); +const highDescriptor = Object.getOwnPropertyDescriptor(Keys.prototype, high); +const getterDescriptor = Object.getOwnPropertyDescriptor(Keys.prototype, high + "g"); +instance[high + "g"] = 19; +const staticGetterDescriptor = Object.getOwnPropertyDescriptor(Keys, staticAccess); +Keys[staticAccess] = 23; +const getterOnly = new GetterOnlyKey(); +let getterOnlyThrows = false; +try { + getterOnly[getterOnlyKey] = 42; +} catch (error) { + getterOnlyThrows = error instanceof TypeError; +} +const shadowedSetter = new ShadowedSetterKey(); +Object.defineProperty(shadowedSetter, shadowedSetterKey, { + value: 7, + writable: true, + configurable: true, +}); +shadowedSetter[shadowedSetterKey] = 8; + +function factory(key) { + return class { + [key]() { return key; } + static get [key + "s"]() { return key; } + }; +} +function factoryWithLateStatic(key) { + function readLateStatic() { return Late[key]; } + const Late = class {}; + Object.defineProperty(Late, key, { value: "late-static" }); + return { Late, readLateStatic }; +} +const First = factory(high); +const firstMethod = First.prototype[high]; +const firstStaticDescriptor = Object.getOwnPropertyDescriptor(First, high + "s"); +const firstStaticValue = First[high + "s"]; +const Second = factory(low); +const secondMethod = Second.prototype[low]; +const first = new First(); +const second = new Second(); +const lateStatic = factoryWithLateStatic(high); + +class Parent { [high]() { return "parent"; } } +class Child extends Parent {} +class StaticParent { static [high]() { return "parent-static"; } } +class StaticChild extends StaticParent {} + +class Duplicates { + [high]() { return "earlier"; } + [high]() { return "last"; } + get [low + "a"]() { return 31; } + set [low + "a"](value) { this.saved = value; } +} +const duplicate = new Duplicates(); +duplicate[low + "a"] = 37; +let staticPrototypeThrows = false; +try { + class InvalidStaticPrototype { static ["prototype"]() {} } + void InvalidStaticPrototype; +} catch (error) { + staticPrototypeThrows = error instanceof TypeError; +} + +const result = { + ownKeys: Reflect.ownKeys(Keys.prototype).map(units), + names: [ + units(Object.getOwnPropertyDescriptor(Keys.prototype, high).value.name), + units(Object.getOwnPropertyDescriptor(Keys, high).value.name), + units(getterDescriptor.get.name), + units(getterDescriptor.set.name), + units(staticGetterDescriptor.get.name), + units(staticGetterDescriptor.set.name), + units(firstStaticDescriptor.get.name), + ], + values: [ + instance[high]("ok"), instance[low]("ok"), instance[replacement]("ok"), + instance[escapedText]("ok"), instance.ascii("ok"), instance["é"]("ok"), + instance["𝄞"]("ok"), instance[""]("ok"), instance[high + "g"], instance.saved, + Keys[high]("ok"), Keys[staticAccess], first[high](), second[low](), + firstStaticValue, StaticChild[high](), + new Child()[high](), duplicate[high](), duplicate[low + "a"], duplicate.saved, + instance[symbolKey](), lateStatic.readLateStatic(), + ], + descriptor: [highDescriptor.enumerable, highDescriptor.configurable, highDescriptor.writable], + keyKinds: [ + typeof instance[high], typeof instance[low], typeof instance[replacement], + typeof instance[escapedText], typeof instance[""], typeof instance[symbolKey], + ], + // Keep the WTF-8 formal/rest controls here. Default-parameter length is + // tracked separately because Perry's generic entryless computed-method + // metadata currently counts the defaulted parameter. + lengths: [Keys.prototype[formalKey].length, Keys.prototype[restKey].length], + identities: [ + First.prototype[high] === firstMethod, + Second.prototype[low] === secondMethod, + firstMethod !== secondMethod, + ], + staticDescriptorOwnership: [ + Object.getOwnPropertyDescriptor(StaticParent, high) !== undefined, + Object.getOwnPropertyDescriptor(StaticChild, high) === undefined, + ], + accessorControls: [ + getterOnly[getterOnlyKey], getterOnlyThrows, + Object.hasOwn(getterOnly, getterOnlyKey), shadowedSetter[shadowedSetterKey], + shadowedSetter.setterRan === undefined, + ], + staticPrototypeThrows, + coercions, +}; + +delete Keys.prototype[high]; +Keys.prototype[high] = function readded() { return "readded"; }; +result.readded = [ + instance[high](), + Reflect.ownKeys(Keys.prototype).map(units), +]; +console.log(JSON.stringify(result));