diff --git a/Cargo.lock b/Cargo.lock index c82789bd8e8..1e82823eaf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] name = "byteorder" @@ -1043,9 +1043,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "os_str_bytes" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" [[package]] name = "parking_lot" @@ -1356,9 +1356,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.34.7" +version = "0.34.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f117495127afb702af6706f879fb2b5c008c38ccf3656afc514e26f35bdb8180" +checksum = "2079c267b8394eb529872c3cf92e181c378b41fea36e68130357b52493701d2e" dependencies = [ "bitflags", "errno", @@ -1517,9 +1517,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "str-buf" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "string-interner" @@ -1570,9 +1570,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", diff --git a/boa_engine/src/builtins/mod.rs b/boa_engine/src/builtins/mod.rs index 938c4b7c3b0..2d4bcd5596c 100644 --- a/boa_engine/src/builtins/mod.rs +++ b/boa_engine/src/builtins/mod.rs @@ -20,6 +20,7 @@ pub mod math; pub mod nan; pub mod number; pub mod object; +pub mod promise; pub mod proxy; pub mod reflect; pub mod regexp; @@ -57,6 +58,7 @@ pub(crate) use self::{ number::Number, object::for_in_iterator::ForInIterator, object::Object as BuiltInObjectObject, + promise::Promise, proxy::Proxy, reflect::Reflect, regexp::RegExp, @@ -182,7 +184,8 @@ pub fn init(context: &mut Context) { AggregateError, Reflect, Generator, - GeneratorFunction + GeneratorFunction, + Promise }; #[cfg(feature = "intl")] diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs new file mode 100644 index 00000000000..2ca33b60183 --- /dev/null +++ b/boa_engine/src/builtins/promise/mod.rs @@ -0,0 +1,710 @@ +//! This module implements the global `Promise` object. + +#[cfg(test)] +mod tests; + +mod promise_job; + +use self::promise_job::PromiseJob; +use super::JsArgs; +use crate::{ + builtins::BuiltIn, + context::intrinsics::StandardConstructors, + job::JobCallback, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsObject, ObjectData, + }, + property::Attribute, + value::JsValue, + Context, JsResult, +}; +use boa_gc::{Finalize, Gc, Trace}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PromiseState { + Pending, + Fulfilled, + Rejected, +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct Promise { + promise_result: Option, + #[unsafe_ignore_trace] + promise_state: PromiseState, + promise_fulfill_reactions: Vec, + promise_reject_reactions: Vec, + promise_is_handled: bool, +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct ReactionRecord { + promise_capability: Option, + #[unsafe_ignore_trace] + reaction_type: ReactionType, + handler: Option, +} + +#[derive(Debug, Clone, Copy)] +enum ReactionType { + Fulfill, + Reject, +} + +#[derive(Debug, Clone, Trace, Finalize)] +struct PromiseCapability { + promise: JsValue, + resolve: JsValue, + reject: JsValue, +} + +#[derive(Debug, Trace, Finalize)] +struct PromiseCapabilityCaptures { + promise_capability: Gc>, +} + +#[derive(Debug, Trace, Finalize)] +struct ReactionJobCaptures { + reaction: ReactionRecord, + argument: JsValue, +} + +impl PromiseCapability { + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-newpromisecapability + fn new(c: &JsValue, context: &mut Context) -> JsResult { + match c.as_constructor() { + // 1. If IsConstructor(C) is false, throw a TypeError exception. + None => context.throw_type_error("PromiseCapability: expected constructor"), + Some(c) => { + let c = c.clone(); + + // 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 }. + let promise_capability = Gc::new(boa_gc::Cell::new(Self { + promise: JsValue::Undefined, + reject: JsValue::Undefined, + resolve: JsValue::Undefined, + })); + + // 4. Let executorClosure be a new Abstract Closure with parameters (resolve, reject) that captures promiseCapability and performs the following steps when called: + // 5. Let executor be CreateBuiltinFunction(executorClosure, 2, "", « »). + let executor = FunctionBuilder::closure_with_captures( + context, + |_this, args: &[JsValue], captures: &mut PromiseCapabilityCaptures, context| { + let promise_capability: &mut Self = + &mut captures.promise_capability.try_borrow_mut().expect("msg"); + + // a. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception. + if !promise_capability.resolve.is_undefined() { + return context.throw_type_error( + "promiseCapability.[[Resolve]] is not undefined", + ); + } + + // b. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception. + if !promise_capability.reject.is_undefined() { + return context + .throw_type_error("promiseCapability.[[Reject]] is not undefined"); + } + + let resolve = args.get_or_undefined(0); + let reject = args.get_or_undefined(1); + + // c. Set promiseCapability.[[Resolve]] to resolve. + promise_capability.resolve = resolve.clone(); + + // d. Set promiseCapability.[[Reject]] to reject. + promise_capability.reject = reject.clone(); + + // e. Return undefined. + Ok(JsValue::Undefined) + }, + PromiseCapabilityCaptures { + promise_capability: promise_capability.clone(), + }, + ) + .name("") + .length(2) + .build() + .into(); + + // 6. Let promise be ? Construct(C, « executor »). + let promise = c.construct(&[executor], &c.clone().into(), context)?; + + let promise_capability: &mut Self = + &mut promise_capability.try_borrow_mut().expect("msg"); + + let resolve = promise_capability.resolve.clone(); + let reject = promise_capability.reject.clone(); + + // 7. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception. + if !resolve.is_callable() { + return context + .throw_type_error("promiseCapability.[[Resolve]] is not callable"); + } + + // 8. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception. + if !reject.is_callable() { + return context + .throw_type_error("promiseCapability.[[Reject]] is not callable"); + } + + // 9. Set promiseCapability.[[Promise]] to promise. + promise_capability.reject = promise; + + // 10. Return promiseCapability. + Ok(promise_capability.clone()) + } + } + } +} + +impl BuiltIn for Promise { + const NAME: &'static str = "Promise"; + + const ATTRIBUTE: Attribute = Attribute::WRITABLE + .union(Attribute::NON_ENUMERABLE) + .union(Attribute::CONFIGURABLE); + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().promise().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .method(Self::then, "then", 1) + .build() + .conv::() + .pipe(Some) + } +} + +#[derive(Debug)] +struct ResolvingFunctionsRecord { + resolve: JsValue, + reject: JsValue, +} + +#[derive(Debug, Trace, Finalize)] +struct RejectResolveCaptures { + promise: JsObject, + already_resolved: bool, +} + +impl Promise { + const LENGTH: usize = 1; + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise-executor + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return context.throw_type_error("Promise NewTarget cannot be undefined"); + } + + let executor = args.get_or_undefined(0); + + // 2. If IsCallable(executor) is false, throw a TypeError exception. + if !executor.is_callable() { + return context.throw_type_error("Promise executor is not callable"); + } + + // 3. Let promise be ? OrdinaryCreateFromConstructor(NewTarget, "%Promise.prototype%", « [[PromiseState]], [[PromiseResult]], [[PromiseFulfillReactions]], [[PromiseRejectReactions]], [[PromiseIsHandled]] »). + let promise = + get_prototype_from_constructor(new_target, StandardConstructors::promise, context)?; + + let promise = JsObject::from_proto_and_data( + promise, + ObjectData::promise(Self { + promise_result: None, + // 4. Set promise.[[PromiseState]] to pending. + promise_state: PromiseState::Pending, + // 5. Set promise.[[PromiseFulfillReactions]] to a new empty List. + promise_fulfill_reactions: Vec::new(), + // 6. Set promise.[[PromiseRejectReactions]] to a new empty List. + promise_reject_reactions: Vec::new(), + // 7. Set promise.[[PromiseIsHandled]] to false. + promise_is_handled: false, + }), + ); + + // // 8. Let resolvingFunctions be CreateResolvingFunctions(promise). + let resolving_functions = Self::create_resolving_functions(&promise, context); + + // // 9. Let completion Completion(Call(executor, undefined, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)be ). + let completion = context.call( + executor, + &JsValue::Undefined, + &[ + resolving_functions.resolve, + resolving_functions.reject.clone(), + ], + ); + + // 10. If completion is an abrupt completion, then + if let Err(value) = completion { + // a. Perform ? Call(resolvingFunctions.[[Reject]], undefined, « completion.[[Value]] »). + context.call(&resolving_functions.reject, &JsValue::Undefined, &[value])?; + } + + // 11. Return promise. + promise.conv::().pipe(Ok) + } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createresolvingfunctions + fn create_resolving_functions( + promise: &JsObject, + context: &mut Context, + ) -> ResolvingFunctionsRecord { + // TODO: can this not be a rust struct? + // 1. Let alreadyResolved be the Record { [[Value]]: false }. + let already_resolved = false; + + // 5. Set resolve.[[Promise]] to promise. + // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. + let resolve_captures = RejectResolveCaptures { + already_resolved, + 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 = FunctionBuilder::closure_with_captures( + context, + |_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 { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + *already_resolved = 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 = + context.construct_type_error("SameValue(resolution, promise) is true"); + + // b. Perform RejectPromise(promise, selfResolutionError). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .reject(&self_resolution_error, context)?; + + // c. Return undefined. + return Ok(JsValue::Undefined); + } + + // 8. If Type(resolution) is not Object, then + if !resolution.is_object() { + // a. Perform FulfillPromise(promise, resolution). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .fulfill(resolution, context)?; + + // b. Return undefined. + return Ok(JsValue::Undefined); + } + + // 9. Let then be Completion(Get(resolution, "then")). + let then = resolution + .as_object() + .unwrap_or_else(|| unreachable!()) + .get("then", context); + + let then = match then { + // 10. If then is an abrupt completion, then + Err(value) => { + // a. Perform RejectPromise(promise, then.[[Value]]). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .reject(&value, context)?; + + // b. Return undefined. + return Ok(JsValue::Undefined); + } + Ok(then) => then, + }; + + // 11. Let thenAction be then.[[Value]]. + let then_action = then + .as_object() + .expect("resolution.[[then]] should be an object") + .get("Value", context)?; + + // 12. If IsCallable(thenAction) is false, then + if !then_action.is_callable() { + // a. Perform FulfillPromise(promise, resolution). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .fulfill(resolution, context)?; + + // b. Return undefined. + return Ok(JsValue::Undefined); + } + + // 13. Let thenJobCallback be HostMakeJobCallback(thenAction). + let then_job_callback = JobCallback::make_job_callback(then_action); + + // 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). + let job: JobCallback = PromiseJob::new_promise_resolve_thenable_job( + promise.clone(), + resolution.clone(), + then_job_callback, + context, + ); + + // 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + context.host_enqueue_promise_job(job); + + // 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 = FunctionBuilder::closure_with_captures( + context, + |_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 { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + *already_resolved = true; + + let reason = args.get_or_undefined(0); + // 7. Perform RejectPromise(promise, reason). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .reject(reason, context)?; + + // 8. Return undefined. + Ok(JsValue::Undefined) + }, + reject_captures, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }. + let resolve = resolve.conv::(); + let reject = reject.conv::(); + ResolvingFunctionsRecord { resolve, reject } + } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-fulfillpromise + pub fn fulfill(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { + // 1. Assert: The value of promise.[[PromiseState]] is pending. + assert_eq!( + self.promise_state, + PromiseState::Pending, + "promise was not pending" + ); + + // 2. Let reactions be promise.[[PromiseFulfillReactions]]. + let reactions = &self.promise_fulfill_reactions; + + // 7. Perform TriggerPromiseReactions(reactions, value). + Self::trigger_promise_reactions(reactions, value, context); + // reordering this statement does not affect the semantics + + // 3. Set promise.[[PromiseResult]] to value. + self.promise_result = Some(value.clone()); + + // 4. Set promise.[[PromiseFulfillReactions]] to undefined. + self.promise_fulfill_reactions = Vec::new(); + + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + self.promise_reject_reactions = Vec::new(); + + // 6. Set promise.[[PromiseState]] to fulfilled. + self.promise_state = PromiseState::Fulfilled; + + // 8. Return unused. + Ok(()) + } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-rejectpromise + pub fn reject(&mut self, reason: &JsValue, context: &mut Context) -> JsResult<()> { + // 1. Assert: The value of promise.[[PromiseState]] is pending. + match self.promise_state { + PromiseState::Pending => (), + _ => return context.throw_error("Expected promise.[[PromiseState]] to be pending"), + } + + // 2. Let reactions be promise.[[PromiseRejectReactions]]. + let reactions = &self.promise_reject_reactions; + + // 8. Perform TriggerPromiseReactions(reactions, reason). + Self::trigger_promise_reactions(reactions, reason, context); + // reordering this statement does not affect the semantics + + // 3. Set promise.[[PromiseResult]] to reason. + self.promise_result = Some(reason.clone()); + + // 4. Set promise.[[PromiseFulfillReactions]] to undefined. + self.promise_fulfill_reactions = Vec::new(); + + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + self.promise_reject_reactions = Vec::new(); + + // 6. Set promise.[[PromiseState]] to rejected. + self.promise_state = PromiseState::Rejected; + + // 7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject"). + if !self.promise_is_handled { + // TODO + } + + // 9. Return unused. + Ok(()) + } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-triggerpromisereactions + pub fn trigger_promise_reactions( + reactions: &[ReactionRecord], + 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 = + PromiseJob::new_promise_reaction_job(reaction.clone(), argument.clone(), context); + + // b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + context.host_enqueue_promise_job(job); + } + + // 2. Return unused. + } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.then + pub 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_obj = match promise.as_object() { + Some(obj) => obj, + None => return context.throw_type_error("IsPromise(promise) is false"), + }; + + // 3. Let C be ? SpeciesConstructor(promise, %Promise%). + let c = promise_obj.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).clone(); + let on_rejected = args.get_or_undefined(1).clone(); + + // 5. Return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability). + promise_obj + .borrow_mut() + .as_promise_mut() + .expect("IsPromise(promise) is false") + .perform_promise_then(on_fulfilled, on_rejected, Some(result_capability), context) + .pipe(Ok) + } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performpromisethen + fn perform_promise_then( + &mut self, + on_fulfilled: JsValue, + on_rejected: JsValue, + result_capability: Option, + context: &mut Context, + ) -> JsValue { + // 1. Assert: IsPromise(promise) is true. + + // 2. If resultCapability is not present, then + // a. Set resultCapability to undefined. + + let on_fulfilled_job_callback: Option = + // 3. If IsCallable(onFulfilled) is false, then + if on_fulfilled.is_callable() { + // 4. Else, + // a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled). + Some(JobCallback::make_job_callback(on_fulfilled)) + } else { + // a. Let onFulfilledJobCallback be empty. + None + }; + + let on_rejected_job_callback: Option = + // 5. If IsCallable(onRejected) is false, then + if on_rejected.is_callable() { + // 6. Else, + // a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected). + Some(JobCallback::make_job_callback(on_rejected)) + } else { + // a. Let onRejectedJobCallback be empty. + None + }; + + // 7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }. + let fulfill_reaction = ReactionRecord { + promise_capability: result_capability.clone(), + reaction_type: ReactionType::Fulfill, + handler: on_fulfilled_job_callback, + }; + + // 8. Let rejectReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Reject, [[Handler]]: onRejectedJobCallback }. + let reject_reaction = ReactionRecord { + promise_capability: result_capability.clone(), + reaction_type: ReactionType::Reject, + handler: on_rejected_job_callback, + }; + + match self.promise_state { + // 9. If promise.[[PromiseState]] is pending, then + PromiseState::Pending => { + // a. Append fulfillReaction as the last element of the List that is promise.[[PromiseFulfillReactions]]. + self.promise_fulfill_reactions.push(fulfill_reaction); + + // b. Append rejectReaction as the last element of the List that is promise.[[PromiseRejectReactions]]. + self.promise_reject_reactions.push(reject_reaction); + } + + // 10. Else if promise.[[PromiseState]] is fulfilled, then + PromiseState::Fulfilled => { + // a. Let value be promise.[[PromiseResult]]. + let value = self + .promise_result + .clone() + .expect("promise.[[PromiseResult]] cannot be empty"); + + // b. Let fulfillJob be NewPromiseReactionJob(fulfillReaction, value). + let fulfill_job = + PromiseJob::new_promise_reaction_job(fulfill_reaction, value, context); + + // c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]). + context.host_enqueue_promise_job(fulfill_job); + } + + // 11. Else, + // a. Assert: The value of promise.[[PromiseState]] is rejected. + PromiseState::Rejected => { + // b. Let reason be promise.[[PromiseResult]]. + let reason = self + .promise_result + .clone() + .expect("promise.[[PromiseResult]] cannot be empty"); + + // c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle"). + if !self.promise_is_handled { + // HostPromiseRejectionTracker(promise, "handle") + todo!(); // TODO + } + + // d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason). + let reject_job = + PromiseJob::new_promise_reaction_job(reject_reaction, reason, context); + + // e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]). + context.host_enqueue_promise_job(reject_job); + + // 12. Set promise.[[PromiseIsHandled]] to true. + self.promise_is_handled = true; + } + } + + match result_capability { + // 13. If resultCapability is undefined, then + // a. Return undefined. + None => JsValue::Undefined, + + // 14. Else, + // a. Return resultCapability.[[Promise]]. + Some(result_capability) => result_capability.promise.clone(), + } + } +} diff --git a/boa_engine/src/builtins/promise/promise_job.rs b/boa_engine/src/builtins/promise/promise_job.rs new file mode 100644 index 00000000000..4d9fe026392 --- /dev/null +++ b/boa_engine/src/builtins/promise/promise_job.rs @@ -0,0 +1,182 @@ +use super::{Promise, PromiseCapability, ReactionJobCaptures}; +use crate::{ + builtins::promise::{ReactionRecord, ReactionType}, + job::JobCallback, + object::{FunctionBuilder, JsObject}, + Context, JsValue, +}; +use boa_gc::{Finalize, Trace}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct PromiseJob; + +impl PromiseJob { + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-newpromisereactionjob + pub(crate) fn new_promise_reaction_job( + reaction: ReactionRecord, + argument: JsValue, + context: &mut Context, + ) -> JobCallback { + // 1. Let job be a new Job Abstract Closure with no parameters that captures reaction and argument and performs the following steps when called: + let job = FunctionBuilder::closure_with_captures( + context, + |_this, _args, captures, context| { + let ReactionJobCaptures { reaction, argument } = captures; + + let ReactionRecord { + // a. Let promiseCapability be reaction.[[Capability]]. + promise_capability, + // b. Let type be reaction.[[Type]]. + reaction_type, + // c. Let handler be reaction.[[Handler]]. + handler, + } = reaction; + + let handler_result = match handler { + // d. If handler is empty, then + None => match reaction_type { + // i. If type is Fulfill, let handlerResult be NormalCompletion(argument). + ReactionType::Fulfill => Ok(argument.clone()), + // ii. Else, + // 1. Assert: type is Reject. + ReactionType::Reject => { + // 2. Let handlerResult be ThrowCompletion(argument). + Ok(context.construct_error("argument")) // TODO: convert argument to string, somehow + } + }, + // e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)). + Some(handler) => { + handler.call_job_callback(&JsValue::Undefined, &[argument.clone()], context) + } + }; + + match promise_capability { + None => { + // f. If promiseCapability is undefined, then + // i. Assert: handlerResult is not an abrupt completion. + assert!( + handler_result.is_ok(), + "Assertion: failed" + ); + + // ii. Return empty. + Ok(JsValue::Undefined) + } + Some(promise_capability_record) => { + // g. Assert: promiseCapability is a PromiseCapability Record. + let PromiseCapability { + promise: _, + resolve, + reject, + } = promise_capability_record; + + match handler_result { + // h. If handlerResult is an abrupt completion, then + Err(value) => { + // i. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »). + context.call(reject, &JsValue::Undefined, &[value]) + } + + // i. Else, + Ok(value) => { + // i. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »). + context.call(resolve, &JsValue::Undefined, &[value]) + } + } + } + } + }, + ReactionJobCaptures { reaction, argument }, + ) + .build() + .into(); + + // 2. Let handlerRealm be null. + // 3. If reaction.[[Handler]] is not empty, then + // a. Let getHandlerRealmResult be Completion(GetFunctionRealm(reaction.[[Handler]].[[Callback]])). + // b. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]]. + // c. Else, set handlerRealm to the current Realm Record. + // d. NOTE: handlerRealm is never null unless the handler is undefined. When the handler is a revoked Proxy and no ECMAScript code runs, handlerRealm is used to create error objects. + // 4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }. + JobCallback::make_job_callback(job) + } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob + pub(crate) fn new_promise_resolve_thenable_job( + promise_to_resolve: JsObject, + thenable: JsValue, + then: JobCallback, + context: &mut Context, + ) -> JobCallback { + // 1. Let job be a new Job Abstract Closure with no parameters that captures promiseToResolve, thenable, and then and performs the following steps when called: + let job = FunctionBuilder::closure_with_captures( + context, + |_this: &JsValue, _args: &[JsValue], captures, context: &mut Context| { + let JobCapture { + promise_to_resolve, + thenable, + then, + } = captures; + + // a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve). + let resolving_functions = + Promise::create_resolving_functions(promise_to_resolve, context); + + // b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)). + let then_call_result = then.call_job_callback( + thenable, + &[ + resolving_functions.resolve, + resolving_functions.reject.clone(), + ], + context, + ); + + // c. If thenCallResult is an abrupt completion, then + if let Err(value) = then_call_result { + // i. Return ? Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »). + return context.call( + &resolving_functions.reject, + &JsValue::Undefined, + &[value], + ); + } + + // d. Return ? thenCallResult. + then_call_result + }, + JobCapture::new(promise_to_resolve, thenable, then), + ) + .build(); + + // 2. Let getThenRealmResult be Completion(GetFunctionRealm(then.[[Callback]])). + // 3. If getThenRealmResult is a normal completion, let thenRealm be getThenRealmResult.[[Value]]. + // 4. Else, let thenRealm be the current Realm Record. + // 5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked Proxy and no code runs, thenRealm is used to create error objects. + // 6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }. + JobCallback::make_job_callback(job.into()) + } +} + +#[derive(Debug, Trace, Finalize)] +struct JobCapture { + promise_to_resolve: JsObject, + thenable: JsValue, + then: JobCallback, +} + +impl JobCapture { + fn new(promise_to_resolve: JsObject, thenable: JsValue, then: JobCallback) -> Self { + Self { + promise_to_resolve, + thenable, + then, + } + } +} diff --git a/boa_engine/src/builtins/promise/tests.rs b/boa_engine/src/builtins/promise/tests.rs new file mode 100644 index 00000000000..f32dcd2ade9 --- /dev/null +++ b/boa_engine/src/builtins/promise/tests.rs @@ -0,0 +1,19 @@ +use crate::{forward, Context}; + +#[test] +fn promise() { + let mut context = Context::default(); + let init = r#" + let count = 0; + const promise = new Promise((resolve, reject) => { + count += 1; + resolve(undefined); + }).then((_) => (count += 1)); + count += 1; + count; + "#; + let result = context.eval(init).unwrap(); + assert_eq!(result.as_number(), Some(2_f64)); + let after_completion = forward(&mut context, "count"); + assert_eq!(after_completion, String::from("3")); +} diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index 43f4887cb34..eec70548f5b 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -110,6 +110,7 @@ pub struct StandardConstructors { array_buffer: StandardConstructor, data_view: StandardConstructor, date_time_format: StandardConstructor, + promise: StandardConstructor, } impl Default for StandardConstructors { @@ -165,6 +166,7 @@ impl Default for StandardConstructors { array_buffer: StandardConstructor::default(), data_view: StandardConstructor::default(), date_time_format: StandardConstructor::default(), + promise: StandardConstructor::default(), }; // The value of `Array.prototype` is the Array prototype object. @@ -372,6 +374,11 @@ impl StandardConstructors { pub fn date_time_format(&self) -> &StandardConstructor { &self.date_time_format } + + #[inline] + pub fn promise(&self) -> &StandardConstructor { + &self.promise + } } /// Cached intrinsic objects diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index e95bc92e09d..a940483281a 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -5,6 +5,8 @@ pub mod intrinsics; #[cfg(feature = "intl")] mod icu; +use std::collections::VecDeque; + use intrinsics::{IntrinsicObjects, Intrinsics}; #[cfg(feature = "console")] @@ -13,6 +15,7 @@ use crate::{ builtins::{self, function::NativeFunctionSignature}, bytecompiler::ByteCompiler, class::{Class, ClassBuilder}, + job::JobCallback, object::{FunctionBuilder, GlobalPropertyMap, JsObject, ObjectData}, property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, @@ -97,6 +100,8 @@ pub struct Context { icu: icu::Icu, pub(crate) vm: Vm, + + pub(crate) promise_job_queue: VecDeque, } impl Default for Context { @@ -707,10 +712,17 @@ impl Context { self.realm.set_global_binding_number(); let result = self.run(); self.vm.pop_frame(); + self.run_queued_jobs(); let (result, _) = result?; Ok(result) } + fn run_queued_jobs(&mut self) { + while let Some(job) = self.promise_job_queue.pop_front() { + job.run(self); + } + } + /// Return the intrinsic constructors and objects. #[inline] pub fn intrinsics(&self) -> &Intrinsics { @@ -728,6 +740,18 @@ impl Context { pub(crate) fn icu(&self) -> &icu::Icu { &self.icu } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob + pub fn host_enqueue_promise_job(&mut self, job: JobCallback /* , realm: Realm */) { + // If realm is not null ... + // TODO + // Let scriptOrModule be ... + // TODO + self.promise_job_queue.push_back(job); + } } /// Builder for the [`Context`] type. /// @@ -795,6 +819,7 @@ impl ContextBuilder { icu::Icu::new(Box::new(icu_testdata::get_provider())) .expect("Failed to initialize default icu data.") }), + promise_job_queue: VecDeque::new(), }; // Add new builtIns to Context Realm diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs new file mode 100644 index 00000000000..c79c1fea684 --- /dev/null +++ b/boa_engine/src/job.rs @@ -0,0 +1,37 @@ +use crate::{Context, JsResult, JsValue}; + +use gc::{Finalize, Trace}; + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct JobCallback { + callback: JsValue, +} + +impl JobCallback { + pub fn make_job_callback(callback: JsValue) -> Self { + Self { callback } + } + + pub fn call_job_callback( + &self, + v: &JsValue, + argument_list: &[JsValue], + context: &mut Context, + ) -> JsResult { + let callback = match self.callback { + JsValue::Object(ref object) if object.is_callable() => object.clone(), + _ => panic!("Callback is not a callable object"), + }; + + callback.__call__(v, argument_list, context) + } + + pub fn run(&self, context: &mut Context) { + let callback = match self.callback { + JsValue::Object(ref object) if object.is_callable() => object.clone(), + _ => panic!("Callback is not a callable object"), + }; + + let _callback_result = callback.__call__(&JsValue::Undefined, &[], context); + } +} diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 8f550f5f824..626db695619 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -77,6 +77,7 @@ pub mod bytecompiler; pub mod class; pub mod context; pub mod environments; +pub mod job; pub mod object; pub mod property; pub mod realm; diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 5eb4d6e9722..2bd8d95c37d 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -40,7 +40,7 @@ use crate::{ set::set_iterator::SetIterator, string::string_iterator::StringIterator, typed_array::integer_indexed_object::IntegerIndexed, - DataView, Date, RegExp, + DataView, Date, Promise, RegExp, }, context::intrinsics::StandardConstructor, property::{Attribute, PropertyDescriptor, PropertyKey}, @@ -172,6 +172,7 @@ pub enum ObjectKind { IntegerIndexed(IntegerIndexed), #[cfg(feature = "intl")] DateTimeFormat(Box), + Promise(Promise), } impl ObjectData { @@ -255,6 +256,14 @@ impl ObjectData { } } + /// Create the `Promise` object data + pub fn promise(promise: Promise) -> Self { + Self { + kind: ObjectKind::Promise(promise), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + /// Create the `ForInIterator` object data pub fn for_in_iterator(for_in_iterator: ForInIterator) -> Self { Self { @@ -473,6 +482,7 @@ impl Display for ObjectKind { Self::DataView(_) => "DataView", #[cfg(feature = "intl")] Self::DateTimeFormat(_) => "DateTimeFormat", + Self::Promise(_) => "Promise", }) } } @@ -1203,6 +1213,41 @@ impl Object { } } + /// Checks if it is a `Promise` object. + #[inline] + pub fn is_promise(&self) -> bool { + matches!( + self.data, + ObjectData { + kind: ObjectKind::Promise(_), + .. + } + ) + } + + /// Gets the promise data if the object is a promise. + #[inline] + pub fn as_promise(&self) -> Option<&Promise> { + match self.data { + ObjectData { + kind: ObjectKind::Promise(ref promise), + .. + } => Some(promise), + _ => None, + } + } + + #[inline] + pub fn as_promise_mut(&mut self) -> Option<&mut Promise> { + match self.data { + ObjectData { + kind: ObjectKind::Promise(ref mut promise), + .. + } => Some(promise), + _ => None, + } + } + /// Return `true` if it is a native object and the native type is `T`. #[inline] pub fn is(&self) -> bool diff --git a/test_ignore.txt b/test_ignore.txt index 9fa40b450e6..321c3f7fdba 100644 --- a/test_ignore.txt +++ b/test_ignore.txt @@ -1,6 +1,5 @@ // Not implemented yet: flag:module -flag:async // Non-implemented features: feature:json-modules