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 1 month 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. 518
      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. 58
      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::{
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<JsValue> {
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 {
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()
}

35
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);

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).
// 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());

518
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.
///
/// 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<JsValue>),
pub enum IndexedProperties {
/// Dense [`i32`] storage.
DenseI32(ThinVec<i32>),
/// 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<f64>),
/// Dense [`JsValue`] storage.
DenseElement(ThinVec<JsValue>),
/// Sparse [`JsValue`] storage.
Sparse(Box<FxHashMap<u32, PropertyDescriptor>>),
}
impl Default for IndexedProperties {
#[inline]
fn default() -> Self {
Self::Dense(ThinVec::new())
Self::DenseI32(ThinVec::new())
}
}
impl IndexedProperties {
fn new(elements: ThinVec<JsValue>) -> Self {
Self::Dense(elements)
pub(crate) fn from_dense_js_value(elements: ThinVec<JsValue>) -> Self {
if elements.is_empty() {
return Self::default();
}
Self::DenseElement(elements)
}
/// Get a property descriptor if it exists.
fn get(&self, key: u32) -> Option<PropertyDescriptor> {
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<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);
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::<ThinVec<_>>();
// 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::<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;
}
// 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::<ThinVec<JsValue>>();
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<JsValue>) -> 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<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.
pub(crate) const fn dense_indexed_properties(&self) -> Option<&ThinVec<JsValue>> {
if let IndexedProperties::Dense(properties) = &self.indexed_properties {
Some(properties)
} else {
None
pub(crate) fn to_dense_indexed_properties(&self) -> Option<ThinVec<JsValue>> {
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<JsValue>> {
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<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`.
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`.
Sparse(hash_map::Iter<'a, u32, PropertyDescriptor>),
@ -555,26 +798,34 @@ impl Iterator for IndexProperties<'_> {
type Item = (u32, PropertyDescriptor);
fn next(&mut self) -> Option<Self::Item> {
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<usize>) {
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<Self::Item> {
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<usize>) {
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(),
}
}

10
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<JsValue>,
elements: ThinVec<JsValue>,
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(),
};

26
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]

10
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);

5
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();

14
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);
}
}

5
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();

58
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::<Proxy>()) {
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);
}
}
}

24
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.

Loading…
Cancel
Save