Skip to content

Commit

Permalink
Implement Promise.all
Browse files Browse the repository at this point in the history
  • Loading branch information
raskad committed Jun 25, 2022
1 parent d355d3c commit 63a44e0
Showing 1 changed file with 250 additions and 8 deletions.
258 changes: 250 additions & 8 deletions boa_engine/src/builtins/promise/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -316,6 +317,247 @@ impl Promise {
promise.conv::<JsValue>().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<JsValue> {
// 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<JsObject> {
#[derive(Debug, Trace, Finalize)]
struct ResolveElementCaptures {
#[unsafe_ignore_trace]
already_called: Rc<Cell<bool>>,
index: usize,
values: GcCell<Vec<JsValue>>,
capability_resolve: JsFunction,
#[unsafe_ignore_trace]
remaining_elements_count: Rc<Cell<i32>>,
}

// 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:
Expand Down Expand Up @@ -670,7 +912,7 @@ impl Promise {
&mut iterator_record,
c,
&promise_capability,
&promise_resolve,
&promise_resolve.into(),
context,
);

Expand Down Expand Up @@ -1185,16 +1427,16 @@ impl Promise {
fn get_promise_resolve(
promise_constructor: &JsObject,
context: &mut Context,
) -> JsResult<JsValue> {
) -> JsResult<JsObject> {
// 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)
}
}

0 comments on commit 63a44e0

Please sign in to comment.