From 2b9024403f4e989d564751464bb3ee63e1a739c3 Mon Sep 17 00:00:00 2001 From: y21 <30553356+y21@users.noreply.github.com> Date: Sun, 17 Mar 2024 14:35:33 +0100 Subject: [PATCH] implement experimental support for holey arrays --- crates/dash_vm/src/gc/trace.rs | 7 + crates/dash_vm/src/js_std/array.rs | 11 +- crates/dash_vm/src/value/array.rs | 279 ----------- crates/dash_vm/src/value/array/holey.rs | 231 +++++++++ crates/dash_vm/src/value/array/mod.rs | 617 ++++++++++++++++++++++++ crates/dash_vm/src/value/object.rs | 4 +- testrunner/src/util.rs | 5 - 7 files changed, 864 insertions(+), 290 deletions(-) delete mode 100755 crates/dash_vm/src/value/array.rs create mode 100644 crates/dash_vm/src/value/array/holey.rs create mode 100644 crates/dash_vm/src/value/array/mod.rs diff --git a/crates/dash_vm/src/gc/trace.rs b/crates/dash_vm/src/gc/trace.rs index 5560a0cb..1af73f9a 100644 --- a/crates/dash_vm/src/gc/trace.rs +++ b/crates/dash_vm/src/gc/trace.rs @@ -61,6 +61,13 @@ unsafe impl Trace for Vec { } } +unsafe impl Trace for (A, B) { + fn trace(&self, cx: &mut TraceCtxt<'_>) { + self.0.trace(cx); + self.1.trace(cx); + } +} + unsafe impl Trace for HashSet { fn trace(&self, cx: &mut TraceCtxt<'_>) { for t in self.iter() { diff --git a/crates/dash_vm/src/js_std/array.rs b/crates/dash_vm/src/js_std/array.rs index d31429cc..b8a6306c 100644 --- a/crates/dash_vm/src/js_std/array.rs +++ b/crates/dash_vm/src/js_std/array.rs @@ -14,8 +14,7 @@ use crate::value::{array, Root, Value, ValueContext}; pub fn constructor(cx: CallContext) -> Result { let size = cx.args.first().unwrap_or_undefined().to_length_u(cx.scope)?; - // TODO: filling it with undefined values isn't right, but we don't have holey arrays yet. - let array = Array::from_vec(cx.scope, vec![PropertyValue::static_default(Value::undefined()); size]); + let array = Array::with_hole(cx.scope, size); Ok(cx.scope.register(array).into()) } @@ -146,8 +145,12 @@ pub fn fill(cx: CallContext) -> Result { let value = cx.args.first().unwrap_or_undefined(); for i in 0..len { - let pk = cx.scope.intern_usize(i); - this.set_property(cx.scope, pk.into(), PropertyValue::static_default(value.clone()))?; + array::spec_array_set_property(cx.scope, &this, i, PropertyValue::static_default(value.clone()))?; + } + + if let Some(arr) = cx.this.downcast_ref::() { + // all holes were replaced with values, so there cannot be holes + arr.force_convert_to_non_holey(); } Ok(this) diff --git a/crates/dash_vm/src/value/array.rs b/crates/dash_vm/src/value/array.rs deleted file mode 100755 index 870fd95b..00000000 --- a/crates/dash_vm/src/value/array.rs +++ /dev/null @@ -1,279 +0,0 @@ -use std::any::Any; -use std::cell::{Cell, RefCell}; - -use dash_proc_macro::Trace; - -use crate::gc::handle::Handle; -use crate::gc::interner::sym; -use crate::localscope::LocalScope; -use crate::value::object::PropertyDataDescriptor; -use crate::{delegate, throw, Vm}; - -use super::object::{NamedObject, Object, PropertyKey, PropertyValue, PropertyValueKind}; -use super::ops::conversions::ValueConversion; -use super::primitive::array_like_keys; -use super::{Root, Unrooted, Value}; - -pub const MAX_LENGTH: usize = 4294967295; - -#[derive(Debug, Trace)] -pub struct Array { - items: RefCell>, - obj: NamedObject, -} - -fn get_named_object(vm: &Vm) -> NamedObject { - NamedObject::with_prototype_and_constructor(vm.statics.array_prototype.clone(), vm.statics.array_ctor.clone()) -} - -impl Array { - pub fn new(vm: &Vm) -> Self { - Array { - items: RefCell::new(Vec::new()), - obj: get_named_object(vm), - } - } - - pub fn from_vec(vm: &Vm, values: Vec) -> Self { - Array { - items: RefCell::new(values), - obj: get_named_object(vm), - } - } - - pub fn with_obj(obj: NamedObject) -> Self { - Self { - items: RefCell::new(Vec::new()), - obj, - } - } - - pub fn inner(&self) -> &RefCell> { - &self.items - } -} - -impl Object for Array { - fn get_own_property_descriptor( - &self, - sc: &mut LocalScope, - key: PropertyKey, - ) -> Result, Unrooted> { - let items = self.items.borrow(); - - if let PropertyKey::String(key) = &key { - if key.sym() == sym::length { - return Ok(Some(PropertyValue { - kind: PropertyValueKind::Static(Value::number(items.len() as f64)), - descriptor: PropertyDataDescriptor::WRITABLE, - })); - } - - if let Ok(index) = key.res(sc).parse::() { - if index < MAX_LENGTH { - if let Some(element) = items.get(index).cloned() { - return Ok(Some(element)); - } - } - } - } - - self.obj.get_property_descriptor(sc, key) - } - - fn set_property(&self, sc: &mut LocalScope, key: PropertyKey, value: PropertyValue) -> Result<(), Value> { - if let PropertyKey::String(key) = &key { - let mut items = self.items.borrow_mut(); - - if key.sym() == sym::length { - // TODO: this shouldnt be undefined - let value = value.kind().get_or_apply(sc, Value::undefined()).root(sc)?; - let new_len = value.to_number(sc)? as usize; - - if new_len > MAX_LENGTH { - throw!(sc, RangeError, "Invalid array length"); - } - - items.resize(new_len, PropertyValue::static_default(Value::undefined())); - return Ok(()); - } - - if let Ok(index) = key.res(sc).parse::() { - if index < MAX_LENGTH { - if index >= items.len() { - items.resize(index + 1, PropertyValue::static_default(Value::undefined())); - } - - items[index] = value; - return Ok(()); - } - } - } - - self.obj.set_property(sc, key, value) - } - - fn delete_property(&self, sc: &mut LocalScope, key: PropertyKey) -> Result { - if let PropertyKey::String(key) = &key { - if key.sym() == sym::length { - return Ok(Unrooted::new(Value::undefined())); - } - - if let Ok(index) = key.res(sc).parse::() { - let mut items = self.items.borrow_mut(); - - if let Some(item) = items.get_mut(index) { - let old = std::mem::replace(item, PropertyValue::static_default(Value::null())); - return Ok(Unrooted::new(match old.into_kind() { - PropertyValueKind::Static(value) => value, - PropertyValueKind::Trap { .. } => Value::undefined(), - })); - } - } - } - - self.obj.delete_property(sc, key) - } - - fn apply( - &self, - scope: &mut LocalScope, - callee: Handle, - this: Value, - args: Vec, - ) -> Result { - self.obj.apply(scope, callee, this, args) - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn set_prototype(&self, sc: &mut LocalScope, value: Value) -> Result<(), Value> { - self.obj.set_prototype(sc, value) - } - - fn get_prototype(&self, sc: &mut LocalScope) -> Result { - self.obj.get_prototype(sc) - } - - fn own_keys(&self, sc: &mut LocalScope<'_>) -> Result, Value> { - let items = self.items.borrow(); - Ok(array_like_keys(sc, items.len()).collect()) - } -} - -#[derive(Debug, Trace)] -pub struct ArrayIterator { - index: Cell, - length: usize, - value: Value, - obj: NamedObject, -} - -impl Object for ArrayIterator { - delegate!( - obj, - get_own_property_descriptor, - get_property, - get_property_descriptor, - set_property, - delete_property, - set_prototype, - get_prototype, - own_keys - ); - - fn apply( - &self, - scope: &mut LocalScope, - callee: Handle, - this: Value, - args: Vec, - ) -> Result { - self.obj.apply(scope, callee, this, args) - } - - fn as_any(&self) -> &dyn Any { - self - } -} - -impl ArrayIterator { - pub fn new(sc: &mut LocalScope, value: Value) -> Result { - let length = value.length_of_array_like(sc)?; - - Ok(ArrayIterator { - index: Cell::new(0), - length, - value, - obj: NamedObject::with_prototype_and_constructor( - sc.statics.array_iterator_prototype.clone(), - sc.statics.object_ctor.clone(), - ), - }) - } - - pub fn empty() -> Self { - Self { - index: Cell::new(0), - length: 0, - value: Value::null(), - obj: NamedObject::null(), - } - } - - pub fn next(&self, sc: &mut LocalScope) -> Result, Unrooted> { - let index = self.index.get(); - - if index < self.length { - self.index.set(index + 1); - let index = sc.intern_usize(index); - self.value.get_property(sc, index.into()).map(Some) - } else { - Ok(None) - } - } -} - -pub fn spec_array_get_property(scope: &mut LocalScope, target: &Value, index: usize) -> Result { - // specialize array path - // TODO: broken because of externals.. edit: is it? - if let Some(arr) = target.downcast_ref::() { - let inner = arr.inner().borrow(); - return match inner.get(index) { - Some(value) => value.get_or_apply(scope, Value::undefined()), - None => Ok(Value::undefined().into()), - }; - } - - let index = scope.intern_usize(index); - match target.get_property(scope, index.into()) { - Ok(v) => Ok(v), - Err(v) => Ok(v), - } -} - -pub fn spec_array_set_property( - scope: &mut LocalScope, - target: &Value, - index: usize, - value: PropertyValue, -) -> Result<(), Value> { - // specialize array path - if let Some(arr) = target.downcast_ref::() { - let mut inner = arr.inner().borrow_mut(); - - if index < MAX_LENGTH { - if index >= inner.len() { - inner.resize(index + 1, PropertyValue::static_default(Value::undefined())); - } - - inner[index] = value; - return Ok(()); - } - } - - let index = scope.intern_usize(index); - target.set_property(scope, index.into(), value) -} diff --git a/crates/dash_vm/src/value/array/holey.rs b/crates/dash_vm/src/value/array/holey.rs new file mode 100644 index 00000000..ca2ba813 --- /dev/null +++ b/crates/dash_vm/src/value/array/holey.rs @@ -0,0 +1,231 @@ +use crate::gc::trace::Trace; + +use super::MaybeHoley; + +/// An array type that can have holes. +// +// INTERNAL INVARIANTS: +// - there should never be zero-sized holes +// - there should be no consecutive holes; they should be represented as one +// - for example, instead of [Hole(2), Hole(2)], it should be [Hole(4)] +#[derive(Debug)] +pub struct HoleyArray(Vec>); + +unsafe impl Trace for HoleyArray { + fn trace(&self, cx: &mut crate::gc::trace::TraceCtxt<'_>) { + for v in &self.0 { + match v { + Element::Value(v) => v.trace(cx), + Element::Hole { count: _ } => {} + } + } + } +} +impl Default for HoleyArray { + fn default() -> Self { + Self(Default::default()) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Element { + Value(T), + Hole { count: usize }, +} +impl Element { + pub fn elements(&self) -> usize { + match *self { + Self::Value(_) => 1, + Self::Hole { count } => count, + } + } +} + +impl From>> for HoleyArray { + fn from(value: Vec>) -> Self { + Self(value) + } +} + +struct Lookup { + chunk_start: usize, + chunk_count: usize, + holey_index: usize, +} + +impl HoleyArray { + pub fn into_inner(self) -> Vec> { + self.0 + } + + pub fn inner(&self) -> &[Element] { + &self.0 + } + + /// Checks if there are any holes in this array + pub fn has_hole(&self) -> bool { + self.0.iter().any(|e| matches!(e, Element::Hole { .. })) + } + + pub fn compute_len(&self) -> usize { + self.0.iter().map(Element::elements).sum() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// "Looks up" an index + fn lookup_index(&self, at: usize) -> Option { + let mut norm = 0; + for (index, element) in self.0.iter().enumerate() { + match *element { + Element::Value(_) => { + if norm == at { + return Some(Lookup { + chunk_start: norm, + chunk_count: 1, + holey_index: index, + }); + } else { + norm += 1; + } + } + Element::Hole { count } => { + let range = norm..norm + count; + if range.contains(&at) { + return Some(Lookup { + chunk_start: norm, + chunk_count: count, + holey_index: index, + }); + } else { + norm += count; + } + } + } + } + None + } + + pub fn get(&self, at: usize) -> Option> { + self.lookup_index(at) + .map(|Lookup { holey_index, .. }| match &self.0[holey_index] { + Element::Value(v) => MaybeHoley::Some(v), + Element::Hole { .. } => MaybeHoley::Hole, + }) + } + + pub fn clear(&mut self) { + self.0.clear(); + } + + pub fn resize(&mut self, to: usize) { + if to == 0 { + self.clear(); + return; + } + let len = self.compute_len(); + + if to <= len { + let Lookup { + chunk_start, + holey_index, + chunk_count, + } = self.lookup_index(to - 1).unwrap(); + self.0.truncate(holey_index + 1); + if let Some(Element::Hole { count }) = self.0.last_mut() { + let sub = (chunk_start + chunk_count) - to; + *count -= sub; + + if *count == 0 { + self.0.pop(); + } + } + // TODO: merge potential holeys? and remove ones at the end + } else if let Some(Element::Hole { count }) = self.0.last_mut() { + // make the last hole larger if there is already one + *count += to - len; + } else { + self.0.push(Element::Hole { count: to - len }); + } + } + pub fn set(&mut self, at: usize, value: T) { + if let Some(Lookup { + chunk_start, + chunk_count, + holey_index, + }) = self.lookup_index(at) + { + match &mut self.0[holey_index] { + Element::Value(existing) => *existing = value, + Element::Hole { count } if *count == 1 => { + self.0[holey_index] = Element::Value(value); + } + Element::Hole { .. } => { + let left = at - chunk_start; + let right = ((chunk_start + chunk_count) - at) - 1; + + match (left, right) { + (0, 0) => { + // Setting at 0 @ Hole(1) -> val + // Covered by the match arm guard `if *count == 1` above + panic!("zero-sized hole after set which should be covered by other arm"); + } + (0, right) => { + // Setting at 0 @ [Hole(2)] -> [val, Hole(1)] + self.0.splice( + holey_index..holey_index + 1, + [Element::Value(value), Element::Hole { count: right }], + ); + } + (left, 0) => { + // Setting at 1 @ [Holey(2)] -> [Hole(1), val] + self.0.splice( + holey_index..holey_index + 1, + [Element::Hole { count: left }, Element::Value(value)], + ); + } + (left, right) => { + // Setting at 1 @ [Hole(3)] -> [Hole(1), val, Hole(1)] + self.0.splice( + holey_index..holey_index + 1, + [ + Element::Hole { count: left }, + Element::Value(value), + Element::Hole { count: right }, + ], + ); + } + } + } + } + } else { + // out of bounds: can just push + let dist = at - self.compute_len(); + if dist > 0 { + self.0.push(Element::Hole { count: dist }); + } + self.0.push(Element::Value(value)); + } + } + + pub fn push(&mut self, value: T) { + self.0.push(Element::Value(value)); + } + + pub fn remove(&mut self, at: usize) { + if let Some(Lookup { holey_index, .. }) = self.lookup_index(at) { + match &mut self.0[holey_index] { + Element::Value(_) => drop(self.0.remove(holey_index)), + Element::Hole { count } => { + *count -= 1; + if *count == 0 { + self.0.remove(holey_index); + } + } + } + // TODO: this needs to merge or remove duplicate holes now, and remove ones at the end + } + } +} diff --git a/crates/dash_vm/src/value/array/mod.rs b/crates/dash_vm/src/value/array/mod.rs new file mode 100644 index 00000000..0397a542 --- /dev/null +++ b/crates/dash_vm/src/value/array/mod.rs @@ -0,0 +1,617 @@ +use std::any::Any; +use std::cell::{Cell, RefCell}; +use std::cmp::Ordering; +use std::mem; + +use dash_log::debug; +use dash_proc_macro::Trace; + +use crate::gc::handle::Handle; +use crate::gc::interner::sym; +use crate::localscope::LocalScope; +use crate::value::object::PropertyDataDescriptor; +use crate::{delegate, throw, Vm}; + +use self::holey::{Element, HoleyArray}; + +use super::object::{NamedObject, Object, PropertyKey, PropertyValue, PropertyValueKind}; +use super::ops::conversions::ValueConversion; +use super::primitive::array_like_keys; +use super::{Root, Unrooted, Value}; + +mod holey; + +pub const MAX_LENGTH: usize = 4294967295; + +#[derive(Debug)] +pub enum ArrayInner { + NonHoley(Vec), + Holey(HoleyArray), +} + +#[derive(Debug, PartialEq, Eq)] +pub enum MaybeHoley { + Some(T), + Hole, +} + +impl ArrayInner { + /// Computes the length. + /// NOTE: this can potentially be an expensive operation for holey arrays, if it has an unusual high number of holes + pub fn len(&self) -> usize { + match self { + ArrayInner::NonHoley(v) => v.len(), + ArrayInner::Holey(v) => v.compute_len(), + } + } + + pub fn is_empty(&self) -> bool { + match self { + ArrayInner::NonHoley(v) => v.is_empty(), + ArrayInner::Holey(v) => v.is_empty(), + } + } + + pub fn get(&self, at: usize) -> Option> { + match self { + ArrayInner::NonHoley(v) => v.get(at).map(MaybeHoley::Some), + ArrayInner::Holey(v) => v.get(at), + } + } + + /// Resizes this array, potentially switching to a holey kind. + pub fn resize(&mut self, to: usize) { + match self { + ArrayInner::NonHoley(v) => { + let len = v.len(); + if to <= len { + v.truncate(to); + } else { + debug!("out of bounds resize, convert to holey array"); + let hole = to - len; + let mut holey = mem::take(v).into_iter().map(Element::Value).collect::>(); + holey.push(Element::Hole { count: hole }); + *self = ArrayInner::Holey(HoleyArray::from(holey)); + } + } + ArrayInner::Holey(v) => v.resize(to), + } + } + + pub fn set(&mut self, at: usize, value: E) { + match self { + ArrayInner::NonHoley(v) => { + match at.cmp(&v.len()) { + Ordering::Less => v[at] = value, + Ordering::Equal => v.push(value), + Ordering::Greater => { + // resize us, causing self to have a hole and do the set logic below + self.resize(at); + self.set(at, value); + debug_assert!(matches!(self, Self::Holey(_))); + } + } + } + ArrayInner::Holey(v) => { + v.set(at, value); + } + } + } + + pub fn push(&mut self, value: E) { + match self { + ArrayInner::NonHoley(v) => v.push(value), + ArrayInner::Holey(v) => v.push(value), + } + } + + pub fn remove(&mut self, at: usize) { + match self { + ArrayInner::NonHoley(v) => { + if at < v.len() { + v.remove(at); + } + } + ArrayInner::Holey(v) => v.remove(at), + } + } +} + +unsafe impl crate::gc::trace::Trace for ArrayInner { + fn trace(&self, cx: &mut crate::gc::trace::TraceCtxt<'_>) { + match self { + ArrayInner::NonHoley(v) => v.trace(cx), + ArrayInner::Holey(v) => v.trace(cx), + } + } +} + +#[derive(Debug, Trace)] +pub struct Array { + pub items: RefCell>, + obj: NamedObject, +} + +fn get_named_object(vm: &Vm) -> NamedObject { + NamedObject::with_prototype_and_constructor(vm.statics.array_prototype.clone(), vm.statics.array_ctor.clone()) +} + +impl Array { + pub fn new(vm: &Vm) -> Self { + Self::with_obj(get_named_object(vm)) + } + + /// Creates a non-holey array from a vec of values + pub fn from_vec(vm: &Vm, items: Vec) -> Self { + Self { + items: RefCell::new(ArrayInner::NonHoley(items)), + obj: get_named_object(vm), + } + } + + /// Creates a holey array with a given length + pub fn with_hole(vm: &Vm, len: usize) -> Self { + Self { + items: RefCell::new(ArrayInner::Holey(HoleyArray::from(vec![Element::Hole { count: len }]))), + obj: get_named_object(vm), + } + } + + /// Tries to convert this holey array into a non-holey array + pub fn try_convert_to_non_holey(&self) { + let values = if let ArrayInner::Holey(elements) = &mut *self.items.borrow_mut() { + if !elements.has_hole() { + mem::take(elements) + .into_inner() + .into_iter() + .map(|element| match element { + Element::Value(value) => value, + _ => unreachable!(), + }) + .collect::>() + } else { + return; + } + } else { + return; + }; + *self.items.borrow_mut() = ArrayInner::NonHoley(values); + } + + /// Converts this potentially-holey array into a non-holey array, assuming that it succeeds. + /// In other words, this assumes that there aren't any holes and change the kind to be non-holey. + /// + /// This can be useful to call after an operation that is guaranteed to remove any holes (e.g. filling an array) + pub fn force_convert_to_non_holey(&self) { + let values = if let ArrayInner::Holey(elements) = &mut *self.items.borrow_mut() { + mem::take(elements) + .into_inner() + .into_iter() + .map(|element| match element { + Element::Value(value) => value, + other => unreachable!("expected value element but got {other:?}"), + }) + .collect::>() + } else { + return; + }; + *self.items.borrow_mut() = ArrayInner::NonHoley(values); + } + + pub fn with_obj(obj: NamedObject) -> Self { + Self { + items: RefCell::new(ArrayInner::NonHoley(Vec::new())), + obj, + } + } +} + +impl Object for Array { + fn get_own_property_descriptor( + &self, + sc: &mut LocalScope, + key: PropertyKey, + ) -> Result, Unrooted> { + let items = self.items.borrow(); + + if let PropertyKey::String(key) = &key { + if key.sym() == sym::length { + return Ok(Some(PropertyValue { + kind: PropertyValueKind::Static(Value::number(items.len() as f64)), + descriptor: PropertyDataDescriptor::WRITABLE, + })); + } + + if let Ok(index) = key.res(sc).parse::() { + if index < MAX_LENGTH { + if let Some(element) = items.get(index) { + match element { + MaybeHoley::Some(v) => return Ok(Some(v.clone())), + MaybeHoley::Hole => return Ok(Some(PropertyValue::static_default(Value::undefined()))), + } + } + } + } + } + + self.obj.get_property_descriptor(sc, key) + } + + fn set_property(&self, sc: &mut LocalScope, key: PropertyKey, value: PropertyValue) -> Result<(), Value> { + if let PropertyKey::String(key) = &key { + let mut items = self.items.borrow_mut(); + + if key.sym() == sym::length { + // TODO: this shouldnt be undefined + let value = value.kind().get_or_apply(sc, Value::undefined()).root(sc)?; + let new_len = value.to_number(sc)? as usize; + + if new_len > MAX_LENGTH { + throw!(sc, RangeError, "Invalid array length"); + } + + items.resize(new_len); + return Ok(()); + } + + if let Ok(index) = key.res(sc).parse::() { + if index < MAX_LENGTH { + items.set(index, value); + return Ok(()); + } + } + } + + self.obj.set_property(sc, key, value) + } + + fn delete_property(&self, sc: &mut LocalScope, key: PropertyKey) -> Result { + if let PropertyKey::String(key) = &key { + if key.sym() == sym::length { + return Ok(Unrooted::new(Value::undefined())); + } + + if let Ok(index) = key.res(sc).parse::() { + let mut items = self.items.borrow_mut(); + items.remove(index); + } + } + + self.obj.delete_property(sc, key) + } + + fn apply( + &self, + scope: &mut LocalScope, + callee: Handle, + this: Value, + args: Vec, + ) -> Result { + self.obj.apply(scope, callee, this, args) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn set_prototype(&self, sc: &mut LocalScope, value: Value) -> Result<(), Value> { + self.obj.set_prototype(sc, value) + } + + fn get_prototype(&self, sc: &mut LocalScope) -> Result { + self.obj.get_prototype(sc) + } + + fn own_keys(&self, sc: &mut LocalScope<'_>) -> Result, Value> { + let items = self.items.borrow(); + Ok(array_like_keys(sc, items.len()).collect()) + } +} + +#[derive(Debug, Trace)] +pub struct ArrayIterator { + index: Cell, + length: usize, + value: Value, + obj: NamedObject, +} + +impl Object for ArrayIterator { + delegate!( + obj, + get_own_property_descriptor, + get_property, + get_property_descriptor, + set_property, + delete_property, + set_prototype, + get_prototype, + own_keys + ); + + fn apply( + &self, + scope: &mut LocalScope, + callee: Handle, + this: Value, + args: Vec, + ) -> Result { + self.obj.apply(scope, callee, this, args) + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +impl ArrayIterator { + pub fn new(sc: &mut LocalScope, value: Value) -> Result { + let length = value.length_of_array_like(sc)?; + + Ok(ArrayIterator { + index: Cell::new(0), + length, + value, + obj: NamedObject::with_prototype_and_constructor( + sc.statics.array_iterator_prototype.clone(), + sc.statics.object_ctor.clone(), + ), + }) + } + + pub fn empty() -> Self { + Self { + index: Cell::new(0), + length: 0, + value: Value::null(), + obj: NamedObject::null(), + } + } + + pub fn next(&self, sc: &mut LocalScope) -> Result, Unrooted> { + let index = self.index.get(); + + if index < self.length { + self.index.set(index + 1); + let index = sc.intern_usize(index); + self.value.get_property(sc, index.into()).map(Some) + } else { + Ok(None) + } + } +} + +/// Equivalent to calling get_property, but specialized for arrays +pub fn spec_array_get_property(scope: &mut LocalScope, target: &Value, index: usize) -> Result { + if let Some(arr) = target.downcast_ref::() { + let inner = arr.items.borrow(); + return match inner.get(index) { + Some(MaybeHoley::Some(value)) => value.get_or_apply(scope, Value::undefined()), + Some(MaybeHoley::Hole) | None => Ok(Value::undefined().into()), + }; + } + + let index = scope.intern_usize(index); + match target.get_property(scope, index.into()) { + Ok(v) => Ok(v), + Err(v) => Ok(v), + } +} + +/// Equivalent to calling set_property, but specialized for arrays +pub fn spec_array_set_property( + scope: &mut LocalScope, + target: &Value, + index: usize, + value: PropertyValue, +) -> Result<(), Value> { + // specialize array path + if let Some(arr) = target.downcast_ref::() { + let mut inner = arr.items.borrow_mut(); + + if index < MAX_LENGTH { + inner.set(index, value); + return Ok(()); + } + } + + let index = scope.intern_usize(index); + target.set_property(scope, index.into(), value) +} + +#[cfg(test)] +mod tests { + use crate::value::array::holey::HoleyArray; + use crate::value::array::{ArrayInner, Element, MaybeHoley}; + + #[test] + fn non_holey_get() { + let arr = ArrayInner::NonHoley(vec![1, 2, 3]); + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&1))); + assert_eq!(arr.get(1), Some(MaybeHoley::Some(&2))); + assert_eq!(arr.get(2), Some(MaybeHoley::Some(&3))); + } + + #[test] + fn holey_get() { + let arr = ArrayInner::Holey(HoleyArray::from(vec![ + Element::Value(1), + Element::Hole { count: 3 }, + Element::Value(2), + Element::Hole { count: 5 }, + Element::Value(3), + Element::Hole { count: 5 }, + ])); + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&1))); + assert_eq!(arr.get(1), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(2), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(3), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(4), Some(MaybeHoley::Some(&2))); + assert_eq!(arr.get(5), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(6), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(7), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(8), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(9), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(10), Some(MaybeHoley::Some(&3))); + assert_eq!(arr.get(13), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(17), None); + } + + #[test] + fn non_holey_resize() { + let mut arr = ArrayInner::NonHoley(vec![1, 2, 3, 4]); + arr.resize(2); + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&1))); + assert_eq!(arr.get(1), Some(MaybeHoley::Some(&2))); + assert_eq!(arr.get(2), None); + arr.resize(3); + assert!(matches!(arr, ArrayInner::Holey(..))); + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&1))); + assert_eq!(arr.get(1), Some(MaybeHoley::Some(&2))); + assert_eq!(arr.get(2), Some(MaybeHoley::Hole)); + arr.resize(0); + assert_eq!(arr.get(0), None); + arr.resize(1); + assert_eq!(arr.get(0), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(1), None); + } + + #[test] + fn holey_resize() { + let mut arr = ArrayInner::Holey(HoleyArray::from(vec![ + Element::Value(1), + Element::Hole { count: 3 }, + Element::Value(2), + Element::Hole { count: 5 }, + Element::Value(3), + Element::Hole { count: 5 }, + ])); + let len = arr.len(); + arr.resize(2); + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&1))); + assert_eq!(arr.get(1), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(2), None); + for i in 3..len { + assert_eq!(arr.get(i), None); + } + arr.resize(1000); + assert_eq!(arr.get(1), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(999), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(1000), None); + } + + #[test] + fn holey_set() { + let mut arr = ArrayInner::Holey(HoleyArray::from(vec![ + Element::Value(1), + Element::Hole { count: 3 }, + Element::Value(2), + Element::Hole { count: 5 }, + Element::Value(3), + Element::Hole { count: 5 }, + ])); + arr.set(0, 2); + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&2))); + assert_eq!(arr.get(1), Some(MaybeHoley::Hole)); + + // in the middle of a 3-element hole should split it into (Hole(1), Some, Hole(1)) + arr.set(2, 3); + assert_eq!(arr.get(2), Some(MaybeHoley::Some(&3))); + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&2))); + assert_eq!(arr.get(1), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(3), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(4), Some(MaybeHoley::Some(&2))); + + // arr is now [Some(2), Hole(1), Some(3)], check that setting a Hole(1) works + arr.set(1, 4); + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&2))); + assert_eq!(arr.get(1), Some(MaybeHoley::Some(&4))); + assert_eq!(arr.get(2), Some(MaybeHoley::Some(&3))); + + // setting at len + let len = arr.len(); + assert_eq!(arr.get(len), None); + arr.set(len, 999); + assert_eq!(arr.get(len), Some(MaybeHoley::Some(&999))); + + // setting two past len should have a Hole(2) before it + let len = arr.len(); + assert_eq!(arr.get(len + 2), None); + arr.set(len + 2, 1000); + assert_eq!(arr.get(len + 2), Some(MaybeHoley::Some(&1000))); + assert_eq!(arr.get(len + 1), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(len), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(len + 3), None); + } + + #[test] + fn non_holey_set() { + let mut arr = ArrayInner::NonHoley(vec![1, 2, 3]); + arr.set(3, 4); + assert!(matches!(arr, ArrayInner::NonHoley(_))); + arr.set(5, 6); + assert!(matches!(arr, ArrayInner::Holey(_))); + assert_eq!(arr.get(5), Some(MaybeHoley::Some(&6))); + assert_eq!(arr.get(4), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(6), None); + } + + #[test] + fn holey_remove() { + let mut arr = ArrayInner::Holey(HoleyArray::from(vec![ + Element::Value(1), + Element::Hole { count: 3 }, + Element::Value(2), + Element::Hole { count: 5 }, + Element::Value(3), + Element::Hole { count: 5 }, + ])); + arr.remove(2); + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&1))); + assert_eq!(arr.get(1), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(2), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(3), Some(MaybeHoley::Some(&2))); + arr.remove(0); + assert_eq!(arr.get(0), Some(MaybeHoley::Hole)); + assert_eq!(arr.get(1), Some(MaybeHoley::Hole)); + + arr.remove(0); + arr.remove(0); + // Hole should now be gone + assert_eq!(arr.get(0), Some(MaybeHoley::Some(&2))); + } + + fn holey(arr: &ArrayInner) -> &[Element] { + match arr { + ArrayInner::NonHoley(_) => unreachable!(), + ArrayInner::Holey(h) => h.inner(), + } + } + + #[test] + fn zero_sized_zoles() { + // Tests match arms in HoleyArray::set + let mut arr: ArrayInner = ArrayInner::Holey(HoleyArray::from(vec![Element::Hole { count: 1 }])); + arr.set(0, 4); + assert_eq!(holey(&arr), &[Element::Value(4)]); + + let mut arr: ArrayInner = ArrayInner::Holey(HoleyArray::from(vec![Element::Hole { count: 2 }])); + arr.set(0, 4); + assert_eq!(holey(&arr), &[Element::Value(4), Element::Hole { count: 1 }]); + + let mut arr: ArrayInner = ArrayInner::Holey(HoleyArray::from(vec![Element::Hole { count: 2 }])); + arr.set(1, 4); + assert_eq!(holey(&arr), &[Element::Hole { count: 1 }, Element::Value(4)]); + + let mut arr: ArrayInner = ArrayInner::Holey(HoleyArray::from(vec![Element::Hole { count: 3 }])); + arr.set(1, 4); + assert_eq!( + holey(&arr), + &[ + Element::Hole { count: 1 }, + Element::Value(4), + Element::Hole { count: 1 } + ] + ); + } +} diff --git a/crates/dash_vm/src/value/object.rs b/crates/dash_vm/src/value/object.rs index c1895da7..cf5409ec 100755 --- a/crates/dash_vm/src/value/object.rs +++ b/crates/dash_vm/src/value/object.rs @@ -247,7 +247,7 @@ impl Default for PropertyDataDescriptor { } } -#[derive(Debug, Clone, Trace)] +#[derive(Debug, Clone, Trace, PartialEq, Eq)] pub struct PropertyValue { pub kind: PropertyValueKind, pub descriptor: PropertyDataDescriptor, @@ -406,7 +406,7 @@ impl PropertyValue { } // TODO: these handles should be "hidden" behind something similar to the `Unrooted` value -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum PropertyValueKind { /// Accessor property Trap { get: Option, set: Option }, diff --git a/testrunner/src/util.rs b/testrunner/src/util.rs index 1d529b63..3e19f724 100644 --- a/testrunner/src/util.rs +++ b/testrunner/src/util.rs @@ -5,11 +5,6 @@ use std::io; // This is a list of tests that cannot be run currently, because they abort the process or run into an infinite loop pub const IGNORED_TESTS: &[&str] = &[ // very very large arrays - "Array/15.4.5.1-5-1.js", - "Array/15.4.5.1-5-2.js", - "Array/length/15.4.5.1-3.d-3.js", - "Array/length/S15.4.5.1_A1.1_T1.js", - "Array/length/S15.4.5.2_A3_T4.js", "Array/prototype/indexOf/length-near-integer-limit.js", "Array/prototype/includes/length-boundaries.js", "Array/prototype/concat/arg-length-exceeding-integer-limit.js",