From bf47815a49c71d4251bd6f8297efa8bbff03681a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Wed, 29 Mar 2023 18:29:47 +0000 Subject: [PATCH] Implement `JsPromise` wrapper (#2758) This Pull Request closes #2687. It changes the following: - Adds a `JsPromise` wrapper to manipulate promises from Rust. - Cleans up the `promise` module to ease the integration of `JsPromise` with it. cc @lastmjs --- .../src/builtins/async_generator/mod.rs | 27 +- .../iterable/async_from_sync_iterator.rs | 27 +- boa_engine/src/builtins/promise/mod.rs | 1382 +++++++++-------- boa_engine/src/lib.rs | 1 + boa_engine/src/native_function.rs | 19 +- boa_engine/src/object/builtins/jspromise.rs | 885 +++++++++++ boa_engine/src/object/builtins/mod.rs | 8 +- boa_engine/src/object/mod.rs | 1 - boa_engine/src/vm/code_block.rs | 7 +- boa_engine/src/vm/opcode/await_stm/mod.rs | 12 +- 10 files changed, 1631 insertions(+), 738 deletions(-) create mode 100644 boa_engine/src/object/builtins/jspromise.rs diff --git a/boa_engine/src/builtins/async_generator/mod.rs b/boa_engine/src/builtins/async_generator/mod.rs index fc29113c3c..0fbeb5cdba 100644 --- a/boa_engine/src/builtins/async_generator/mod.rs +++ b/boa_engine/src/builtins/async_generator/mod.rs @@ -114,12 +114,7 @@ impl AsyncGenerator { // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). let promise_capability = PromiseCapability::new( - &context - .intrinsics() - .constructors() - .promise() - .constructor() - .into(), + &context.intrinsics().constructors().promise().constructor(), context, ) .expect("cannot fail with promise constructor"); @@ -207,12 +202,7 @@ impl AsyncGenerator { // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). let promise_capability = PromiseCapability::new( - &context - .intrinsics() - .constructors() - .promise() - .constructor() - .into(), + &context.intrinsics().constructors().promise().constructor(), context, ) .expect("cannot fail with promise constructor"); @@ -295,12 +285,7 @@ impl AsyncGenerator { // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). let promise_capability = PromiseCapability::new( - &context - .intrinsics() - .constructors() - .promise() - .constructor() - .into(), + &context.intrinsics().constructors().promise().constructor(), context, ) .expect("cannot fail with promise constructor"); @@ -557,7 +542,7 @@ impl AsyncGenerator { // 6. Let promise be ? PromiseResolve(%Promise%, completion.[[Value]]). let promise_completion = Promise::promise_resolve( - context.intrinsics().constructors().promise().constructor(), + &context.intrinsics().constructors().promise().constructor(), value, context, ); @@ -653,8 +638,8 @@ impl AsyncGenerator { // 11. Perform PerformPromiseThen(promise, onFulfilled, onRejected). Promise::perform_promise_then( &promise, - &on_fulfilled.into(), - &on_rejected.into(), + Some(on_fulfilled), + Some(on_rejected), None, context, ); diff --git a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs index 5b1f67cee9..dd0202ce01 100644 --- a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs +++ b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs @@ -100,12 +100,7 @@ impl AsyncFromSyncIterator { // 3. Let promiseCapability be ! NewPromiseCapability(%Promise%). let promise_capability = PromiseCapability::new( - &context - .intrinsics() - .constructors() - .promise() - .constructor() - .into(), + &context.intrinsics().constructors().promise().constructor(), context, ) .expect("cannot fail with promise constructor"); @@ -145,12 +140,7 @@ impl AsyncFromSyncIterator { // 3. Let promiseCapability be ! NewPromiseCapability(%Promise%). let promise_capability = PromiseCapability::new( - &context - .intrinsics() - .constructors() - .promise() - .constructor() - .into(), + &context.intrinsics().constructors().promise().constructor(), context, ) .expect("cannot fail with promise constructor"); @@ -243,12 +233,7 @@ impl AsyncFromSyncIterator { // 3. Let promiseCapability be ! NewPromiseCapability(%Promise%). let promise_capability = PromiseCapability::new( - &context - .intrinsics() - .constructors() - .promise() - .constructor() - .into(), + &context.intrinsics().constructors().promise().constructor(), context, ) .expect("cannot fail with promise constructor"); @@ -348,7 +333,7 @@ impl AsyncFromSyncIterator { // 6. Let valueWrapper be Completion(PromiseResolve(%Promise%, value)). let value_wrapper = Promise::promise_resolve( - context.intrinsics().constructors().promise().constructor(), + &context.intrinsics().constructors().promise().constructor(), value, context, ); @@ -381,8 +366,8 @@ impl AsyncFromSyncIterator { // 11. Perform PerformPromiseThen(valueWrapper, onFulfilled, undefined, promiseCapability). Promise::perform_promise_then( &value_wrapper, - &on_fulfilled.into(), - &JsValue::Undefined, + Some(on_fulfilled), + None, Some(promise_capability.clone()), context, ); diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 549f8d827b..6a1a25cde7 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -5,7 +5,7 @@ mod tests; use super::{iterable::IteratorRecord, BuiltInBuilder, BuiltInConstructor, IntrinsicObject}; use crate::{ - builtins::{error::ErrorKind, Array, BuiltInObject}, + builtins::{Array, BuiltInObject}, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, error::JsNativeError, job::{JobCallback, NativeJob}, @@ -14,7 +14,7 @@ use crate::{ internal_methods::get_prototype_from_constructor, FunctionObjectBuilder, JsFunction, JsObject, ObjectData, CONSTRUCTOR, }, - property::{Attribute, PropertyDescriptorBuilder}, + property::Attribute, string::utf16, symbol::JsSymbol, value::JsValue, @@ -25,6 +25,107 @@ use boa_profiler::Profiler; use std::{cell::Cell, rc::Rc}; use tap::{Conv, Pipe}; +// ==================== Public API ==================== + +/// The current state of a [`Promise`]. +#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)] +pub enum PromiseState { + /// The promise hasn't been resolved. + Pending, + /// The promise was fulfilled with a success value. + Fulfilled(JsValue), + /// The promise was rejected with a failure reason. + Rejected(JsValue), +} + +impl PromiseState { + /// Gets the inner `JsValue` of a fulfilled promise state, or returns `None` if + /// the state is not `Fulfilled`. + pub const fn as_fulfilled(&self) -> Option<&JsValue> { + match self { + PromiseState::Fulfilled(v) => Some(v), + _ => None, + } + } + + /// Gets the inner `JsValue` of a rejected promise state, or returns `None` if + /// the state is not `Rejected`. + pub const fn as_rejected(&self) -> Option<&JsValue> { + match self { + PromiseState::Rejected(v) => Some(v), + _ => None, + } + } +} + +/// The internal representation of a `Promise` object. +#[derive(Debug, Trace, Finalize)] +pub struct Promise { + state: PromiseState, + fulfill_reactions: Vec, + reject_reactions: Vec, + handled: bool, +} + +/// The operation type of the [`HostPromiseRejectionTracker`][fn] abstract operation. +/// +/// # Note +/// +/// Per the spec: +/// +/// > If operation is "handle", an implementation should not hold a reference to promise in a way +/// that would interfere with garbage collection. An implementation may hold a reference to promise +/// if operation is "reject", since it is expected that rejections will be rare and not on hot code paths. +/// +/// [fn]: https://tc39.es/ecma262/#sec-host-promise-rejection-tracker +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OperationType { + /// A promise was rejected without any handlers. + Reject, + /// A handler was added to a rejected promise for the first time. + Handle, +} + +/// Functions used to resolve a pending promise. +/// +/// This is equivalent to the parameters `resolveFunc` and `rejectFunc` of the executor passed to +/// the [`Promise()`] constructor. +/// +/// Both functions are always associated with the promise from which they were created. This +/// means that by simply calling `resolve.call(this, &[values], context)` or +/// `reject.call(this, &[error], context)`, the state of the original promise will be updated with +/// the resolution value. +/// +/// [`Promise()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise +#[derive(Debug, Clone)] +pub struct ResolvingFunctions { + /// The `resolveFunc` parameter of the executor passed to `Promise()`. + pub resolve: JsFunction, + /// The `rejectFunc` parameter of the executor passed to `Promise()`. + pub reject: JsFunction, +} + +/// The internal `PromiseCapability` data type. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-promisecapability-records +#[derive(Debug, Clone, Trace, Finalize)] +// TODO: make crate-only +pub struct PromiseCapability { + /// The `[[Promise]]` field. + promise: JsObject, + + /// The `[[Resolve]]` field. + resolve: JsFunction, + + /// The `[[Reject]]` field. + reject: JsFunction, +} + +// ==================== Private API ==================== + /// `IfAbruptRejectPromise ( value, capability )` /// /// `IfAbruptRejectPromise` is a shorthand for a sequence of algorithm steps that use a `PromiseCapability` Record. @@ -55,22 +156,6 @@ macro_rules! if_abrupt_reject_promise { pub(crate) use if_abrupt_reject_promise; -#[derive(Debug, Clone, Trace, Finalize)] -pub(crate) enum PromiseState { - Pending, - Fulfilled(JsValue), - Rejected(JsValue), -} - -/// The internal representation of a `Promise` object. -#[derive(Debug, Trace, Finalize)] -pub struct Promise { - state: PromiseState, - fulfill_reactions: Vec, - reject_reactions: Vec, - handled: bool, -} - /// The internal `PromiseReaction` data type. /// /// More information: @@ -102,43 +187,6 @@ enum ReactionType { Reject, } -/// The operation type of the [`HostPromiseRejectionTracker`][fn] abstract operation. -/// -/// # Note -/// -/// Per the spec: -/// -/// > If operation is "handle", an implementation should not hold a reference to promise in a way -/// that would interfere with garbage collection. An implementation may hold a reference to promise -/// if operation is "reject", since it is expected that rejections will be rare and not on hot code paths. -/// -/// [fn]: https://tc39.es/ecma262/#sec-host-promise-rejection-tracker -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OperationType { - /// A promise was rejected without any handlers. - Reject, - /// A handler was added to a rejected promise for the first time. - Handle, -} - -/// The internal `PromiseCapability` data type. -/// -/// More information: -/// - [ECMAScript reference][spec] -/// -/// [spec]: https://tc39.es/ecma262/#sec-promisecapability-records -#[derive(Debug, Clone, Trace, Finalize)] -pub struct PromiseCapability { - /// The `[[Promise]]` field. - promise: JsObject, - - /// The `[[Resolve]]` field. - resolve: JsFunction, - - /// The `[[Reject]]` field. - reject: JsFunction, -} - impl PromiseCapability { /// `NewPromiseCapability ( C )` /// @@ -146,7 +194,7 @@ impl PromiseCapability { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-newpromisecapability - pub(crate) fn new(c: &JsValue, context: &mut Context<'_>) -> JsResult { + pub(crate) fn new(c: &JsObject, context: &mut Context<'_>) -> JsResult { #[derive(Debug, Clone, Trace, Finalize)] struct RejectResolve { reject: JsValue, @@ -154,11 +202,11 @@ impl PromiseCapability { } // 1. If IsConstructor(C) is false, throw a TypeError exception. - let c = c.as_constructor().ok_or_else(|| { - JsNativeError::typ().with_message("PromiseCapability: expected constructor") - })?; - - let c = c.clone(); + if !c.is_constructor() { + return Err(JsNativeError::typ() + .with_message("PromiseCapability: expected constructor") + .into()); + } // 2. NOTE: C is assumed to be a constructor function that supports the parameter conventions of the Promise constructor (see 27.2.3.1). // 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }. @@ -372,12 +420,6 @@ impl BuiltInConstructor for Promise { } } -#[derive(Debug)] -pub(crate) struct ResolvingFunctionsRecord { - pub(crate) resolve: JsFunction, - pub(crate) reject: JsFunction, -} - impl Promise { /// Creates a new, pending `Promise`. pub(crate) fn new() -> Self { @@ -408,14 +450,13 @@ impl Promise { context: &mut Context<'_>, ) -> JsResult { // 1. Let C be the this value. - let c = this; + let c = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Promise.all() called on a non-object") + })?; // 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); @@ -462,7 +503,7 @@ impl Promise { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-performpromiseall - fn perform_promise_all( + pub(crate) fn perform_promise_all( iterator_record: &mut IteratorRecord, constructor: &JsObject, result_capability: &PromiseCapability, @@ -652,14 +693,13 @@ impl Promise { context: &mut Context<'_>, ) -> JsResult { // 1. Let C be the this value. - let c = this; + let c = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Promise.allSettled() called on a non-object") + })?; // 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); @@ -706,7 +746,7 @@ impl Promise { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-performpromiseallsettled - fn perform_promise_all_settled( + pub(crate) fn perform_promise_all_settled( iterator_record: &mut IteratorRecord, constructor: &JsObject, result_capability: &PromiseCapability, @@ -999,14 +1039,13 @@ impl Promise { context: &mut Context<'_>, ) -> JsResult { // 1. Let C be the this value. - let c = this; + let c = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Promise.any() called on a non-object") + })?; // 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); @@ -1053,7 +1092,7 @@ impl Promise { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-performpromiseany - fn perform_promise_any( + pub(crate) fn perform_promise_any( iterator_record: &mut IteratorRecord, constructor: &JsObject, result_capability: &PromiseCapability, @@ -1104,33 +1143,19 @@ impl Promise { // iii. If remainingElementsCount.[[Value]] is 0, then if remaining_elements_count.get() == 0 { // 1. Let error be a newly created AggregateError object. - let error = JsObject::from_proto_and_data( - context - .intrinsics() - .constructors() - .aggregate_error() - .prototype(), - ObjectData::error(ErrorKind::Aggregate), - ); - // 2. Perform ! DefinePropertyOrThrow(error, "errors", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: CreateArrayFromList(errors) }). - error - .define_property_or_throw( - utf16!("errors"), - PropertyDescriptorBuilder::new() - .configurable(true) - .enumerable(false) - .writable(true) - .value(Array::create_array_from_list( - errors.borrow().as_slice().iter().cloned(), - context, - )), - context, - ) - .expect("cannot fail per spec"); + let error = JsNativeError::aggregate( + errors + .borrow() + .iter() + .cloned() + .map(JsError::from_opaque) + .collect(), + ) + .with_message("no promise in Promise.any was fulfilled."); // 3. Return ThrowCompletion(error). - return Err(JsError::from_opaque(error.into())); + return Err(error.into()); } // iv. Return resultCapability.[[Promise]]. @@ -1201,34 +1226,22 @@ impl Promise { // 10. If remainingElementsCount.[[Value]] is 0, then if captures.remaining_elements_count.get() == 0 { // a. Let error be a newly created AggregateError object. - let error = JsObject::from_proto_and_data( - context - .intrinsics() - .constructors() - .aggregate_error() - .prototype(), - ObjectData::error(ErrorKind::Aggregate), - ); - // b. Perform ! DefinePropertyOrThrow(error, "errors", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: CreateArrayFromList(errors) }). - error - .define_property_or_throw( - utf16!("errors"), - PropertyDescriptorBuilder::new() - .configurable(true) - .enumerable(false) - .writable(true) - .value(Array::create_array_from_list( - captures.errors.borrow().as_slice().iter().cloned(), - context, - )), - context, - ) - .expect("cannot fail per spec"); + let error = JsNativeError::aggregate( + captures + .errors + .borrow() + .iter() + .cloned() + .map(JsError::from_opaque) + .collect(), + ) + .with_message("no promise in Promise.any was fulfilled."); + // c. Return ? Call(promiseCapability.[[Reject]], undefined, « error »). return captures.capability_reject.call( &JsValue::undefined(), - &[error.into()], + &[error.to_opaque(context).into()], context, ); } @@ -1265,371 +1278,71 @@ impl Promise { } } - /// `CreateResolvingFunctions ( promise )` + /// `Promise.race ( iterable )` + /// + /// The `race` function returns a new promise which is settled in the same way as the first + /// passed promise to settle. It resolves all elements of the passed `iterable` to promises. /// /// More information: /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] /// - /// [spec]: https://tc39.es/ecma262/#sec-createresolvingfunctions - pub(crate) fn create_resolving_functions( - promise: &JsObject, + /// [spec]: https://tc39.es/ecma262/#sec-promise.race + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race + pub(crate) fn race( + this: &JsValue, + args: &[JsValue], context: &mut Context<'_>, - ) -> ResolvingFunctionsRecord { - #[derive(Debug, Trace, Finalize)] - struct RejectResolveCaptures { - promise: JsObject, - #[unsafe_ignore_trace] - already_resolved: Rc>, - } - - // 1. Let alreadyResolved be the Record { [[Value]]: false }. - let already_resolved = Rc::new(Cell::new(false)); - - // 5. Set resolve.[[Promise]] to promise. - // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. - let resolve_captures = RejectResolveCaptures { - already_resolved: already_resolved.clone(), - promise: promise.clone(), - }; - - // 2. Let stepsResolve be the algorithm steps defined in Promise Resolve Functions. - // 3. Let lengthResolve be the number of non-optional parameters of the function definition in Promise Resolve Functions. - // 4. Let resolve be CreateBuiltinFunction(stepsResolve, lengthResolve, "", « [[Promise]], [[AlreadyResolved]] »). - let resolve = FunctionObjectBuilder::new( - context, - NativeFunction::from_copy_closure_with_captures( - |_this, args, captures, context| { - // https://tc39.es/ecma262/#sec-promise-resolve-functions + ) -> JsResult { + let iterable = args.get_or_undefined(0); - // 1. Let F be the active function object. - // 2. Assert: F has a [[Promise]] internal slot whose value is an Object. - // 3. Let promise be F.[[Promise]]. - // 4. Let alreadyResolved be F.[[AlreadyResolved]]. - let RejectResolveCaptures { - promise, - already_resolved, - } = captures; + // 1. Let C be the this value. + let c = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Promise.race() called on a non-object") + })?; - // 5. If alreadyResolved.[[Value]] is true, return undefined. - if already_resolved.get() { - return Ok(JsValue::Undefined); - } + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(c, context)?; - // 6. Set alreadyResolved.[[Value]] to true. - already_resolved.set(true); + // 3. Let promiseResolve be Completion(GetPromiseResolve(C)). + let promise_resolve = Self::get_promise_resolve(c, context); - let resolution = args.get_or_undefined(0); + // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). + if_abrupt_reject_promise!(promise_resolve, promise_capability, context); - // 7. If SameValue(resolution, promise) is true, then - if JsValue::same_value(resolution, &promise.clone().into()) { - // a. Let selfResolutionError be a newly created TypeError object. - let self_resolution_error = JsNativeError::typ() - .with_message("SameValue(resolution, promise) is true") - .to_opaque(context); + // 5. Let iteratorRecord be Completion(GetIterator(iterable)). + let iterator_record = iterable.get_iterator(context, None, None); - // b. Perform RejectPromise(promise, selfResolutionError). - Self::reject_promise(promise, self_resolution_error.into(), context); + // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability). + if_abrupt_reject_promise!(iterator_record, promise_capability, context); + let mut iterator_record = iterator_record; - // c. Return undefined. - return Ok(JsValue::Undefined); - } + // 7. Let result be Completion(PerformPromiseRace(iteratorRecord, C, promiseCapability, promiseResolve)). + let mut result = Self::perform_promise_race( + &mut iterator_record, + c, + &promise_capability, + &promise_resolve, + context, + ) + .map(JsValue::from); - let Some(then) = resolution.as_object() else { - // 8. If Type(resolution) is not Object, then - // a. Perform FulfillPromise(promise, resolution). - Self::fulfill_promise(promise, resolution.clone(), context); + // 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. Return undefined. - return Ok(JsValue::Undefined); - }; + // b. IfAbruptRejectPromise(result, promiseCapability). + if_abrupt_reject_promise!(result, promise_capability, context); - // 9. Let then be Completion(Get(resolution, "then")). - let then_action = match then.get(utf16!("then"), context) { - // 10. If then is an abrupt completion, then - Err(e) => { - // a. Perform RejectPromise(promise, then.[[Value]]). - Self::reject_promise(promise, e.to_opaque(context), context); - - // b. Return undefined. - return Ok(JsValue::Undefined); - } - // 11. Let thenAction be then.[[Value]]. - Ok(then) => then, - }; - - // 12. If IsCallable(thenAction) is false, then - let Some(then_action) = then_action.as_object().cloned().and_then(JsFunction::from_object) else { - // a. Perform FulfillPromise(promise, resolution). - Self::fulfill_promise(promise, resolution.clone(), context); - - // b. Return undefined. - return Ok(JsValue::Undefined); - }; - - // 13. Let thenJobCallback be HostMakeJobCallback(thenAction). - let then_job_callback = context.host_hooks().make_job_callback(then_action, context); - - // 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). - let job = new_promise_resolve_thenable_job( - promise.clone(), - resolution.clone(), - then_job_callback, - ); - - // 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). - context.job_queue().enqueue_promise_job(job, context); - - // 16. Return undefined. - Ok(JsValue::Undefined) - }, - resolve_captures, - ), - ) - .name("") - .length(1) - .constructor(false) - .build(); - - // 10. Set reject.[[Promise]] to promise. - // 11. Set reject.[[AlreadyResolved]] to alreadyResolved. - let reject_captures = RejectResolveCaptures { - promise: promise.clone(), - already_resolved, - }; - - // 7. Let stepsReject be the algorithm steps defined in Promise Reject Functions. - // 8. Let lengthReject be the number of non-optional parameters of the function definition in Promise Reject Functions. - // 9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »). - let reject = FunctionObjectBuilder::new( - context, - NativeFunction::from_copy_closure_with_captures( - |_this, args, captures, context| { - // https://tc39.es/ecma262/#sec-promise-reject-functions - - // 1. Let F be the active function object. - // 2. Assert: F has a [[Promise]] internal slot whose value is an Object. - // 3. Let promise be F.[[Promise]]. - // 4. Let alreadyResolved be F.[[AlreadyResolved]]. - let RejectResolveCaptures { - promise, - already_resolved, - } = captures; - - // 5. If alreadyResolved.[[Value]] is true, return undefined. - if already_resolved.get() { - return Ok(JsValue::Undefined); - } - - // 6. Set alreadyResolved.[[Value]] to true. - already_resolved.set(true); - - // 7. Perform RejectPromise(promise, reason). - Self::reject_promise(promise, args.get_or_undefined(0).clone(), context); - - // 8. Return undefined. - Ok(JsValue::Undefined) - }, - reject_captures, - ), - ) - .name("") - .length(1) - .constructor(false) - .build(); - - // 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }. - ResolvingFunctionsRecord { resolve, reject } - } - - /// `FulfillPromise ( promise, value )` - /// - /// The abstract operation `FulfillPromise` takes arguments `promise` and `value` and returns - /// `unused`. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-fulfillpromise - /// - /// # Panics - /// - /// Panics if `Promise` is not pending. - fn fulfill_promise(promise: &JsObject, value: JsValue, context: &mut Context<'_>) { - let mut promise = promise.borrow_mut(); - let promise = promise - .as_promise_mut() - .expect("IsPromise(promise) is false"); - - // 1. Assert: The value of promise.[[PromiseState]] is pending. - assert!( - matches!(promise.state, PromiseState::Pending), - "promise was not pending" - ); - - // reordering these statements does not affect the semantics - - // 2. Let reactions be promise.[[PromiseFulfillReactions]]. - // 4. Set promise.[[PromiseFulfillReactions]] to undefined. - let reactions = std::mem::take(&mut promise.fulfill_reactions); - - // 5. Set promise.[[PromiseRejectReactions]] to undefined. - promise.reject_reactions.clear(); - - // 7. Perform TriggerPromiseReactions(reactions, value). - Self::trigger_promise_reactions(reactions, &value, context); - - // 3. Set promise.[[PromiseResult]] to value. - // 6. Set promise.[[PromiseState]] to fulfilled. - promise.state = PromiseState::Fulfilled(value); - - // 8. Return unused. - } - - /// `RejectPromise ( promise, reason )` - /// - /// The abstract operation `RejectPromise` takes arguments `promise` and `reason` and returns - /// `unused`. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-rejectpromise - /// - /// # Panics - /// - /// Panics if `Promise` is not pending. - pub fn reject_promise(promise: &JsObject, reason: JsValue, context: &mut Context<'_>) { - let handled = { - let mut promise = promise.borrow_mut(); - let promise = promise - .as_promise_mut() - .expect("IsPromise(promise) is false"); - - // 1. Assert: The value of promise.[[PromiseState]] is pending. - assert!( - matches!(promise.state, PromiseState::Pending), - "Expected promise.[[PromiseState]] to be pending" - ); - - // reordering these statements does not affect the semantics - - // 2. Let reactions be promise.[[PromiseRejectReactions]]. - // 5. Set promise.[[PromiseRejectReactions]] to undefined. - let reactions = std::mem::take(&mut promise.reject_reactions); - - // 4. Set promise.[[PromiseFulfillReactions]] to undefined. - promise.fulfill_reactions.clear(); - - // 8. Perform TriggerPromiseReactions(reactions, reason). - Self::trigger_promise_reactions(reactions, &reason, context); - - // 3. Set promise.[[PromiseResult]] to reason. - // 6. Set promise.[[PromiseState]] to rejected. - promise.state = PromiseState::Rejected(reason); - - promise.handled - }; - - // 7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject"). - if !handled { - context - .host_hooks() - .promise_rejection_tracker(promise, OperationType::Reject, context); - } - - // 9. Return unused. - } - - /// `TriggerPromiseReactions ( reactions, argument )` - /// - /// The abstract operation `TriggerPromiseReactions` takes arguments `reactions` (a `List` of - /// `PromiseReaction` Records) and `argument` and returns unused. It enqueues a new `Job` for - /// each record in `reactions`. Each such `Job` processes the `[[Type]]` and `[[Handler]]` of - /// the `PromiseReaction` Record, and if the `[[Handler]]` is not `empty`, calls it passing the - /// given argument. If the `[[Handler]]` is `empty`, the behaviour is determined by the - /// `[[Type]]`. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-triggerpromisereactions - fn trigger_promise_reactions( - reactions: Vec, - argument: &JsValue, - context: &mut Context<'_>, - ) { - // 1. For each element reaction of reactions, do - for reaction in reactions { - // a. Let job be NewPromiseReactionJob(reaction, argument). - let job = new_promise_reaction_job(reaction, argument.clone()); - - // b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). - context.job_queue().enqueue_promise_job(job, context); - } - - // 2. Return unused. - } - - /// `Promise.race ( iterable )` - /// - /// The `race` function returns a new promise which is settled in the same way as the first - /// passed promise to settle. It resolves all elements of the passed `iterable` to promises. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - [MDN documentation][mdn] - /// - /// [spec]: https://tc39.es/ecma262/#sec-promise.race - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race - pub fn race(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { - let iterable = args.get_or_undefined(0); - - // 1. Let C be the this value. - let c = this; - - // 2. Let promiseCapability be ? NewPromiseCapability(C). - let promise_capability = PromiseCapability::new(c, context)?; - - // 3. Let promiseResolve be Completion(GetPromiseResolve(C)). - let promise_resolve = - Self::get_promise_resolve(c.as_object().expect("this was not an object"), context); - - // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). - if_abrupt_reject_promise!(promise_resolve, promise_capability, context); - - // 5. Let iteratorRecord be Completion(GetIterator(iterable)). - let iterator_record = iterable.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(PerformPromiseRace(iteratorRecord, C, promiseCapability, promiseResolve)). - let mut result = Self::perform_promise_race( - &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); - - Ok(result) - } else { - // 9. Return ? result. - result - } - } + Ok(result) + } else { + // 9. Return ? result. + result + } + } /// `PerformPromiseRace ( iteratorRecord, constructor, resultCapability, promiseResolve )` /// @@ -1642,13 +1355,14 @@ impl Promise { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-performpromiserace - fn perform_promise_race( + pub(crate) fn perform_promise_race( iterator_record: &mut IteratorRecord, - constructor: &JsValue, + constructor: &JsObject, result_capability: &PromiseCapability, promise_resolve: &JsObject, context: &mut Context<'_>, ) -> JsResult { + let constructor = constructor.clone().into(); // 1. Repeat, loop { // a. Let next be Completion(IteratorStep(iteratorRecord)). @@ -1683,7 +1397,7 @@ impl Promise { let next_value = next_value?; // h. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »). - let next_promise = promise_resolve.call(constructor, &[next_value], context)?; + let next_promise = promise_resolve.call(&constructor, &[next_value], context)?; // i. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], resultCapability.[[Reject]] »). next_promise.invoke( @@ -1705,27 +1419,39 @@ impl Promise { /// /// [spec]: https://tc39.es/ecma262/#sec-promise.reject /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject - pub fn reject( + pub(crate) fn reject( this: &JsValue, args: &[JsValue], context: &mut Context<'_>, ) -> JsResult { - let r = args.get_or_undefined(0); + let r = args.get_or_undefined(0).clone(); // 1. Let C be the this value. - let c = this; + let c = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Promise.reject() called on a non-object") + })?; + + Promise::promise_reject(c, &JsError::from_opaque(r), context).map(JsValue::from) + } + + /// Utility function to create a rejected promise. + pub(crate) fn promise_reject( + c: &JsObject, + e: &JsError, + context: &mut Context<'_>, + ) -> JsResult { + let e = e.to_opaque(context); // 2. Let promiseCapability be ? NewPromiseCapability(C). let promise_capability = PromiseCapability::new(c, context)?; // 3. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »). - promise_capability .reject - .call(&JsValue::undefined(), &[r.clone()], context)?; + .call(&JsValue::undefined(), &[e], context)?; // 4. Return promiseCapability.[[Promise]]. - Ok(promise_capability.promise.clone().into()) + Ok(promise_capability.promise.clone()) } /// `Promise.resolve ( x )` @@ -1736,7 +1462,7 @@ impl Promise { /// /// [spec]: https://tc39.es/ecma262/#sec-promise.resolve /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve - pub fn resolve( + pub(crate) fn resolve( this: &JsValue, args: &[JsValue], context: &mut Context<'_>, @@ -1744,17 +1470,53 @@ impl Promise { let x = args.get_or_undefined(0); // 1. Let C be the this value. - let c = this; + // 2. If Type(C) is not Object, throw a TypeError exception. + let c = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Promise.resolve() called on a non-object") + })?; - if let Some(c) = c.as_object() { - // 3. Return ? PromiseResolve(C, x). - Self::promise_resolve(c.clone(), x.clone(), context).map(JsValue::from) - } else { - // 2. If Type(C) is not Object, throw a TypeError exception. - Err(JsNativeError::typ() - .with_message("Promise.resolve() called on a non-object") - .into()) + // 3. Return ? PromiseResolve(C, x). + Self::promise_resolve(c, x.clone(), context).map(JsValue::from) + } + + /// `PromiseResolve ( C, x )` + /// + /// The abstract operation `PromiseResolve` takes arguments `C` (a constructor) and `x` (an + /// ECMAScript language value) and returns either a normal completion containing an ECMAScript + /// language value or a throw completion. It returns a new promise resolved with `x`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise-resolve + pub(crate) fn promise_resolve( + c: &JsObject, + x: JsValue, + context: &mut Context<'_>, + ) -> JsResult { + // 1. If IsPromise(x) is true, then + if let Some(x) = x.as_promise() { + // a. Let xConstructor be ? Get(x, "constructor"). + let x_constructor = x.get(CONSTRUCTOR, context)?; + // b. If SameValue(xConstructor, C) is true, return x. + if x_constructor + .as_object() + .map_or(false, |o| JsObject::equals(o, c)) + { + return Ok(x.clone()); + } } + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(&c.clone(), context)?; + + // 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). + promise_capability + .resolve + .call(&JsValue::Undefined, &[x], context)?; + + // 4. Return promiseCapability.[[Promise]]. + Ok(promise_capability.promise.clone()) } /// `get Promise [ @@species ]` @@ -1781,7 +1543,11 @@ impl Promise { /// /// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.catch /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch - pub fn catch(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + pub(crate) fn catch( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { let on_rejected = args.get_or_undefined(0); // 1. Let promise be the this value. @@ -1802,7 +1568,7 @@ impl Promise { /// /// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.finally /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally - pub fn finally( + pub(crate) fn finally( this: &JsValue, args: &[JsValue], context: &mut Context<'_>, @@ -1825,7 +1591,7 @@ impl Promise { let on_finally = args.get_or_undefined(0); - let Some(on_finally) = on_finally.as_callable() else { + let Some(on_finally) = on_finally.as_object().cloned().and_then(JsFunction::from_object) else { // 5. If IsCallable(onFinally) is false, then // a. Let thenFinally be onFinally. // b. Let catchFinally be onFinally. @@ -1834,124 +1600,126 @@ impl Promise { return then.call(this, &[on_finally.clone(), on_finally.clone()], context); }; - let (then_finally, catch_finally) = { - /// Capture object for the `thenFinallyClosure` abstract closure. - #[derive(Debug, Trace, Finalize)] - struct FinallyCaptures { - on_finally: JsObject, - c: JsObject, - } + let (then_finally, catch_finally) = + Self::then_catch_finally_closures(c, on_finally, context); - // a. Let thenFinallyClosure be a new Abstract Closure with parameters (value) that captures onFinally and C and performs the following steps when called: - let then_finally_closure = FunctionObjectBuilder::new( - context, - NativeFunction::from_copy_closure_with_captures( - |_this, args, captures, context| { - /// Capture object for the abstract `returnValue` closure. - #[derive(Debug, Trace, Finalize)] - struct ReturnValueCaptures { - value: JsValue, - } + // 7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »). + let then = promise.get(utf16!("then"), context)?; + then.call(this, &[then_finally.into(), catch_finally.into()], context) + } - let value = args.get_or_undefined(0); + pub(crate) fn then_catch_finally_closures( + c: JsObject, + on_finally: JsFunction, + context: &mut Context<'_>, + ) -> (JsFunction, JsFunction) { + /// Capture object for the `thenFinallyClosure` abstract closure. + #[derive(Debug, Trace, Finalize)] + struct FinallyCaptures { + on_finally: JsFunction, + c: JsObject, + } - // i. Let result be ? Call(onFinally, undefined). - let result = - captures - .on_finally - .call(&JsValue::undefined(), &[], context)?; + // a. Let thenFinallyClosure be a new Abstract Closure with parameters (value) that captures onFinally and C and performs the following steps when called: + let then_finally_closure = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_this, args, captures, context| { + /// Capture object for the abstract `returnValue` closure. + #[derive(Debug, Trace, Finalize)] + struct ReturnValueCaptures { + value: JsValue, + } - // ii. Let promise be ? PromiseResolve(C, result). - let promise = Self::promise_resolve(captures.c.clone(), result, context)?; + let value = args.get_or_undefined(0); - // iii. Let returnValue be a new Abstract Closure with no parameters that captures value and performs the following steps when called: - let return_value = FunctionObjectBuilder::new( - context, - NativeFunction::from_copy_closure_with_captures( - |_this, _args, captures, _context| { - // 1. Return value. - Ok(captures.value.clone()) - }, - ReturnValueCaptures { - value: value.clone(), - }, - ), - ); + // i. Let result be ? Call(onFinally, undefined). + let result = captures + .on_finally + .call(&JsValue::undefined(), &[], context)?; - // iv. Let valueThunk be CreateBuiltinFunction(returnValue, 0, "", « »). - let value_thunk = return_value.length(0).name("").build(); + // ii. Let promise be ? PromiseResolve(C, result). + let promise = Self::promise_resolve(&captures.c, result, context)?; - // v. Return ? Invoke(promise, "then", « valueThunk »). - promise.invoke(utf16!("then"), &[value_thunk.into()], context) - }, - FinallyCaptures { - on_finally: on_finally.clone(), - c: c.clone(), - }, - ), - ); + // iii. Let returnValue be a new Abstract Closure with no parameters that captures value and performs the following steps when called: + let return_value = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_this, _args, captures, _context| { + // 1. Return value. + Ok(captures.value.clone()) + }, + ReturnValueCaptures { + value: value.clone(), + }, + ), + ); - // b. Let thenFinally be CreateBuiltinFunction(thenFinallyClosure, 1, "", « »). - let then_finally = then_finally_closure.length(1).name("").build(); + // iv. Let valueThunk be CreateBuiltinFunction(returnValue, 0, "", « »). + let value_thunk = return_value.length(0).name("").build(); - // c. Let catchFinallyClosure be a new Abstract Closure with parameters (reason) that captures onFinally and C and performs the following steps when called: - let catch_finally_closure = FunctionObjectBuilder::new( - context, - NativeFunction::from_copy_closure_with_captures( - |_this, args, captures, context| { - /// Capture object for the abstract `throwReason` closure. - #[derive(Debug, Trace, Finalize)] - struct ThrowReasonCaptures { - reason: JsValue, - } + // v. Return ? Invoke(promise, "then", « valueThunk »). + promise.invoke(utf16!("then"), &[value_thunk.into()], context) + }, + FinallyCaptures { + on_finally: on_finally.clone(), + c: c.clone(), + }, + ), + ); - let reason = args.get_or_undefined(0); + // b. Let thenFinally be CreateBuiltinFunction(thenFinallyClosure, 1, "", « »). + let then_finally = then_finally_closure.length(1).name("").build(); - // i. Let result be ? Call(onFinally, undefined). - let result = - captures - .on_finally - .call(&JsValue::undefined(), &[], context)?; + // c. Let catchFinallyClosure be a new Abstract Closure with parameters (reason) that captures onFinally and C and performs the following steps when called: + let catch_finally_closure = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_this, args, captures, context| { + /// Capture object for the abstract `throwReason` closure. + #[derive(Debug, Trace, Finalize)] + struct ThrowReasonCaptures { + reason: JsValue, + } - // ii. Let promise be ? PromiseResolve(C, result). - let promise = Self::promise_resolve(captures.c.clone(), result, context)?; + let reason = args.get_or_undefined(0); - // iii. Let throwReason be a new Abstract Closure with no parameters that captures reason and performs the following steps when called: - let throw_reason = FunctionObjectBuilder::new( - context, - NativeFunction::from_copy_closure_with_captures( - |_this, _args, captures, _context| { - // 1. Return ThrowCompletion(reason). - Err(JsError::from_opaque(captures.reason.clone())) - }, - ThrowReasonCaptures { - reason: reason.clone(), - }, - ), - ); + // i. Let result be ? Call(onFinally, undefined). + let result = captures + .on_finally + .call(&JsValue::undefined(), &[], context)?; - // iv. Let thrower be CreateBuiltinFunction(throwReason, 0, "", « »). - let thrower = throw_reason.length(0).name("").build(); + // ii. Let promise be ? PromiseResolve(C, result). + let promise = Self::promise_resolve(&captures.c, result, context)?; - // v. Return ? Invoke(promise, "then", « thrower »). - promise.invoke(utf16!("then"), &[thrower.into()], context) - }, - FinallyCaptures { - on_finally: on_finally.clone(), - c, - }, - ), - ); + // iii. Let throwReason be a new Abstract Closure with no parameters that captures reason and performs the following steps when called: + let throw_reason = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_this, _args, captures, _context| { + // 1. Return ThrowCompletion(reason). + Err(JsError::from_opaque(captures.reason.clone())) + }, + ThrowReasonCaptures { + reason: reason.clone(), + }, + ), + ); - // d. Let catchFinally be CreateBuiltinFunction(catchFinallyClosure, 1, "", « »). - let catch_finally = catch_finally_closure.length(1).name("").build(); + // iv. Let thrower be CreateBuiltinFunction(throwReason, 0, "", « »). + let thrower = throw_reason.length(0).name("").build(); - (then_finally.into(), catch_finally.into()) - }; + // v. Return ? Invoke(promise, "then", « thrower »). + promise.invoke(utf16!("then"), &[thrower.into()], context) + }, + FinallyCaptures { on_finally, c }, + ), + ); - // 7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »). - let then = promise.get(utf16!("then"), context)?; - then.call(this, &[then_finally, catch_finally], context) + // d. Let catchFinally be CreateBuiltinFunction(catchFinallyClosure, 1, "", « »). + let catch_finally = catch_finally_closure.length(1).name("").build(); + + (then_finally, catch_finally) } /// `Promise.prototype.then ( onFulfilled, onRejected )` @@ -1962,24 +1730,48 @@ impl Promise { /// /// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.then /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then - pub fn then(this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { + pub(crate) fn then( + this: &JsValue, + args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { // 1. Let promise be the this value. let promise = this; // 2. If IsPromise(promise) is false, throw a TypeError exception. let promise = promise.as_promise().ok_or_else(|| { - JsNativeError::typ() - .with_message("Promise.prototype.then: provided value is not a promise") + JsNativeError::typ().with_message("Promise.prototype.then: this is not a promise") })?; + let on_fulfilled = args + .get_or_undefined(0) + .as_object() + .cloned() + .and_then(JsFunction::from_object); + let on_rejected = args + .get_or_undefined(1) + .as_object() + .cloned() + .and_then(JsFunction::from_object); + + // continues in `Promise::inner_then` + Promise::inner_then(promise, on_fulfilled, on_rejected, context).map(JsValue::from) + } + + /// Schedules callback functions for the eventual completion of `promise` — either fulfillment + /// or rejection. + pub(crate) fn inner_then( + promise: &JsObject, + on_fulfilled: Option, + on_rejected: Option, + context: &mut Context<'_>, + ) -> JsResult { // 3. Let C be ? SpeciesConstructor(promise, %Promise%). let c = promise.species_constructor(StandardConstructors::promise, context)?; // 4. Let resultCapability be ? NewPromiseCapability(C). - let result_capability = PromiseCapability::new(&c.into(), context)?; - - let on_fulfilled = args.get_or_undefined(0); - let on_rejected = args.get_or_undefined(1); + let result_capability = PromiseCapability::new(&c, context)?; + let result_promise = result_capability.promise.clone(); // 5. Return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability). Self::perform_promise_then( @@ -1988,9 +1780,9 @@ impl Promise { on_rejected, Some(result_capability), context, - ) - .map_or_else(JsValue::undefined, JsValue::from) - .pipe(Ok) + ); + + Ok(result_promise) } /// `PerformPromiseThen ( promise, onFulfilled, onRejected [ , resultCapability ] )` @@ -2001,11 +1793,11 @@ impl Promise { /// [spec]: https://tc39.es/ecma262/#sec-performpromisethen pub(crate) fn perform_promise_then( promise: &JsObject, - on_fulfilled: &JsValue, - on_rejected: &JsValue, + on_fulfilled: Option, + on_rejected: Option, result_capability: Option, context: &mut Context<'_>, - ) -> Option { + ) { // 1. Assert: IsPromise(promise) is true. // 2. If resultCapability is not present, then @@ -2013,20 +1805,16 @@ impl Promise { // 3. If IsCallable(onFulfilled) is false, then // a. Let onFulfilledJobCallback be empty. + // Argument already asserts this. let on_fulfilled_job_callback = on_fulfilled - .as_object() - .cloned() - .and_then(JsFunction::from_object) // 4. Else, // a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled). .map(|f| context.host_hooks().make_job_callback(f, context)); // 5. If IsCallable(onRejected) is false, then // a. Let onRejectedJobCallback be empty. + // Argument already asserts this. let on_rejected_job_callback = on_rejected - .as_object() - .cloned() - .and_then(JsFunction::from_object) // 6. Else, // a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected). .map(|f| context.host_hooks().make_job_callback(f, context)); @@ -2040,7 +1828,7 @@ impl Promise { // 8. Let rejectReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Reject, [[Handler]]: onRejectedJobCallback }. let reject_reaction = ReactionRecord { - promise_capability: result_capability.clone(), + promise_capability: result_capability, reaction_type: ReactionType::Reject, handler: on_rejected_job_callback, }; @@ -2109,47 +1897,7 @@ impl Promise { // a. Return undefined. // 14. Else, // a. Return resultCapability.[[Promise]]. - result_capability.map(|cap| cap.promise.clone()) - } - - /// `PromiseResolve ( C, x )` - /// - /// The abstract operation `PromiseResolve` takes arguments `C` (a constructor) and `x` (an - /// ECMAScript language value) and returns either a normal completion containing an ECMAScript - /// language value or a throw completion. It returns a new promise resolved with `x`. - /// - /// More information: - /// - [ECMAScript reference][spec] - /// - /// [spec]: https://tc39.es/ecma262/#sec-promise-resolve - pub(crate) fn promise_resolve( - c: JsObject, - x: JsValue, - context: &mut Context<'_>, - ) -> JsResult { - // 1. If IsPromise(x) is true, then - if let Some(x) = x.as_promise() { - // a. Let xConstructor be ? Get(x, "constructor"). - let x_constructor = x.get(CONSTRUCTOR, context)?; - // b. If SameValue(xConstructor, C) is true, return x. - if x_constructor - .as_object() - .map_or(false, |o| JsObject::equals(o, &c)) - { - return Ok(x.clone()); - } - } - - // 2. Let promiseCapability be ? NewPromiseCapability(C). - let promise_capability = PromiseCapability::new(&c.into(), context)?; - - // 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). - promise_capability - .resolve - .call(&JsValue::Undefined, &[x], context)?; - - // 4. Return promiseCapability.[[Promise]]. - Ok(promise_capability.promise.clone()) + // skipped because we can already access the promise from `result_capability` } /// `GetPromiseResolve ( promiseConstructor )` @@ -2162,7 +1910,7 @@ impl Promise { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-getpromiseresolve - fn get_promise_resolve( + pub(crate) fn get_promise_resolve( promise_constructor: &JsObject, context: &mut Context<'_>, ) -> JsResult { @@ -2176,6 +1924,312 @@ impl Promise { .into() }) } + + /// `CreateResolvingFunctions ( promise )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createresolvingfunctions + pub(crate) fn create_resolving_functions( + promise: &JsObject, + context: &mut Context<'_>, + ) -> ResolvingFunctions { + /// `TriggerPromiseReactions ( reactions, argument )` + /// + /// The abstract operation `TriggerPromiseReactions` takes arguments `reactions` (a `List` of + /// `PromiseReaction` Records) and `argument` and returns unused. It enqueues a new `Job` for + /// each record in `reactions`. Each such `Job` processes the `[[Type]]` and `[[Handler]]` of + /// the `PromiseReaction` Record, and if the `[[Handler]]` is not `empty`, calls it passing the + /// given argument. If the `[[Handler]]` is `empty`, the behaviour is determined by the + /// `[[Type]]`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-triggerpromisereactions + fn trigger_promise_reactions( + reactions: Vec, + argument: &JsValue, + context: &mut Context<'_>, + ) { + // 1. For each element reaction of reactions, do + for reaction in reactions { + // a. Let job be NewPromiseReactionJob(reaction, argument). + let job = new_promise_reaction_job(reaction, argument.clone()); + + // b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + context.job_queue().enqueue_promise_job(job, context); + } + // 2. Return unused. + } + + /// `FulfillPromise ( promise, value )` + /// + /// The abstract operation `FulfillPromise` takes arguments `promise` and `value` and returns + /// `unused`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-fulfillpromise + /// + /// # Panics + /// + /// Panics if `Promise` is not pending. + fn fulfill_promise(promise: &JsObject, value: JsValue, context: &mut Context<'_>) { + let mut promise = promise.borrow_mut(); + let promise = promise + .as_promise_mut() + .expect("IsPromise(promise) is false"); + + // 1. Assert: The value of promise.[[PromiseState]] is pending. + assert!( + matches!(promise.state, PromiseState::Pending), + "promise was not pending" + ); + + // reordering these statements does not affect the semantics + + // 2. Let reactions be promise.[[PromiseFulfillReactions]]. + // 4. Set promise.[[PromiseFulfillReactions]] to undefined. + let reactions = std::mem::take(&mut promise.fulfill_reactions); + + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + promise.reject_reactions.clear(); + + // 7. Perform TriggerPromiseReactions(reactions, value). + trigger_promise_reactions(reactions, &value, context); + + // 3. Set promise.[[PromiseResult]] to value. + // 6. Set promise.[[PromiseState]] to fulfilled. + promise.state = PromiseState::Fulfilled(value); + + // 8. Return unused. + } + + /// `RejectPromise ( promise, reason )` + /// + /// The abstract operation `RejectPromise` takes arguments `promise` and `reason` and returns + /// `unused`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-rejectpromise + /// + /// # Panics + /// + /// Panics if `Promise` is not pending. + fn reject_promise(promise: &JsObject, reason: JsValue, context: &mut Context<'_>) { + let handled = { + let mut promise = promise.borrow_mut(); + let promise = promise + .as_promise_mut() + .expect("IsPromise(promise) is false"); + + // 1. Assert: The value of promise.[[PromiseState]] is pending. + assert!( + matches!(promise.state, PromiseState::Pending), + "Expected promise.[[PromiseState]] to be pending" + ); + + // reordering these statements does not affect the semantics + + // 2. Let reactions be promise.[[PromiseRejectReactions]]. + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + let reactions = std::mem::take(&mut promise.reject_reactions); + + // 4. Set promise.[[PromiseFulfillReactions]] to undefined. + promise.fulfill_reactions.clear(); + + // 8. Perform TriggerPromiseReactions(reactions, reason). + trigger_promise_reactions(reactions, &reason, context); + + // 3. Set promise.[[PromiseResult]] to reason. + // 6. Set promise.[[PromiseState]] to rejected. + promise.state = PromiseState::Rejected(reason); + + promise.handled + }; + + // 7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject"). + if !handled { + context.host_hooks().promise_rejection_tracker( + promise, + OperationType::Reject, + context, + ); + } + + // 9. Return unused. + } + + #[derive(Debug, Trace, Finalize)] + struct RejectResolveCaptures { + promise: JsObject, + #[unsafe_ignore_trace] + already_resolved: Rc>, + } + + // 1. Let alreadyResolved be the Record { [[Value]]: false }. + let already_resolved = Rc::new(Cell::new(false)); + + // 5. Set resolve.[[Promise]] to promise. + // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. + let resolve_captures = RejectResolveCaptures { + already_resolved: already_resolved.clone(), + promise: promise.clone(), + }; + + // 2. Let stepsResolve be the algorithm steps defined in Promise Resolve Functions. + // 3. Let lengthResolve be the number of non-optional parameters of the function definition in Promise Resolve Functions. + // 4. Let resolve be CreateBuiltinFunction(stepsResolve, lengthResolve, "", « [[Promise]], [[AlreadyResolved]] »). + let resolve = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_this, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise-resolve-functions + + // 1. Let F be the active function object. + // 2. Assert: F has a [[Promise]] internal slot whose value is an Object. + // 3. Let promise be F.[[Promise]]. + // 4. Let alreadyResolved be F.[[AlreadyResolved]]. + let RejectResolveCaptures { + promise, + already_resolved, + } = captures; + + // 5. If alreadyResolved.[[Value]] is true, return undefined. + if already_resolved.get() { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + already_resolved.set(true); + + let resolution = args.get_or_undefined(0); + + // 7. If SameValue(resolution, promise) is true, then + if JsValue::same_value(resolution, &promise.clone().into()) { + // a. Let selfResolutionError be a newly created TypeError object. + let self_resolution_error = JsNativeError::typ() + .with_message("SameValue(resolution, promise) is true") + .to_opaque(context); + + // b. Perform RejectPromise(promise, selfResolutionError). + reject_promise(promise, self_resolution_error.into(), context); + + // c. Return undefined. + return Ok(JsValue::Undefined); + } + + let Some(then) = resolution.as_object() else { + // 8. If Type(resolution) is not Object, then + // a. Perform FulfillPromise(promise, resolution). + fulfill_promise(promise, resolution.clone(), context); + + // b. Return undefined. + return Ok(JsValue::Undefined); + }; + + // 9. Let then be Completion(Get(resolution, "then")). + let then_action = match then.get(utf16!("then"), context) { + // 10. If then is an abrupt completion, then + Err(e) => { + // a. Perform RejectPromise(promise, then.[[Value]]). + reject_promise(promise, e.to_opaque(context), context); + + // b. Return undefined. + return Ok(JsValue::Undefined); + } + // 11. Let thenAction be then.[[Value]]. + Ok(then) => then, + }; + + // 12. If IsCallable(thenAction) is false, then + let Some(then_action) = then_action.as_object().cloned().and_then(JsFunction::from_object) else { + // a. Perform FulfillPromise(promise, resolution). + fulfill_promise(promise, resolution.clone(), context); + + // b. Return undefined. + return Ok(JsValue::Undefined); + }; + + // 13. Let thenJobCallback be HostMakeJobCallback(thenAction). + let then_job_callback = context.host_hooks().make_job_callback(then_action, context); + + // 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). + let job = new_promise_resolve_thenable_job( + promise.clone(), + resolution.clone(), + then_job_callback, + ); + + // 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + context.job_queue().enqueue_promise_job(job, context); + + // 16. Return undefined. + Ok(JsValue::Undefined) + }, + resolve_captures, + ), + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // 10. Set reject.[[Promise]] to promise. + // 11. Set reject.[[AlreadyResolved]] to alreadyResolved. + let reject_captures = RejectResolveCaptures { + promise: promise.clone(), + already_resolved, + }; + + // 7. Let stepsReject be the algorithm steps defined in Promise Reject Functions. + // 8. Let lengthReject be the number of non-optional parameters of the function definition in Promise Reject Functions. + // 9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »). + let reject = FunctionObjectBuilder::new( + context, + NativeFunction::from_copy_closure_with_captures( + |_this, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise-reject-functions + + // 1. Let F be the active function object. + // 2. Assert: F has a [[Promise]] internal slot whose value is an Object. + // 3. Let promise be F.[[Promise]]. + // 4. Let alreadyResolved be F.[[AlreadyResolved]]. + let RejectResolveCaptures { + promise, + already_resolved, + } = captures; + + // 5. If alreadyResolved.[[Value]] is true, return undefined. + if already_resolved.get() { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + already_resolved.set(true); + + // 7. Perform RejectPromise(promise, reason). + reject_promise(promise, args.get_or_undefined(0).clone(), context); + + // 8. Return undefined. + Ok(JsValue::Undefined) + }, + reject_captures, + ), + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }. + ResolvingFunctions { resolve, reject } + } } /// More information: diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index a1c2602c5e..c1dda2aa47 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -185,6 +185,7 @@ pub use crate::{ context::Context, error::{JsError, JsNativeError, JsNativeErrorKind}, native_function::NativeFunction, + object::JsObject, string::JsString, symbol::JsSymbol, value::JsValue, diff --git a/boa_engine/src/native_function.rs b/boa_engine/src/native_function.rs index 789eec1454..d1d6448285 100644 --- a/boa_engine/src/native_function.rs +++ b/boa_engine/src/native_function.rs @@ -7,12 +7,7 @@ use std::future::Future; use boa_gc::{custom_trace, Finalize, Gc, Trace}; -use crate::{ - builtins::Promise, - job::NativeJob, - object::{JsObject, ObjectData}, - Context, JsResult, JsValue, -}; +use crate::{job::NativeJob, object::JsPromise, Context, JsResult, JsValue}; /// The required signature for all native built-in function pointers. /// @@ -186,22 +181,16 @@ impl NativeFunction { Fut: Future> + 'static, { Self::from_copy_closure(move |this, args, context| { - let proto = context.intrinsics().constructors().promise().prototype(); - let promise = JsObject::from_proto_and_data(proto, ObjectData::promise(Promise::new())); - let resolving_functions = Promise::create_resolving_functions(&promise, context); + let (promise, resolvers) = JsPromise::new_pending(context); let future = f(this, args, context); let future = async move { let result = future.await; NativeJob::new(move |ctx| match result { - Ok(v) => resolving_functions - .resolve - .call(&JsValue::undefined(), &[v], ctx), + Ok(v) => resolvers.resolve.call(&JsValue::undefined(), &[v], ctx), Err(e) => { let e = e.to_opaque(ctx); - resolving_functions - .reject - .call(&JsValue::undefined(), &[e], ctx) + resolvers.reject.call(&JsValue::undefined(), &[e], ctx) } }) }; diff --git a/boa_engine/src/object/builtins/jspromise.rs b/boa_engine/src/object/builtins/jspromise.rs new file mode 100644 index 0000000000..8618e5dea6 --- /dev/null +++ b/boa_engine/src/object/builtins/jspromise.rs @@ -0,0 +1,885 @@ +//! A Rust API wrapper for Boa's promise Builtin ECMAScript Object + +#![allow(missing_docs)] + +use boa_gc::{Finalize, Trace}; + +use crate::{ + builtins::{ + promise::{PromiseState, ResolvingFunctions}, + Promise, + }, + context::intrinsics::StandardConstructors, + object::{JsObject, JsObjectType, ObjectData}, + Context, JsError, JsNativeError, JsResult, JsValue, +}; + +use super::{JsArray, JsFunction}; + +/// An ECMAScript [promise] object. +/// +/// Known as the concurrency primitive of ECMAScript, this is the main struct used to manipulate, +/// chain and inspect `Promises` from Rust code. +/// +/// # Examples +/// +/// ``` +/// # use boa_engine::{ +/// # builtins::promise::PromiseState, +/// # job::SimpleJobQueue, +/// # js_string, +/// # object::{builtins::JsPromise, FunctionObjectBuilder}, +/// # property::Attribute, +/// # Context, JsArgs, JsError, JsValue, NativeFunction, +/// # }; +/// # use std::error::Error; +/// # fn main() -> Result<(), Box> { +/// let queue = &SimpleJobQueue::new(); +/// let context = &mut Context::builder().job_queue(queue).build()?; +/// +/// context.register_global_property("finally", false, Attribute::all()); +/// +/// let promise = JsPromise::new( +/// |resolvers, context| { +/// let result = js_string!("hello world!").into(); +/// resolvers +/// .resolve +/// .call(&JsValue::undefined(), &[result], context)?; +/// Ok(JsValue::undefined()) +/// }, +/// context, +/// )?; +/// +/// let promise = promise +/// .then( +/// Some( +/// FunctionObjectBuilder::new( +/// context, +/// NativeFunction::from_fn_ptr(|_, args, _| { +/// Err(JsError::from_opaque(args.get_or_undefined(0).clone()).into()) +/// }), +/// ) +/// .build(), +/// ), +/// None, +/// context, +/// )? +/// .catch( +/// FunctionObjectBuilder::new( +/// context, +/// NativeFunction::from_fn_ptr(|_, args, _| { +/// Ok(args.get_or_undefined(0).clone()) +/// }), +/// ) +/// .build(), +/// context, +/// )? +/// .finally( +/// FunctionObjectBuilder::new( +/// context, +/// NativeFunction::from_fn_ptr(|_, _, context| { +/// context +/// .global_object() +/// .clone() +/// .set("finally", JsValue::from(true), true, context)?; +/// Ok(JsValue::undefined()) +/// }), +/// ) +/// .build(), +/// context, +/// )?; +/// +/// context.run_jobs(); +/// +/// assert_eq!( +/// promise.state()?, +/// PromiseState::Fulfilled(js_string!("hello world!").into()) +/// ); +/// +/// assert_eq!( +/// context.global_object().clone().get("finally", context)?, +/// JsValue::from(true) +/// ); +/// +/// # Ok(()) +/// # } +/// ``` +/// +/// [promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +#[derive(Debug, Clone, Trace, Finalize)] +pub struct JsPromise { + inner: JsObject, +} + +impl JsPromise { + /// Creates a new promise object from an executor function. + /// + /// It is equivalent to calling the [`Promise()`] constructor, which makes it share the same + /// execution semantics as the constructor: + /// - The executor function `executor` is called synchronously just after the promise is created. + /// - The executor return value is ignored. + /// - Any error thrown within the execution of `executor` will call the `reject` function + /// of the newly created promise, unless either `resolve` or `reject` were already called + /// beforehand. + /// + /// `executor` receives as an argument the [`ResolvingFunctions`] needed to settle the promise, + /// which can be done by either calling the `resolve` function or the `reject` function. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # job::SimpleJobQueue, + /// # object::builtins::JsPromise, + /// # builtins::promise::PromiseState, + /// # Context, JsValue, js_string + /// # }; + /// # fn main() -> Result<(), Box> { + /// let queue = &SimpleJobQueue::new(); + /// let context = &mut Context::builder().job_queue(queue).build()?; + /// + /// let promise = JsPromise::new(|resolvers, context| { + /// let result = js_string!("hello world").into(); + /// resolvers.resolve.call(&JsValue::undefined(), &[result], context)?; + /// Ok(JsValue::undefined()) + /// }, context)?; + /// + /// context.run_jobs(); + /// + /// assert_eq!(promise.state()?, PromiseState::Fulfilled(js_string!("hello world").into())); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise + pub fn new(executor: F, context: &mut Context<'_>) -> JsResult + where + F: FnOnce(&ResolvingFunctions, &mut Context<'_>) -> JsResult, + { + let promise = JsObject::from_proto_and_data( + context.intrinsics().constructors().promise().prototype(), + ObjectData::promise(Promise::new()), + ); + let resolvers = Promise::create_resolving_functions(&promise, context); + + if let Err(e) = executor(&resolvers, context) { + let e = e.to_opaque(context); + resolvers + .reject + .call(&JsValue::undefined(), &[e], context)?; + } + + Ok(JsPromise { inner: promise }) + } + + /// Creates a new pending promise and returns it and its associated `ResolvingFunctions`. + /// + /// This can be useful when you want to manually settle a promise from Rust code, instead of + /// running an `executor` function that automatically settles the promise on creation + /// (see [`JsPromise::new`]). + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # object::builtins::JsPromise, + /// # builtins::promise::PromiseState, + /// # Context, JsValue + /// # }; + /// # fn main() -> Result<(), Box> { + /// let context = &mut Context::default(); + /// + /// let (promise, resolvers) = JsPromise::new_pending(context); + /// + /// assert_eq!(promise.state()?, PromiseState::Pending); + /// + /// resolvers.reject.call(&JsValue::undefined(), &[5.into()], context)?; + /// + /// assert_eq!(promise.state()?, PromiseState::Rejected(5.into())); + /// + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn new_pending(context: &mut Context<'_>) -> (JsPromise, ResolvingFunctions) { + let promise = JsObject::from_proto_and_data( + context.intrinsics().constructors().promise().prototype(), + ObjectData::promise(Promise::new()), + ); + let resolvers = Promise::create_resolving_functions(&promise, context); + let promise = JsPromise::from_object(promise) + .expect("this shouldn't fail with a newly created promise"); + + (promise, resolvers) + } + + /// Wraps an existing object with the `JsPromise` interface, returning `Err` if the object + /// is not a valid promise. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # object::builtins::JsPromise, + /// # builtins::promise::PromiseState, + /// # Context, JsObject, JsValue, Source + /// # }; + /// # fn main() -> Result<(), Box> { + /// let context = &mut Context::default(); + /// + /// let promise = context.eval_script(Source::from_bytes("new Promise((resolve, reject) => resolve())"))?; + /// let promise = promise.as_object().cloned().unwrap(); + /// + /// let promise = JsPromise::from_object(promise)?; + /// + /// assert_eq!(promise.state()?, PromiseState::Fulfilled(JsValue::undefined())); + /// + /// assert!(JsPromise::from_object(JsObject::with_null_proto()).is_err()); + /// + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn from_object(object: JsObject) -> JsResult { + if !object.is_promise() { + return Err(JsNativeError::typ() + .with_message("`object` is not a Promise") + .into()); + } + Ok(JsPromise { inner: object }) + } + + /// Resolves a `JsValue` into a `JsPromise`. + /// + /// Equivalent to the [`Promise.resolve()`] static method. + /// + /// This function is mainly used to wrap a plain `JsValue` into a fulfilled promise, but it can + /// also flatten nested layers of [thenables], which essentially converts them into native + /// promises. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # object::builtins::JsPromise, + /// # builtins::promise::PromiseState, + /// # Context, js_string + /// # }; + /// # fn main() -> Result<(), Box> { + /// let context = &mut Context::default(); + /// + /// let promise = JsPromise::resolve(js_string!("resolved!"), context)?; + /// + /// assert_eq!(promise.state()?, PromiseState::Fulfilled(js_string!("resolved!").into())); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise.resolve()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve + /// [thenables]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables + pub fn resolve>(value: V, context: &mut Context<'_>) -> JsResult { + Promise::promise_resolve( + &context.intrinsics().constructors().promise().constructor(), + value.into(), + context, + ) + .and_then(JsPromise::from_object) + } + + /// Creates a `JsPromise` that is rejected with the reason `error`. + /// + /// Equivalent to the [`Promise.reject`] static method. + /// + /// `JsPromise::reject` is pretty similar to [`JsPromise::resolve`], with the difference that + /// it always wraps `error` into a rejected promise, even if `error` is a promise or a [thenable]. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # object::builtins::JsPromise, + /// # builtins::promise::PromiseState, + /// # Context, js_string, JsError + /// # }; + /// # fn main() -> Result<(), Box> { + /// let context = &mut Context::default(); + /// + /// let promise = JsPromise::reject(JsError::from_opaque(js_string!("oops!").into()), context)?; + /// + /// assert_eq!(promise.state()?, PromiseState::Rejected(js_string!("oops!").into())); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise.reject`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject + /// [thenable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables + pub fn reject>(error: E, context: &mut Context<'_>) -> JsResult { + Promise::promise_reject( + &context.intrinsics().constructors().promise().constructor(), + &error.into(), + context, + ) + .and_then(JsPromise::from_object) + } + + /// Gets the current state of the promise. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # object::builtins::JsPromise, + /// # builtins::promise::PromiseState, + /// # Context + /// # }; + /// # fn main() -> Result<(), Box> { + /// let context = &mut Context::default(); + /// + /// let promise = JsPromise::new_pending(context).0; + /// + /// assert_eq!(promise.state()?, PromiseState::Pending); + /// + /// # Ok(()) + /// # } + /// ``` + #[inline] + pub fn state(&self) -> JsResult { + // TODO: if we can guarantee that objects cannot change type after creation, + // we can remove this throw. + let promise = self.inner.borrow(); + let promise = promise + .as_promise() + .ok_or_else(|| JsNativeError::typ().with_message("object is not a Promise"))?; + + Ok(promise.state().clone()) + } + + /// Schedules callback functions to run when the promise settles. + /// + /// Equivalent to the [`Promise.prototype.then`] method. + /// + /// The return value is a promise that is always pending on return, regardless of the current + /// state of the original promise. Two handlers can be provided as callbacks to be executed when + /// the original promise settles: + /// + /// - If the original promise is fulfilled, `on_fulfilled` is called with the fulfillment value + /// of the original promise. + /// - If the original promise is rejected, `on_rejected` is called with the rejection reason + /// of the original promise. + /// + /// The return value of the handlers can be used to mutate the state of the created promise. If + /// the callback: + /// + /// - returns a value: the created promise gets fulfilled with the returned value. + /// - doesn't return: the created promise gets fulfilled with undefined. + /// - throws: the created promise gets rejected with the thrown error as its value. + /// - returns a fulfilled promise: the created promise gets fulfilled with that promise's value as its value. + /// - returns a rejected promise: the created promise gets rejected with that promise's value as its value. + /// - returns another pending promise: the created promise remains pending but becomes settled with that + /// promise's value as its value immediately after that promise becomes settled. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # builtins::promise::PromiseState, + /// # job::SimpleJobQueue, + /// # js_string, + /// # object::{builtins::JsPromise, FunctionObjectBuilder}, + /// # Context, JsArgs, JsError, JsValue, NativeFunction, + /// # }; + /// # fn main() -> Result<(), Box> { + /// let queue = &SimpleJobQueue::new(); + /// let context = &mut Context::builder().job_queue(queue).build()?; + /// + /// let promise = JsPromise::new( + /// |resolvers, context| { + /// resolvers + /// .resolve + /// .call(&JsValue::undefined(), &[255.255.into()], context)?; + /// Ok(JsValue::undefined()) + /// }, + /// context, + /// )?.then( + /// Some( + /// FunctionObjectBuilder::new( + /// context, + /// NativeFunction::from_fn_ptr(|_, args, context| { + /// args.get_or_undefined(0).to_string(context).map(JsValue::from) + /// }), + /// ) + /// .build(), + /// ), + /// None, + /// context, + /// )?; + /// + /// context.run_jobs(); + /// + /// assert_eq!( + /// promise.state()?, + /// PromiseState::Fulfilled(js_string!("255.255").into()) + /// ); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise.prototype.then`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + #[inline] + pub fn then( + &self, + on_fulfilled: Option, + on_rejected: Option, + context: &mut Context<'_>, + ) -> JsResult { + let result_promise = Promise::inner_then(self, on_fulfilled, on_rejected, context)?; + JsPromise::from_object(result_promise) + } + + /// Schedules a callback to run when the promise is rejected. + /// + /// Equivalent to the [`Promise.prototype.catch`] method. + /// + /// This is essentially a shortcut for calling [`promise.then(None, Some(function))`][then], which + /// only handles the error case and leaves the fulfilled case untouched. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # js_string, + /// # builtins::promise::PromiseState, + /// # job::SimpleJobQueue, + /// # object::{builtins::JsPromise, FunctionObjectBuilder}, + /// # Context, JsArgs, JsNativeError, JsValue, NativeFunction, + /// # }; + /// # fn main() -> Result<(), Box> { + /// let queue = &SimpleJobQueue::new(); + /// let context = &mut Context::builder().job_queue(queue).build()?; + /// + /// let promise = JsPromise::new( + /// |resolvers, context| { + /// let error = JsNativeError::typ().with_message("thrown"); + /// let error = error.to_opaque(context); + /// resolvers + /// .reject + /// .call(&JsValue::undefined(), &[error.into()], context)?; + /// Ok(JsValue::undefined()) + /// }, + /// context, + /// )?.catch( + /// FunctionObjectBuilder::new( + /// context, + /// NativeFunction::from_fn_ptr(|_, args, context| { + /// args.get_or_undefined(0).to_string(context).map(JsValue::from) + /// }), + /// ) + /// .build(), + /// context, + /// )?; + /// + /// context.run_jobs(); + /// + /// assert_eq!( + /// promise.state()?, + /// PromiseState::Fulfilled(js_string!("TypeError: thrown").into()) + /// ); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise.prototype.catch`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch + /// [then]: JsPromise::then + #[inline] + pub fn catch(&self, on_rejected: JsFunction, context: &mut Context<'_>) -> JsResult { + self.then(None, Some(on_rejected), context) + } + + /// Schedules a callback to run when the promise is rejected. + /// + /// Equivalent to the [`Promise.prototype.finally()`] method. + /// + /// While this could be seen as a shortcut for calling [`promise.then(Some(function), Some(function))`][then], + /// it has slightly different semantics than `then`: + /// - `on_finally` doesn't receive any argument, unlike `on_fulfilled` and `on_rejected`. + /// - `finally()` is transparent; a call like `Promise.resolve("first").finally(() => "second")` + /// returns a promise fulfilled with the value `"first"`, which would return `"second"` if `finally` + /// was a shortcut of `then`. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # job::SimpleJobQueue, + /// # object::{builtins::JsPromise, FunctionObjectBuilder}, + /// # property::Attribute, + /// # Context, JsNativeError, JsValue, NativeFunction, + /// # }; + /// # fn main() -> Result<(), Box> { + /// let queue = &SimpleJobQueue::new(); + /// let context = &mut Context::builder().job_queue(queue).build()?; + /// + /// context.register_global_property("finally", false, Attribute::all()); + /// + /// let promise = JsPromise::new( + /// |resolvers, context| { + /// let error = JsNativeError::typ().with_message("thrown"); + /// let error = error.to_opaque(context); + /// resolvers + /// .reject + /// .call(&JsValue::undefined(), &[error.into()], context)?; + /// Ok(JsValue::undefined()) + /// }, + /// context, + /// )?.finally( + /// FunctionObjectBuilder::new( + /// context, + /// NativeFunction::from_fn_ptr(|_, _, context| { + /// context + /// .global_object() + /// .clone() + /// .set("finally", JsValue::from(true), true, context)?; + /// Ok(JsValue::undefined()) + /// }), + /// ) + /// .build(), + /// context, + /// )?; + /// + /// context.run_jobs(); + /// + /// assert_eq!( + /// context.global_object().clone().get("finally", context)?, + /// JsValue::from(true) + /// ); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise.prototype.finally()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally + /// [then]: JsPromise::then + #[inline] + pub fn finally( + &self, + on_finally: JsFunction, + context: &mut Context<'_>, + ) -> JsResult { + let c = self.species_constructor(StandardConstructors::promise, context)?; + let (then, catch) = Promise::then_catch_finally_closures(c, on_finally, context); + self.then(Some(then), Some(catch), context) + } + + /// Waits for a list of promises to settle with fulfilled values, rejecting the aggregate promise + /// when any of the inner promises is rejected. + /// + /// Equivalent to the [`Promise.all`] static method. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # job::SimpleJobQueue, + /// # js_string, + /// # object::builtins::{JsArray, JsPromise}, + /// # Context, JsNativeError, JsValue, + /// # }; + /// # fn main() -> Result<(), Box> { + /// let queue = &SimpleJobQueue::new(); + /// let context = &mut Context::builder().job_queue(queue).build()?; + /// + /// let promise1 = JsPromise::all( + /// [ + /// JsPromise::resolve(0, context)?, + /// JsPromise::resolve(2, context)?, + /// JsPromise::resolve(4, context)?, + /// ], + /// context, + /// )?; + /// + /// let promise2 = JsPromise::all( + /// [ + /// JsPromise::resolve(1, context)?, + /// JsPromise::reject(JsNativeError::typ(), context)?, + /// JsPromise::resolve(3, context)?, + /// ], + /// context, + /// )?; + /// + /// context.run_jobs(); + /// + /// let array = promise1.state()?.as_fulfilled().and_then(JsValue::as_object).unwrap().clone(); + /// let array = JsArray::from_object(array)?; + /// assert_eq!(array.at(0, context)?, 0.into()); + /// assert_eq!(array.at(1, context)?, 2.into()); + /// assert_eq!(array.at(2, context)?, 4.into()); + /// + /// let error = promise2.state()?.as_rejected().unwrap().clone(); + /// assert_eq!(error.to_string(context)?, js_string!("TypeError")); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise.all`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all + pub fn all(promises: I, context: &mut Context<'_>) -> JsResult + where + I: IntoIterator, + { + let promises = JsArray::from_iter(promises.into_iter().map(JsValue::from), context); + + let c = &context + .intrinsics() + .constructors() + .promise() + .constructor() + .into(); + + let value = Promise::all(c, &[promises.into()], context)?; + let value = value + .as_object() + .expect("Promise.all always returns an object on success"); + + JsPromise::from_object(value.clone()) + } + + /// Waits for a list of promises to settle, fulfilling with an array of the outcomes of every + /// promise. + /// + /// Equivalent to the [`Promise.allSettled`] static method. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # job::SimpleJobQueue, + /// # js_string, + /// # object::builtins::{JsArray, JsPromise}, + /// # Context, JsNativeError, JsValue, + /// # }; + /// # fn main() -> Result<(), Box> { + /// let queue = &SimpleJobQueue::new(); + /// let context = &mut Context::builder().job_queue(queue).build()?; + /// + /// + /// let promise = JsPromise::all_settled( + /// [ + /// JsPromise::resolve(1, context)?, + /// JsPromise::reject(JsNativeError::typ(), context)?, + /// JsPromise::resolve(3, context)?, + /// ], + /// context, + /// )?; + /// + /// context.run_jobs(); + /// + /// let array = promise.state()?.as_fulfilled().and_then(JsValue::as_object).unwrap().clone(); + /// let array = JsArray::from_object(array)?; + /// + /// let a = array.at(0, context)?.as_object().unwrap().clone(); + /// assert_eq!(a.get("status", context)?, js_string!("fulfilled").into()); + /// assert_eq!(a.get("value", context)?, 1.into()); + /// + /// let b = array.at(1, context)?.as_object().unwrap().clone(); + /// assert_eq!(b.get("status", context)?, js_string!("rejected").into()); + /// assert_eq!(b.get("reason", context)?.to_string(context)?, js_string!("TypeError")); + /// + /// let c = array.at(2, context)?.as_object().unwrap().clone(); + /// assert_eq!(c.get("status", context)?, js_string!("fulfilled").into()); + /// assert_eq!(c.get("value", context)?, 3.into()); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise.allSettled`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled + pub fn all_settled(promises: I, context: &mut Context<'_>) -> JsResult + where + I: IntoIterator, + { + let promises = JsArray::from_iter(promises.into_iter().map(JsValue::from), context); + + let c = &context + .intrinsics() + .constructors() + .promise() + .constructor() + .into(); + + let value = Promise::all_settled(c, &[promises.into()], context)?; + let value = value + .as_object() + .expect("Promise.allSettled always returns an object on success"); + + JsPromise::from_object(value.clone()) + } + + /// Returns the first promise that fulfills from a list of promises. + /// + /// Equivalent to the [`Promise.any`] static method. + /// + /// If after settling all promises in `promises` there isn't a fulfilled promise, the returned + /// promise will be rejected with an `AggregatorError` containing the rejection values of every + /// promise; this includes the case where `promises` is an empty iterator. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # builtins::promise::PromiseState, + /// # job::SimpleJobQueue, + /// # js_string, + /// # object::builtins::JsPromise, + /// # Context, JsNativeError, + /// # }; + /// # fn main() -> Result<(), Box> { + /// let queue = &SimpleJobQueue::new(); + /// let context = &mut Context::builder().job_queue(queue).build()?; + /// + /// + /// let promise = JsPromise::any( + /// [ + /// JsPromise::reject(JsNativeError::syntax(), context)?, + /// JsPromise::reject(JsNativeError::typ(), context)?, + /// JsPromise::resolve(js_string!("fulfilled"), context)?, + /// JsPromise::reject(JsNativeError::range(), context)?, + /// ], + /// context, + /// )?; + /// + /// context.run_jobs(); + /// + /// assert_eq!(promise.state()?, PromiseState::Fulfilled(js_string!("fulfilled").into())); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise.any`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any + pub fn any(promises: I, context: &mut Context<'_>) -> JsResult + where + I: IntoIterator, + { + let promises = JsArray::from_iter(promises.into_iter().map(JsValue::from), context); + + let c = &context + .intrinsics() + .constructors() + .promise() + .constructor() + .into(); + + let value = Promise::any(c, &[promises.into()], context)?; + let value = value + .as_object() + .expect("Promise.any always returns an object on success"); + + JsPromise::from_object(value.clone()) + } + + /// Returns the first promise that settles from a list of promises. + /// + /// Equivalent to the [`Promise.race`] static method. + /// + /// If the provided iterator is empty, the returned promise will remain on the pending state + /// forever. + /// + /// # Examples + /// + /// ``` + /// # use std::error::Error; + /// # use boa_engine::{ + /// # builtins::promise::PromiseState, + /// # job::SimpleJobQueue, + /// # js_string, + /// # object::builtins::JsPromise, + /// # Context, JsValue, + /// # }; + /// # fn main() -> Result<(), Box> { + /// let queue = &SimpleJobQueue::new(); + /// let context = &mut Context::builder().job_queue(queue).build()?; + /// + /// let (a, resolvers_a) = JsPromise::new_pending(context); + /// let (b, resolvers_b) = JsPromise::new_pending(context); + /// let (c, resolvers_c) = JsPromise::new_pending(context); + /// + /// let promise = JsPromise::race([a, b, c], context)?; + /// + /// resolvers_b.reject.call(&JsValue::undefined(), &[], context); + /// resolvers_a.resolve.call(&JsValue::undefined(), &[5.into()], context); + /// resolvers_c.reject.call(&JsValue::undefined(), &[js_string!("c error").into()], context); + /// + /// context.run_jobs(); + /// + /// assert_eq!( + /// promise.state()?, + /// PromiseState::Rejected(JsValue::undefined()) + /// ); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Promise.race`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race + pub fn race(promises: I, context: &mut Context<'_>) -> JsResult + where + I: IntoIterator, + { + let promises = JsArray::from_iter(promises.into_iter().map(JsValue::from), context); + + let c = &context + .intrinsics() + .constructors() + .promise() + .constructor() + .into(); + + let value = Promise::race(c, &[promises.into()], context)?; + let value = value + .as_object() + .expect("Promise.race always returns an object on success"); + + JsPromise::from_object(value.clone()) + } +} + +impl From for JsObject { + #[inline] + fn from(o: JsPromise) -> Self { + o.inner.clone() + } +} + +impl From for JsValue { + #[inline] + fn from(o: JsPromise) -> Self { + o.inner.clone().into() + } +} + +impl std::ops::Deref for JsPromise { + type Target = JsObject; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl JsObjectType for JsPromise {} diff --git a/boa_engine/src/object/builtins/mod.rs b/boa_engine/src/object/builtins/mod.rs index 435a233774..f0d61475e4 100644 --- a/boa_engine/src/object/builtins/mod.rs +++ b/boa_engine/src/object/builtins/mod.rs @@ -1,6 +1,6 @@ //! All Rust API wrappers for Boa's ECMAScript objects. //! -//! The structs available in this module provide functionality to interact with the implemented ECMAScript object from Rust. +//! The structs available in this module provide functionality to interact with native ECMAScript objects from Rust. mod jsarray; mod jsarraybuffer; @@ -10,7 +10,8 @@ mod jsfunction; mod jsgenerator; mod jsmap; mod jsmap_iterator; -pub(crate) mod jsproxy; +mod jspromise; +mod jsproxy; mod jsregexp; mod jsset; mod jsset_iterator; @@ -24,7 +25,8 @@ pub use jsfunction::*; pub use jsgenerator::*; pub use jsmap::*; pub use jsmap_iterator::*; -pub use jsproxy::{JsProxy, JsRevocableProxy}; +pub use jspromise::*; +pub use jsproxy::{JsProxy, JsProxyBuilder, JsRevocableProxy}; pub use jsregexp::JsRegExp; pub use jsset::*; pub use jsset_iterator::*; diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index a104a46673..142471cd35 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -75,7 +75,6 @@ mod property_map; pub(crate) use builtins::*; -pub use builtins::jsproxy::JsProxyBuilder; pub use jsobject::*; pub(crate) trait JsObjectType: diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index ec3ce4bf88..adf42be389 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -575,12 +575,7 @@ pub(crate) fn create_function_object( let function = if r#async { let promise_capability = PromiseCapability::new( - &context - .intrinsics() - .constructors() - .promise() - .constructor() - .into(), + &context.intrinsics().constructors().promise().constructor(), context, ) .expect("cannot fail per spec"); diff --git a/boa_engine/src/vm/opcode/await_stm/mod.rs b/boa_engine/src/vm/opcode/await_stm/mod.rs index fbb51eca7c..01e568a982 100644 --- a/boa_engine/src/vm/opcode/await_stm/mod.rs +++ b/boa_engine/src/vm/opcode/await_stm/mod.rs @@ -27,13 +27,11 @@ impl Operation for Await { let value = context.vm.pop(); // 2. Let promise be ? PromiseResolve(%Promise%, value). - let resolve_result = Promise::promise_resolve( - context.intrinsics().constructors().promise().constructor(), + let promise = Promise::promise_resolve( + &context.intrinsics().constructors().promise().constructor(), value, context, - ); - - let promise = resolve_result?; + )?; // 3. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures asyncContext and performs the following steps when called: // 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). @@ -124,8 +122,8 @@ impl Operation for Await { // 7. Perform PerformPromiseThen(promise, onFulfilled, onRejected). Promise::perform_promise_then( &promise, - &on_fulfilled.into(), - &on_rejected.into(), + Some(on_fulfilled), + Some(on_rejected), None, context, );