Browse Source

Dense array storage variants for `i32` and `f64` (#3760)

* Implement `i32` dense arrays

* Implement `f64` dense arrays

* Implement debug object indexed elements type function

* Apply review

* Simplify `IndexedProperties::remove()`
pull/3762/head
Haled Odat 8 months ago committed by GitHub
parent
commit
d3e539593f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 33
      cli/src/debug/object.rs
  2. 35
      core/engine/src/builtins/array/mod.rs
  3. 2
      core/engine/src/builtins/temporal/mod.rs
  4. 456
      core/engine/src/object/property_map.rs
  5. 10
      core/engine/src/object/shape/shared_shape/template.rs
  6. 26
      core/engine/src/value/mod.rs
  7. 10
      core/engine/src/vm/opcode/call/mod.rs
  8. 5
      core/engine/src/vm/opcode/environment/mod.rs
  9. 14
      core/engine/src/vm/opcode/get/property.rs
  10. 5
      core/engine/src/vm/opcode/new/mod.rs
  11. 54
      core/engine/src/vm/opcode/set/property.rs
  12. 24
      docs/boa_object.md

33
cli/src/debug/object.rs

@ -1,6 +1,7 @@
use boa_engine::{ use boa_engine::{
js_string, object::ObjectInitializer, Context, JsNativeError, JsObject, JsResult, JsValue, js_string,
NativeFunction, object::{IndexProperties, ObjectInitializer},
Context, JsNativeError, JsObject, JsResult, JsValue, NativeFunction,
}; };
/// Returns objects pointer in memory. /// Returns objects pointer in memory.
@ -21,8 +22,36 @@ fn id(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
Ok(js_string!(format!("0x{:X}", ptr.cast::<()>() as usize)).into()) 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<JsValue> {
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 { pub(super) fn create_object(context: &mut Context) -> JsObject {
ObjectInitializer::new(context) ObjectInitializer::new(context)
.function(NativeFunction::from_fn_ptr(id), js_string!("id"), 1) .function(NativeFunction::from_fn_ptr(id), js_string!("id"), 1)
.function(
NativeFunction::from_fn_ptr(indexed_elements_type),
js_string!("indexedElementsType"),
1,
)
.build() .build()
} }

35
core/engine/src/builtins/array/mod.rs

@ -15,9 +15,10 @@ use boa_profiler::Profiler;
use thin_vec::ThinVec; use thin_vec::ThinVec;
use crate::{ use crate::{
builtins::iterable::{if_abrupt_close_iterator, IteratorHint}, builtins::{
builtins::BuiltInObject, iterable::{if_abrupt_close_iterator, IteratorHint},
builtins::Number, BuiltInObject, Number,
},
context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
error::JsNativeError, error::JsNativeError,
js_string, js_string,
@ -27,7 +28,7 @@ use crate::{
ordinary_get_own_property, InternalMethodContext, InternalObjectMethods, ordinary_get_own_property, InternalMethodContext, InternalObjectMethods,
ORDINARY_INTERNAL_METHODS, ORDINARY_INTERNAL_METHODS,
}, },
JsData, JsObject, CONSTRUCTOR, IndexedProperties, JsData, JsObject, CONSTRUCTOR,
}, },
property::{Attribute, PropertyDescriptor, PropertyKey, PropertyNameKind}, property::{Attribute, PropertyDescriptor, PropertyKey, PropertyNameKind},
realm::Realm, realm::Realm,
@ -403,7 +404,11 @@ impl Array {
.intrinsics() .intrinsics()
.templates() .templates()
.array() .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. /// Utility function for concatenating array objects.
@ -1210,6 +1215,26 @@ impl Array {
// slot-based dense property maps. // slot-based dense property maps.
if o.is_array() { if o.is_array() {
let mut o_borrow = o.borrow_mut(); 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 let Some(dense) = o_borrow.properties_mut().dense_indexed_properties_mut() {
if len <= dense.len() as u64 { if len <= dense.len() as u64 {
let v = dense.remove(0); let v = dense.remove(0);

2
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). // 1. Let number be ? ToNumber(argument).
// 2. If IsIntegralNumber(number) is false, throw a RangeError exception. // 2. If IsIntegralNumber(number) is false, throw a RangeError exception.
// 3. Return ℝ(number). // 3. Return ℝ(number).
if !arg.is_integer() { if !arg.is_integral_number() {
return Err(JsNativeError::range() return Err(JsNativeError::range()
.with_message("value to convert is not an integral number.") .with_message("value to convert is not an integral number.")
.into()); .into());

456
core/engine/src/object/property_map.rs

@ -36,58 +36,76 @@ unsafe impl<K: Trace> Trace for OrderedHashMap<K> {
/// This represents all the indexed properties. /// This represents all the indexed properties.
/// ///
/// The index properties can be stored in two storage methods: /// The index properties can be stored in two storage methods:
/// - `Dense` Storage
/// - `Sparse` Storage
/// ///
/// By default it is dense storage. /// ## Dense Storage
#[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. /// 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`. /// 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 /// 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. /// the value field and construct the data property descriptor on demand.
/// ///
/// This storage method is used by default. /// ## Sparse Storage
Dense(ThinVec<JsValue>), ///
/// This storage is used as a backup if the element keys are not continuous or the property descriptors
/// 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`. /// 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. /// 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. /// It is also slower because we need to do a hash lookup.
#[derive(Debug, Trace, Finalize)]
pub enum IndexedProperties {
/// Dense [`i32`] storage.
DenseI32(ThinVec<i32>),
/// Dense [`f64`] storage.
DenseF64(ThinVec<f64>),
/// Dense [`JsValue`] storage.
DenseElement(ThinVec<JsValue>),
/// Sparse [`JsValue`] storage.
Sparse(Box<FxHashMap<u32, PropertyDescriptor>>), Sparse(Box<FxHashMap<u32, PropertyDescriptor>>),
} }
impl Default for IndexedProperties { impl Default for IndexedProperties {
#[inline] #[inline]
fn default() -> Self { fn default() -> Self {
Self::Dense(ThinVec::new()) Self::DenseI32(ThinVec::new())
} }
} }
impl IndexedProperties { impl IndexedProperties {
fn new(elements: ThinVec<JsValue>) -> Self { pub(crate) fn from_dense_js_value(elements: ThinVec<JsValue>) -> Self {
Self::Dense(elements) if elements.is_empty() {
return Self::default();
}
Self::DenseElement(elements)
} }
/// Get a property descriptor if it exists. /// Get a property descriptor if it exists.
fn get(&self, key: u32) -> Option<PropertyDescriptor> { fn get(&self, key: u32) -> Option<PropertyDescriptor> {
match self { let value = match self {
Self::Sparse(ref map) => map.get(&key).cloned(), Self::DenseI32(ref vec) => vec.get(key as usize).copied()?.into(),
Self::Dense(ref vec) => vec.get(key as usize).map(|value| { 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() PropertyDescriptorBuilder::new()
.writable(true) .writable(true)
.enumerable(true) .enumerable(true)
.configurable(true) .configurable(true)
.value(value.clone()) .value(value)
.build() .build(),
}), )
}
} }
/// Helper function for converting from a dense storage type to sparse storage type. /// Helper function for converting from a dense storage type to sparse storage type.
fn convert_dense_to_sparse(vec: &mut ThinVec<JsValue>) -> FxHashMap<u32, PropertyDescriptor> { fn convert_dense_to_sparse<T>(vec: &mut ThinVec<T>) -> FxHashMap<u32, PropertyDescriptor>
where
T: Into<JsValue>,
{
let data = std::mem::take(vec); let data = std::mem::take(vec);
data.into_iter() data.into_iter()
@ -99,128 +117,255 @@ impl IndexedProperties {
.writable(true) .writable(true)
.enumerable(true) .enumerable(true)
.configurable(true) .configurable(true)
.value(value) .value(value.into())
.build(), .build(),
) )
}) })
.collect() .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. /// Inserts a property descriptor with the specified key.
fn insert(&mut self, key: u32, property: PropertyDescriptor) -> bool { fn insert(&mut self, key: u32, property: PropertyDescriptor) -> bool {
let vec = match self { if !property.writable().unwrap_or(false)
Self::Sparse(map) => return map.insert(key, property).is_some(), || !property.enumerable().unwrap_or(false)
Self::Dense(vec) => { || !property.configurable().unwrap_or(false)
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)
{ {
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. // Fast Path: continues array access.
Self::DenseI32(vec) if key <= vec.len() as u32 => {
let len = vec.len() as u32;
let mut value = property // If it can fit in a i32 and the truncated version is
.value() // equal to the original then it is an integer.
.cloned() let is_rational_integer = |n: f64| n.to_bits() == f64::from(n as i32).to_bits();
.expect("already checked that the property descriptor has a value field");
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::<ThinVec<_>>();
// If the key is pointing one past the last element, we push it! // 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. // Since the previous key is the current key - 1. Meaning that the elements are continuos.
if key == len { if key == len {
vec.push(value); vec.push(value);
*self = Self::DenseF64(vec);
return false; return false;
} }
// If it the key points in at a already taken index, swap and return it. // If it the key points in at a already taken index, set it.
std::mem::swap(&mut vec[key as usize], &mut value); vec[key as usize] = value;
*self = Self::DenseF64(vec);
return true; return true;
} }
value => {
let mut vec = vec
.iter()
.copied()
.map(JsValue::from)
.collect::<ThinVec<JsValue>>();
vec // 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;
} }
}; };
// Slow path: converting to sparse storage. // If the key is pointing one past the last element, we push it!
let mut map = Self::convert_dense_to_sparse(vec); //
let replaced = map.insert(key, property).is_some(); // Since the previous key is the current key - 1. Meaning that the elements are continuos.
*self = Self::Sparse(Box::new(map)); if key == len {
vec.push(value);
return false;
}
replaced // 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;
/// Removes a property descriptor with the specified key. if let Some(value) = value.as_number() {
fn remove(&mut self, key: u32) -> bool { // If the key is pointing one past the last element, we push it!
let vec = match self { //
Self::Sparse(map) => { // Since the previous key is the current key - 1. Meaning that the elements are continuos.
return map.remove(&key).is_some(); 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;
return true;
} }
Self::Dense(vec) => {
// Fast Path: contiguous storage.
// Has no elements or out of range, nothing to delete! let mut vec = vec
if vec.is_empty() || key as usize >= vec.len() { .iter()
.copied()
.map(JsValue::from)
.collect::<ThinVec<JsValue>>();
// 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; 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. // Since the previous key is the current key - 1. Meaning that the elements are continuos.
if key as usize == vec.len().wrapping_sub(1) { if key == len {
vec.pop().expect("Already checked if it is out of bounds"); vec.push(value);
return true; 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(); let removed = map.remove(&key).is_some();
*self = Self::Sparse(Box::new(map)); *self = Self::Sparse(Box::new(map));
removed 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. /// Check if we contain the key to a property descriptor.
fn contains_key(&self, key: u32) -> bool { fn contains_key(&self, key: u32) -> bool {
match self { 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::Sparse(map) => map.contains_key(&key),
Self::Dense(vec) => (0..vec.len() as u32).contains(&key),
} }
} }
fn iter(&self) -> IndexProperties<'_> { fn iter(&self) -> IndexProperties<'_> {
match self { 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()), Self::Sparse(map) => IndexProperties::Sparse(map.iter()),
} }
} }
fn keys(&self) -> IndexPropertyKeys<'_> { fn keys(&self) -> IndexPropertyKeys<'_> {
match self { 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()), Self::Sparse(map) => IndexPropertyKeys::Sparse(map.keys()),
} }
} }
fn values(&self) -> IndexPropertyValues<'_> { fn values(&self) -> IndexPropertyValues<'_> {
match self { 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()), 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. /// A [`PropertyMap`] contains all the properties of an object.
/// ///
/// The property values are stored in different data structures based on keys. /// The property values are stored in different data structures based on keys.
#[derive(Default, Debug, Trace, Finalize)] #[derive(Default, Debug, Trace, Finalize)]
pub struct PropertyMap { pub struct PropertyMap {
/// Properties stored with integers as keys. /// Properties stored with integers as keys.
indexed_properties: IndexedProperties, pub(crate) indexed_properties: IndexedProperties,
pub(crate) shape: Shape, pub(crate) shape: Shape,
pub(crate) storage: ObjectStorage, pub(crate) storage: ObjectStorage,
@ -230,9 +375,9 @@ impl PropertyMap {
/// Create a new [`PropertyMap`]. /// Create a new [`PropertyMap`].
#[must_use] #[must_use]
#[inline] #[inline]
pub fn new(shape: Shape, elements: ThinVec<JsValue>) -> Self { pub fn new(shape: Shape, indexed_properties: IndexedProperties) -> Self {
Self { Self {
indexed_properties: IndexedProperties::new(elements), indexed_properties,
shape, shape,
storage: Vec::default(), storage: Vec::default(),
} }
@ -449,21 +594,113 @@ impl PropertyMap {
/// Overrides all the indexed properties, setting it to dense storage. /// Overrides all the indexed properties, setting it to dense storage.
pub(crate) fn override_indexed_properties(&mut self, properties: ThinVec<JsValue>) { pub(crate) fn override_indexed_properties(&mut self, properties: ThinVec<JsValue>) {
self.indexed_properties = IndexedProperties::Dense(properties); self.indexed_properties = IndexedProperties::DenseElement(properties);
}
pub(crate) fn get_dense_property(&self, index: u32) -> Option<JsValue> {
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::<ThinVec<_>>();
properties[index] = *value;
self.indexed_properties = IndexedProperties::DenseF64(properties);
return true;
}
value => {
let mut properties = properties
.iter()
.copied()
.map(JsValue::from)
.collect::<ThinVec<_>>();
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::<ThinVec<_>>();
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. /// Returns the vec of dense indexed properties if they exist.
pub(crate) const fn dense_indexed_properties(&self) -> Option<&ThinVec<JsValue>> { pub(crate) fn to_dense_indexed_properties(&self) -> Option<ThinVec<JsValue>> {
if let IndexedProperties::Dense(properties) = &self.indexed_properties { match &self.indexed_properties {
Some(properties) IndexedProperties::DenseI32(properties) => {
} else { Some(properties.iter().copied().map(JsValue::from).collect())
None }
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. /// Returns the vec of dense indexed properties if they exist.
pub(crate) fn dense_indexed_properties_mut(&mut self) -> Option<&mut ThinVec<JsValue>> { pub(crate) fn dense_indexed_properties_mut(&mut self) -> Option<&mut ThinVec<JsValue>> {
if let IndexedProperties::Dense(properties) = &mut self.indexed_properties { if let IndexedProperties::DenseElement(properties) = &mut self.indexed_properties {
Some(properties) Some(properties)
} else { } else {
None None
@ -544,8 +781,14 @@ impl ExactSizeIterator for Iter<'_> {
/// An iterator over the indexed property entries of an `Object`. /// An iterator over the indexed property entries of an `Object`.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum IndexProperties<'a> { pub enum IndexProperties<'a> {
/// An iterator over dense i32, Vec backed indexed property entries of an `Object`.
DenseI32(std::iter::Enumerate<std::slice::Iter<'a, i32>>),
/// An iterator over dense f64, Vec backed indexed property entries of an `Object`.
DenseF64(std::iter::Enumerate<std::slice::Iter<'a, f64>>),
/// An iterator over dense, Vec backed indexed property entries of an `Object`. /// An iterator over dense, Vec backed indexed property entries of an `Object`.
Dense(std::iter::Enumerate<std::slice::Iter<'a, JsValue>>), DenseElement(std::iter::Enumerate<std::slice::Iter<'a, JsValue>>),
/// An iterator over sparse, HashMap backed indexed property entries of an `Object`. /// An iterator over sparse, HashMap backed indexed property entries of an `Object`.
Sparse(hash_map::Iter<'a, u32, PropertyDescriptor>), Sparse(hash_map::Iter<'a, u32, PropertyDescriptor>),
@ -555,9 +798,18 @@ impl Iterator for IndexProperties<'_> {
type Item = (u32, PropertyDescriptor); type Item = (u32, PropertyDescriptor);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self { let (index, value) = match self {
Self::Dense(vec) => vec.next().map(|(index, value)| { 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, index as u32,
PropertyDescriptorBuilder::new() PropertyDescriptorBuilder::new()
.writable(true) .writable(true)
@ -565,16 +817,15 @@ impl Iterator for IndexProperties<'_> {
.enumerable(true) .enumerable(true)
.value(value.clone()) .value(value.clone())
.build(), .build(),
) ))
}),
Self::Sparse(map) => map.next().map(|(index, value)| (*index, value.clone())),
}
} }
#[inline] #[inline]
fn size_hint(&self) -> (usize, Option<usize>) { fn size_hint(&self) -> (usize, Option<usize>) {
match self { 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(), Self::Sparse(map) => map.size_hint(),
} }
} }
@ -584,7 +835,9 @@ impl ExactSizeIterator for IndexProperties<'_> {
#[inline] #[inline]
fn len(&self) -> usize { fn len(&self) -> usize {
match self { 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(), Self::Sparse(map) => map.len(),
} }
} }
@ -636,9 +889,16 @@ impl FusedIterator for IndexPropertyKeys<'_> {}
/// An iterator over the index values (`Property`) of an `Object`. /// An iterator over the index values (`Property`) of an `Object`.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(variant_size_differences)]
pub enum IndexPropertyValues<'a> { pub enum IndexPropertyValues<'a> {
/// An iterator over dense, Vec backed indexed property entries of an `Object`. /// 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`. /// An iterator over sparse, HashMap backed indexed property entries of an `Object`.
Sparse(hash_map::Values<'a, u32, PropertyDescriptor>), Sparse(hash_map::Values<'a, u32, PropertyDescriptor>),
@ -648,23 +908,29 @@ impl Iterator for IndexPropertyValues<'_> {
type Item = PropertyDescriptor; type Item = PropertyDescriptor;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self { let value = match self {
Self::Dense(vec) => vec.next().map(|value| { 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() PropertyDescriptorBuilder::new()
.writable(true) .writable(true)
.configurable(true) .configurable(true)
.enumerable(true) .enumerable(true)
.value(value.clone()) .value(value)
.build() .build(),
}), )
Self::Sparse(map) => map.next().cloned(),
}
} }
#[inline] #[inline]
fn size_hint(&self) -> (usize, Option<usize>) { fn size_hint(&self) -> (usize, Option<usize>) {
match self { 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(), Self::Sparse(map) => map.size_hint(),
} }
} }
@ -674,7 +940,9 @@ impl ExactSizeIterator for IndexPropertyValues<'_> {
#[inline] #[inline]
fn len(&self) -> usize { fn len(&self) -> usize {
match self { 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(), Self::Sparse(map) => map.len(),
} }
} }

10
core/engine/src/object/shape/shared_shape/template.rs

@ -2,7 +2,9 @@ use boa_gc::{Finalize, Trace};
use thin_vec::ThinVec; use thin_vec::ThinVec;
use crate::{ use crate::{
object::{shape::slot::SlotAttributes, JsObject, NativeObject, Object, PropertyMap}, object::{
shape::slot::SlotAttributes, IndexedProperties, JsObject, NativeObject, Object, PropertyMap,
},
property::{Attribute, PropertyKey}, property::{Attribute, PropertyKey},
JsValue, JsValue,
}; };
@ -110,7 +112,7 @@ impl ObjectTemplate {
let mut object = Object { let mut object = Object {
data, data,
extensible: true, extensible: true,
properties: PropertyMap::new(self.shape.clone().into(), ThinVec::default()), properties: PropertyMap::new(self.shape.clone().into(), IndexedProperties::default()),
private_elements: ThinVec::new(), private_elements: ThinVec::new(),
}; };
@ -127,13 +129,13 @@ impl ObjectTemplate {
&self, &self,
data: T, data: T,
storage: Vec<JsValue>, storage: Vec<JsValue>,
elements: ThinVec<JsValue>, indexed_properties: IndexedProperties,
) -> JsObject { ) -> JsObject {
let internal_methods = data.internal_methods(); let internal_methods = data.internal_methods();
let mut object = Object { let mut object = Object {
data, data,
extensible: true, extensible: true,
properties: PropertyMap::new(self.shape.clone().into(), elements), properties: PropertyMap::new(self.shape.clone().into(), indexed_properties),
private_elements: ThinVec::new(), private_elements: ThinVec::new(),
}; };

26
core/engine/src/value/mod.rs

@ -241,10 +241,15 @@ impl JsValue {
matches!(self, Self::Rational(_)) 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] #[must_use]
#[allow(clippy::float_cmp)] #[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 // If it can fit in a i32 and the truncated version is
// equal to the original then it is an integer. // equal to the original then it is an integer.
let is_rational_integer = |n: f64| n == f64::from(n as i32); 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. /// Returns true if the value is a number.
#[inline] #[inline]
#[must_use] #[must_use]

10
core/engine/src/vm/opcode/call/mod.rs

@ -106,9 +106,8 @@ impl Operation for CallEvalSpread {
let arguments = arguments_array_object let arguments = arguments_array_object
.borrow() .borrow()
.properties() .properties()
.dense_indexed_properties() .to_dense_indexed_properties()
.expect("arguments array in call spread function must be dense") .expect("arguments array in call spread function must be dense");
.clone();
let at = context.vm.stack.len(); let at = context.vm.stack.len();
let func = context.vm.stack[at - 1].clone(); let func = context.vm.stack[at - 1].clone();
@ -217,9 +216,8 @@ impl Operation for CallSpread {
let arguments = arguments_array_object let arguments = arguments_array_object
.borrow() .borrow()
.properties() .properties()
.dense_indexed_properties() .to_dense_indexed_properties()
.expect("arguments array in call spread function must be dense") .expect("arguments array in call spread function must be dense");
.clone();
let argument_count = arguments.len(); let argument_count = arguments.len();
context.vm.push_values(&arguments); context.vm.push_values(&arguments);

5
core/engine/src/vm/opcode/environment/mod.rs

@ -181,9 +181,8 @@ impl Operation for SuperCallSpread {
let arguments = arguments_array_object let arguments = arguments_array_object
.borrow() .borrow()
.properties() .properties()
.dense_indexed_properties() .to_dense_indexed_properties()
.expect("arguments array in call spread function must be dense") .expect("arguments array in call spread function must be dense");
.clone();
let super_constructor = context.vm.pop(); let super_constructor = context.vm.pop();

14
core/engine/src/vm/opcode/get/property.rs

@ -115,12 +115,9 @@ impl Operation for GetPropertyByValue {
if object.is_array() { if object.is_array() {
if let PropertyKey::Index(index) = &key { if let PropertyKey::Index(index) = &key {
let object_borrowed = object.borrow(); let object_borrowed = object.borrow();
if let Some(element) = object_borrowed if let Some(element) = object_borrowed.properties().get_dense_property(index.get())
.properties()
.dense_indexed_properties()
.and_then(|vec| vec.get(index.get() as usize))
{ {
context.vm.push(element.clone()); context.vm.push(element);
return Ok(CompletionType::Normal); return Ok(CompletionType::Normal);
} }
} }
@ -162,13 +159,10 @@ impl Operation for GetPropertyByValuePush {
if object.is_array() { if object.is_array() {
if let PropertyKey::Index(index) = &key { if let PropertyKey::Index(index) = &key {
let object_borrowed = object.borrow(); let object_borrowed = object.borrow();
if let Some(element) = object_borrowed if let Some(element) = object_borrowed.properties().get_dense_property(index.get())
.properties()
.dense_indexed_properties()
.and_then(|vec| vec.get(index.get() as usize))
{ {
context.vm.push(key); context.vm.push(key);
context.vm.push(element.clone()); context.vm.push(element);
return Ok(CompletionType::Normal); return Ok(CompletionType::Normal);
} }
} }

5
core/engine/src/vm/opcode/new/mod.rs

@ -70,9 +70,8 @@ impl Operation for NewSpread {
let arguments = arguments_array_object let arguments = arguments_array_object
.borrow() .borrow()
.properties() .properties()
.dense_indexed_properties() .to_dense_indexed_properties()
.expect("arguments array in call spread function must be dense") .expect("arguments array in call spread function must be dense");
.clone();
let func = context.vm.pop(); let func = context.vm.pop();

54
core/engine/src/vm/opcode/set/property.rs

@ -1,7 +1,5 @@
use boa_macros::utf16;
use crate::{ use crate::{
builtins::{function::set_function_name, Proxy}, builtins::function::set_function_name,
object::{internal_methods::InternalMethodContext, shape::slot::SlotAttributes}, object::{internal_methods::InternalMethodContext, shape::slot::SlotAttributes},
property::{PropertyDescriptor, PropertyKey}, property::{PropertyDescriptor, PropertyKey},
vm::{opcode::Operation, CompletionType}, vm::{opcode::Operation, CompletionType},
@ -145,62 +143,16 @@ impl Operation for SetPropertyByValue {
break 'fast_path; break 'fast_path;
} }
let shape = object_borrowed.shape().clone(); if object_borrowed
if let Some(dense_elements) = object_borrowed
.properties_mut() .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::<Proxy>()) {
break 'fast_path;
}
dense_elements.push(value.clone());
context.vm.push(value); 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); return Ok(CompletionType::Normal);
} }
} }
} }
} }
}
// Slow path: // Slow path:
let succeeded = let succeeded =

24
docs/boa_object.md

@ -163,6 +163,30 @@ $boa.object.id(o) // '0x7F5B3251B718'
$boa.object.id($boa) // '0x7F5B3251B5D8' $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` ## Module `$boa.optimizer`
This modules contains getters and setters for enabling and disabling optimizations. This modules contains getters and setters for enabling and disabling optimizations.

Loading…
Cancel
Save