diff --git a/boa/src/builtins/array/array_iterator.rs b/boa/src/builtins/array/array_iterator.rs new file mode 100644 index 0000000000..29beb9ad60 --- /dev/null +++ b/boa/src/builtins/array/array_iterator.rs @@ -0,0 +1,139 @@ +use crate::{ + builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, Value}, + object::ObjectData, + property::{Attribute, Property}, + BoaProfiler, Context, Result, +}; +use gc::{Finalize, Trace}; + +#[derive(Debug, Clone, Finalize, Trace)] +pub enum ArrayIterationKind { + Key, + Value, + KeyAndValue, +} + +/// The Array Iterator object represents an iteration over an array. It implements the iterator protocol. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-array-iterator-objects +#[derive(Debug, Clone, Finalize, Trace)] +pub struct ArrayIterator { + array: Value, + next_index: u32, + kind: ArrayIterationKind, +} + +impl ArrayIterator { + pub(crate) const NAME: &'static str = "ArrayIterator"; + + fn new(array: Value, kind: ArrayIterationKind) -> Self { + ArrayIterator { + array, + kind, + next_index: 0, + } + } + + /// CreateArrayIterator( array, kind ) + /// + /// Creates a new iterator over the given array. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createarrayiterator + pub(crate) fn create_array_iterator( + ctx: &Context, + array: Value, + kind: ArrayIterationKind, + ) -> Result { + let array_iterator = Value::new_object(Some(ctx.global_object())); + array_iterator.set_data(ObjectData::ArrayIterator(Self::new(array, kind))); + array_iterator + .as_object_mut() + .expect("array iterator object") + .set_prototype_instance(ctx.iterator_prototypes().array_iterator().into()); + Ok(array_iterator) + } + + /// %ArrayIteratorPrototype%.next( ) + /// + /// Gets the next result in the array. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%.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(array_iterator) = object.as_array_iterator_mut() { + let index = array_iterator.next_index; + if array_iterator.array.is_undefined() { + return Ok(create_iter_result_object(ctx, Value::undefined(), true)); + } + let len = array_iterator + .array + .get_field("length") + .as_number() + .ok_or_else(|| ctx.construct_type_error("Not an array"))? + as u32; + if array_iterator.next_index >= len { + array_iterator.array = Value::undefined(); + return Ok(create_iter_result_object(ctx, Value::undefined(), true)); + } + array_iterator.next_index = index + 1; + match array_iterator.kind { + ArrayIterationKind::Key => { + Ok(create_iter_result_object(ctx, index.into(), false)) + } + ArrayIterationKind::Value => { + let element_value = array_iterator.array.get_field(index); + Ok(create_iter_result_object(ctx, element_value, false)) + } + ArrayIterationKind::KeyAndValue => { + let element_value = array_iterator.array.get_field(index); + let result = Array::make_array( + &Value::new_object(Some(ctx.global_object())), + &[index.into(), element_value], + ctx, + )?; + Ok(create_iter_result_object(ctx, result, false)) + } + } + } else { + ctx.throw_type_error("`this` is not an ArrayIterator") + } + } else { + ctx.throw_type_error("`this` is not an ArrayIterator") + } + } + + /// Create the %ArrayIteratorPrototype% object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%-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 array_iterator = Value::new_object(Some(global)); + make_builtin_fn(Self::next, "next", &array_iterator, 0, ctx); + array_iterator + .as_object_mut() + .expect("array 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 = + Property::data_descriptor(Value::string("Array Iterator"), Attribute::CONFIGURABLE); + array_iterator.set_property(to_string_tag, to_string_tag_property); + array_iterator + } +} diff --git a/boa/src/builtins/array/mod.rs b/boa/src/builtins/array/mod.rs index f90f3dbea6..2d25b0270d 100644 --- a/boa/src/builtins/array/mod.rs +++ b/boa/src/builtins/array/mod.rs @@ -9,11 +9,13 @@ //! [spec]: https://tc39.es/ecma262/#sec-array-objects //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array +pub mod array_iterator; #[cfg(test)] mod tests; use super::function::{make_builtin_fn, make_constructor_fn}; use crate::{ + builtins::array::array_iterator::{ArrayIterationKind, ArrayIterator}, object::{ObjectData, PROTOTYPE}, property::{Attribute, Property}, value::{same_value_zero, Value}, @@ -1091,6 +1093,60 @@ impl Array { Ok(accumulator) } + /// `Array.prototype.values( )` + /// + /// The values method returns an iterable that iterates over the values in the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.values + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values + pub(crate) fn values( + this: &Value, + _args: &[Value], + interpreter: &mut Context, + ) -> Result { + ArrayIterator::create_array_iterator(interpreter, this.clone(), ArrayIterationKind::Value) + } + + /// `Array.prototype.keys( )` + /// + /// The keys method returns an iterable that iterates over the indexes in the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.values + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values + pub(crate) fn keys(this: &Value, _args: &[Value], interpreter: &mut Context) -> Result { + ArrayIterator::create_array_iterator(interpreter, this.clone(), ArrayIterationKind::Key) + } + + /// `Array.prototype.entries( )` + /// + /// The entries method returns an iterable that iterates over the key-value pairs in the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.values + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values + pub(crate) fn entries( + this: &Value, + _args: &[Value], + interpreter: &mut Context, + ) -> Result { + ArrayIterator::create_array_iterator( + interpreter, + this.clone(), + ArrayIterationKind::KeyAndValue, + ) + } + /// Initialise the `Array` object on the global object. #[inline] pub(crate) fn init(interpreter: &mut Context) -> (&'static str, Value) { @@ -1137,6 +1193,15 @@ impl Array { 2, interpreter, ); + make_builtin_fn(Self::values, "values", &prototype, 0, interpreter); + make_builtin_fn(Self::keys, "keys", &prototype, 0, interpreter); + make_builtin_fn(Self::entries, "entries", &prototype, 0, interpreter); + + let symbol_iterator = interpreter.well_known_symbols().iterator_symbol(); + prototype.set_property( + symbol_iterator, + Property::default().value(prototype.get_field("values")), + ); let array = make_constructor_fn( Self::NAME, diff --git a/boa/src/builtins/array/tests.rs b/boa/src/builtins/array/tests.rs index fa066ad979..876de0e853 100644 --- a/boa/src/builtins/array/tests.rs +++ b/boa/src/builtins/array/tests.rs @@ -1078,3 +1078,135 @@ fn call_array_constructor_with_one_argument() { // let result = forward(&mut engine, "one.length"); // assert_eq!(result, "1"); } + +#[test] +fn array_values_simple() { + let mut engine = Context::new(); + let init = r#" + var iterator = [1, 2, 3].values(); + var next = iterator.next(); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "next.value"), "1"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "2"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "3"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "true"); +} + +#[test] +fn array_keys_simple() { + let mut engine = Context::new(); + let init = r#" + var iterator = [1, 2, 3].keys(); + var next = iterator.next(); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "next.value"), "0"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "1"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "2"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "true"); +} + +#[test] +fn array_entries_simple() { + let mut engine = Context::new(); + let init = r#" + var iterator = [1, 2, 3].entries(); + var next = iterator.next(); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "next.value"), "[ 0, 1 ]"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "[ 1, 2 ]"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "[ 2, 3 ]"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "true"); +} + +#[test] +fn array_values_empty() { + let mut engine = Context::new(); + let init = r#" + var iterator = [].values(); + var next = iterator.next(); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "true"); +} + +#[test] +fn array_values_sparse() { + let mut engine = Context::new(); + let init = r#" + var array = Array(); + array[3] = 5; + var iterator = array.values(); + var next = iterator.next(); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "5"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "true"); +} + +#[test] +fn array_symbol_iterator() { + let mut engine = Context::new(); + let init = r#" + var iterator = [1, 2, 3][Symbol.iterator](); + var next = iterator.next(); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "next.value"), "1"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "2"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "3"); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iterator.next()"); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "true"); +} + +#[test] +fn array_values_symbol_iterator() { + let mut engine = Context::new(); + let init = r#" + var iterator = [1, 2, 3].values(); + iterator === iterator[Symbol.iterator](); + "#; + assert_eq!(forward(&mut engine, init), "true"); +} diff --git a/boa/src/builtins/iterable/mod.rs b/boa/src/builtins/iterable/mod.rs new file mode 100644 index 0000000000..881f223ef5 --- /dev/null +++ b/boa/src/builtins/iterable/mod.rs @@ -0,0 +1,154 @@ +use crate::{ + builtins::string::string_iterator::StringIterator, + builtins::{ + function::{BuiltInFunction, Function, FunctionFlags}, + ArrayIterator, + }, + object::GcObject, + object::{Object, PROTOTYPE}, + property::Property, + BoaProfiler, Context, Result, Value, +}; + +#[derive(Debug, Default)] +pub struct IteratorPrototypes { + iterator_prototype: GcObject, + array_iterator: GcObject, + string_iterator: GcObject, +} + +impl IteratorPrototypes { + pub fn init(ctx: &mut Context) -> Self { + let iterator_prototype = create_iterator_prototype(ctx); + Self { + iterator_prototype: iterator_prototype + .as_gc_object() + .expect("Iterator prototype is not an object"), + 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) + .as_gc_object() + .expect("String Iterator Prototype is not an object"), + } + } + + pub fn array_iterator(&self) -> GcObject { + self.array_iterator.clone() + } + + pub fn iterator_prototype(&self) -> GcObject { + self.iterator_prototype.clone() + } + + pub fn string_iterator(&self) -> GcObject { + self.string_iterator.clone() + } +} + +/// CreateIterResultObject( value, done ) +/// +/// Generates an object supporting the IteratorResult interface. +pub fn create_iter_result_object(ctx: &mut Context, value: Value, done: bool) -> Value { + let object = Value::new_object(Some(ctx.global_object())); + let value_property = Property::default().value(value); + let done_property = Property::default().value(Value::boolean(done)); + object.set_property("value", value_property); + object.set_property("done", done_property); + object +} + +/// Get an iterator record +pub fn get_iterator(ctx: &mut Context, iterable: Value) -> Result { + let iterator_function = iterable + .get_property(ctx.well_known_symbols().iterator_symbol()) + .and_then(|mut p| p.value.take()) + .ok_or_else(|| ctx.construct_type_error("Not an iterable"))?; + let iterator_object = ctx.call(&iterator_function, &iterable, &[])?; + let next_function = iterator_object + .get_property("next") + .and_then(|mut p| p.value.take()) + .ok_or_else(|| ctx.construct_type_error("Could not find property `next`"))?; + Ok(IteratorRecord::new(iterator_object, next_function)) +} + +/// Create the %IteratorPrototype% object +/// +/// More information: +/// - [ECMA reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-%iteratorprototype%-object +fn create_iterator_prototype(ctx: &mut Context) -> Value { + let global = ctx.global_object(); + let _timer = BoaProfiler::global().start_event("Iterator Prototype", "init"); + + let iterator_prototype = Value::new_object(Some(global)); + let mut function = Object::function( + Function::BuiltIn( + BuiltInFunction(|v, _, _| Ok(v.clone())), + FunctionFlags::CALLABLE, + ), + global.get_field("Function").get_field(PROTOTYPE), + ); + function.insert_field("length", Value::from(0)); + function.insert_field("name", Value::string("[Symbol.iterator]")); + + let symbol_iterator = ctx.well_known_symbols().iterator_symbol(); + iterator_prototype.set_field(symbol_iterator, Value::from(function)); + iterator_prototype +} + +#[derive(Debug)] +pub struct IteratorRecord { + iterator_object: Value, + next_function: Value, +} + +impl IteratorRecord { + fn new(iterator_object: Value, next_function: Value) -> Self { + Self { + iterator_object, + next_function, + } + } + + /// Get the next value in the iterator + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-iteratornext + pub(crate) fn next(&self, ctx: &mut Context) -> Result { + let next = ctx.call(&self.next_function, &self.iterator_object, &[])?; + let done = next + .get_property("done") + .and_then(|mut p| p.value.take()) + .and_then(|v| v.as_boolean()) + .ok_or_else(|| ctx.construct_type_error("Could not find property `done`"))?; + let next_result = next + .get_property("value") + .and_then(|mut p| p.value.take()) + .unwrap_or_default(); + Ok(IteratorResult::new(next_result, done)) + } +} + +#[derive(Debug)] +pub struct IteratorResult { + value: Value, + done: bool, +} + +impl IteratorResult { + fn new(value: Value, done: bool) -> Self { + Self { value, done } + } + + pub fn is_done(&self) -> bool { + self.done + } + + pub fn value(self) -> Value { + self.value + } +} diff --git a/boa/src/builtins/mod.rs b/boa/src/builtins/mod.rs index 9e1241d84a..da592c010e 100644 --- a/boa/src/builtins/mod.rs +++ b/boa/src/builtins/mod.rs @@ -9,6 +9,7 @@ pub mod error; pub mod function; pub mod global_this; pub mod infinity; +pub mod iterable; pub mod json; pub mod map; pub mod math; @@ -21,7 +22,7 @@ pub mod symbol; pub mod undefined; pub(crate) use self::{ - array::Array, + array::{array_iterator::ArrayIterator, Array}, bigint::BigInt, boolean::Boolean, console::Console, @@ -49,6 +50,7 @@ pub fn init(interpreter: &mut Context) { // The `Function` global must be initialized before other types. function::init, Object::init, + Symbol::init, Array::init, BigInt::init, Boolean::init, @@ -59,7 +61,6 @@ pub fn init(interpreter: &mut Context) { Number::init, RegExp::init, String::init, - Symbol::init, Console::init, // Global error types. Error::init, diff --git a/boa/src/builtins/string/mod.rs b/boa/src/builtins/string/mod.rs index 03e6b2ae99..1d92b24be2 100644 --- a/boa/src/builtins/string/mod.rs +++ b/boa/src/builtins/string/mod.rs @@ -9,10 +9,14 @@ //! [spec]: https://tc39.es/ecma262/#sec-string-object //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String +pub mod string_iterator; #[cfg(test)] mod tests; use super::function::{make_builtin_fn, make_constructor_fn}; +use crate::builtins::function::{BuiltInFunction, Function, FunctionFlags}; +use crate::builtins::string::string_iterator::StringIterator; +use crate::object::PROTOTYPE; use crate::{ builtins::RegExp, object::{Object, ObjectData}, @@ -21,12 +25,42 @@ use crate::{ BoaProfiler, Context, Result, }; use regex::Regex; -use std::string::String as StdString; use std::{ + char::decode_utf16, cmp::{max, min}, f64::NAN, + string::String as StdString, }; +pub(crate) fn code_point_at(string: RcString, position: i32) -> Option<(u32, u8, bool)> { + let size = string.encode_utf16().count() as i32; + if position < 0 || position >= size { + return None; + } + let mut encoded = string.encode_utf16(); + let first = encoded.nth(position as usize)?; + if !is_leading_surrogate(first) && !is_trailing_surrogate(first) { + return Some((first as u32, 1, false)); + } + if is_trailing_surrogate(first) || position + 1 == size { + return Some((first as u32, 1, true)); + } + let second = encoded.next()?; + if !is_trailing_surrogate(second) { + return Some((first as u32, 1, true)); + } + let cp = (first as u32 - 0xD800) * 0x400 + (second as u32 - 0xDC00) + 0x10000; + Some((cp, 2, false)) +} + +fn is_leading_surrogate(value: u16) -> bool { + value >= 0xD800 && value <= 0xDBFF +} + +fn is_trailing_surrogate(value: u16) -> bool { + value >= 0xDC00 && value <= 0xDFFF +} + /// JavaScript `String` implementation. #[derive(Debug, Clone, Copy)] pub(crate) struct String; @@ -72,7 +106,7 @@ impl String { None => RcString::default(), }; - let length = string.chars().count(); + let length = string.encode_utf16().count(); this.set_field("length", Value::from(length as i32)); @@ -922,7 +956,7 @@ impl String { .expect("failed to get argument for String method") .to_integer(ctx)? as i32 }; - let length = primitive_val.chars().count() as i32; + let length = primitive_val.encode_utf16().count() as i32; // If less than 2 args specified, end is the length of the this object converted to a String let end = if args.len() < 2 { length @@ -940,12 +974,14 @@ impl String { let to = max(final_start, final_end) as usize; // Extract the part of the string contained between the start index and the end index // where start is guaranteed to be smaller or equals to end - let extracted_string: StdString = primitive_val - .chars() - .skip(from) - .take(to.wrapping_sub(from)) - .collect(); - Ok(Value::from(extracted_string)) + let extracted_string: std::result::Result = decode_utf16( + primitive_val + .encode_utf16() + .skip(from) + .take(to.wrapping_sub(from)), + ) + .collect(); + Ok(Value::from(extracted_string.expect("Invalid string"))) } /// `String.prototype.substr( start[, length] )` @@ -1062,6 +1098,10 @@ impl String { RegExp::match_all(&re, this.to_string(ctx)?.to_string()) } + pub(crate) fn iterator(this: &Value, _args: &[Value], ctx: &mut Context) -> Result { + StringIterator::create_string_iterator(ctx, this.clone()) + } + /// Initialise the `String` object on the global object. #[inline] pub(crate) fn init(interpreter: &mut Context) -> (&'static str, Value) { @@ -1118,6 +1158,15 @@ impl String { make_builtin_fn(Self::match_all, "matchAll", &prototype, 1, interpreter); make_builtin_fn(Self::replace, "replace", &prototype, 2, interpreter); + let symbol_iterator = interpreter.well_known_symbols().iterator_symbol(); + let mut function = Object::function( + Function::BuiltIn(BuiltInFunction(Self::iterator), FunctionFlags::CALLABLE), + global.get_field("Function").get_field(PROTOTYPE), + ); + function.insert_field("length", Value::from(0)); + function.insert_field("name", Value::string("[Symbol.iterator]")); + prototype.set_field(symbol_iterator, Value::from(function)); + let string_object = make_constructor_fn( Self::NAME, Self::LENGTH, diff --git a/boa/src/builtins/string/string_iterator.rs b/boa/src/builtins/string/string_iterator.rs new file mode 100644 index 0000000000..c75f6b679c --- /dev/null +++ b/boa/src/builtins/string/string_iterator.rs @@ -0,0 +1,89 @@ +use crate::builtins::string::code_point_at; +use crate::{ + builtins::{function::make_builtin_fn, iterable::create_iter_result_object}, + object::ObjectData, + property::{Attribute, Property}, + BoaProfiler, Context, Result, Value, +}; +use gc::{Finalize, Trace}; + +#[derive(Debug, Clone, Finalize, Trace)] +pub struct StringIterator { + string: Value, + next_index: i32, +} + +impl StringIterator { + fn new(string: Value) -> Self { + Self { + string, + next_index: 0, + } + } + + pub fn create_string_iterator(ctx: &mut Context, string: Value) -> Result { + let string_iterator = Value::new_object(Some(ctx.global_object())); + string_iterator.set_data(ObjectData::StringIterator(Self::new(string))); + string_iterator + .as_object_mut() + .expect("array iterator object") + .set_prototype_instance(ctx.iterator_prototypes().string_iterator().into()); + Ok(string_iterator) + } + + pub 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(string_iterator) = object.as_string_iterator_mut() { + if string_iterator.string.is_undefined() { + return Ok(create_iter_result_object(ctx, Value::undefined(), true)); + } + let native_string = string_iterator.string.to_string(ctx)?; + let len = native_string.encode_utf16().count() as i32; + let position = string_iterator.next_index; + if position >= len { + string_iterator.string = Value::undefined(); + return Ok(create_iter_result_object(ctx, Value::undefined(), true)); + } + let (_, code_unit_count, _) = + code_point_at(native_string, position).expect("Invalid code point position"); + string_iterator.next_index += code_unit_count as i32; + let result_string = crate::builtins::string::String::substring( + &string_iterator.string, + &[position.into(), string_iterator.next_index.into()], + ctx, + )?; + Ok(create_iter_result_object(ctx, result_string, false)) + } else { + ctx.throw_type_error("`this` is not an ArrayIterator") + } + } else { + ctx.throw_type_error("`this` is not an ArrayIterator") + } + } + + /// Create the %ArrayIteratorPrototype% object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%-object + pub(crate) fn create_prototype(ctx: &mut Context, iterator_prototype: Value) -> Value { + let global = ctx.global_object(); + let _timer = BoaProfiler::global().start_event("String Iterator", "init"); + + // Create prototype + let array_iterator = Value::new_object(Some(global)); + make_builtin_fn(Self::next, "next", &array_iterator, 0, ctx); + array_iterator + .as_object_mut() + .expect("array 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 = + Property::data_descriptor(Value::string("String Iterator"), Attribute::CONFIGURABLE); + array_iterator.set_property(to_string_tag, to_string_tag_property); + array_iterator + } +} diff --git a/boa/src/builtins/string/tests.rs b/boa/src/builtins/string/tests.rs index 5778e37e09..3bc8ea1951 100644 --- a/boa/src/builtins/string/tests.rs +++ b/boa/src/builtins/string/tests.rs @@ -679,3 +679,102 @@ fn last_index_non_integer_position_argument() { ); assert_eq!(forward(&mut engine, "'abcx'.lastIndexOf('x', null)"), "3"); } + +#[test] +fn empty_iter() { + let mut engine = Context::new(); + let init = r#" + let iter = new String()[Symbol.iterator](); + let next = iter.next(); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "true"); +} + +#[test] +fn ascii_iter() { + let mut engine = Context::new(); + let init = r#" + let iter = new String("Hello World")[Symbol.iterator](); + let next = iter.next(); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "next.value"), "\"H\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"e\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"l\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"l\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"o\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\" \""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"W\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"o\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"r\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"l\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"d\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "true"); +} + +#[test] +fn unicode_iter() { + let mut engine = Context::new(); + let init = r#" + let iter = new String("C🙂🙂l W🙂rld")[Symbol.iterator](); + let next = iter.next(); + "#; + forward(&mut engine, init); + assert_eq!(forward(&mut engine, "next.value"), "\"C\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"🙂\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"🙂\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"l\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\" \""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"W\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"🙂\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"r\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"l\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "\"d\""); + assert_eq!(forward(&mut engine, "next.done"), "false"); + forward(&mut engine, "next = iter.next()"); + assert_eq!(forward(&mut engine, "next.value"), "undefined"); + assert_eq!(forward(&mut engine, "next.done"), "true"); +} diff --git a/boa/src/context.rs b/boa/src/context.rs index aa7b64ddbc..d6b95188df 100644 --- a/boa/src/context.rs +++ b/boa/src/context.rs @@ -4,6 +4,7 @@ use crate::{ builtins::{ self, function::{Function, FunctionFlags, NativeFunction}, + iterable::IteratorPrototypes, symbol::{Symbol, WellKnownSymbols}, Console, }, @@ -50,6 +51,8 @@ pub struct Context { /// Cached well known symbols well_known_symbols: WellKnownSymbols, + + iterator_prototypes: IteratorPrototypes, } impl Default for Context { @@ -63,13 +66,14 @@ impl Default for Context { symbol_count, console: Console::default(), well_known_symbols, + iterator_prototypes: IteratorPrototypes::default(), }; // Add new builtIns to Context Realm // At a later date this can be removed from here and called explicitly, // but for now we almost always want these default builtins context.create_intrinsics(); - + context.iterator_prototypes = IteratorPrototypes::init(&mut context); context } } @@ -517,4 +521,9 @@ impl Context { pub fn well_known_symbols(&self) -> &WellKnownSymbols { &self.well_known_symbols } + + #[inline] + pub fn iterator_prototypes(&self) -> &IteratorPrototypes { + &self.iterator_prototypes + } } diff --git a/boa/src/object/gcobject.rs b/boa/src/object/gcobject.rs index aa6e262b2c..546aed91a6 100644 --- a/boa/src/object/gcobject.rs +++ b/boa/src/object/gcobject.rs @@ -29,7 +29,7 @@ pub type Ref<'object> = GcCellRef<'object, Object>; pub type RefMut<'object> = GcCellRefMut<'object, Object>; /// Garbage collected `Object`. -#[derive(Trace, Finalize, Clone)] +#[derive(Trace, Finalize, Clone, Default)] pub struct GcObject(Gc>); // This is needed for the call method since we cannot mutate the function itself since we diff --git a/boa/src/object/mod.rs b/boa/src/object/mod.rs index fe92ddc76c..3776f67513 100644 --- a/boa/src/object/mod.rs +++ b/boa/src/object/mod.rs @@ -1,7 +1,10 @@ //! This module implements the Rust representation of a JavaScript object. use crate::{ - builtins::{function::Function, map::ordered_map::OrderedMap, BigInt, Date, RegExp}, + builtins::{ + array::array_iterator::ArrayIterator, function::Function, map::ordered_map::OrderedMap, + string::string_iterator::StringIterator, BigInt, Date, RegExp, + }, property::{Property, PropertyKey}, value::{RcBigInt, RcString, RcSymbol, Value}, BoaProfiler, @@ -62,12 +65,14 @@ pub struct Object { #[derive(Debug, Trace, Finalize)] pub enum ObjectData { Array, + ArrayIterator(ArrayIterator), Map(OrderedMap), RegExp(Box), BigInt(RcBigInt), Boolean(bool), Function(Function), String(RcString), + StringIterator(StringIterator), Number(f64), Symbol(RcSymbol), Error, @@ -84,10 +89,12 @@ impl Display for ObjectData { "{}", match self { Self::Array => "Array", + Self::ArrayIterator(_) => "ArrayIterator", Self::Function(_) => "Function", Self::RegExp(_) => "RegExp", Self::Map(_) => "Map", Self::String(_) => "String", + Self::StringIterator(_) => "StringIterator", Self::Symbol(_) => "Symbol", Self::Error => "Error", Self::Ordinary => "Ordinary", @@ -252,6 +259,36 @@ impl Object { } } + /// Checks if it is an `ArrayIterator` object. + #[inline] + pub fn is_array_iterator(&self) -> bool { + matches!(self.data, ObjectData::ArrayIterator(_)) + } + + #[inline] + pub fn as_array_iterator(&self) -> Option<&ArrayIterator> { + match self.data { + ObjectData::ArrayIterator(ref iter) => Some(iter), + _ => None, + } + } + + #[inline] + pub fn as_array_iterator_mut(&mut self) -> Option<&mut ArrayIterator> { + match &mut self.data { + ObjectData::ArrayIterator(iter) => Some(iter), + _ => None, + } + } + + #[inline] + pub fn as_string_iterator_mut(&mut self) -> Option<&mut StringIterator> { + match &mut self.data { + ObjectData::StringIterator(iter) => Some(iter), + _ => None, + } + } + /// Checks if it is a `Map` object.pub #[inline] pub fn is_map(&self) -> bool { diff --git a/boa/src/syntax/ast/keyword.rs b/boa/src/syntax/ast/keyword.rs index f4d003bcbc..566d25b322 100644 --- a/boa/src/syntax/ast/keyword.rs +++ b/boa/src/syntax/ast/keyword.rs @@ -291,6 +291,16 @@ pub enum Keyword { /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new New, + /// The `of` keyword. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-for-in-and-for-of-statements + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of + Of, + /// The `return` keyword /// /// More information: @@ -467,6 +477,7 @@ impl Keyword { Self::Import => "import", Self::Let => "let", Self::New => "new", + Self::Of => "of", Self::Return => "return", Self::Super => "super", Self::Switch => "switch", @@ -538,6 +549,7 @@ impl FromStr for Keyword { "import" => Ok(Self::Import), "let" => Ok(Self::Let), "new" => Ok(Self::New), + "of" => Ok(Self::Of), "return" => Ok(Self::Return), "super" => Ok(Self::Super), "switch" => Ok(Self::Switch), diff --git a/boa/src/syntax/ast/node/declaration/const_decl_list/mod.rs b/boa/src/syntax/ast/node/declaration/const_decl_list/mod.rs index 40d95ef230..103d4de69e 100644 --- a/boa/src/syntax/ast/node/declaration/const_decl_list/mod.rs +++ b/boa/src/syntax/ast/node/declaration/const_decl_list/mod.rs @@ -38,7 +38,11 @@ pub struct ConstDeclList { impl Executable for ConstDeclList { fn run(&self, interpreter: &mut Context) -> Result { for decl in self.as_ref() { - let val = decl.init().run(interpreter)?; + let val = if let Some(init) = decl.init() { + init.run(interpreter)? + } else { + return interpreter.throw_syntax_error("missing = in const declaration"); + }; interpreter .realm_mut() @@ -99,25 +103,29 @@ impl From for Node { #[derive(Clone, Debug, Trace, Finalize, PartialEq)] pub struct ConstDecl { name: Identifier, - init: Node, + init: Option, } impl fmt::Display for ConstDecl { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} = {}", self.name, self.init) + fmt::Display::fmt(&self.name, f)?; + if let Some(ref init) = self.init { + write!(f, " = {}", init)?; + } + Ok(()) } } impl ConstDecl { /// Creates a new variable declaration. - pub(in crate::syntax) fn new(name: N, init: I) -> Self + pub(in crate::syntax) fn new(name: N, init: Option) -> Self where N: Into, I: Into, { Self { name: name.into(), - init: init.into(), + init: init.map(|n| n.into()), } } @@ -127,7 +135,7 @@ impl ConstDecl { } /// Gets the initialization node for the variable, if any. - pub fn init(&self) -> &Node { + pub fn init(&self) -> &Option { &self.init } } diff --git a/boa/src/syntax/ast/node/iteration/for_of_loop/mod.rs b/boa/src/syntax/ast/node/iteration/for_of_loop/mod.rs new file mode 100644 index 0000000000..f076786bae --- /dev/null +++ b/boa/src/syntax/ast/node/iteration/for_of_loop/mod.rs @@ -0,0 +1,208 @@ +use crate::{ + builtins::iterable::get_iterator, + environment::lexical_environment::{new_declarative_environment, VariableScope}, + exec::{Executable, InterpreterState}, + syntax::ast::node::Node, + BoaProfiler, Context, Result, Value, +}; +use gc::{Finalize, Trace}; +use std::fmt; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Debug, Trace, Finalize, PartialEq)] +pub struct ForOfLoop { + variable: Box, + iterable: Box, + body: Box, +} + +impl ForOfLoop { + pub fn new(variable: V, iterable: I, body: B) -> Self + where + V: Into, + I: Into, + B: Into, + { + Self { + variable: Box::new(variable.into()), + iterable: Box::new(iterable.into()), + body: Box::new(body.into()), + } + } + + pub fn variable(&self) -> &Node { + &self.variable + } + + pub fn iterable(&self) -> &Node { + &self.iterable + } + + pub fn body(&self) -> &Node { + &self.body + } + + pub fn display(&self, f: &mut fmt::Formatter<'_>, indentation: usize) -> fmt::Result { + write!(f, "for ({} of {}) {{", self.variable, self.iterable)?; + self.body().display(f, indentation + 1)?; + f.write_str("}") + } +} + +impl fmt::Display for ForOfLoop { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.display(f, 0) + } +} + +impl From for Node { + fn from(for_of: ForOfLoop) -> Node { + Self::ForOfLoop(for_of) + } +} + +impl Executable for ForOfLoop { + fn run(&self, interpreter: &mut Context) -> Result { + let _timer = BoaProfiler::global().start_event("ForOf", "exec"); + let iterable = self.iterable().run(interpreter)?; + let iterator = get_iterator(interpreter, iterable)?; + let mut result = Value::undefined(); + + loop { + { + let env = &mut interpreter.realm_mut().environment; + env.push(new_declarative_environment(Some( + env.get_current_environment_ref().clone(), + ))); + } + let iterator_result = iterator.next(interpreter)?; + if iterator_result.is_done() { + break; + } + let next_result = iterator_result.value(); + + match self.variable() { + Node::Identifier(ref name) => { + let environment = &mut interpreter.realm_mut().environment; + + if environment.has_binding(name.as_ref()) { + // Binding already exists + environment.set_mutable_binding(name.as_ref(), next_result.clone(), true); + } else { + environment.create_mutable_binding( + name.as_ref().to_owned(), + true, + VariableScope::Function, + ); + environment.initialize_binding(name.as_ref(), next_result.clone()); + } + } + Node::VarDeclList(ref list) => match list.as_ref() { + [var] => { + let environment = &mut interpreter.realm_mut().environment; + + if var.init().is_some() { + return interpreter.throw_syntax_error("a declaration in the head of a for-of loop can't have an initializer"); + } + + if environment.has_binding(var.name()) { + environment.set_mutable_binding(var.name(), next_result, true); + } else { + environment.create_mutable_binding( + var.name().to_owned(), + false, + VariableScope::Function, + ); + environment.initialize_binding(var.name(), next_result); + } + } + _ => { + return interpreter.throw_syntax_error( + "only one variable can be declared in the head of a for-of loop", + ) + } + }, + Node::LetDeclList(ref list) => match list.as_ref() { + [var] => { + let environment = &mut interpreter.realm_mut().environment; + + if var.init().is_some() { + return interpreter.throw_syntax_error("a declaration in the head of a for-of loop can't have an initializer"); + } + + environment.create_mutable_binding( + var.name().to_owned(), + false, + VariableScope::Block, + ); + environment.initialize_binding(var.name(), next_result); + } + _ => { + return interpreter.throw_syntax_error( + "only one variable can be declared in the head of a for-of loop", + ) + } + }, + Node::ConstDeclList(ref list) => match list.as_ref() { + [var] => { + let environment = &mut interpreter.realm_mut().environment; + + if var.init().is_some() { + return interpreter.throw_syntax_error("a declaration in the head of a for-of loop can't have an initializer"); + } + + environment.create_immutable_binding( + var.name().to_owned(), + false, + VariableScope::Block, + ); + environment.initialize_binding(var.name(), next_result); + } + _ => { + return interpreter.throw_syntax_error( + "only one variable can be declared in the head of a for-of loop", + ) + } + }, + Node::Assign(_) => { + return interpreter.throw_syntax_error( + "a declaration in the head of a for-of loop can't have an initializer", + ); + } + _ => { + return interpreter + .throw_syntax_error("unknown left hand side in head of for-of loop") + } + } + + result = self.body().run(interpreter)?; + match interpreter.executor().get_current_state() { + InterpreterState::Break(_label) => { + // TODO break to label. + + // Loops 'consume' breaks. + interpreter + .executor() + .set_current_state(InterpreterState::Executing); + break; + } + InterpreterState::Continue(_label) => { + // TODO continue to label. + interpreter + .executor() + .set_current_state(InterpreterState::Executing); + // after breaking out of the block, continue execution of the loop + } + InterpreterState::Return => return Ok(result), + InterpreterState::Executing => { + // Continue execution. + } + } + let _ = interpreter.realm_mut().environment.pop(); + } + Ok(result) + } +} diff --git a/boa/src/syntax/ast/node/iteration/mod.rs b/boa/src/syntax/ast/node/iteration/mod.rs index 73a227bb8f..7fa4f8afb6 100644 --- a/boa/src/syntax/ast/node/iteration/mod.rs +++ b/boa/src/syntax/ast/node/iteration/mod.rs @@ -1,7 +1,8 @@ //! Iteration nodes pub use self::{ - continue_node::Continue, do_while_loop::DoWhileLoop, for_loop::ForLoop, while_loop::WhileLoop, + continue_node::Continue, do_while_loop::DoWhileLoop, for_loop::ForLoop, for_of_loop::ForOfLoop, + while_loop::WhileLoop, }; #[cfg(test)] @@ -32,4 +33,5 @@ macro_rules! handle_state_with_labels { pub mod continue_node; pub mod do_while_loop; pub mod for_loop; +pub mod for_of_loop; pub mod while_loop; diff --git a/boa/src/syntax/ast/node/iteration/tests.rs b/boa/src/syntax/ast/node/iteration/tests.rs index 571fe1defe..7340eb3ce4 100644 --- a/boa/src/syntax/ast/node/iteration/tests.rs +++ b/boa/src/syntax/ast/node/iteration/tests.rs @@ -1,4 +1,4 @@ -use crate::exec; +use crate::{exec, forward, Context}; #[test] fn while_loop_late_break() { @@ -192,6 +192,133 @@ fn do_while_loop_continue() { assert_eq!(&exec(scenario), "[ 1, 2 ]"); } +#[test] +fn for_of_loop_declaration() { + let mut engine = Context::new(); + let scenario = r#" + var result = 0; + for (i of [1, 2, 3]) { + result = i; + } + "#; + engine.eval(scenario).unwrap(); + assert_eq!(&forward(&mut engine, "result"), "3"); + assert_eq!(&forward(&mut engine, "i"), "3"); +} + +#[test] +fn for_of_loop_var() { + let mut engine = Context::new(); + let scenario = r#" + var result = 0; + for (var i of [1, 2, 3]) { + result = i; + } + "#; + engine.eval(scenario).unwrap(); + assert_eq!(&forward(&mut engine, "result"), "3"); + assert_eq!(&forward(&mut engine, "i"), "3"); +} + +#[test] +fn for_of_loop_let() { + let mut engine = Context::new(); + let scenario = r#" + var result = 0; + for (let i of [1, 2, 3]) { + result = i; + } + "#; + engine.eval(scenario).unwrap(); + assert_eq!(&forward(&mut engine, "result"), "3"); + assert_eq!( + &forward( + &mut engine, + r#" + try { + i + } catch(e) { + e.toString() + } + "# + ), + "\"ReferenceError: i is not defined\"" + ); +} + +#[test] +fn for_of_loop_const() { + let mut engine = Context::new(); + let scenario = r#" + var result = 0; + for (let i of [1, 2, 3]) { + result = i; + } + "#; + engine.eval(scenario).unwrap(); + assert_eq!(&forward(&mut engine, "result"), "3"); + assert_eq!( + &forward( + &mut engine, + r#" + try { + i + } catch(e) { + e.toString() + } + "# + ), + "\"ReferenceError: i is not defined\"" + ); +} + +#[test] +fn for_of_loop_break() { + let mut engine = Context::new(); + let scenario = r#" + var result = 0; + for (var i of [1, 2, 3]) { + if (i > 1) + break; + result = i + } + "#; + engine.eval(scenario).unwrap(); + assert_eq!(&forward(&mut engine, "result"), "1"); + assert_eq!(&forward(&mut engine, "i"), "2"); +} + +#[test] +fn for_of_loop_continue() { + let mut engine = Context::new(); + let scenario = r#" + var result = 0; + for (var i of [1, 2, 3]) { + if (i == 3) + continue; + result = i + } + "#; + engine.eval(scenario).unwrap(); + assert_eq!(&forward(&mut engine, "result"), "2"); + assert_eq!(&forward(&mut engine, "i"), "3"); +} + +#[test] +fn for_of_loop_return() { + let mut engine = Context::new(); + let scenario = r#" + function foo() { + for (i of [1, 2, 3]) { + if (i > 1) + return i; + } + } + "#; + engine.eval(scenario).unwrap(); + assert_eq!(&forward(&mut engine, "foo()"), "2"); +} + #[test] fn for_loop_break_label() { let scenario = r#" diff --git a/boa/src/syntax/ast/node/mod.rs b/boa/src/syntax/ast/node/mod.rs index e039eb78c4..5771c485fd 100644 --- a/boa/src/syntax/ast/node/mod.rs +++ b/boa/src/syntax/ast/node/mod.rs @@ -30,7 +30,7 @@ pub use self::{ expression::{Call, New}, field::{GetConstField, GetField}, identifier::Identifier, - iteration::{Continue, DoWhileLoop, ForLoop, WhileLoop}, + iteration::{Continue, DoWhileLoop, ForLoop, ForOfLoop, WhileLoop}, object::Object, operator::{Assign, BinOp, UnaryOp}, return_smt::Return, @@ -114,6 +114,9 @@ pub enum Node { /// A `for` statement. [More information](./iteration/struct.ForLoop.html). ForLoop(ForLoop), + /// A `for...of` statement. [More information](./iteration/struct.ForOf.html). + ForOfLoop(ForOfLoop), + /// An 'if' statement. [More information](./conditional/struct.If.html). If(If), @@ -209,6 +212,7 @@ impl Node { Self::Const(ref c) => write!(f, "{}", c), Self::ConditionalOp(ref cond_op) => Display::fmt(cond_op, f), Self::ForLoop(ref for_loop) => for_loop.display(f, indentation), + Self::ForOfLoop(ref for_of) => for_of.display(f, indentation), Self::This => write!(f, "this"), Self::Try(ref try_catch) => try_catch.display(f, indentation), Self::Break(ref break_smt) => Display::fmt(break_smt, f), @@ -263,6 +267,7 @@ impl Executable for Node { Node::WhileLoop(ref while_loop) => while_loop.run(interpreter), Node::DoWhileLoop(ref do_while) => do_while.run(interpreter), Node::ForLoop(ref for_loop) => for_loop.run(interpreter), + Node::ForOfLoop(ref for_of_loop) => for_of_loop.run(interpreter), Node::If(ref if_smt) => if_smt.run(interpreter), Node::ConditionalOp(ref op) => op.run(interpreter), Node::Switch(ref switch) => switch.run(interpreter), diff --git a/boa/src/syntax/parser/expression/primary/object_initializer/tests.rs b/boa/src/syntax/parser/expression/primary/object_initializer/tests.rs index 2a58d44cac..60292bc920 100644 --- a/boa/src/syntax/parser/expression/primary/object_initializer/tests.rs +++ b/boa/src/syntax/parser/expression/primary/object_initializer/tests.rs @@ -23,9 +23,11 @@ fn check_object_literal() { b: false, }; ", - vec![ - ConstDeclList::from(vec![ConstDecl::new("x", Object::from(object_properties))]).into(), - ], + vec![ConstDeclList::from(vec![ConstDecl::new( + "x", + Some(Object::from(object_properties)), + )]) + .into()], ); } @@ -47,9 +49,11 @@ fn check_object_short_function() { b() {}, }; ", - vec![ - ConstDeclList::from(vec![ConstDecl::new("x", Object::from(object_properties))]).into(), - ], + vec![ConstDeclList::from(vec![ConstDecl::new( + "x", + Some(Object::from(object_properties)), + )]) + .into()], ); } @@ -75,9 +79,11 @@ fn check_object_short_function_arguments() { b(test) {} }; ", - vec![ - ConstDeclList::from(vec![ConstDecl::new("x", Object::from(object_properties))]).into(), - ], + vec![ConstDeclList::from(vec![ConstDecl::new( + "x", + Some(Object::from(object_properties)), + )]) + .into()], ); } @@ -98,9 +104,11 @@ fn check_object_getter() { get b() {} }; ", - vec![ - ConstDeclList::from(vec![ConstDecl::new("x", Object::from(object_properties))]).into(), - ], + vec![ConstDeclList::from(vec![ConstDecl::new( + "x", + Some(Object::from(object_properties)), + )]) + .into()], ); } @@ -125,8 +133,10 @@ fn check_object_setter() { set b(test) {} }; ", - vec![ - ConstDeclList::from(vec![ConstDecl::new("x", Object::from(object_properties))]).into(), - ], + vec![ConstDeclList::from(vec![ConstDecl::new( + "x", + Some(Object::from(object_properties)), + )]) + .into()], ); } diff --git a/boa/src/syntax/parser/statement/declaration/lexical.rs b/boa/src/syntax/parser/statement/declaration/lexical.rs index e816f5eb48..20ad78c98b 100644 --- a/boa/src/syntax/parser/statement/declaration/lexical.rs +++ b/boa/src/syntax/parser/statement/declaration/lexical.rs @@ -37,11 +37,17 @@ pub(super) struct LexicalDeclaration { allow_in: AllowIn, allow_yield: AllowYield, allow_await: AllowAwait, + const_init_required: bool, } impl LexicalDeclaration { /// Creates a new `LexicalDeclaration` parser. - pub(super) fn new(allow_in: I, allow_yield: Y, allow_await: A) -> Self + pub(super) fn new( + allow_in: I, + allow_yield: Y, + allow_await: A, + const_init_required: bool, + ) -> Self where I: Into, Y: Into, @@ -51,6 +57,7 @@ impl LexicalDeclaration { allow_in: allow_in.into(), allow_yield: allow_yield.into(), allow_await: allow_await.into(), + const_init_required, } } } @@ -66,14 +73,22 @@ where let tok = cursor.next()?.ok_or(ParseError::AbruptEnd)?; match tok.kind() { - TokenKind::Keyword(Keyword::Const) => { - BindingList::new(self.allow_in, self.allow_yield, self.allow_await, true) - .parse(cursor) - } - TokenKind::Keyword(Keyword::Let) => { - BindingList::new(self.allow_in, self.allow_yield, self.allow_await, false) - .parse(cursor) - } + TokenKind::Keyword(Keyword::Const) => BindingList::new( + self.allow_in, + self.allow_yield, + self.allow_await, + true, + self.const_init_required, + ) + .parse(cursor), + TokenKind::Keyword(Keyword::Let) => BindingList::new( + self.allow_in, + self.allow_yield, + self.allow_await, + false, + self.const_init_required, + ) + .parse(cursor), _ => unreachable!("unknown token found: {:?}", tok), } } @@ -94,11 +109,18 @@ struct BindingList { allow_yield: AllowYield, allow_await: AllowAwait, is_const: bool, + const_init_required: bool, } impl BindingList { /// Creates a new `BindingList` parser. - fn new(allow_in: I, allow_yield: Y, allow_await: A, is_const: bool) -> Self + fn new( + allow_in: I, + allow_yield: Y, + allow_await: A, + is_const: bool, + const_init_required: bool, + ) -> Self where I: Into, Y: Into, @@ -109,6 +131,7 @@ impl BindingList { allow_yield: allow_yield.into(), allow_await: allow_await.into(), is_const, + const_init_required, } } } @@ -133,14 +156,18 @@ where .parse(cursor)?; if self.is_const { - if let Some(init) = init { - const_decls.push(ConstDecl::new(ident, init)); + if self.const_init_required { + if init.is_some() { + const_decls.push(ConstDecl::new(ident, init)); + } else { + return Err(ParseError::expected( + vec![TokenKind::Punctuator(Punctuator::Assign)], + cursor.next()?.ok_or(ParseError::AbruptEnd)?, + "const declaration", + )); + } } else { - return Err(ParseError::expected( - vec![TokenKind::Punctuator(Punctuator::Assign)], - cursor.next()?.ok_or(ParseError::AbruptEnd)?, - "const declaration", - )); + const_decls.push(ConstDecl::new(ident, init)) } } else { let_decls.push(LetDecl::new(ident, init)); @@ -148,6 +175,12 @@ where match cursor.peek_semicolon()? { SemicolonResult::Found(_) => break, + SemicolonResult::NotFound(tk) + if tk.kind() == &TokenKind::Keyword(Keyword::Of) + || tk.kind() == &TokenKind::Keyword(Keyword::In) => + { + break + } SemicolonResult::NotFound(tk) if tk.kind() == &TokenKind::Punctuator(Punctuator::Comma) => { diff --git a/boa/src/syntax/parser/statement/declaration/mod.rs b/boa/src/syntax/parser/statement/declaration/mod.rs index cba21cdc02..d1c1c9dd98 100644 --- a/boa/src/syntax/parser/statement/declaration/mod.rs +++ b/boa/src/syntax/parser/statement/declaration/mod.rs @@ -35,10 +35,11 @@ use std::io::Read; pub(super) struct Declaration { allow_yield: AllowYield, allow_await: AllowAwait, + const_init_required: bool, } impl Declaration { - pub(super) fn new(allow_yield: Y, allow_await: A) -> Self + pub(super) fn new(allow_yield: Y, allow_await: A, const_init_required: bool) -> Self where Y: Into, A: Into, @@ -46,6 +47,7 @@ impl Declaration { Self { allow_yield: allow_yield.into(), allow_await: allow_await.into(), + const_init_required, } } } @@ -65,7 +67,13 @@ where HoistableDeclaration::new(self.allow_yield, self.allow_await, false).parse(cursor) } TokenKind::Keyword(Keyword::Const) | TokenKind::Keyword(Keyword::Let) => { - LexicalDeclaration::new(true, self.allow_yield, self.allow_await).parse(cursor) + LexicalDeclaration::new( + true, + self.allow_yield, + self.allow_await, + self.const_init_required, + ) + .parse(cursor) } _ => unreachable!("unknown token found: {:?}", tok), } diff --git a/boa/src/syntax/parser/statement/declaration/tests.rs b/boa/src/syntax/parser/statement/declaration/tests.rs index fbc2f64685..5ed9bd2d14 100644 --- a/boa/src/syntax/parser/statement/declaration/tests.rs +++ b/boa/src/syntax/parser/statement/declaration/tests.rs @@ -124,7 +124,7 @@ fn multiple_let_declaration() { fn const_declaration() { check_parser( "const a = 5;", - vec![ConstDeclList::from(ConstDecl::new("a", Const::from(5))).into()], + vec![ConstDeclList::from(ConstDecl::new("a", Some(Const::from(5)))).into()], ); } @@ -133,12 +133,12 @@ fn const_declaration() { fn const_declaration_keywords() { check_parser( "const yield = 5;", - vec![ConstDeclList::from(ConstDecl::new("yield", Const::from(5))).into()], + vec![ConstDeclList::from(ConstDecl::new("yield", Some(Const::from(5)))).into()], ); check_parser( "const await = 5;", - vec![ConstDeclList::from(ConstDecl::new("await", Const::from(5))).into()], + vec![ConstDeclList::from(ConstDecl::new("await", Some(Const::from(5)))).into()], ); } @@ -147,7 +147,7 @@ fn const_declaration_keywords() { fn const_declaration_no_spaces() { check_parser( "const a=5;", - vec![ConstDeclList::from(ConstDecl::new("a", Const::from(5))).into()], + vec![ConstDeclList::from(ConstDecl::new("a", Some(Const::from(5)))).into()], ); } @@ -163,8 +163,8 @@ fn multiple_const_declaration() { check_parser( "const a = 5, c = 6;", vec![ConstDeclList::from(vec![ - ConstDecl::new("a", Const::from(5)), - ConstDecl::new("c", Const::from(6)), + ConstDecl::new("a", Some(Const::from(5))), + ConstDecl::new("c", Some(Const::from(6))), ]) .into()], ); diff --git a/boa/src/syntax/parser/statement/iteration/for_statement.rs b/boa/src/syntax/parser/statement/iteration/for_statement.rs index 8e53392fb7..0c388c8853 100644 --- a/boa/src/syntax/parser/statement/iteration/for_statement.rs +++ b/boa/src/syntax/parser/statement/iteration/for_statement.rs @@ -11,7 +11,7 @@ use crate::syntax::lexer::TokenKind; use crate::{ syntax::{ ast::{ - node::{ForLoop, Node}, + node::{ForLoop, ForOfLoop, Node}, Const, Keyword, Punctuator, }, parser::{ @@ -65,7 +65,7 @@ impl TokenParser for ForStatement where R: Read, { - type Output = ForLoop; + type Output = Node; fn parse(self, cursor: &mut Cursor) -> Result { let _timer = BoaProfiler::global().start_event("ForStatement", "Parsing"); @@ -82,7 +82,7 @@ where ) } TokenKind::Keyword(Keyword::Let) | TokenKind::Keyword(Keyword::Const) => { - Some(Declaration::new(self.allow_yield, self.allow_await).parse(cursor)?) + Some(Declaration::new(self.allow_yield, self.allow_await, false).parse(cursor)?) } TokenKind::Punctuator(Punctuator::Semicolon) => None, _ => Some(Expression::new(true, self.allow_yield, self.allow_await).parse(cursor)?), @@ -93,8 +93,14 @@ where Some(tok) if tok.kind() == &TokenKind::Keyword(Keyword::In) => { unimplemented!("for...in statement") } - Some(tok) if tok.kind() == &TokenKind::identifier("of") => { - unimplemented!("for...of statement") + Some(tok) if tok.kind() == &TokenKind::Keyword(Keyword::Of) && init.is_some() => { + let _ = cursor.next(); + let iterable = + Expression::new(true, self.allow_yield, self.allow_await).parse(cursor)?; + cursor.expect(Punctuator::CloseParen, "for of statement")?; + let body = Statement::new(self.allow_yield, self.allow_await, self.allow_return) + .parse(cursor)?; + return Ok(ForOfLoop::new(init.unwrap(), iterable, body).into()); } _ => {} } @@ -124,6 +130,6 @@ where Statement::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor)?; // TODO: do not encapsulate the `for` in a block just to have an inner scope. - Ok(ForLoop::new(init, cond, step, body)) + Ok(ForLoop::new(init, cond, step, body).into()) } } diff --git a/boa/src/syntax/parser/statement/mod.rs b/boa/src/syntax/parser/statement/mod.rs index 08b419f4d1..35b0d7c66f 100644 --- a/boa/src/syntax/parser/statement/mod.rs +++ b/boa/src/syntax/parser/statement/mod.rs @@ -38,9 +38,11 @@ use self::{ use super::{AllowAwait, AllowReturn, AllowYield, Cursor, ParseError, TokenParser}; -use crate::syntax::lexer::{InputElement, TokenKind}; use crate::{ - syntax::ast::{node, Keyword, Node, Punctuator}, + syntax::{ + ast::{node, Keyword, Node, Punctuator}, + lexer::{InputElement, TokenKind}, + }, BoaProfiler, }; use labelled_stm::LabelledStatement; @@ -373,7 +375,7 @@ where TokenKind::Keyword(Keyword::Function) | TokenKind::Keyword(Keyword::Const) | TokenKind::Keyword(Keyword::Let) => { - Declaration::new(self.allow_yield, self.allow_await).parse(cursor) + Declaration::new(self.allow_yield, self.allow_await, true).parse(cursor) } _ => { Statement::new(self.allow_yield, self.allow_await, self.allow_return).parse(cursor) diff --git a/boa/src/value/mod.rs b/boa/src/value/mod.rs index cb3d53e362..992ada7393 100644 --- a/boa/src/value/mod.rs +++ b/boa/src/value/mod.rs @@ -303,6 +303,14 @@ impl Value { } } + #[inline] + pub fn as_gc_object(&self) -> Option { + match self { + Self::Object(o) => Some(o.clone()), + _ => None, + } + } + #[inline] pub fn as_object_mut(&self) -> Option> { match *self { @@ -317,6 +325,13 @@ impl Value { matches!(self, Self::Symbol(_)) } + pub fn as_symbol(&self) -> Option { + match self { + Self::Symbol(symbol) => Some(symbol.clone()), + _ => None, + } + } + /// Returns true if the value is a function #[inline] pub fn is_function(&self) -> bool { @@ -398,6 +413,14 @@ impl Value { matches!(self, Self::Boolean(_)) } + #[inline] + pub fn as_boolean(&self) -> Option { + match self { + Self::Boolean(boolean) => Some(*boolean), + _ => None, + } + } + /// Returns true if the value is a bigint. #[inline] pub fn is_bigint(&self) -> bool { @@ -916,7 +939,7 @@ impl Value { /// [table]: https://tc39.es/ecma262/#table-14 /// [spec]: https://tc39.es/ecma262/#sec-requireobjectcoercible #[inline] - pub fn require_object_coercible<'a>(&'a self, ctx: &mut Context) -> Result<&'a Value> { + pub fn require_object_coercible(&self, ctx: &mut Context) -> Result<&Value> { if self.is_null_or_undefined() { Err(ctx.construct_type_error("cannot convert null or undefined to Object")) } else {