diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 2c4382bd765..71ab8e7fa2e 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -20,7 +20,7 @@ use crate::{ value::JsValue, Context, JsResult, }; -use boa_gc::{Finalize, Gc, Trace}; +use boa_gc::{Cell as GcCell, Finalize, Gc, Trace}; use boa_profiler::Profiler; use std::{cell::Cell, rc::Rc}; use tap::{Conv, Pipe}; @@ -218,6 +218,7 @@ impl BuiltIn for Promise { ) .name(Self::NAME) .length(Self::LENGTH) + .static_method(Self::all, "all", 1) .static_method(Self::race, "race", 1) .static_method(Self::reject, "reject", 1) .static_method(Self::resolve, "resolve", 1) @@ -316,6 +317,247 @@ impl Promise { promise.conv::().pipe(Ok) } + /// `Promise.all ( iterable )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.all + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all + pub(crate) fn all( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let C be the this value. + let c = this; + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(c, context)?; + + // Note: We already checked that `C` is a constructor in `NewPromiseCapability(C)`. + let c = c.as_object().expect("must be a constructor"); + + // 3. Let promiseResolve be Completion(GetPromiseResolve(C)). + let promise_resolve = Self::get_promise_resolve(c, context); + + // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). + if_abrupt_reject_promise!(promise_resolve, promise_capability, context); + + // 5. Let iteratorRecord be Completion(GetIterator(iterable)). + let iterator_record = args.get_or_undefined(0).get_iterator(context, None, None); + + // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability). + if_abrupt_reject_promise!(iterator_record, promise_capability, context); + let mut iterator_record = iterator_record; + + // 7. Let result be Completion(PerformPromiseAll(iteratorRecord, C, promiseCapability, promiseResolve)). + let mut result = Self::perform_promise_all( + &mut iterator_record, + c, + &promise_capability, + &promise_resolve, + context, + ) + .map(JsValue::from); + + // 8. If result is an abrupt completion, then + if result.is_err() { + // a. If iteratorRecord.[[Done]] is false, set result to Completion(IteratorClose(iteratorRecord, result)). + if !iterator_record.done() { + result = iterator_record.close(result, context); + } + + // b. IfAbruptRejectPromise(result, promiseCapability). + if_abrupt_reject_promise!(result, promise_capability, context); + + return Ok(result); + } + + // 9. Return ? result. + result + } + + /// `PerformPromiseAll ( iteratorRecord, constructor, resultCapability, promiseResolve )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performpromiseall + fn perform_promise_all( + iterator_record: &mut IteratorRecord, + constructor: &JsObject, + result_capability: &PromiseCapability, + promise_resolve: &JsObject, + context: &mut Context, + ) -> JsResult { + #[derive(Debug, Trace, Finalize)] + struct ResolveElementCaptures { + #[unsafe_ignore_trace] + already_called: Rc>, + index: usize, + values: GcCell>, + capability_resolve: JsFunction, + #[unsafe_ignore_trace] + remaining_elements_count: Rc>, + } + + // 1. Let values be a new empty List. + let values = GcCell::new(Vec::new()); + + // 2. Let remainingElementsCount be the Record { [[Value]]: 1 }. + let remaining_elements_count = Rc::new(Cell::new(1)); + + // 3. Let index be 0. + let mut index = 0; + + // 4. Repeat, + loop { + // a. Let next be Completion(IteratorStep(iteratorRecord)). + let next = iterator_record.step(context); + + let next_value = match next { + Err(e) => { + // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // c. ReturnIfAbrupt(next). + return Err(e); + } + // d. If next is false, then + Ok(None) => { + // i. Set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + remaining_elements_count.set(remaining_elements_count.get() - 1); + + // iii. If remainingElementsCount.[[Value]] is 0, then + if remaining_elements_count.get() == 0 { + // 1. Let valuesArray be CreateArrayFromList(values). + let values_array = crate::builtins::Array::create_array_from_list( + values.into_inner(), + context, + ); + + // 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »). + result_capability.resolve.call( + &JsValue::undefined(), + &[values_array.into()], + context, + )?; + } + + // iv. Return resultCapability.[[Promise]]. + return Ok(result_capability.promise.clone()); + } + Ok(Some(next)) => { + // e. Let nextValue be Completion(IteratorValue(next)). + let next_value = next.value(context); + + match next_value { + Err(e) => { + // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // g. ReturnIfAbrupt(nextValue). + return Err(e); + } + Ok(next_value) => next_value, + } + } + }; + + // h. Append undefined to values. + values.borrow_mut().push(JsValue::Undefined); + + // i. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »). + let next_promise = + promise_resolve.call(&constructor.clone().into(), &[next_value], context)?; + + // j. Let steps be the algorithm steps defined in Promise.all Resolve Element Functions. + // k. Let length be the number of non-optional parameters of the function definition in Promise.all Resolve Element Functions. + // l. Let onFulfilled be CreateBuiltinFunction(steps, length, "", « [[AlreadyCalled]], [[Index]], [[Values]], [[Capability]], [[RemainingElements]] »). + // m. Set onFulfilled.[[AlreadyCalled]] to false. + // n. Set onFulfilled.[[Index]] to index. + // o. Set onFulfilled.[[Values]] to values. + // p. Set onFulfilled.[[Capability]] to resultCapability. + // q. Set onFulfilled.[[RemainingElements]] to remainingElementsCount. + let on_fulfilled = FunctionBuilder::closure_with_captures( + context, + |_, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise.all-resolve-element-functions + + // 1. Let F be the active function object. + // 2. If F.[[AlreadyCalled]] is true, return undefined. + if captures.already_called.get() { + return Ok(JsValue::undefined()); + } + + // 3. Set F.[[AlreadyCalled]] to true. + captures.already_called.set(true); + + // 4. Let index be F.[[Index]]. + // 5. Let values be F.[[Values]]. + // 6. Let promiseCapability be F.[[Capability]]. + // 7. Let remainingElementsCount be F.[[RemainingElements]]. + + // 8. Set values[index] to x. + captures.values.borrow_mut()[captures.index] = args.get_or_undefined(0).clone(); + + // 9. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + captures + .remaining_elements_count + .set(captures.remaining_elements_count.get() - 1); + + // 10. If remainingElementsCount.[[Value]] is 0, then + if captures.remaining_elements_count.get() == 0 { + // a. Let valuesArray be CreateArrayFromList(values). + let values_array = crate::builtins::Array::create_array_from_list( + captures.values.clone().into_inner(), + context, + ); + + // b. Return ? Call(promiseCapability.[[Resolve]], undefined, « valuesArray »). + return captures.capability_resolve.call( + &JsValue::undefined(), + &[values_array.into()], + context, + ); + } + + // 11. Return undefined. + Ok(JsValue::undefined()) + }, + ResolveElementCaptures { + already_called: Rc::new(Cell::new(false)), + index, + values: values.clone(), + capability_resolve: result_capability.resolve.clone(), + remaining_elements_count: remaining_elements_count.clone(), + }, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // r. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] + 1. + remaining_elements_count.set(remaining_elements_count.get() + 1); + + // s. Perform ? Invoke(nextPromise, "then", « onFulfilled, resultCapability.[[Reject]] »). + next_promise.invoke( + "then", + &[on_fulfilled.into(), result_capability.reject.clone().into()], + context, + )?; + + // t. Set index to index + 1. + index += 1; + } + } + /// `CreateResolvingFunctions ( promise )` /// /// More information: @@ -670,7 +912,7 @@ impl Promise { &mut iterator_record, c, &promise_capability, - &promise_resolve, + &promise_resolve.into(), context, ); @@ -1185,16 +1427,16 @@ impl Promise { fn get_promise_resolve( promise_constructor: &JsObject, context: &mut Context, - ) -> JsResult { + ) -> JsResult { // 1. Let promiseResolve be ? Get(promiseConstructor, "resolve"). let promise_resolve = promise_constructor.get("resolve", context)?; // 2. If IsCallable(promiseResolve) is false, throw a TypeError exception. - if !promise_resolve.is_callable() { - return context.throw_type_error("retrieving a non-callable promise resolver"); + if let Some(promise_resolve) = promise_resolve.as_callable() { + // 3. Return promiseResolve. + Ok(promise_resolve.clone()) + } else { + context.throw_type_error("retrieving a non-callable promise resolver") } - - // 3. Return promiseResolve. - Ok(promise_resolve) } }