diff --git a/cli/src/debug/object.rs b/cli/src/debug/object.rs index c81d91be68..1df662784d 100644 --- a/cli/src/debug/object.rs +++ b/cli/src/debug/object.rs @@ -1,6 +1,7 @@ use boa_engine::{ - js_string, object::ObjectInitializer, Context, JsNativeError, JsObject, JsResult, JsValue, - NativeFunction, + js_string, + object::{IndexProperties, ObjectInitializer}, + Context, JsNativeError, JsObject, JsResult, JsValue, NativeFunction, }; /// Returns objects pointer in memory. @@ -21,8 +22,36 @@ fn id(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult { Ok(js_string!(format!("0x{:X}", ptr.cast::<()>() as usize)).into()) } +/// Returns objects pointer in memory. +fn indexed_elements_type(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult { + let Some(value) = args.first() else { + return Err(JsNativeError::typ() + .with_message("expected object argument") + .into()); + }; + + let Some(object) = value.as_object() else { + return Err(JsNativeError::typ() + .with_message(format!("expected object, got {}", value.type_of())) + .into()); + }; + + let typ = match object.borrow().properties().index_properties() { + IndexProperties::DenseI32(_) => "DenseI32", + IndexProperties::DenseF64(_) => "DenseF64", + IndexProperties::DenseElement(_) => "DenseElement", + IndexProperties::Sparse(_) => "SparseElement", + }; + Ok(js_string!(typ).into()) +} + pub(super) fn create_object(context: &mut Context) -> JsObject { ObjectInitializer::new(context) .function(NativeFunction::from_fn_ptr(id), js_string!("id"), 1) + .function( + NativeFunction::from_fn_ptr(indexed_elements_type), + js_string!("indexedElementsType"), + 1, + ) .build() } diff --git a/core/engine/src/builtins/array/mod.rs b/core/engine/src/builtins/array/mod.rs index 1de8a2179a..78461e871d 100644 --- a/core/engine/src/builtins/array/mod.rs +++ b/core/engine/src/builtins/array/mod.rs @@ -15,9 +15,10 @@ use boa_profiler::Profiler; use thin_vec::ThinVec; use crate::{ - builtins::iterable::{if_abrupt_close_iterator, IteratorHint}, - builtins::BuiltInObject, - builtins::Number, + builtins::{ + iterable::{if_abrupt_close_iterator, IteratorHint}, + BuiltInObject, Number, + }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, error::JsNativeError, js_string, @@ -27,7 +28,7 @@ use crate::{ ordinary_get_own_property, InternalMethodContext, InternalObjectMethods, ORDINARY_INTERNAL_METHODS, }, - JsData, JsObject, CONSTRUCTOR, + IndexedProperties, JsData, JsObject, CONSTRUCTOR, }, property::{Attribute, PropertyDescriptor, PropertyKey, PropertyNameKind}, realm::Realm, @@ -403,7 +404,11 @@ impl Array { .intrinsics() .templates() .array() - .create_with_indexed_properties(Array, vec![JsValue::new(length)], elements) + .create_with_indexed_properties( + Array, + vec![JsValue::new(length)], + IndexedProperties::from_dense_js_value(elements), + ) } /// Utility function for concatenating array objects. @@ -1210,6 +1215,26 @@ impl Array { // slot-based dense property maps. if o.is_array() { let mut o_borrow = o.borrow_mut(); + if let IndexedProperties::DenseI32(dense) = + &mut o_borrow.properties_mut().indexed_properties + { + if len <= dense.len() as u64 { + let v = dense.remove(0); + drop(o_borrow); + Self::set_length(&o, len - 1, context)?; + return Ok(v.into()); + } + } + if let IndexedProperties::DenseF64(dense) = + &mut o_borrow.properties_mut().indexed_properties + { + if len <= dense.len() as u64 { + let v = dense.remove(0); + drop(o_borrow); + Self::set_length(&o, len - 1, context)?; + return Ok(v.into()); + } + } if let Some(dense) = o_borrow.properties_mut().dense_indexed_properties_mut() { if len <= dense.len() as u64 { let v = dense.remove(0); diff --git a/core/engine/src/builtins/temporal/mod.rs b/core/engine/src/builtins/temporal/mod.rs index 9ef7c5a97f..f7b88efd4c 100644 --- a/core/engine/src/builtins/temporal/mod.rs +++ b/core/engine/src/builtins/temporal/mod.rs @@ -305,7 +305,7 @@ pub(crate) fn to_integer_if_integral(arg: &JsValue, context: &mut Context) -> Js // 1. Let number be ? ToNumber(argument). // 2. If IsIntegralNumber(number) is false, throw a RangeError exception. // 3. Return ℝ(number). - if !arg.is_integer() { + if !arg.is_integral_number() { return Err(JsNativeError::range() .with_message("value to convert is not an integral number.") .into()); diff --git a/core/engine/src/object/property_map.rs b/core/engine/src/object/property_map.rs index 53c14512b7..a480653df2 100644 --- a/core/engine/src/object/property_map.rs +++ b/core/engine/src/object/property_map.rs @@ -36,58 +36,76 @@ unsafe impl Trace for OrderedHashMap { /// This represents all the indexed properties. /// /// The index properties can be stored in two storage methods: -/// - `Dense` Storage -/// - `Sparse` Storage /// -/// By default it is dense storage. +/// ## Dense Storage +/// +/// Dense storage holds a contiguous array of properties where the index in the array is the key of the property. +/// These are known to be data descriptors with a value field, writable field set to `true`, configurable field set to `true`, enumerable field set to `true`. +/// +/// Since we know the properties of the property descriptors (and they are all the same) we can omit it and just store only +/// the value field and construct the data property descriptor on demand. +/// +/// ## Sparse Storage +/// +/// This storage is used as a backup if the element keys are not continuous or the property descriptors +/// are not data descriptors with with a value field, writable field set to `true`, configurable field set to `true`, enumerable field set to `true`. +/// +/// This method uses more space, since we also have to store the property descriptors, not just the value. +/// It is also slower because we need to do a hash lookup. #[derive(Debug, Trace, Finalize)] -enum IndexedProperties { - /// Dense storage holds a contiguous array of properties where the index in the array is the key of the property. - /// These are known to be data descriptors with a value field, writable field set to `true`, configurable field set to `true`, enumerable field set to `true`. - /// - /// Since we know the properties of the property descriptors (and they are all the same) we can omit it and just store only - /// the value field and construct the data property descriptor on demand. - /// - /// This storage method is used by default. - Dense(ThinVec), +pub enum IndexedProperties { + /// Dense [`i32`] storage. + DenseI32(ThinVec), - /// Sparse storage this storage is used as a backup if the element keys are not continuous or the property descriptors - /// are not data descriptors with with a value field, writable field set to `true`, configurable field set to `true`, enumerable field set to `true`. - /// - /// This method uses more space, since we also have to store the property descriptors, not just the value. - /// It is also slower because we need to do a hash lookup. + /// Dense [`f64`] storage. + DenseF64(ThinVec), + + /// Dense [`JsValue`] storage. + DenseElement(ThinVec), + + /// Sparse [`JsValue`] storage. Sparse(Box>), } impl Default for IndexedProperties { #[inline] fn default() -> Self { - Self::Dense(ThinVec::new()) + Self::DenseI32(ThinVec::new()) } } impl IndexedProperties { - fn new(elements: ThinVec) -> Self { - Self::Dense(elements) + pub(crate) fn from_dense_js_value(elements: ThinVec) -> Self { + if elements.is_empty() { + return Self::default(); + } + Self::DenseElement(elements) } /// Get a property descriptor if it exists. fn get(&self, key: u32) -> Option { - match self { - Self::Sparse(ref map) => map.get(&key).cloned(), - Self::Dense(ref vec) => vec.get(key as usize).map(|value| { - PropertyDescriptorBuilder::new() - .writable(true) - .enumerable(true) - .configurable(true) - .value(value.clone()) - .build() - }), - } + let value = match self { + Self::DenseI32(ref vec) => vec.get(key as usize).copied()?.into(), + Self::DenseF64(ref vec) => vec.get(key as usize).copied()?.into(), + Self::DenseElement(ref vec) => vec.get(key as usize)?.clone(), + Self::Sparse(ref map) => return map.get(&key).cloned(), + }; + + Some( + PropertyDescriptorBuilder::new() + .writable(true) + .enumerable(true) + .configurable(true) + .value(value) + .build(), + ) } /// Helper function for converting from a dense storage type to sparse storage type. - fn convert_dense_to_sparse(vec: &mut ThinVec) -> FxHashMap { + fn convert_dense_to_sparse(vec: &mut ThinVec) -> FxHashMap + where + T: Into, + { let data = std::mem::take(vec); data.into_iter() @@ -99,32 +117,108 @@ impl IndexedProperties { .writable(true) .enumerable(true) .configurable(true) - .value(value) + .value(value.into()) .build(), ) }) .collect() } + fn convert_to_sparse_and_insert(&mut self, key: u32, property: PropertyDescriptor) -> bool { + let mut map = match self { + Self::DenseI32(vec) => Self::convert_dense_to_sparse(vec), + Self::DenseF64(vec) => Self::convert_dense_to_sparse(vec), + Self::DenseElement(vec) => Self::convert_dense_to_sparse(vec), + Self::Sparse(map) => { + return map.insert(key, property).is_some(); + } + }; + + map.insert(key, property); + *self = Self::Sparse(Box::new(map)); + false + } + /// Inserts a property descriptor with the specified key. fn insert(&mut self, key: u32, property: PropertyDescriptor) -> bool { - let vec = match self { - Self::Sparse(map) => return map.insert(key, property).is_some(), - Self::Dense(vec) => { + if !property.writable().unwrap_or(false) + || !property.enumerable().unwrap_or(false) + || !property.configurable().unwrap_or(false) + { + return self.convert_to_sparse_and_insert(key, property); + } + let Some(value) = property.value().cloned() else { + return self.convert_to_sparse_and_insert(key, property); + }; + + match self { + // Fast Path: continues array access. + Self::DenseI32(vec) if key <= vec.len() as u32 => { + let len = vec.len() as u32; + + // If it can fit in a i32 and the truncated version is + // equal to the original then it is an integer. + let is_rational_integer = |n: f64| n.to_bits() == f64::from(n as i32).to_bits(); + + let value = match value { + JsValue::Integer(n) => n, + JsValue::Rational(n) if is_rational_integer(n) => n as i32, + JsValue::Rational(value) => { + let mut vec = vec.iter().copied().map(f64::from).collect::>(); + + // If the key is pointing one past the last element, we push it! + // + // Since the previous key is the current key - 1. Meaning that the elements are continuos. + if key == len { + vec.push(value); + *self = Self::DenseF64(vec); + return false; + } + + // If it the key points in at a already taken index, set it. + vec[key as usize] = value; + *self = Self::DenseF64(vec); + return true; + } + value => { + let mut vec = vec + .iter() + .copied() + .map(JsValue::from) + .collect::>(); + + // If the key is pointing one past the last element, we push it! + // + // Since the previous key is the current key - 1. Meaning that the elements are continuos. + if key == len { + vec.push(value); + *self = Self::DenseElement(vec); + return false; + } + + // If it the key points in at a already taken index, set it. + vec[key as usize] = value; + *self = Self::DenseElement(vec); + return true; + } + }; + + // If the key is pointing one past the last element, we push it! + // + // Since the previous key is the current key - 1. Meaning that the elements are continuos. + if key == len { + vec.push(value); + return false; + } + + // If it the key points in at a already taken index, swap and return it. + vec[key as usize] = value; + true + } + Self::DenseF64(vec) if key <= vec.len() as u32 => { let len = vec.len() as u32; - if key <= len - && property.value().is_some() - && property.writable().unwrap_or(false) - && property.enumerable().unwrap_or(false) - && property.configurable().unwrap_or(false) - { - // Fast Path: continues array access. - - let mut value = property - .value() - .cloned() - .expect("already checked that the property descriptor has a value field"); + if let Some(value) = value.as_number() { // If the key is pointing one past the last element, we push it! // // Since the previous key is the current key - 1. Meaning that the elements are continuos. @@ -134,93 +228,144 @@ impl IndexedProperties { } // If it the key points in at a already taken index, swap and return it. - std::mem::swap(&mut vec[key as usize], &mut value); + vec[key as usize] = value; return true; } - vec - } - }; - - // Slow path: converting to sparse storage. - let mut map = Self::convert_dense_to_sparse(vec); - let replaced = map.insert(key, property).is_some(); - *self = Self::Sparse(Box::new(map)); + let mut vec = vec + .iter() + .copied() + .map(JsValue::from) + .collect::>(); - replaced - } - - /// Removes a property descriptor with the specified key. - fn remove(&mut self, key: u32) -> bool { - let vec = match self { - Self::Sparse(map) => { - return map.remove(&key).is_some(); - } - Self::Dense(vec) => { - // Fast Path: contiguous storage. - - // Has no elements or out of range, nothing to delete! - if vec.is_empty() || key as usize >= vec.len() { + // If the key is pointing one past the last element, we push it! + // + // Since the previous key is the current key - 1. Meaning that the elements are continuos. + if key == len { + vec.push(value); + *self = Self::DenseElement(vec); return false; } - // If the key is pointing at the last element, then we pop it. + // If it the key points in at a already taken index, set it. + vec[key as usize] = value; + *self = Self::DenseElement(vec); + true + } + Self::DenseElement(vec) if key <= vec.len() as u32 => { + let len = vec.len() as u32; + + // If the key is pointing one past the last element, we push it! // - // It does not make the storage sparse. - if key as usize == vec.len().wrapping_sub(1) { - vec.pop().expect("Already checked if it is out of bounds"); - return true; + // Since the previous key is the current key - 1. Meaning that the elements are continuos. + if key == len { + vec.push(value); + return false; } - vec + // If it the key points in at a already taken index, set it. + vec[key as usize] = value; + true } + Self::Sparse(map) => map.insert(key, property).is_some(), + _ => self.convert_to_sparse_and_insert(key, property), + } + } + + fn convert_to_sparse_and_remove(&mut self, key: u32) -> bool { + let mut map = match self { + IndexedProperties::DenseI32(vec) => Self::convert_dense_to_sparse(vec), + IndexedProperties::DenseF64(vec) => Self::convert_dense_to_sparse(vec), + IndexedProperties::DenseElement(vec) => Self::convert_dense_to_sparse(vec), + IndexedProperties::Sparse(map) => return map.remove(&key).is_some(), }; - // Slow Path: conversion to sparse storage. - let mut map = Self::convert_dense_to_sparse(vec); let removed = map.remove(&key).is_some(); *self = Self::Sparse(Box::new(map)); - removed } + /// Removes a property descriptor with the specified key. + fn remove(&mut self, key: u32) -> bool { + match self { + // Fast Paths: contiguous storage. + // + // If the key is pointing at the last element, then we pop it. + Self::DenseI32(vec) if (key + 1) == vec.len() as u32 => { + vec.pop(); + true + } + // If the key is out of range then don't do anything. + Self::DenseI32(vec) if key >= vec.len() as u32 => false, + Self::DenseF64(vec) if (key + 1) == vec.len() as u32 => { + vec.pop(); + true + } + Self::DenseF64(vec) if key >= vec.len() as u32 => false, + Self::DenseElement(vec) if (key + 1) == vec.len() as u32 => { + vec.pop(); + true + } + Self::DenseElement(vec) if key >= vec.len() as u32 => false, + // Slow Paths: non-contiguous storage. + Self::Sparse(map) => map.remove(&key).is_some(), + _ => self.convert_to_sparse_and_remove(key), + } + } + /// Check if we contain the key to a property descriptor. fn contains_key(&self, key: u32) -> bool { match self { + Self::DenseI32(vec) => (0..vec.len() as u32).contains(&key), + Self::DenseF64(vec) => (0..vec.len() as u32).contains(&key), + Self::DenseElement(vec) => (0..vec.len() as u32).contains(&key), Self::Sparse(map) => map.contains_key(&key), - Self::Dense(vec) => (0..vec.len() as u32).contains(&key), } } fn iter(&self) -> IndexProperties<'_> { match self { - Self::Dense(vec) => IndexProperties::Dense(vec.iter().enumerate()), + Self::DenseI32(vec) => IndexProperties::DenseI32(vec.iter().enumerate()), + Self::DenseF64(vec) => IndexProperties::DenseF64(vec.iter().enumerate()), + Self::DenseElement(vec) => IndexProperties::DenseElement(vec.iter().enumerate()), Self::Sparse(map) => IndexProperties::Sparse(map.iter()), } } fn keys(&self) -> IndexPropertyKeys<'_> { match self { - Self::Dense(vec) => IndexPropertyKeys::Dense(0..vec.len() as u32), + Self::DenseI32(vec) => IndexPropertyKeys::Dense(0..vec.len() as u32), + Self::DenseF64(vec) => IndexPropertyKeys::Dense(0..vec.len() as u32), + Self::DenseElement(vec) => IndexPropertyKeys::Dense(0..vec.len() as u32), Self::Sparse(map) => IndexPropertyKeys::Sparse(map.keys()), } } fn values(&self) -> IndexPropertyValues<'_> { match self { - Self::Dense(vec) => IndexPropertyValues::Dense(vec.iter()), + Self::DenseI32(vec) => IndexPropertyValues::DenseI32(vec.iter()), + Self::DenseF64(vec) => IndexPropertyValues::DenseF64(vec.iter()), + Self::DenseElement(vec) => IndexPropertyValues::DenseElement(vec.iter()), Self::Sparse(map) => IndexPropertyValues::Sparse(map.values()), } } } +impl<'a> IntoIterator for &'a IndexedProperties { + type IntoIter = IndexProperties<'a>; + type Item = (u32, PropertyDescriptor); + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + /// A [`PropertyMap`] contains all the properties of an object. /// /// The property values are stored in different data structures based on keys. #[derive(Default, Debug, Trace, Finalize)] pub struct PropertyMap { /// Properties stored with integers as keys. - indexed_properties: IndexedProperties, + pub(crate) indexed_properties: IndexedProperties, pub(crate) shape: Shape, pub(crate) storage: ObjectStorage, @@ -230,9 +375,9 @@ impl PropertyMap { /// Create a new [`PropertyMap`]. #[must_use] #[inline] - pub fn new(shape: Shape, elements: ThinVec) -> Self { + pub fn new(shape: Shape, indexed_properties: IndexedProperties) -> Self { Self { - indexed_properties: IndexedProperties::new(elements), + indexed_properties, shape, storage: Vec::default(), } @@ -449,21 +594,113 @@ impl PropertyMap { /// Overrides all the indexed properties, setting it to dense storage. pub(crate) fn override_indexed_properties(&mut self, properties: ThinVec) { - self.indexed_properties = IndexedProperties::Dense(properties); + self.indexed_properties = IndexedProperties::DenseElement(properties); + } + + pub(crate) fn get_dense_property(&self, index: u32) -> Option { + let index = index as usize; + match &self.indexed_properties { + IndexedProperties::DenseI32(properties) => { + properties.get(index).copied().map(JsValue::from) + } + IndexedProperties::DenseF64(properties) => { + properties.get(index).copied().map(JsValue::from) + } + IndexedProperties::DenseElement(properties) => properties.get(index).cloned(), + IndexedProperties::Sparse(_) => None, + } + } + + pub(crate) fn set_dense_property(&mut self, index: u32, value: &JsValue) -> bool { + let index = index as usize; + + match &mut self.indexed_properties { + IndexedProperties::DenseI32(properties) => { + let Some(element) = properties.get_mut(index) else { + return false; + }; + + // If it can fit in a i32 and the truncated version is + // equal to the original then it is an integer. + let is_rational_integer = |n: f64| n.to_bits() == f64::from(n as i32).to_bits(); + + let value = match value { + JsValue::Integer(n) => *n, + JsValue::Rational(n) if is_rational_integer(*n) => *n as i32, + JsValue::Rational(value) => { + let mut properties = properties + .iter() + .copied() + .map(f64::from) + .collect::>(); + properties[index] = *value; + self.indexed_properties = IndexedProperties::DenseF64(properties); + + return true; + } + value => { + let mut properties = properties + .iter() + .copied() + .map(JsValue::from) + .collect::>(); + properties[index] = value.clone(); + self.indexed_properties = IndexedProperties::DenseElement(properties); + + return true; + } + }; + + *element = value; + true + } + IndexedProperties::DenseF64(properties) => { + let Some(element) = properties.get_mut(index) else { + return false; + }; + + let Some(value) = value.as_number() else { + let mut properties = properties + .iter() + .copied() + .map(JsValue::from) + .collect::>(); + properties[index] = value.clone(); + self.indexed_properties = IndexedProperties::DenseElement(properties); + return true; + }; + + *element = value; + true + } + IndexedProperties::DenseElement(properties) => { + let Some(element) = properties.get_mut(index) else { + return false; + }; + *element = value.clone(); + true + } + IndexedProperties::Sparse(_) => false, + } } /// Returns the vec of dense indexed properties if they exist. - pub(crate) const fn dense_indexed_properties(&self) -> Option<&ThinVec> { - if let IndexedProperties::Dense(properties) = &self.indexed_properties { - Some(properties) - } else { - None + pub(crate) fn to_dense_indexed_properties(&self) -> Option> { + match &self.indexed_properties { + IndexedProperties::DenseI32(properties) => { + Some(properties.iter().copied().map(JsValue::from).collect()) + } + IndexedProperties::DenseF64(properties) => { + Some(properties.iter().copied().map(JsValue::from).collect()) + } + IndexedProperties::DenseElement(properties) => Some(properties.clone()), + IndexedProperties::Sparse(_) => None, } } /// Returns the vec of dense indexed properties if they exist. pub(crate) fn dense_indexed_properties_mut(&mut self) -> Option<&mut ThinVec> { - if let IndexedProperties::Dense(properties) = &mut self.indexed_properties { + if let IndexedProperties::DenseElement(properties) = &mut self.indexed_properties { Some(properties) } else { None @@ -544,8 +781,14 @@ impl ExactSizeIterator for Iter<'_> { /// An iterator over the indexed property entries of an `Object`. #[derive(Debug, Clone)] pub enum IndexProperties<'a> { + /// An iterator over dense i32, Vec backed indexed property entries of an `Object`. + DenseI32(std::iter::Enumerate>), + + /// An iterator over dense f64, Vec backed indexed property entries of an `Object`. + DenseF64(std::iter::Enumerate>), + /// An iterator over dense, Vec backed indexed property entries of an `Object`. - Dense(std::iter::Enumerate>), + DenseElement(std::iter::Enumerate>), /// An iterator over sparse, HashMap backed indexed property entries of an `Object`. Sparse(hash_map::Iter<'a, u32, PropertyDescriptor>), @@ -555,26 +798,34 @@ impl Iterator for IndexProperties<'_> { type Item = (u32, PropertyDescriptor); fn next(&mut self) -> Option { - match self { - Self::Dense(vec) => vec.next().map(|(index, value)| { - ( - index as u32, - PropertyDescriptorBuilder::new() - .writable(true) - .configurable(true) - .enumerable(true) - .value(value.clone()) - .build(), - ) - }), - Self::Sparse(map) => map.next().map(|(index, value)| (*index, value.clone())), - } + let (index, value) = match self { + Self::DenseI32(vec) => vec + .next() + .map(|(index, value)| (index, JsValue::from(*value)))?, + Self::DenseF64(vec) => vec + .next() + .map(|(index, value)| (index, JsValue::from(*value)))?, + Self::DenseElement(vec) => vec.next().map(|(index, value)| (index, value.clone()))?, + Self::Sparse(map) => return map.next().map(|(index, value)| (*index, value.clone())), + }; + + Some(( + index as u32, + PropertyDescriptorBuilder::new() + .writable(true) + .configurable(true) + .enumerable(true) + .value(value.clone()) + .build(), + )) } #[inline] fn size_hint(&self) -> (usize, Option) { match self { - Self::Dense(vec) => vec.size_hint(), + Self::DenseI32(vec) => vec.size_hint(), + Self::DenseF64(vec) => vec.size_hint(), + Self::DenseElement(vec) => vec.size_hint(), Self::Sparse(map) => map.size_hint(), } } @@ -584,7 +835,9 @@ impl ExactSizeIterator for IndexProperties<'_> { #[inline] fn len(&self) -> usize { match self { - Self::Dense(vec) => vec.len(), + Self::DenseI32(vec) => vec.len(), + Self::DenseF64(vec) => vec.len(), + Self::DenseElement(vec) => vec.len(), Self::Sparse(map) => map.len(), } } @@ -636,9 +889,16 @@ impl FusedIterator for IndexPropertyKeys<'_> {} /// An iterator over the index values (`Property`) of an `Object`. #[derive(Debug, Clone)] +#[allow(variant_size_differences)] pub enum IndexPropertyValues<'a> { /// An iterator over dense, Vec backed indexed property entries of an `Object`. - Dense(std::slice::Iter<'a, JsValue>), + DenseI32(std::slice::Iter<'a, i32>), + + /// An iterator over dense, Vec backed indexed property entries of an `Object`. + DenseF64(std::slice::Iter<'a, f64>), + + /// An iterator over dense, Vec backed indexed property entries of an `Object`. + DenseElement(std::slice::Iter<'a, JsValue>), /// An iterator over sparse, HashMap backed indexed property entries of an `Object`. Sparse(hash_map::Values<'a, u32, PropertyDescriptor>), @@ -648,23 +908,29 @@ impl Iterator for IndexPropertyValues<'_> { type Item = PropertyDescriptor; fn next(&mut self) -> Option { - match self { - Self::Dense(vec) => vec.next().map(|value| { - PropertyDescriptorBuilder::new() - .writable(true) - .configurable(true) - .enumerable(true) - .value(value.clone()) - .build() - }), - Self::Sparse(map) => map.next().cloned(), - } + let value = match self { + Self::DenseI32(vec) => vec.next().copied()?.into(), + Self::DenseF64(vec) => vec.next().copied()?.into(), + Self::DenseElement(vec) => vec.next().cloned()?, + Self::Sparse(map) => return map.next().cloned(), + }; + + Some( + PropertyDescriptorBuilder::new() + .writable(true) + .configurable(true) + .enumerable(true) + .value(value) + .build(), + ) } #[inline] fn size_hint(&self) -> (usize, Option) { match self { - Self::Dense(vec) => vec.size_hint(), + Self::DenseI32(vec) => vec.size_hint(), + Self::DenseF64(vec) => vec.size_hint(), + Self::DenseElement(vec) => vec.size_hint(), Self::Sparse(map) => map.size_hint(), } } @@ -674,7 +940,9 @@ impl ExactSizeIterator for IndexPropertyValues<'_> { #[inline] fn len(&self) -> usize { match self { - Self::Dense(vec) => vec.len(), + Self::DenseI32(vec) => vec.len(), + Self::DenseF64(vec) => vec.len(), + Self::DenseElement(vec) => vec.len(), Self::Sparse(map) => map.len(), } } diff --git a/core/engine/src/object/shape/shared_shape/template.rs b/core/engine/src/object/shape/shared_shape/template.rs index 2f04618e4a..cd9eb1c9d3 100644 --- a/core/engine/src/object/shape/shared_shape/template.rs +++ b/core/engine/src/object/shape/shared_shape/template.rs @@ -2,7 +2,9 @@ use boa_gc::{Finalize, Trace}; use thin_vec::ThinVec; use crate::{ - object::{shape::slot::SlotAttributes, JsObject, NativeObject, Object, PropertyMap}, + object::{ + shape::slot::SlotAttributes, IndexedProperties, JsObject, NativeObject, Object, PropertyMap, + }, property::{Attribute, PropertyKey}, JsValue, }; @@ -110,7 +112,7 @@ impl ObjectTemplate { let mut object = Object { data, extensible: true, - properties: PropertyMap::new(self.shape.clone().into(), ThinVec::default()), + properties: PropertyMap::new(self.shape.clone().into(), IndexedProperties::default()), private_elements: ThinVec::new(), }; @@ -127,13 +129,13 @@ impl ObjectTemplate { &self, data: T, storage: Vec, - elements: ThinVec, + indexed_properties: IndexedProperties, ) -> JsObject { let internal_methods = data.internal_methods(); let mut object = Object { data, extensible: true, - properties: PropertyMap::new(self.shape.clone().into(), elements), + properties: PropertyMap::new(self.shape.clone().into(), indexed_properties), private_elements: ThinVec::new(), }; diff --git a/core/engine/src/value/mod.rs b/core/engine/src/value/mod.rs index 07ae303563..a38c9fd8bb 100644 --- a/core/engine/src/value/mod.rs +++ b/core/engine/src/value/mod.rs @@ -241,10 +241,15 @@ impl JsValue { matches!(self, Self::Rational(_)) } - /// Returns true if the value is integer. + /// Determines if argument is a finite integral Number value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isintegralnumber #[must_use] #[allow(clippy::float_cmp)] - pub fn is_integer(&self) -> bool { + pub fn is_integral_number(&self) -> bool { // If it can fit in a i32 and the truncated version is // equal to the original then it is an integer. let is_rational_integer = |n: f64| n == f64::from(n as i32); @@ -256,6 +261,23 @@ impl JsValue { } } + /// Returns true if the value can be reprented as an integer. + /// + /// Similar to [`JsValue::is_integral_number()`] except that it returns `false` for `-0`. + #[must_use] + #[allow(clippy::float_cmp)] + pub fn is_integer(&self) -> bool { + // If it can fit in a i32 and the truncated version is + // equal to the original then it is an integer. + let is_rational_integer = |n: f64| n.to_bits() == f64::from(n as i32).to_bits(); + + match *self { + Self::Integer(_) => true, + Self::Rational(n) if is_rational_integer(n) => true, + _ => false, + } + } + /// Returns true if the value is a number. #[inline] #[must_use] diff --git a/core/engine/src/vm/opcode/call/mod.rs b/core/engine/src/vm/opcode/call/mod.rs index 2e357952f9..4519f589c9 100644 --- a/core/engine/src/vm/opcode/call/mod.rs +++ b/core/engine/src/vm/opcode/call/mod.rs @@ -106,9 +106,8 @@ impl Operation for CallEvalSpread { let arguments = arguments_array_object .borrow() .properties() - .dense_indexed_properties() - .expect("arguments array in call spread function must be dense") - .clone(); + .to_dense_indexed_properties() + .expect("arguments array in call spread function must be dense"); let at = context.vm.stack.len(); let func = context.vm.stack[at - 1].clone(); @@ -217,9 +216,8 @@ impl Operation for CallSpread { let arguments = arguments_array_object .borrow() .properties() - .dense_indexed_properties() - .expect("arguments array in call spread function must be dense") - .clone(); + .to_dense_indexed_properties() + .expect("arguments array in call spread function must be dense"); let argument_count = arguments.len(); context.vm.push_values(&arguments); diff --git a/core/engine/src/vm/opcode/environment/mod.rs b/core/engine/src/vm/opcode/environment/mod.rs index 937d8d67fa..0f3dcb4b9c 100644 --- a/core/engine/src/vm/opcode/environment/mod.rs +++ b/core/engine/src/vm/opcode/environment/mod.rs @@ -181,9 +181,8 @@ impl Operation for SuperCallSpread { let arguments = arguments_array_object .borrow() .properties() - .dense_indexed_properties() - .expect("arguments array in call spread function must be dense") - .clone(); + .to_dense_indexed_properties() + .expect("arguments array in call spread function must be dense"); let super_constructor = context.vm.pop(); diff --git a/core/engine/src/vm/opcode/get/property.rs b/core/engine/src/vm/opcode/get/property.rs index dbe4602086..395ea12045 100644 --- a/core/engine/src/vm/opcode/get/property.rs +++ b/core/engine/src/vm/opcode/get/property.rs @@ -115,12 +115,9 @@ impl Operation for GetPropertyByValue { if object.is_array() { if let PropertyKey::Index(index) = &key { let object_borrowed = object.borrow(); - if let Some(element) = object_borrowed - .properties() - .dense_indexed_properties() - .and_then(|vec| vec.get(index.get() as usize)) + if let Some(element) = object_borrowed.properties().get_dense_property(index.get()) { - context.vm.push(element.clone()); + context.vm.push(element); return Ok(CompletionType::Normal); } } @@ -162,13 +159,10 @@ impl Operation for GetPropertyByValuePush { if object.is_array() { if let PropertyKey::Index(index) = &key { let object_borrowed = object.borrow(); - if let Some(element) = object_borrowed - .properties() - .dense_indexed_properties() - .and_then(|vec| vec.get(index.get() as usize)) + if let Some(element) = object_borrowed.properties().get_dense_property(index.get()) { context.vm.push(key); - context.vm.push(element.clone()); + context.vm.push(element); return Ok(CompletionType::Normal); } } diff --git a/core/engine/src/vm/opcode/new/mod.rs b/core/engine/src/vm/opcode/new/mod.rs index 8719d6ea59..81a48ea173 100644 --- a/core/engine/src/vm/opcode/new/mod.rs +++ b/core/engine/src/vm/opcode/new/mod.rs @@ -70,9 +70,8 @@ impl Operation for NewSpread { let arguments = arguments_array_object .borrow() .properties() - .dense_indexed_properties() - .expect("arguments array in call spread function must be dense") - .clone(); + .to_dense_indexed_properties() + .expect("arguments array in call spread function must be dense"); let func = context.vm.pop(); diff --git a/core/engine/src/vm/opcode/set/property.rs b/core/engine/src/vm/opcode/set/property.rs index cb99f85aed..c1233b395c 100644 --- a/core/engine/src/vm/opcode/set/property.rs +++ b/core/engine/src/vm/opcode/set/property.rs @@ -1,7 +1,5 @@ -use boa_macros::utf16; - use crate::{ - builtins::{function::set_function_name, Proxy}, + builtins::function::set_function_name, object::{internal_methods::InternalMethodContext, shape::slot::SlotAttributes}, property::{PropertyDescriptor, PropertyKey}, vm::{opcode::Operation, CompletionType}, @@ -145,58 +143,12 @@ impl Operation for SetPropertyByValue { break 'fast_path; } - let shape = object_borrowed.shape().clone(); - - if let Some(dense_elements) = object_borrowed + if object_borrowed .properties_mut() - .dense_indexed_properties_mut() + .set_dense_property(index.get(), &value) { - let index = index.get() as usize; - if let Some(element) = dense_elements.get_mut(index) { - *element = value; - context.vm.push(element.clone()); - return Ok(CompletionType::Normal); - } else if dense_elements.len() == index { - // Cannot use fast path if the [[prototype]] is a proxy object, - // because we have to the call prototypes [[set]] on non-existing property, - // and proxy objects can override [[set]]. - let prototype = shape.prototype(); - if prototype.map_or(false, |x| x.is::()) { - break 'fast_path; - } - - dense_elements.push(value.clone()); - context.vm.push(value); - - let len = dense_elements.len() as u32; - let length_key = PropertyKey::from(utf16!("length")); - let length = object_borrowed - .properties_mut() - .get(&length_key) - .expect("Arrays must have length property"); - - if length.expect_writable() { - // We have to get the max of previous length and len(dense_elements) + 1, - // this is needed if user spacifies `new Array(n)` then adds properties from 0, 1, etc. - let len = length - .expect_value() - .to_u32(context) - .expect("length should have a u32 value") - .max(len); - object_borrowed.insert( - length_key, - PropertyDescriptor::builder() - .value(len) - .writable(true) - .enumerable(length.expect_enumerable()) - .configurable(false) - .build(), - ); - } else if context.vm.frame().code_block.strict() { - return Err(JsNativeError::typ().with_message("TypeError: Cannot assign to read only property 'length' of array object").into()); - } - return Ok(CompletionType::Normal); - } + context.vm.push(value); + return Ok(CompletionType::Normal); } } } diff --git a/docs/boa_object.md b/docs/boa_object.md index da32712fe3..e5e5a92f67 100644 --- a/docs/boa_object.md +++ b/docs/boa_object.md @@ -163,6 +163,30 @@ $boa.object.id(o) // '0x7F5B3251B718' $boa.object.id($boa) // '0x7F5B3251B5D8' ``` +## Function `$boa.object.indexedStorageType(object)` + +This function returns indexed storage type. + +Example: + +```JavaScript +let a = [1, 2] + +$boa.object.indexedStorageType(a) // 'DenseI32' + +a.push(0xdeadbeef) +$boa.object.indexedStorageType(a) // 'DenseI32' + +a.push(0.5) +$boa.object.indexedStorageType(a) // 'DenseF64' + +a.push("Hello") +$boa.object.indexedStorageType(a) // 'DenseElement' + +a[100] = 100 // Make a hole +$boa.object.indexedStorageType(a) // 'SparseElement' +``` + ## Module `$boa.optimizer` This modules contains getters and setters for enabling and disabling optimizations.