Skip to content

Commit

Permalink
Implement Array.from (#1831)
Browse files Browse the repository at this point in the history
<!---
Thank you for contributing to Boa! Please fill out the template below, and remove or add any
information as you feel neccesary.
--->

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 <halidodat@gmail.com>
  • Loading branch information
nrabulinski and HalidOdat committed Mar 12, 2022
1 parent 128f836 commit 9eb6a78
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 0 deletions.
164 changes: 164 additions & 0 deletions boa_engine/src/builtins/array/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<JsValue> {
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
Expand Down
20 changes: 20 additions & 0 deletions boa_engine/src/builtins/iterable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

0 comments on commit 9eb6a78

Please sign in to comment.