Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[builtin Map] Map.prototype.entries method and map iterator #847

Merged
merged 11 commits into from
Oct 15, 2020
2 changes: 2 additions & 0 deletions boa/src/builtins/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 10 additions & 1 deletion boa/src/builtins/iterable/mod.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,6 +12,7 @@ pub struct IteratorPrototypes {
iterator_prototype: GcObject,
array_iterator: GcObject,
string_iterator: GcObject,
map_iterator: GcObject,
}

impl IteratorPrototypes {
Expand All @@ -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"),
}
}

Expand All @@ -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 )
Expand Down
156 changes: 156 additions & 0 deletions boa/src/builtins/map/map_iterator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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<Value> {
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<Value> {
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 {
// TODO result of e is a Record, check impact
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]: TODO https://tc39.es/ecma262/#sec-%arrayiteratorprototype%-object
croraf marked this conversation as resolved.
Show resolved Hide resolved
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
}
}
38 changes: 37 additions & 1 deletion boa/src/builtins/map/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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<Value> {
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(
Expand Down
10 changes: 10 additions & 0 deletions boa/src/builtins/map/ordered_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ where
self.0.get(key)
}

/// Get a key-value pair by index
/// Valid indices are 0 <= index < self.len()
/// Computes in O(1) time.
///
/// TODO perhaps return Record as per
/// https://tc39.es/ecma262/#sec-%mapiteratorprototype%.next bullet 12.a.
croraf marked this conversation as resolved.
Show resolved Hide resolved
croraf marked this conversation as resolved.
Show resolved Hide resolved
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()
Expand Down
73 changes: 71 additions & 2 deletions boa/src/builtins/map/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down
1 change: 1 addition & 0 deletions boa/src/builtins/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading