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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/12471-wtf8-class-keys.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 11 additions & 2 deletions crates/perry-runtime/src/closure/dynamic_props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
6 changes: 3 additions & 3 deletions crates/perry-runtime/src/closure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"))]
Expand Down
140 changes: 96 additions & 44 deletions crates/perry-runtime/src/closure/props.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ pub(crate) unsafe fn bag_get(ptr: usize, key: &[u8]) -> Option<f64> {
}

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
Expand All @@ -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<f64>) {
unsafe fn declared_value_replaced_bytes(ptr: usize, key: &[u8], declared: Option<f64>) {
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));
Expand All @@ -265,25 +271,30 @@ unsafe fn declared_value_replaced(ptr: usize, key: &str, declared: Option<f64>)
/// # 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
Expand All @@ -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)
Expand All @@ -320,37 +344,38 @@ 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.
///
/// # 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
}

Expand Down Expand Up @@ -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<String> {
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<Vec<u8>> {
let bag = bag_of(ptr);
if bag.is_null() {
return Vec::new();
Expand All @@ -406,7 +438,7 @@ pub(crate) unsafe fn bag_accessor_names(ptr: usize) -> Vec<String> {
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
Expand All @@ -419,6 +451,13 @@ pub(crate) unsafe fn bag_accessor_names(ptr: usize) -> Vec<String> {
/// # Safety
/// `ptr` is a proven, live closure cell.
pub(crate) unsafe fn bag_own_key_names(ptr: usize) -> Vec<String> {
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<Vec<u8>> {
let bag = bag_of(ptr);
if bag.is_null() {
return Vec::new();
Expand All @@ -443,7 +482,7 @@ pub(crate) unsafe fn bag_own_key_names(ptr: usize) -> Vec<String> {
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
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/object/accessor_pair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,10 @@ pub(crate) unsafe fn own_accessor(obj: usize, key: &[u8]) -> Option<Accessor> {
/// `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<Accessor>) {
store_own_accessor_bytes(obj, key.as_bytes(), acc);
}

pub(crate) unsafe fn store_own_accessor_bytes(obj: usize, key: &[u8], acc: Option<Accessor>) {
let _no_move = crate::gc::GcSuppressScope::new();
let value = match acc {
Some(acc) => crate::value::js_nanbox_pointer(pair_new(acc) as i64),
Expand Down
15 changes: 8 additions & 7 deletions crates/perry-runtime/src/object/class_image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ pub type StaticAccessorTable = PtrHashMap<u32, StaticNameMap<super::class_regist
/// the runtime's hasher for untrusted string keys (as in `json::parser`), in
/// place of std's slower SipHash. A static call that misses its site guard
/// probes this map once per class on the parent chain.
pub type StaticNameMap<V> = HashMap<String, V, ahash::RandomState>;
pub type StaticNameMap<V> = HashMap<Vec<u8>, 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<u8>), u32>;
/// class_id -> (ctor func_ptr, total param count, signature capture count).
pub type ConstructorTable = PtrHashMap<u32, (usize, u32, u32)>;
/// class_id -> (has_synthetic_arguments, has_rest, parameters before the first
Expand All @@ -126,17 +126,18 @@ pub type ConstructorFlagTable = PtrHashMap<u32, (bool, bool, Option<u32>)>;
/// 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<u8>)`-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<super::class_registry::declarations::ClassDeclarationIndex>,
pub(crate) static_methods: RwLock<Option<StaticMethodTable>>,
pub(crate) static_accessors: RwLock<Option<StaticAccessorTable>>,
pub(crate) string_member_orders: RwLock<Option<StringMemberOrderTable>>,
pub(crate) method_bind_lengths: RwLock<Option<HashMap<(u32, String), u32>>>,
pub(crate) static_method_bind_lengths: RwLock<Option<HashMap<(u32, String), u32>>>,
pub(crate) method_bind_lengths: RwLock<Option<HashMap<(u32, Vec<u8>), u32>>>,
pub(crate) static_method_bind_lengths: RwLock<Option<HashMap<(u32, Vec<u8>), u32>>>,
pub(crate) registered_class_ids: RwLock<Option<PtrHashSet<u32>>>,
pub(crate) parents: RwLock<Option<PtrHashMap<u32, u32>>>,
/// `parent + 1` for every registered edge whose child id is
Expand Down
Loading
Loading