From ce535dd6d68d073d2f8a9f784a7867be99fb721b Mon Sep 17 00:00:00 2001 From: croraf Date: Thu, 15 Oct 2020 19:57:07 +0200 Subject: [PATCH] [builtin Map] Map.prototype.entries method and map iterator (#847) * Initial commit * Improving on Map iterator * Improvements on the iterator * Almost finish the next method of MapIterator * Add different kinds to next * fmt * Add function description. Add test. * Added symbol_iterator method. Refactor to use exactly the same function as "entries". Added test for it, unignored pending test. * Remove TODOs --- boa/src/builtins/array/mod.rs | 2 + boa/src/builtins/iterable/mod.rs | 11 +- boa/src/builtins/map/map_iterator.rs | 155 +++++++++++++++++++++++++++ boa/src/builtins/map/mod.rs | 38 ++++++- boa/src/builtins/map/ordered_map.rs | 7 ++ boa/src/builtins/map/tests.rs | 73 ++++++++++++- boa/src/builtins/mod.rs | 1 + boa/src/object/mod.rs | 11 ++ 8 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 boa/src/builtins/map/map_iterator.rs diff --git a/boa/src/builtins/array/mod.rs b/boa/src/builtins/array/mod.rs index 448f75ae8cb..489d411b5fd 100644 --- a/boa/src/builtins/array/mod.rs +++ b/boa/src/builtins/array/mod.rs @@ -38,12 +38,14 @@ impl BuiltIn for Array { let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); let symbol_iterator = context.well_known_symbols().iterator_symbol(); + let values_function = FunctionBuilder::new(context, Self::values) .name("values") .length(0) .callable(true) .constructable(false) .build(); + let array = ConstructorBuilder::with_standard_object( context, Self::constructor, diff --git a/boa/src/builtins/iterable/mod.rs b/boa/src/builtins/iterable/mod.rs index 1680f0f9f15..0ac748bef53 100644 --- a/boa/src/builtins/iterable/mod.rs +++ b/boa/src/builtins/iterable/mod.rs @@ -1,6 +1,7 @@ use crate::{ builtins::string::string_iterator::StringIterator, builtins::ArrayIterator, + builtins::MapIterator, object::{GcObject, ObjectInitializer}, property::{Attribute, DataDescriptor}, BoaProfiler, Context, Result, Value, @@ -11,6 +12,7 @@ pub struct IteratorPrototypes { iterator_prototype: GcObject, array_iterator: GcObject, string_iterator: GcObject, + map_iterator: GcObject, } impl IteratorPrototypes { @@ -23,9 +25,12 @@ impl IteratorPrototypes { array_iterator: ArrayIterator::create_prototype(ctx, iterator_prototype.clone()) .as_gc_object() .expect("Array Iterator Prototype is not an object"), - string_iterator: StringIterator::create_prototype(ctx, iterator_prototype) + string_iterator: StringIterator::create_prototype(ctx, iterator_prototype.clone()) .as_gc_object() .expect("String Iterator Prototype is not an object"), + map_iterator: MapIterator::create_prototype(ctx, iterator_prototype) + .as_gc_object() + .expect("Map Iterator Prototype is not an object"), } } @@ -40,6 +45,10 @@ impl IteratorPrototypes { pub fn string_iterator(&self) -> GcObject { self.string_iterator.clone() } + + pub fn map_iterator(&self) -> GcObject { + self.map_iterator.clone() + } } /// CreateIterResultObject( value, done ) diff --git a/boa/src/builtins/map/map_iterator.rs b/boa/src/builtins/map/map_iterator.rs new file mode 100644 index 00000000000..e27a76e689b --- /dev/null +++ b/boa/src/builtins/map/map_iterator.rs @@ -0,0 +1,155 @@ +use crate::{ + builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, Value}, + object::ObjectData, + property::{Attribute, DataDescriptor}, + BoaProfiler, Context, Result, +}; +use gc::{Finalize, Trace}; + +#[derive(Debug, Clone, Finalize, Trace)] +pub enum MapIterationKind { + Key, + Value, + KeyAndValue, +} + +/// The Map Iterator object represents an iteration over a map. It implements the iterator protocol. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: TODO https://tc39.es/ecma262/#sec-array-iterator-objects +#[derive(Debug, Clone, Finalize, Trace)] +pub struct MapIterator { + iterated_map: Value, + map_next_index: usize, + map_iteration_kind: MapIterationKind, +} + +impl MapIterator { + pub(crate) const NAME: &'static str = "MapIterator"; + + fn new(map: Value, kind: MapIterationKind) -> Self { + MapIterator { + iterated_map: map, + map_next_index: 0, + map_iteration_kind: kind, + } + } + + /// Abstract operation CreateMapIterator( map, kind ) + /// + /// Creates a new iterator over the given map. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://www.ecma-international.org/ecma-262/11.0/index.html#sec-createmapiterator + pub(crate) fn create_map_iterator( + ctx: &Context, + map: Value, + kind: MapIterationKind, + ) -> Result { + let map_iterator = Value::new_object(Some(ctx.global_object())); + map_iterator.set_data(ObjectData::MapIterator(Self::new(map, kind))); + map_iterator + .as_object_mut() + .expect("map iterator object") + .set_prototype_instance(ctx.iterator_prototypes().map_iterator().into()); + Ok(map_iterator) + } + + /// %MapIteratorPrototype%.next( ) + /// + /// Advances the iterator and gets the next result in the map. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%mapiteratorprototype%.next + pub(crate) fn next(this: &Value, _args: &[Value], ctx: &mut Context) -> Result { + if let Value::Object(ref object) = this { + let mut object = object.borrow_mut(); + if let Some(map_iterator) = object.as_map_iterator_mut() { + let m = &map_iterator.iterated_map; + let mut index = map_iterator.map_next_index; + let item_kind = &map_iterator.map_iteration_kind; + + if map_iterator.iterated_map.is_undefined() { + return Ok(create_iter_result_object(ctx, Value::undefined(), true)); + } + + if let Value::Object(ref object) = m { + if let Some(entries) = object.borrow().as_map_ref() { + let num_entries = entries.len(); + while index < num_entries { + let e = entries.get_index(index); + index += 1; + map_iterator.map_next_index = index; + if let Some((key, value)) = e { + match item_kind { + MapIterationKind::Key => { + return Ok(create_iter_result_object( + ctx, + key.clone(), + false, + )); + } + MapIterationKind::Value => { + return Ok(create_iter_result_object( + ctx, + value.clone(), + false, + )); + } + MapIterationKind::KeyAndValue => { + let result = Array::construct_array( + &Array::new_array(ctx)?, + &[key.clone(), value.clone()], + )?; + return Ok(create_iter_result_object(ctx, result, false)); + } + } + } + } + } else { + return Err(ctx.construct_type_error("'this' is not a Map")); + } + } else { + return Err(ctx.construct_type_error("'this' is not a Map")); + } + + map_iterator.iterated_map = Value::undefined(); + Ok(create_iter_result_object(ctx, Value::undefined(), true)) + } else { + ctx.throw_type_error("`this` is not an MapIterator") + } + } else { + ctx.throw_type_error("`this` is not an MapIterator") + } + } + + /// Create the %MapIteratorPrototype% object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%mapiteratorprototype%-object + pub(crate) fn create_prototype(ctx: &mut Context, iterator_prototype: Value) -> Value { + let global = ctx.global_object(); + let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); + + // Create prototype + let map_iterator = Value::new_object(Some(global)); + make_builtin_fn(Self::next, "next", &map_iterator, 0, ctx); + map_iterator + .as_object_mut() + .expect("map iterator prototype object") + .set_prototype_instance(iterator_prototype); + + let to_string_tag = ctx.well_known_symbols().to_string_tag_symbol(); + let to_string_tag_property = DataDescriptor::new("Map Iterator", Attribute::CONFIGURABLE); + map_iterator.set_property(to_string_tag, to_string_tag_property); + map_iterator + } +} diff --git a/boa/src/builtins/map/mod.rs b/boa/src/builtins/map/mod.rs index 632da4de0e8..3bfe6de9c7b 100644 --- a/boa/src/builtins/map/mod.rs +++ b/boa/src/builtins/map/mod.rs @@ -2,12 +2,15 @@ use crate::{ builtins::BuiltIn, - object::{ConstructorBuilder, ObjectData, PROTOTYPE}, + object::{ConstructorBuilder, FunctionBuilder, ObjectData, PROTOTYPE}, property::{Attribute, DataDescriptor}, BoaProfiler, Context, Result, Value, }; use ordered_map::OrderedMap; +pub mod map_iterator; +use map_iterator::{MapIterationKind, MapIterator}; + pub mod ordered_map; #[cfg(test)] mod tests; @@ -25,9 +28,28 @@ impl BuiltIn for Map { fn init(context: &mut Context) -> (&'static str, Value, Attribute) { let _timer = BoaProfiler::global().start_event(Self::NAME, "init"); + let iterator_symbol = context.well_known_symbols().iterator_symbol(); + + let entries_function = FunctionBuilder::new(context, Self::entries) + .name("entries") + .length(0) + .callable(true) + .constructable(false) + .build(); + let map_object = ConstructorBuilder::new(context, Self::constructor) .name(Self::NAME) .length(Self::LENGTH) + .property( + "entries", + entries_function.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + iterator_symbol, + entries_function, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) .method(Self::set, "set", 2) .method(Self::delete, "delete", 1) .method(Self::get, "get", 1) @@ -98,6 +120,20 @@ impl Map { Ok(this.clone()) } + /// `Map.prototype.entries()` + /// + /// Returns a new Iterator object that contains the [key, value] pairs for each element in the Map object in insertion order. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://www.ecma-international.org/ecma-262/11.0/index.html#sec-map.prototype.entries + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries + pub(crate) fn entries(this: &Value, _: &[Value], ctx: &mut Context) -> Result { + MapIterator::create_map_iterator(ctx, this.clone(), MapIterationKind::KeyAndValue) + } + /// Helper function to set the size property. fn set_size(this: &Value, size: usize) { let size = DataDescriptor::new( diff --git a/boa/src/builtins/map/ordered_map.rs b/boa/src/builtins/map/ordered_map.rs index d29f9b556b2..e1bbeafc710 100644 --- a/boa/src/builtins/map/ordered_map.rs +++ b/boa/src/builtins/map/ordered_map.rs @@ -94,6 +94,13 @@ where self.0.get(key) } + /// Get a key-value pair by index + /// Valid indices are 0 <= index < self.len() + /// Computes in O(1) time. + pub fn get_index(&self, index: usize) -> Option<(&K, &V)> { + self.0.get_index(index) + } + /// Return an iterator over the key-value pairs of the map, in their order pub fn iter(&self) -> Iter<'_, K, V> { self.0.iter() diff --git a/boa/src/builtins/map/tests.rs b/boa/src/builtins/map/tests.rs index fa714cc896d..481d3d8c1c6 100644 --- a/boa/src/builtins/map/tests.rs +++ b/boa/src/builtins/map/tests.rs @@ -43,9 +43,78 @@ fn clone() { assert_eq!(result, "2"); } -// TODO depends on the https://github.com/boa-dev/boa/issues/810 #[test] -#[ignore] +fn symbol_iterator() { + let mut engine = Context::new(); + let init = r#" + const map1 = new Map(); + map1.set('0', 'foo'); + map1.set(1, 'bar'); + const iterator = map1[Symbol.iterator](); + let item1 = iterator.next(); + let item2 = iterator.next(); + let item3 = iterator.next(); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "item1.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut engine, "item1.value[0]"); + assert_eq!(result, "\"0\""); + let result = forward(&mut engine, "item1.value[1]"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut engine, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut engine, "item2.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut engine, "item2.value[0]"); + assert_eq!(result, "1"); + let result = forward(&mut engine, "item2.value[1]"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut engine, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut engine, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut engine, "item3.done"); + assert_eq!(result, "true"); +} + +// Should behave the same as symbol_iterator +#[test] +fn entries() { + let mut engine = Context::new(); + let init = r#" + const map1 = new Map(); + map1.set('0', 'foo'); + map1.set(1, 'bar'); + const entriesIterator = map1.entries(); + let item1 = entriesIterator.next(); + let item2 = entriesIterator.next(); + let item3 = entriesIterator.next(); + "#; + forward(&mut engine, init); + let result = forward(&mut engine, "item1.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut engine, "item1.value[0]"); + assert_eq!(result, "\"0\""); + let result = forward(&mut engine, "item1.value[1]"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut engine, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut engine, "item2.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut engine, "item2.value[0]"); + assert_eq!(result, "1"); + let result = forward(&mut engine, "item2.value[1]"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut engine, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut engine, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut engine, "item3.done"); + assert_eq!(result, "true"); +} + +#[test] fn merge() { let mut engine = Context::new(); let init = r#" diff --git a/boa/src/builtins/mod.rs b/boa/src/builtins/mod.rs index 89096330770..ae5af7f269e 100644 --- a/boa/src/builtins/mod.rs +++ b/boa/src/builtins/mod.rs @@ -32,6 +32,7 @@ pub(crate) use self::{ global_this::GlobalThis, infinity::Infinity, json::Json, + map::map_iterator::MapIterator, map::Map, math::Math, nan::NaN, diff --git a/boa/src/object/mod.rs b/boa/src/object/mod.rs index 65b72cd8eea..480db90c337 100644 --- a/boa/src/object/mod.rs +++ b/boa/src/object/mod.rs @@ -4,6 +4,7 @@ use crate::{ builtins::{ array::array_iterator::ArrayIterator, function::{BuiltInFunction, Function, FunctionFlags, NativeFunction}, + map::map_iterator::MapIterator, map::ordered_map::OrderedMap, string::string_iterator::StringIterator, BigInt, Date, RegExp, @@ -76,6 +77,7 @@ pub enum ObjectData { Array, ArrayIterator(ArrayIterator), Map(OrderedMap), + MapIterator(MapIterator), RegExp(Box), BigInt(RcBigInt), Boolean(bool), @@ -102,6 +104,7 @@ impl Display for ObjectData { Self::Function(_) => "Function", Self::RegExp(_) => "RegExp", Self::Map(_) => "Map", + Self::MapIterator(_) => "MapIterator", Self::String(_) => "String", Self::StringIterator(_) => "StringIterator", Self::Symbol(_) => "Symbol", @@ -327,6 +330,14 @@ impl Object { } } + #[inline] + pub fn as_map_iterator_mut(&mut self) -> Option<&mut MapIterator> { + match &mut self.data { + ObjectData::MapIterator(iter) => Some(iter), + _ => None, + } + } + /// Checks if it a `String` object. #[inline] pub fn is_string(&self) -> bool {