From 9eb6a78abc86fcfd6d57d8cec906e771d87b489a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nikodem=20Rabuli=C5=84ski?= Date: Sat, 12 Mar 2022 19:33:16 +0000 Subject: [PATCH] Implement Array.from (#1831) This Pull Request fixes/closes #1784. There're still a few tests failing, notably: - `iter-set-elem-prop-non-writable` - we don't have generator functions implemented - `calling-from-valid-1-noStrict`, `iter-map-fn-this-non-strict` - `thisArg` in non-strict mode, when undefined, should be inherited (that's what I'm guessing, I haven't confirmed this, but strict counterparts do pass with `thisArg` being `undefined`) - `source-array-boundary`, `elements-deleted-after` - ~~Not sure yet, still investigating, but they also include thisArg, so perhaps function calling has an underlying issue?~~ Failing because `this` on the top level evaluates to an empty object instead of containing everything from the top scope Co-authored-by: HalidOdat --- boa_engine/src/builtins/array/mod.rs | 164 ++++++++++++++++++++++++ boa_engine/src/builtins/iterable/mod.rs | 20 +++ 2 files changed, 184 insertions(+) diff --git a/boa_engine/src/builtins/array/mod.rs b/boa_engine/src/builtins/array/mod.rs index 58e2301b87e..bb8a9547c9f 100644 --- a/boa_engine/src/builtins/array/mod.rs +++ b/boa_engine/src/builtins/array/mod.rs @@ -19,6 +19,7 @@ use tap::{Conv, Pipe}; use super::JsArgs; use crate::{ builtins::array::array_iterator::ArrayIterator, + builtins::iterable::{if_abrupt_close_iterator, IteratorHint}, builtins::BuiltIn, builtins::Number, context::intrinsics::StandardConstructors, @@ -113,6 +114,7 @@ impl BuiltIn for Array { .method(Self::entries, "entries", 0) .method(Self::copy_within, "copyWithin", 3) // Static Methods + .static_method(Self::from, "from", 1) .static_method(Self::is_array, "isArray", 1) .static_method(Self::of, "of", 0) .build() @@ -368,6 +370,168 @@ impl Array { } } + /// `Array.from(arrayLike)` + /// + /// The Array.from() static method creates a new, + /// shallow-copied Array instance from an array-like or iterable object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.from + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from + pub(crate) fn from( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let items = args.get_or_undefined(0); + let mapfn = args.get_or_undefined(1); + let this_arg = args.get_or_undefined(2); + + // 2. If mapfn is undefined, let mapping be false + // 3. Else, + // a. If IsCallable(mapfn) is false, throw a TypeError exception. + // b. Let mapping be true. + let mapping = match mapfn { + JsValue::Undefined => None, + JsValue::Object(o) if o.is_callable() => Some(o), + _ => return context.throw_type_error(format!("{} is not a function", mapfn.type_of())), + }; + + // 4. Let usingIterator be ? GetMethod(items, @@iterator). + let using_iterator = items + .get_method(WellKnownSymbols::iterator(), context)? + .map(JsValue::from); + + if let Some(using_iterator) = using_iterator { + // 5. If usingIterator is not undefined, then + + // a. If IsConstructor(C) is true, then + // i. Let A be ? Construct(C). + // b. Else, + // i. Let A be ? ArrayCreate(0en). + let a = match this.as_constructor() { + Some(constructor) => constructor + .construct(&[], this, context)? + .as_object() + .cloned() + .ok_or_else(|| { + context.construct_type_error("Object constructor didn't return an object") + })?, + _ => Self::array_create(0, None, context)?, + }; + + // c. Let iteratorRecord be ? GetIterator(items, sync, usingIterator). + let iterator_record = + items.get_iterator(context, Some(IteratorHint::Sync), Some(using_iterator))?; + + // d. Let k be 0. + // e. Repeat, + // i. If k ≥ 2^53 - 1 (MAX_SAFE_INTEGER), then + // ... + // x. Set k to k + 1. + for k in 0..9_007_199_254_740_991_u64 { + // iii. Let next be ? IteratorStep(iteratorRecord). + let next = iterator_record.step(context)?; + + // iv. If next is false, then + let next = if let Some(next) = next { + next + } else { + // 1. Perform ? Set(A, "length", 𝔽(k), true). + a.set("length", k, true, context)?; + + // 2. Return A. + return Ok(a.into()); + }; + + // v. Let nextValue be ? IteratorValue(next). + let next_value = next.value(context)?; + + // vi. If mapping is true, then + let mapped_value = if let Some(mapfn) = mapping { + // 1. Let mappedValue be Call(mapfn, thisArg, « nextValue, 𝔽(k) »). + let mapped_value = mapfn.call(this_arg, &[next_value, k.into()], context); + + // 2. IfAbruptCloseIterator(mappedValue, iteratorRecord). + if_abrupt_close_iterator!(mapped_value, iterator_record, context) + } else { + // vii. Else, let mappedValue be nextValue. + next_value + }; + + // viii. Let defineStatus be CreateDataPropertyOrThrow(A, Pk, mappedValue). + let define_status = a.create_data_property_or_throw(k, mapped_value, context); + + // ix. IfAbruptCloseIterator(defineStatus, iteratorRecord). + if_abrupt_close_iterator!(define_status, iterator_record, context); + } + + // NOTE: The loop above has to return before it reaches iteration limit, + // which is why it's safe to have this as the fallback return + // + // 1. Let error be ThrowCompletion(a newly created TypeError object). + let error = context.throw_type_error("Invalid array length"); + + // 2. Return ? IteratorClose(iteratorRecord, error). + iterator_record.close(error, context) + } else { + // 6. NOTE: items is not an Iterable so assume it is an array-like object. + // 7. Let arrayLike be ! ToObject(items). + let array_like = items + .to_object(context) + .expect("should not fail according to spec"); + + // 8. Let len be ? LengthOfArrayLike(arrayLike). + let len = array_like.length_of_array_like(context)?; + + // 9. If IsConstructor(C) is true, then + // a. Let A be ? Construct(C, « 𝔽(len) »). + // 10. Else, + // a. Let A be ? ArrayCreate(len). + let a = match this.as_constructor() { + Some(constructor) => constructor + .construct(&[len.into()], this, context)? + .as_object() + .cloned() + .ok_or_else(|| { + context.construct_type_error("Object constructor didn't return an object") + })?, + _ => Self::array_create(len, None, context)?, + }; + + // 11. Let k be 0. + // 12. Repeat, while k < len, + // ... + // f. Set k to k + 1. + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ? Get(arrayLike, Pk). + let k_value = array_like.get(k, context)?; + + let mapped_value = if let Some(mapfn) = mapping { + // c. If mapping is true, then + // i. Let mappedValue be ? Call(mapfn, thisArg, « kValue, 𝔽(k) »). + mapfn.call(this_arg, &[k_value, k.into()], context)? + } else { + // d. Else, let mappedValue be kValue. + k_value + }; + + // e. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue). + a.create_data_property_or_throw(k, mapped_value, context)?; + } + + // 13. Perform ? Set(A, "length", 𝔽(len), true). + a.set("length", len, true, context)?; + + // 14. Return A. + Ok(a.into()) + } + } + /// `Array.isArray( arg )` /// /// The isArray function takes one argument arg, and returns the Boolean value true diff --git a/boa_engine/src/builtins/iterable/mod.rs b/boa_engine/src/builtins/iterable/mod.rs index a5c773c6c70..08eebd96ea0 100644 --- a/boa_engine/src/builtins/iterable/mod.rs +++ b/boa_engine/src/builtins/iterable/mod.rs @@ -411,3 +411,23 @@ pub(crate) fn iterable_to_list( // 6. Return values. Ok(values) } + +/// A shorthand for a sequence of algorithm steps that use an Iterator Record +/// +/// More information: +/// - [ECMA reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ifabruptcloseiterator +macro_rules! if_abrupt_close_iterator { + ($value:expr, $iterator_record:expr, $context:expr) => { + match $value { + // 1. If value is an abrupt completion, return ? IteratorClose(iteratorRecord, value). + Err(err) => return $iterator_record.close(Err(err), $context), + // 2. Else if value is a Completion Record, set value to value. + Ok(value) => value, + } + }; +} + +// Export macro to crate level +pub(crate) use if_abrupt_close_iterator;