From 32a105b8173654c15c55315bf6ad11b94a0171fa Mon Sep 17 00:00:00 2001 From: Iban Eguia Date: Wed, 8 Jun 2022 22:57:10 +0200 Subject: [PATCH 01/13] Added support for promises and an execution stack Co-authored-by: aaronmunsters --- Cargo.lock | 7 + boa_engine/Cargo.toml | 1 + boa_engine/src/builtins/mod.rs | 5 +- boa_engine/src/builtins/promise/mod.rs | 731 ++++++++++++++++++ .../src/builtins/promise/promise_job.rs | 194 +++++ boa_engine/src/builtins/promise/tests.rs | 19 + boa_engine/src/context/intrinsics.rs | 7 + boa_engine/src/context/mod.rs | 31 + boa_engine/src/job.rs | 39 + boa_engine/src/lib.rs | 1 + boa_engine/src/object/mod.rs | 47 +- 11 files changed, 1080 insertions(+), 2 deletions(-) create mode 100644 boa_engine/src/builtins/promise/mod.rs create mode 100644 boa_engine/src/builtins/promise/promise_job.rs create mode 100644 boa_engine/src/builtins/promise/tests.rs create mode 100644 boa_engine/src/job.rs diff --git a/Cargo.lock b/Cargo.lock index ea54e6d4faf..d68fd7079a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,7 @@ dependencies = [ "num-integer", "num-traits", "once_cell", + "queues", "rand", "regress", "rustc-hash", @@ -1214,6 +1215,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "queues" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1475abae4f8ad4998590fe3acfe20104f0a5d48fc420c817cd2c09c3f56151f0" + [[package]] name = "quote" version = "1.0.18" diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 875472c7b91..06ca4493376 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -49,6 +49,7 @@ fast-float = "0.2.0" unicode-normalization = "0.1.19" dyn-clone = "1.0.5" once_cell = "1.12.0" +queues = "1.0.2" tap = "1.0.1" icu_locale_canonicalizer = { version = "0.6.0", features = ["serde"], optional = true } icu_locid = { version = "0.6.0", features = ["serde"], optional = true } 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..ba8e8296aef --- /dev/null +++ b/boa_engine/src/builtins/promise/mod.rs @@ -0,0 +1,731 @@ +//! This module implements the global `Promise` object. + +#![allow(dead_code, unused_results, unused_variables)] + +#[cfg(test)] +mod tests; + +mod promise_job; + +use boa_gc::{Finalize, Gc, Trace}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +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 self::promise_job::PromiseJob; + +use super::JsArgs; + +/// JavaScript `Array` built-in implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Array; + +#[derive(Debug, Clone, Trace, Finalize)] +enum PromiseState { + Pending, + Fulfilled, + Rejected, +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct Promise { + promise_result: Option, + 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, + reaction_type: ReactionType, + handler: Option, +} + +#[derive(Debug, Clone, Trace, Finalize)] +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>, +} + +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) + } +} + +struct ResolvedRecord { + value: bool, +} + +struct ResolvingFunctionsRecord { + resolve: JsValue, + reject: JsValue, +} + +#[derive(Debug, Trace, Finalize)] +struct RejectResolveCaptures { + promise: JsObject, + already_resolved: JsObject, +} + +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![], + // 6. Set promise.[[PromiseRejectReactions]] to a new empty List. + promise_reject_reactions: vec![], + // 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]] »). + let _reject_result = + 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, + ) -> JsResult { + // TODO: can this not be a rust struct? + // 1. Let alreadyResolved be the Record { [[Value]]: false }. + let already_resolved = JsObject::empty(); + already_resolved.set("Value", JsValue::from(false), true, context)?; + + 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 = 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 + .get("Value", context)? + .as_boolean() + .unwrap_or(false) + { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + already_resolved.set("Value", true, true, context)?; + + 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("rsolution.[[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(Box::new(job)); + + // 16. Return undefined. + Ok(JsValue::Undefined) + }, + resolve_captures, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // 5. Set resolve.[[Promise]] to promise. + resolve.set("Promise", promise.clone(), true, context)?; + + // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. + resolve.set("AlreadyResolved", already_resolved.clone(), true, context)?; + + let reject_captures = RejectResolveCaptures { + promise: promise.clone(), + already_resolved: already_resolved.clone(), + }; + + // 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 + .get("Value", context)? + .as_boolean() + .unwrap_or(false) + { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + already_resolved.set("Value", true, true, context)?; + + 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(); + + // 10. Set reject.[[Promise]] to promise. + reject.set("Promise", promise.clone(), true, context)?; + + // 11. Set reject.[[AlreadyResolved]] to alreadyResolved. + reject.set("AlreadyResolved", already_resolved, true, context)?; + + // 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }. + let resolve = resolve.conv::(); + let reject = reject.conv::(); + Ok(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. + match self.promise_state { + PromiseState::Pending => (), + _ => return context.throw_error("Expected promise.[[PromiseState]] to be 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![]; + + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + self.promise_reject_reactions = vec![]; + + // 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![]; + + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + self.promise_reject_reactions = vec![]; + + // 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(Box::new(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(Box::new(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(Box::new(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..241ee6df830 --- /dev/null +++ b/boa_engine/src/builtins/promise/promise_job.rs @@ -0,0 +1,194 @@ +use gc::{Finalize, Trace}; + +use crate::{ + builtins::promise::{ReactionRecord, ReactionType}, + job::JobCallback, + object::{FunctionBuilder, JsObject}, + Context, JsValue, +}; + +use super::{Promise, PromiseCapability}; + +pub(crate) struct PromiseJob; + +#[derive(Debug, Trace, Finalize)] +struct ReactionJobCaptures { + reaction: ReactionRecord, + argument: JsValue, +} + +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 => + // i. If type is Fulfill, let handlerResult be NormalCompletion(argument). + { + if let ReactionType::Fulfill = reaction_type { + Ok(argument.clone()) + } else { + // ii. Else, + // 1. Assert: type is Reject. + match reaction_type { + ReactionType::Reject => (), + ReactionType::Fulfill => panic!(), + } + // 2. Let handlerResult be ThrowCompletion(argument). + Ok(context.construct_error("ThrowCompletion(argument)")) + } + } + // 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 + if handler_result.is_err() { + // i. Assert: handlerResult is not an abrupt completion. + panic!("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..289cf43883b 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -13,6 +13,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, @@ -24,6 +25,7 @@ use crate::{ use boa_gc::Gc; use boa_interner::{Interner, Sym}; use boa_profiler::Profiler; +use queues::{queue, IsQueue, Queue}; #[cfg(feature = "intl")] use icu_provider::DataError; @@ -97,6 +99,8 @@ pub struct Context { icu: icu::Icu, pub(crate) vm: Vm, + + pub(crate) promise_job_queue: Queue>, } impl Default for Context { @@ -707,10 +711,21 @@ 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 self.promise_job_queue.size() != 0 { + let job = self + .promise_job_queue + .remove() + .expect("Job Queue should not be empty"); + job.run(self); + } + } + /// Return the intrinsic constructors and objects. #[inline] pub fn intrinsics(&self) -> &Intrinsics { @@ -728,6 +743,21 @@ 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: Box /* , realm: Realm */) { + // If realm is not null ... + // TODO + // Let scriptOrModule be ... + // TODO + match self.promise_job_queue.add(job) { + Ok(Some(_)) | Err(_) => panic!("Promise queue error"), + _ => (), + } + } } /// Builder for the [`Context`] type. /// @@ -795,6 +825,7 @@ impl ContextBuilder { icu::Icu::new(Box::new(icu_testdata::get_provider())) .expect("Failed to initialize default icu data.") }), + promise_job_queue: queue![], }; // 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..9d72ef59ba9 --- /dev/null +++ b/boa_engine/src/job.rs @@ -0,0 +1,39 @@ +use crate::{Context, JsResult, JsValue}; + +use gc::{Finalize, Trace}; + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct JobCallback { + callback: Box, +} + +impl JobCallback { + pub fn make_job_callback(callback: JsValue) -> Self { + Self { + callback: Box::new(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 From 7b47697407930dc945d515465d1b586784aeb6d2 Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Wed, 8 Jun 2022 23:03:11 +0200 Subject: [PATCH 02/13] Removed the queues dependency, removed ignored async tests --- Cargo.lock | 7 ------- boa_engine/Cargo.toml | 1 - boa_engine/src/builtins/promise/mod.rs | 8 ++++---- boa_engine/src/context/mod.rs | 20 +++++++------------- test_ignore.txt | 1 - 5 files changed, 11 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d68fd7079a3..ea54e6d4faf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,6 @@ dependencies = [ "num-integer", "num-traits", "once_cell", - "queues", "rand", "regress", "rustc-hash", @@ -1215,12 +1214,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "queues" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1475abae4f8ad4998590fe3acfe20104f0a5d48fc420c817cd2c09c3f56151f0" - [[package]] name = "quote" version = "1.0.18" diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 06ca4493376..875472c7b91 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -49,7 +49,6 @@ fast-float = "0.2.0" unicode-normalization = "0.1.19" dyn-clone = "1.0.5" once_cell = "1.12.0" -queues = "1.0.2" tap = "1.0.1" icu_locale_canonicalizer = { version = "0.6.0", features = ["serde"], optional = true } icu_locid = { version = "0.6.0", features = ["serde"], optional = true } diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index ba8e8296aef..b4777a27491 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -404,7 +404,7 @@ impl Promise { ); // 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). - context.host_enqueue_promise_job(Box::new(job)); + context.host_enqueue_promise_job(job); // 16. Return undefined. Ok(JsValue::Undefined) @@ -575,7 +575,7 @@ impl Promise { PromiseJob::new_promise_reaction_job(reaction.clone(), argument.clone(), context); // b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). - context.host_enqueue_promise_job(Box::new(job)); + context.host_enqueue_promise_job(job); } // 2. Return unused. @@ -688,7 +688,7 @@ impl Promise { PromiseJob::new_promise_reaction_job(fulfill_reaction, value, context); // c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]). - context.host_enqueue_promise_job(Box::new(fulfill_job)); + context.host_enqueue_promise_job(fulfill_job); } // 11. Else, @@ -711,7 +711,7 @@ impl Promise { PromiseJob::new_promise_reaction_job(reject_reaction, reason, context); // e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]). - context.host_enqueue_promise_job(Box::new(reject_job)); + context.host_enqueue_promise_job(reject_job); // 12. Set promise.[[PromiseIsHandled]] to true. self.promise_is_handled = true; diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 289cf43883b..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")] @@ -25,7 +27,6 @@ use crate::{ use boa_gc::Gc; use boa_interner::{Interner, Sym}; use boa_profiler::Profiler; -use queues::{queue, IsQueue, Queue}; #[cfg(feature = "intl")] use icu_provider::DataError; @@ -100,7 +101,7 @@ pub struct Context { pub(crate) vm: Vm, - pub(crate) promise_job_queue: Queue>, + pub(crate) promise_job_queue: VecDeque, } impl Default for Context { @@ -717,11 +718,7 @@ impl Context { } fn run_queued_jobs(&mut self) { - while self.promise_job_queue.size() != 0 { - let job = self - .promise_job_queue - .remove() - .expect("Job Queue should not be empty"); + while let Some(job) = self.promise_job_queue.pop_front() { job.run(self); } } @@ -748,15 +745,12 @@ impl Context { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob - pub fn host_enqueue_promise_job(&mut self, job: Box /* , realm: Realm */) { + pub fn host_enqueue_promise_job(&mut self, job: JobCallback /* , realm: Realm */) { // If realm is not null ... // TODO // Let scriptOrModule be ... // TODO - match self.promise_job_queue.add(job) { - Ok(Some(_)) | Err(_) => panic!("Promise queue error"), - _ => (), - } + self.promise_job_queue.push_back(job); } } /// Builder for the [`Context`] type. @@ -825,7 +819,7 @@ impl ContextBuilder { icu::Icu::new(Box::new(icu_testdata::get_provider())) .expect("Failed to initialize default icu data.") }), - promise_job_queue: queue![], + promise_job_queue: VecDeque::new(), }; // Add new builtIns to Context Realm 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 From 076f4f85062cd75efe707f03b0dff258273597d4 Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Thu, 9 Jun 2022 17:34:41 +0200 Subject: [PATCH 03/13] Implemented a bunch of requested fixes --- boa_engine/src/builtins/promise/mod.rs | 115 +++++++----------- .../src/builtins/promise/promise_job.rs | 72 +++++------ boa_engine/src/job.rs | 10 +- 3 files changed, 81 insertions(+), 116 deletions(-) diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index b4777a27491..2ca33b60183 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -1,16 +1,12 @@ //! This module implements the global `Promise` object. -#![allow(dead_code, unused_results, unused_variables)] - #[cfg(test)] mod tests; mod promise_job; -use boa_gc::{Finalize, Gc, Trace}; -use boa_profiler::Profiler; -use tap::{Conv, Pipe}; - +use self::promise_job::PromiseJob; +use super::JsArgs; use crate::{ builtins::BuiltIn, context::intrinsics::StandardConstructors, @@ -23,16 +19,11 @@ use crate::{ value::JsValue, Context, JsResult, }; +use boa_gc::{Finalize, Gc, Trace}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; -use self::promise_job::PromiseJob; - -use super::JsArgs; - -/// JavaScript `Array` built-in implementation. -#[derive(Debug, Clone, Copy)] -pub(crate) struct Array; - -#[derive(Debug, Clone, Trace, Finalize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PromiseState { Pending, Fulfilled, @@ -42,6 +33,7 @@ enum PromiseState { #[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, @@ -51,11 +43,12 @@ pub struct Promise { #[derive(Debug, Clone, Trace, Finalize)] pub struct ReactionRecord { promise_capability: Option, + #[unsafe_ignore_trace] reaction_type: ReactionType, handler: Option, } -#[derive(Debug, Clone, Trace, Finalize)] +#[derive(Debug, Clone, Copy)] enum ReactionType { Fulfill, Reject, @@ -73,6 +66,12 @@ struct PromiseCapabilityCaptures { promise_capability: Gc>, } +#[derive(Debug, Trace, Finalize)] +struct ReactionJobCaptures { + reaction: ReactionRecord, + argument: JsValue, +} + impl PromiseCapability { /// More information: /// - [ECMAScript reference][spec] @@ -97,7 +96,7 @@ impl PromiseCapability { // 5. Let executor be CreateBuiltinFunction(executorClosure, 2, "", « »). let executor = FunctionBuilder::closure_with_captures( context, - |this, args: &[JsValue], captures: &mut PromiseCapabilityCaptures, context| { + |_this, args: &[JsValue], captures: &mut PromiseCapabilityCaptures, context| { let promise_capability: &mut Self = &mut captures.promise_capability.try_borrow_mut().expect("msg"); @@ -190,10 +189,7 @@ impl BuiltIn for Promise { } } -struct ResolvedRecord { - value: bool, -} - +#[derive(Debug)] struct ResolvingFunctionsRecord { resolve: JsValue, reject: JsValue, @@ -202,7 +198,7 @@ struct ResolvingFunctionsRecord { #[derive(Debug, Trace, Finalize)] struct RejectResolveCaptures { promise: JsObject, - already_resolved: JsObject, + already_resolved: bool, } impl Promise { @@ -240,16 +236,16 @@ impl Promise { // 4. Set promise.[[PromiseState]] to pending. promise_state: PromiseState::Pending, // 5. Set promise.[[PromiseFulfillReactions]] to a new empty List. - promise_fulfill_reactions: vec![], + promise_fulfill_reactions: Vec::new(), // 6. Set promise.[[PromiseRejectReactions]] to a new empty List. - promise_reject_reactions: vec![], + 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)?; + 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( @@ -264,8 +260,7 @@ impl Promise { // 10. If completion is an abrupt completion, then if let Err(value) = completion { // a. Perform ? Call(resolvingFunctions.[[Reject]], undefined, « completion.[[Value]] »). - let _reject_result = - context.call(&resolving_functions.reject, &JsValue::Undefined, &[value]); + context.call(&resolving_functions.reject, &JsValue::Undefined, &[value])?; } // 11. Return promise. @@ -279,14 +274,15 @@ impl Promise { fn create_resolving_functions( promise: &JsObject, context: &mut Context, - ) -> JsResult { + ) -> ResolvingFunctionsRecord { // TODO: can this not be a rust struct? // 1. Let alreadyResolved be the Record { [[Value]]: false }. - let already_resolved = JsObject::empty(); - already_resolved.set("Value", JsValue::from(false), true, context)?; + let already_resolved = false; + // 5. Set resolve.[[Promise]] to promise. + // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. let resolve_captures = RejectResolveCaptures { - already_resolved: already_resolved.clone(), + already_resolved, promise: promise.clone(), }; @@ -295,7 +291,7 @@ impl Promise { // 4. Let resolve be CreateBuiltinFunction(stepsResolve, lengthResolve, "", « [[Promise]], [[AlreadyResolved]] »). let resolve = FunctionBuilder::closure_with_captures( context, - |this, args, captures, context| { + |_this, args, captures, context| { // https://tc39.es/ecma262/#sec-promise-resolve-functions // 1. Let F be the active function object. @@ -308,16 +304,12 @@ impl Promise { } = captures; // 5. If alreadyResolved.[[Value]] is true, return undefined. - if already_resolved - .get("Value", context)? - .as_boolean() - .unwrap_or(false) - { + if *already_resolved { return Ok(JsValue::Undefined); } // 6. Set alreadyResolved.[[Value]] to true. - already_resolved.set("Value", true, true, context)?; + *already_resolved = true; let resolution = args.get_or_undefined(0); @@ -376,7 +368,7 @@ impl Promise { // 11. Let thenAction be then.[[Value]]. let then_action = then .as_object() - .expect("rsolution.[[then]] should be an object") + .expect("resolution.[[then]] should be an object") .get("Value", context)?; // 12. If IsCallable(thenAction) is false, then @@ -416,15 +408,11 @@ impl Promise { .constructor(false) .build(); - // 5. Set resolve.[[Promise]] to promise. - resolve.set("Promise", promise.clone(), true, context)?; - - // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. - resolve.set("AlreadyResolved", already_resolved.clone(), true, context)?; - + // 10. Set reject.[[Promise]] to promise. + // 11. Set reject.[[AlreadyResolved]] to alreadyResolved. let reject_captures = RejectResolveCaptures { promise: promise.clone(), - already_resolved: already_resolved.clone(), + already_resolved, }; // 7. Let stepsReject be the algorithm steps defined in Promise Reject Functions. @@ -432,7 +420,7 @@ impl Promise { // 9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »). let reject = FunctionBuilder::closure_with_captures( context, - |this, args, captures, context| { + |_this, args, captures, context| { // https://tc39.es/ecma262/#sec-promise-reject-functions // 1. Let F be the active function object. @@ -445,16 +433,12 @@ impl Promise { } = captures; // 5. If alreadyResolved.[[Value]] is true, return undefined. - if already_resolved - .get("Value", context)? - .as_boolean() - .unwrap_or(false) - { + if *already_resolved { return Ok(JsValue::Undefined); } // 6. Set alreadyResolved.[[Value]] to true. - already_resolved.set("Value", true, true, context)?; + *already_resolved = true; let reason = args.get_or_undefined(0); // 7. Perform RejectPromise(promise, reason). @@ -474,16 +458,10 @@ impl Promise { .constructor(false) .build(); - // 10. Set reject.[[Promise]] to promise. - reject.set("Promise", promise.clone(), true, context)?; - - // 11. Set reject.[[AlreadyResolved]] to alreadyResolved. - reject.set("AlreadyResolved", already_resolved, true, context)?; - // 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }. let resolve = resolve.conv::(); let reject = reject.conv::(); - Ok(ResolvingFunctionsRecord { resolve, reject }) + ResolvingFunctionsRecord { resolve, reject } } /// More information: @@ -492,10 +470,11 @@ impl Promise { /// [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. - match self.promise_state { - PromiseState::Pending => (), - _ => return context.throw_error("Expected promise.[[PromiseState]] to be pending"), - } + assert_eq!( + self.promise_state, + PromiseState::Pending, + "promise was not pending" + ); // 2. Let reactions be promise.[[PromiseFulfillReactions]]. let reactions = &self.promise_fulfill_reactions; @@ -508,10 +487,10 @@ impl Promise { self.promise_result = Some(value.clone()); // 4. Set promise.[[PromiseFulfillReactions]] to undefined. - self.promise_fulfill_reactions = vec![]; + self.promise_fulfill_reactions = Vec::new(); // 5. Set promise.[[PromiseRejectReactions]] to undefined. - self.promise_reject_reactions = vec![]; + self.promise_reject_reactions = Vec::new(); // 6. Set promise.[[PromiseState]] to fulfilled. self.promise_state = PromiseState::Fulfilled; @@ -542,10 +521,10 @@ impl Promise { self.promise_result = Some(reason.clone()); // 4. Set promise.[[PromiseFulfillReactions]] to undefined. - self.promise_fulfill_reactions = vec![]; + self.promise_fulfill_reactions = Vec::new(); // 5. Set promise.[[PromiseRejectReactions]] to undefined. - self.promise_reject_reactions = vec![]; + self.promise_reject_reactions = Vec::new(); // 6. Set promise.[[PromiseState]] to rejected. self.promise_state = PromiseState::Rejected; diff --git a/boa_engine/src/builtins/promise/promise_job.rs b/boa_engine/src/builtins/promise/promise_job.rs index 241ee6df830..4d9fe026392 100644 --- a/boa_engine/src/builtins/promise/promise_job.rs +++ b/boa_engine/src/builtins/promise/promise_job.rs @@ -1,22 +1,15 @@ -use gc::{Finalize, Trace}; - +use super::{Promise, PromiseCapability, ReactionJobCaptures}; use crate::{ builtins::promise::{ReactionRecord, ReactionType}, job::JobCallback, object::{FunctionBuilder, JsObject}, Context, JsValue, }; +use boa_gc::{Finalize, Trace}; -use super::{Promise, PromiseCapability}; - +#[derive(Debug, Clone, Copy)] pub(crate) struct PromiseJob; -#[derive(Debug, Trace, Finalize)] -struct ReactionJobCaptures { - reaction: ReactionRecord, - argument: JsValue, -} - impl PromiseJob { /// More information: /// - [ECMAScript reference][spec] @@ -30,7 +23,7 @@ impl PromiseJob { // 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| { + |_this, _args, captures, context| { let ReactionJobCaptures { reaction, argument } = captures; let ReactionRecord { @@ -43,23 +36,17 @@ impl PromiseJob { } = reaction; let handler_result = match handler { - // d. If handler is empty, then - None => - // i. If type is Fulfill, let handlerResult be NormalCompletion(argument). - { - if let ReactionType::Fulfill = reaction_type { - Ok(argument.clone()) - } else { - // ii. Else, - // 1. Assert: type is Reject. - match reaction_type { - ReactionType::Reject => (), - ReactionType::Fulfill => panic!(), - } - // 2. Let handlerResult be ThrowCompletion(argument). - Ok(context.construct_error("ThrowCompletion(argument)")) + // 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) @@ -68,33 +55,34 @@ impl PromiseJob { match promise_capability { None => { - // f. If promiseCapability is undefined, then - if handler_result.is_err() { - // i. Assert: handlerResult is not an abrupt completion. - panic!("Assertion: failed") - } - - // ii. Return empty. + // 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. + // g. Assert: promiseCapability is a PromiseCapability Record. let PromiseCapability { - promise, + promise: _, resolve, reject, } = promise_capability_record; match handler_result { - // h. If handlerResult is an abrupt completion, then + // h. If handlerResult is an abrupt completion, then Err(value) => { - // i. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »). + // i. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »). context.call(reject, &JsValue::Undefined, &[value]) } - // i. Else, + // i. Else, Ok(value) => { - // i. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »). + // i. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »). context.call(resolve, &JsValue::Undefined, &[value]) } } @@ -129,7 +117,7 @@ impl PromiseJob { // 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| { + |_this: &JsValue, _args: &[JsValue], captures, context: &mut Context| { let JobCapture { promise_to_resolve, thenable, @@ -138,7 +126,7 @@ impl PromiseJob { // a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve). let resolving_functions = - Promise::create_resolving_functions(promise_to_resolve, context)?; + 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( diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs index 9d72ef59ba9..c79c1fea684 100644 --- a/boa_engine/src/job.rs +++ b/boa_engine/src/job.rs @@ -4,14 +4,12 @@ use gc::{Finalize, Trace}; #[derive(Debug, Clone, Trace, Finalize)] pub struct JobCallback { - callback: Box, + callback: JsValue, } impl JobCallback { pub fn make_job_callback(callback: JsValue) -> Self { - Self { - callback: Box::new(callback), - } + Self { callback } } pub fn call_job_callback( @@ -20,7 +18,7 @@ impl JobCallback { argument_list: &[JsValue], context: &mut Context, ) -> JsResult { - let callback = match *self.callback { + let callback = match self.callback { JsValue::Object(ref object) if object.is_callable() => object.clone(), _ => panic!("Callback is not a callable object"), }; @@ -29,7 +27,7 @@ impl JobCallback { } pub fn run(&self, context: &mut Context) { - let callback = match *self.callback { + let callback = match self.callback { JsValue::Object(ref object) if object.is_callable() => object.clone(), _ => panic!("Callback is not a callable object"), }; From 1fed0f13ef5d5f05e2153315af5d7393f053e424 Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Sat, 11 Jun 2022 10:44:56 +0200 Subject: [PATCH 04/13] Added some tester implementations needed for async/await testing --- Cargo.lock | 1 + boa_tester/Cargo.toml | 1 + boa_tester/src/exec/js262.rs | 6 ++++-- boa_tester/src/exec/mod.rs | 10 ++++++++-- boa_tester/src/main.rs | 1 + boa_tester/src/read.rs | 6 +++++- 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea54e6d4faf..c16c715b041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,7 @@ dependencies = [ "anyhow", "bitflags", "boa_engine", + "boa_gc", "boa_interner", "colored", "fxhash", diff --git a/boa_tester/Cargo.toml b/boa_tester/Cargo.toml index 83075293cd1..83313984c15 100644 --- a/boa_tester/Cargo.toml +++ b/boa_tester/Cargo.toml @@ -14,6 +14,7 @@ publish = false [dependencies] boa_engine = { path = "../boa_engine", features = ["intl"], version = "0.15.0" } boa_interner = { path = "../boa_interner", version = "0.15.0" } +boa_gc = { path = "../boa_gc", version = "0.15.0" } structopt = "0.3.26" serde = { version = "1.0.137", features = ["derive"] } serde_yaml = "0.8.24" diff --git a/boa_tester/src/exec/js262.rs b/boa_tester/src/exec/js262.rs index 91bb507ed20..f6ad73dd210 100644 --- a/boa_tester/src/exec/js262.rs +++ b/boa_tester/src/exec/js262.rs @@ -13,6 +13,7 @@ pub(super) fn init(context: &mut Context) -> JsObject { .function(create_realm, "createRealm", 0) .function(detach_array_buffer, "detachArrayBuffer", 2) .function(eval_script, "evalScript", 1) + .function(gc, "gc", 0) .property("global", global_obj, Attribute::default()) // .property("agent", agent, Attribute::default()) .build(); @@ -99,7 +100,8 @@ fn eval_script(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsRe /// Wraps the host's garbage collection invocation mechanism, if such a capability exists. /// Must throw an exception if no capability exists. This is necessary for testing the /// semantics of any feature that relies on garbage collection, e.g. the `WeakRef` API. -#[allow(dead_code)] +#[allow(clippy::unnecessary_wraps)] fn gc(_this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult { - todo!() + boa_gc::force_collect(); + Ok(JsValue::undefined()) } diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index ff5748972ab..424275efcfb 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -318,12 +318,18 @@ impl Test { } context - .eval(&harness.assert.as_ref()) + .eval(harness.assert.as_ref()) .map_err(|e| format!("could not run assert.js:\n{}", e.display()))?; context - .eval(&harness.sta.as_ref()) + .eval(harness.sta.as_ref()) .map_err(|e| format!("could not run sta.js:\n{}", e.display()))?; + if self.flags.contains(TestFlags::ASYNC) { + context + .eval(harness.doneprint_handle.as_ref()) + .map_err(|e| format!("could not run doneprintHandle.js:\n{}", e.display()))?; + } + for include in self.includes.iter() { context .eval( diff --git a/boa_tester/src/main.rs b/boa_tester/src/main.rs index 8b33aa9062d..d2f18c12c2e 100644 --- a/boa_tester/src/main.rs +++ b/boa_tester/src/main.rs @@ -342,6 +342,7 @@ fn run_test_suite( struct Harness { assert: Box, sta: Box, + doneprint_handle: Box, includes: FxHashMap, Box>, } diff --git a/boa_tester/src/read.rs b/boa_tester/src/read.rs index 092737ed19f..86cadb7221a 100644 --- a/boa_tester/src/read.rs +++ b/boa_tester/src/read.rs @@ -84,7 +84,7 @@ pub(super) fn read_harness(test262_path: &Path) -> anyhow::Result { let file_name = entry.file_name(); let file_name = file_name.to_string_lossy(); - if file_name == "assert.js" || file_name == "sta.js" { + if file_name == "assert.js" || file_name == "sta.js" || file_name == "doneprintHandle.js" { continue; } @@ -102,10 +102,14 @@ pub(super) fn read_harness(test262_path: &Path) -> anyhow::Result { let sta = fs::read_to_string(test262_path.join("harness/sta.js")) .context("error reading harnes/sta.js")? .into_boxed_str(); + let doneprint_handle = fs::read_to_string(test262_path.join("harness/doneprintHandle.js")) + .context("error reading harnes/doneprintHandle.js")? + .into_boxed_str(); Ok(Harness { assert, sta, + doneprint_handle, includes, }) } From fb5c5233804690fecd9ec5acbda3d5b69e3e2d97 Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Thu, 9 Jun 2022 19:09:32 +0200 Subject: [PATCH 05/13] Added the required glue to interpret async tests in the boa tester --- boa_engine/src/builtins/promise/mod.rs | 2 +- boa_tester/src/exec/mod.rs | 63 ++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 2ca33b60183..87df4473946 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -682,7 +682,7 @@ impl Promise { // c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle"). if !self.promise_is_handled { // HostPromiseRejectionTracker(promise, "handle") - todo!(); // TODO + // TODO } // d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason). diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index 424275efcfb..3c519645211 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -6,7 +6,11 @@ use super::{ Harness, Outcome, Phase, SuiteResult, Test, TestFlags, TestOutcomeResult, TestResult, TestSuite, IGNORED, }; -use boa_engine::{syntax::Parser, Context, JsResult, JsValue}; +use boa_engine::{ + builtins::JsArgs, object::FunctionBuilder, property::Attribute, syntax::Parser, Context, + JsResult, JsValue, +}; +use boa_gc::{Cell, Finalize, Gc, Trace}; use colored::Colorize; use rayon::prelude::*; use std::panic; @@ -165,14 +169,16 @@ impl Test { )) { let res = panic::catch_unwind(|| match self.expected_outcome { Outcome::Positive => { - // TODO: implement async and add `harness/doneprintHandle.js` to the includes. let mut context = Context::default(); - match self.set_up_env(harness, &mut context) { + let callback_obj = CallbackObject::default(); + // TODO: timeout + match self.set_up_env(harness, &mut context, callback_obj.clone()) { Ok(_) => { let res = context.eval(&test_content); - let passed = res.is_ok(); + let passed = res.is_ok() + && matches!(*callback_obj.result.borrow(), Some(true) | None); let text = match res { Ok(val) => val.display().to_string(), Err(e) => format!("Uncaught {}", e.display()), @@ -215,7 +221,8 @@ impl Test { if let Err(e) = Parser::new(test_content.as_bytes()).parse_all(&mut context) { (false, format!("Uncaught {e}")) } else { - match self.set_up_env(harness, &mut context) { + // TODO: timeout + match self.set_up_env(harness, &mut context, CallbackObject::default()) { Ok(_) => match context.eval(&test_content) { Ok(res) => (false, res.display().to_string()), Err(e) => { @@ -306,9 +313,14 @@ impl Test { } /// Sets the environment up to run the test. - fn set_up_env(&self, harness: &Harness, context: &mut Context) -> Result<(), String> { + fn set_up_env( + &self, + harness: &Harness, + context: &mut Context, + callback_obj: CallbackObject, + ) -> Result<(), String> { // Register the print() function. - context.register_global_function("print", 1, test262_print); + Self::register_print_fn(context, callback_obj); // add the $262 object. let _js262 = js262::init(context); @@ -349,9 +361,42 @@ impl Test { Ok(()) } + + /// Registers the print function in the context. + fn register_print_fn(context: &mut Context, callback_object: CallbackObject) { + // We use `FunctionBuilder` to define a closure with additional captures. + let js_function = + FunctionBuilder::closure_with_captures(context, test262_print, callback_object) + .name("print") + .length(1) + .build(); + + context.register_global_property( + "print", + js_function, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ); + } +} + +/// Object which includes the result of the async operation. +#[derive(Debug, Clone, Default, Trace, Finalize)] +struct CallbackObject { + result: Gc>>, } /// `print()` function required by the test262 suite. -fn test262_print(_this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult { - todo!("print() function"); +#[allow(clippy::unnecessary_wraps)] +fn test262_print( + _this: &JsValue, + args: &[JsValue], + captures: &mut CallbackObject, + _context: &mut Context, +) -> JsResult { + if let Some(message) = args.get_or_undefined(0).as_string() { + *captures.result.borrow_mut() = Some(message.as_str() == "Test262:AsyncTestComplete"); + } else { + *captures.result.borrow_mut() = Some(false); + } + Ok(JsValue::undefined()) } From 61dcdd3518e23a146e45ed7907de60c6b079fc4e Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Sat, 11 Jun 2022 13:21:46 +0200 Subject: [PATCH 06/13] Added `Promise.prototype.finally` and `PromiseResolve` --- boa_engine/src/builtins/promise/mod.rs | 191 +++++++++++++++++- .../src/builtins/promise/promise_job.rs | 2 +- boa_engine/src/object/jsobject.rs | 11 + boa_engine/src/value/mod.rs | 13 +- 4 files changed, 211 insertions(+), 6 deletions(-) diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 87df4473946..2e754d5de5f 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -73,6 +73,8 @@ struct ReactionJobCaptures { } impl PromiseCapability { + /// `NewPromiseCapability ( C )` + /// /// More information: /// - [ECMAScript reference][spec] /// @@ -183,6 +185,7 @@ impl BuiltIn for Promise { .name(Self::NAME) .length(Self::LENGTH) .method(Self::then, "then", 1) + .method(Self::finally, "finally", 1) .build() .conv::() .pipe(Some) @@ -204,6 +207,8 @@ struct RejectResolveCaptures { impl Promise { const LENGTH: usize = 1; + /// `Promise ( executor )` + /// /// More information: /// - [ECMAScript reference][spec] /// @@ -267,6 +272,8 @@ impl Promise { promise.conv::().pipe(Ok) } + /// `CreateResolvingFunctions ( promise )` + /// /// More information: /// - [ECMAScript reference][spec] /// @@ -464,6 +471,8 @@ impl Promise { ResolvingFunctionsRecord { resolve, reject } } + /// `FulfillPromise ( promise, value )` + /// /// More information: /// - [ECMAScript reference][spec] /// @@ -505,10 +514,11 @@ impl Promise { /// [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"), - } + assert_eq!( + self.promise_state, + PromiseState::Pending, + "Expected promise.[[PromiseState]] to be pending" + ); // 2. Let reactions be promise.[[PromiseRejectReactions]]. let reactions = &self.promise_reject_reactions; @@ -538,6 +548,8 @@ impl Promise { Ok(()) } + /// `TriggerPromiseReactions ( reactions, argument )` + /// /// More information: /// - [ECMAScript reference][spec] /// @@ -560,6 +572,144 @@ impl Promise { // 2. Return unused. } + /// `Promise.prototype.finally ( onFinally )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.finally + pub fn finally(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let promise be the this value. + let promise = this; + + // 2. If Type(promise) is not Object, throw a TypeError exception. + let promise_obj = if let Some(p) = promise.as_object() { + p + } else { + return context.throw_type_error("finally called with a non-object promise"); + }; + + // 3. Let C be ? SpeciesConstructor(promise, %Promise%). + let c = promise_obj.species_constructor(StandardConstructors::promise, context)?; + + // 4. Assert: IsConstructor(C) is true. + assert!(c.is_constructor()); + + let on_finally = args.get_or_undefined(0); + + // 5. If IsCallable(onFinally) is false, then + let (then_finally, catch_finally) = if on_finally.is_callable() { + /// Capture object for the `thenFinallyClosure` abstract closure. + #[derive(Debug, Trace, Finalize)] + struct FinallyCaptures { + on_finally: JsValue, + c: JsObject, + } + + // 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 = FunctionBuilder::closure_with_captures( + context, + |_this, args, captures, context| { + /// Capture object for the abstract `returnValue` closure. + #[derive(Debug, Trace, Finalize)] + struct ReturnValueCaptures { + value: JsValue, + } + + let value = args.get_or_undefined(0); + + // i. Let result be ? Call(onFinally, undefined). + let result = context.call(&captures.on_finally, &JsValue::undefined(), &[])?; + + // ii. Let promise be ? PromiseResolve(C, result). + let promise = Self::promise_resolve(captures.c.clone(), result, context)?; + + // iii. Let returnValue be a new Abstract Closure with no parameters that captures value and performs the following steps when called: + let return_value = FunctionBuilder::closure_with_captures( + context, + |_this, _args, captures, _context| { + // 1. Return value. + Ok(captures.value.clone()) + }, + ReturnValueCaptures { + value: value.clone(), + }, + ); + + // iv. Let valueThunk be CreateBuiltinFunction(returnValue, 0, "", « »). + let value_thunk = return_value.length(0).name("").build(); + + // v. Return ? Invoke(promise, "then", « valueThunk »). + promise.invoke("then", &[value_thunk.into()], context) + }, + FinallyCaptures { + on_finally: on_finally.clone(), + c: c.clone(), + }, + ); + + // b. Let thenFinally be CreateBuiltinFunction(thenFinallyClosure, 1, "", « »). + let then_finally = then_finally_closure.length(1).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 = FunctionBuilder::closure_with_captures( + context, + |_this, args, captures, context| { + /// Capture object for the abstract `throwReason` closure. + #[derive(Debug, Trace, Finalize)] + struct ThrowReasonCaptures { + reason: JsValue, + } + + let reason = args.get_or_undefined(0); + + // i. Let result be ? Call(onFinally, undefined). + let result = context.call(&captures.on_finally, &JsValue::undefined(), &[])?; + + // ii. Let promise be ? PromiseResolve(C, result). + let promise = Self::promise_resolve(captures.c.clone(), result, context)?; + + // iii. Let throwReason be a new Abstract Closure with no parameters that captures reason and performs the following steps when called: + let throw_reason = FunctionBuilder::closure_with_captures( + context, + |_this, _args, captures, _context| { + // 1. Return ThrowCompletion(reason). + Err(captures.reason.clone()) + }, + ThrowReasonCaptures { + reason: reason.clone(), + }, + ); + + // iv. Let thrower be CreateBuiltinFunction(throwReason, 0, "", « »). + let thrower = throw_reason.length(0).name("").build(); + + // v. Return ? Invoke(promise, "then", « thrower »). + promise.invoke("then", &[thrower.into()], context) + }, + FinallyCaptures { + on_finally: on_finally.clone(), + c, + }, + ); + + // d. Let catchFinally be CreateBuiltinFunction(catchFinallyClosure, 1, "", « »). + let catch_finally = catch_finally_closure.length(1).name("").build(); + + (then_finally.into(), catch_finally.into()) // TODO + } else { + // 6. Else, + // a. Let thenFinally be onFinally. + // b. Let catchFinally be onFinally. + (on_finally.clone(), on_finally.clone()) + }; + + // 7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »). + promise.invoke("then", &[then_finally, catch_finally], context) + } + + /// `Promise.prototype.then ( onFulfilled, onRejected )` + /// /// More information: /// - [ECMAScript reference][spec] /// @@ -592,6 +742,8 @@ impl Promise { .pipe(Ok) } + /// `PerformPromiseThen ( promise, onFulfilled, onRejected [ , resultCapability ] )` + /// /// More information: /// - [ECMAScript reference][spec] /// @@ -707,4 +859,35 @@ impl Promise { Some(result_capability) => result_capability.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 + 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 JsValue::same_value(&x_constructor, &JsValue::from(c.clone())) { + return Ok(JsValue::from(x.clone())); + } + } + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(&JsValue::from(c), context)?; + + // 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). + context.call(&promise_capability.resolve, &JsValue::undefined(), &[x])?; + + // 4. Return promiseCapability.[[Promise]]. + Ok(promise_capability.promise.clone()) + } } diff --git a/boa_engine/src/builtins/promise/promise_job.rs b/boa_engine/src/builtins/promise/promise_job.rs index 4d9fe026392..093b3cba46b 100644 --- a/boa_engine/src/builtins/promise/promise_job.rs +++ b/boa_engine/src/builtins/promise/promise_job.rs @@ -44,7 +44,7 @@ impl PromiseJob { // 1. Assert: type is Reject. ReactionType::Reject => { // 2. Let handlerResult be ThrowCompletion(argument). - Ok(context.construct_error("argument")) // TODO: convert argument to string, somehow + Err(argument.clone()) } }, // e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)). diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index cf75a8b3a04..e144897504f 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -449,6 +449,17 @@ impl JsObject { self.borrow().is_typed_array() } + /// Checks if it's a `Promise` object. + /// + /// # Panics + /// + /// Panics if the object is currently mutably borrowed. + #[inline] + #[track_caller] + pub fn is_promise(&self) -> bool { + self.borrow().is_promise() + } + /// Checks if it's an ordinary object. /// /// # Panics diff --git a/boa_engine/src/value/mod.rs b/boa_engine/src/value/mod.rs index 509249529f9..9b2c900ecb3 100644 --- a/boa_engine/src/value/mod.rs +++ b/boa_engine/src/value/mod.rs @@ -148,7 +148,7 @@ impl JsValue { self.as_object().filter(|obj| obj.is_callable()) } - /// Returns true if the value is a constructor object + /// Returns true if the value is a constructor object. #[inline] pub fn is_constructor(&self) -> bool { matches!(self, Self::Object(obj) if obj.is_constructor()) @@ -159,6 +159,17 @@ impl JsValue { self.as_object().filter(|obj| obj.is_constructor()) } + /// Returns true if the value is a promise object. + #[inline] + pub fn is_promise(&self) -> bool { + matches!(self, Self::Object(obj) if obj.is_promise()) + } + + #[inline] + pub fn as_promise(&self) -> Option<&JsObject> { + self.as_object().filter(|obj| obj.is_promise()) + } + /// Returns true if the value is a symbol. #[inline] pub fn is_symbol(&self) -> bool { From 09ee72344597ac6990d0970df72119456b1a9764 Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Sat, 11 Jun 2022 13:40:16 +0200 Subject: [PATCH 07/13] Removed the most prominent panic --- boa_engine/src/builtins/promise/mod.rs | 36 ++++++++++---------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 2e754d5de5f..f992a203a0c 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -282,7 +282,6 @@ impl Promise { 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; @@ -331,15 +330,18 @@ impl Promise { .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .reject(&self_resolution_error, context)?; + .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). + let then = if let Some(resolution) = resolution.as_object() { + // 9. Let then be Completion(Get(resolution, "then")). + resolution.get("then", context) + } else { + // 8. If Type(resolution) is not Object, then + // a. Perform FulfillPromise(promise, resolution). promise .borrow_mut() .as_promise_mut() @@ -348,15 +350,9 @@ impl Promise { // 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 { + let then_action = match then { // 10. If then is an abrupt completion, then Err(value) => { // a. Perform RejectPromise(promise, then.[[Value]]). @@ -364,20 +360,15 @@ impl Promise { .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .reject(&value, context)?; + .reject(&value, context); // b. Return undefined. return Ok(JsValue::Undefined); } + // 11. Let thenAction be then.[[Value]]. 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). @@ -453,7 +444,7 @@ impl Promise { .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .reject(reason, context)?; + .reject(reason, context); // 8. Return undefined. Ok(JsValue::Undefined) @@ -512,7 +503,7 @@ impl Promise { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-rejectpromise - pub fn reject(&mut self, reason: &JsValue, context: &mut Context) -> JsResult<()> { + pub fn reject(&mut self, reason: &JsValue, context: &mut Context) { // 1. Assert: The value of promise.[[PromiseState]] is pending. assert_eq!( self.promise_state, @@ -545,7 +536,6 @@ impl Promise { } // 9. Return unused. - Ok(()) } /// `TriggerPromiseReactions ( reactions, argument )` From c5af06b0649492e9235be783be798855e7a5d453 Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Sat, 11 Jun 2022 22:14:54 +0200 Subject: [PATCH 08/13] Fixed all the panics --- boa_engine/src/builtins/promise/mod.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index f992a203a0c..b3f4bfcc574 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -201,7 +201,7 @@ struct ResolvingFunctionsRecord { #[derive(Debug, Trace, Finalize)] struct RejectResolveCaptures { promise: JsObject, - already_resolved: bool, + already_resolved: Gc>, } impl Promise { @@ -283,12 +283,12 @@ impl Promise { context: &mut Context, ) -> ResolvingFunctionsRecord { // 1. Let alreadyResolved be the Record { [[Value]]: false }. - let already_resolved = false; + let already_resolved = Gc::new(boa_gc::Cell::new(false)); // 5. Set resolve.[[Promise]] to promise. // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. let resolve_captures = RejectResolveCaptures { - already_resolved, + already_resolved: already_resolved.clone(), promise: promise.clone(), }; @@ -310,12 +310,12 @@ impl Promise { } = captures; // 5. If alreadyResolved.[[Value]] is true, return undefined. - if *already_resolved { + if *already_resolved.borrow() { return Ok(JsValue::Undefined); } // 6. Set alreadyResolved.[[Value]] to true. - *already_resolved = true; + *already_resolved.borrow_mut() = true; let resolution = args.get_or_undefined(0); @@ -431,20 +431,20 @@ impl Promise { } = captures; // 5. If alreadyResolved.[[Value]] is true, return undefined. - if *already_resolved { + if *already_resolved.borrow() { return Ok(JsValue::Undefined); } // 6. Set alreadyResolved.[[Value]] to true. - *already_resolved = true; + *already_resolved.borrow_mut() = true; - let reason = args.get_or_undefined(0); + // 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); + .reject(args.get_or_undefined(0), context); // 8. Return undefined. Ok(JsValue::Undefined) @@ -503,6 +503,7 @@ impl Promise { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-rejectpromise + #[track_caller] pub fn reject(&mut self, reason: &JsValue, context: &mut Context) { // 1. Assert: The value of promise.[[PromiseState]] is pending. assert_eq!( @@ -709,7 +710,7 @@ impl Promise { let promise = this; // 2. If IsPromise(promise) is false, throw a TypeError exception. - let promise_obj = match promise.as_object() { + let promise_obj = match promise.as_promise() { Some(obj) => obj, None => return context.throw_type_error("IsPromise(promise) is false"), }; From 87344271b044c9502f193341be1b44769732829b Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Sat, 11 Jun 2022 22:22:45 +0200 Subject: [PATCH 09/13] Added the `Promise.prototype.catch()` method --- boa_engine/src/builtins/promise/mod.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index b3f4bfcc574..13a054156a3 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -185,6 +185,7 @@ impl BuiltIn for Promise { .name(Self::NAME) .length(Self::LENGTH) .method(Self::then, "then", 1) + .method(Self::catch, "catch", 1) .method(Self::finally, "finally", 1) .build() .conv::() @@ -468,6 +469,7 @@ impl Promise { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-fulfillpromise + #[track_caller] pub fn fulfill(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { // 1. Assert: The value of promise.[[PromiseState]] is pending. assert_eq!( @@ -563,6 +565,25 @@ impl Promise { // 2. Return unused. } + /// `Promise.prototype.catch ( onRejected )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.catch + pub 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. + let promise = this; + // 2. Return ? Invoke(promise, "then", « undefined, onRejected »). + promise.invoke( + "then", + &[JsValue::undefined(), on_rejected.clone()], + context, + ) + } + /// `Promise.prototype.finally ( onFinally )` /// /// More information: From e5b09d2ffe14785190558084b8deb995907ce49d Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Mon, 13 Jun 2022 20:12:38 +0200 Subject: [PATCH 10/13] Added static methods, accessors and more for `Promise` --- boa_engine/src/builtins/iterable/mod.rs | 46 ++++- boa_engine/src/builtins/promise/mod.rs | 264 +++++++++++++++++++++++- 2 files changed, 291 insertions(+), 19 deletions(-) diff --git a/boa_engine/src/builtins/iterable/mod.rs b/boa_engine/src/builtins/iterable/mod.rs index 08eebd96ea0..ee55c6b7626 100644 --- a/boa_engine/src/builtins/iterable/mod.rs +++ b/boa_engine/src/builtins/iterable/mod.rs @@ -202,19 +202,26 @@ pub struct IteratorResult { } impl IteratorResult { - /// Get `done` property of iterator result object. + /// `IteratorComplete ( iterResult )` + /// + /// The abstract operation `IteratorComplete` takes argument `iterResult` (an `Object`) and + /// returns either a normal completion containing a `Boolean` or a throw completion. /// /// More information: /// - [ECMA reference][spec] /// - /// [spec]: https://tc39.es/ecma262/#sec-iteratorclose + /// [spec]: https://tc39.es/ecma262/#sec-iteratorcomplete #[inline] pub fn complete(&self, context: &mut Context) -> JsResult { // 1. Return ToBoolean(? Get(iterResult, "done")). Ok(self.object.get("done", context)?.to_boolean()) } - /// Get `value` property of iterator result object. + /// `IteratorValue ( iterResult )` + /// + /// The abstract operation `IteratorValue` takes argument `iterResult` (an `Object`) and + /// returns either a normal completion containing an ECMAScript language value or a throw + /// completion. /// /// More information: /// - [ECMA reference][spec] @@ -226,13 +233,16 @@ impl IteratorResult { self.object.get("value", context) } } + +/// Iterator Record +/// /// An Iterator Record is a Record value used to encapsulate an -/// `Iterator` or `AsyncIterator` along with the next method. +/// `Iterator` or `AsyncIterator` along with the `next` method. /// /// More information: /// - [ECMA reference][spec] /// -/// [spec]:https://tc39.es/ecma262/#table-iterator-record-fields +/// [spec]: https://tc39.es/ecma262/#sec-iterator-records #[derive(Debug)] pub struct IteratorRecord { /// `[[Iterator]]` @@ -265,7 +275,11 @@ impl IteratorRecord { &self.next_function } - /// Get the next value in the iterator + /// `IteratorNext ( iteratorRecord [ , value ] )` + /// + /// The abstract operation `IteratorNext` takes argument `iteratorRecord` (an `Iterator` + /// Record) and optional argument `value` (an ECMAScript language value) and returns either a + /// normal completion containing an `Object` or a throw completion. /// /// More information: /// - [ECMA reference][spec] @@ -298,7 +312,18 @@ impl IteratorRecord { } } - #[inline] + /// `IteratorStep ( iteratorRecord )` + /// + /// The abstract operation `IteratorStep` takes argument `iteratorRecord` (an `Iterator` + /// Record) and returns either a normal completion containing either an `Object` or `false`, or + /// a throw completion. It requests the next value from `iteratorRecord.[[Iterator]]` by + /// calling `iteratorRecord.[[NextMethod]]` and returns either `false` indicating that the + /// iterator has reached its end or the `IteratorResult` object if a next value is available. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-iteratorstep pub(crate) fn step(&self, context: &mut Context) -> JsResult> { let _timer = Profiler::global().start_event("IteratorRecord::step", "iterator"); @@ -317,7 +342,12 @@ impl IteratorRecord { Ok(Some(result)) } - /// Cleanup the iterator + /// `IteratorClose ( iteratorRecord, completion )` + /// + /// The abstract operation `IteratorClose` takes arguments `iteratorRecord` (an + /// [Iterator Record][Self]) and `completion` (a Completion Record) and returns a Completion + /// Record. It is used to notify an iterator that it should perform any actions it would + /// normally perform when it has reached its completed state. /// /// More information: /// - [ECMA reference][spec] diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 13a054156a3..d7caec73113 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -6,7 +6,7 @@ mod tests; mod promise_job; use self::promise_job::PromiseJob; -use super::JsArgs; +use super::{iterable::IteratorRecord, JsArgs}; use crate::{ builtins::BuiltIn, context::intrinsics::StandardConstructors, @@ -16,6 +16,7 @@ use crate::{ JsObject, ObjectData, }, property::Attribute, + symbol::WellKnownSymbols, value::JsValue, Context, JsResult, }; @@ -23,6 +24,26 @@ use boa_gc::{Finalize, Gc, Trace}; use boa_profiler::Profiler; use tap::{Conv, Pipe}; +/// `IfAbruptRejectPromise ( value, capability )` +/// +/// `IfAbruptRejectPromise` is a shorthand for a sequence of algorithm steps that use a `PromiseCapability` Record. +macro_rules! if_abrupt_reject_promise { + ($value:ident, $capability:expr, $context: expr) => { + let $value = match $value { + // 1. If value is an abrupt completion, then + Err(value) => { + // a. Perform ? Call(capability.[[Reject]], undefined, « value.[[Value]] »). + $context.call(&$capability.reject, &JsValue::undefined(), &[value])?; + + // b. Return capability.[[Promise]]. + return Ok($capability.promise.clone()); + } + // 2. Else if value is a Completion Record, set value to value.[[Value]]. + Ok(value) => value, + }; + }; +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum PromiseState { Pending, @@ -177,6 +198,11 @@ impl BuiltIn for Promise { fn init(context: &mut Context) -> Option { let _timer = Profiler::global().start_event(Self::NAME, "init"); + let get_species = FunctionBuilder::native(context, Self::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + ConstructorBuilder::with_standard_constructor( context, Self::constructor, @@ -184,9 +210,23 @@ impl BuiltIn for Promise { ) .name(Self::NAME) .length(Self::LENGTH) + .static_method(Self::race, "race", 1) + .static_method(Self::reject, "reject", 1) + .static_method(Self::resolve, "resolve", 1) + .static_accessor( + WellKnownSymbols::species(), + Some(get_species), + None, + Attribute::CONFIGURABLE, + ) .method(Self::then, "then", 1) .method(Self::catch, "catch", 1) .method(Self::finally, "finally", 1) + .property( + WellKnownSymbols::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) .build() .conv::() .pipe(Some) @@ -331,7 +371,7 @@ impl Promise { .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .reject(&self_resolution_error, context); + .reject_promise(&self_resolution_error, context); // c. Return undefined. return Ok(JsValue::Undefined); @@ -347,7 +387,7 @@ impl Promise { .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .fulfill(resolution, context)?; + .fulfill_promise(resolution, context)?; // b. Return undefined. return Ok(JsValue::Undefined); @@ -361,7 +401,7 @@ impl Promise { .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .reject(&value, context); + .reject_promise(&value, context); // b. Return undefined. return Ok(JsValue::Undefined); @@ -377,7 +417,7 @@ impl Promise { .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .fulfill(resolution, context)?; + .fulfill_promise(resolution, context)?; // b. Return undefined. return Ok(JsValue::Undefined); @@ -445,7 +485,7 @@ impl Promise { .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .reject(args.get_or_undefined(0), context); + .reject_promise(args.get_or_undefined(0), context); // 8. Return undefined. Ok(JsValue::Undefined) @@ -469,8 +509,7 @@ impl Promise { /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-fulfillpromise - #[track_caller] - pub fn fulfill(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { + pub fn fulfill_promise(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { // 1. Assert: The value of promise.[[PromiseState]] is pending. assert_eq!( self.promise_state, @@ -501,12 +540,13 @@ impl Promise { Ok(()) } + /// `RejectPromise ( promise, reason )` + /// /// More information: /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-rejectpromise - #[track_caller] - pub fn reject(&mut self, reason: &JsValue, context: &mut Context) { + pub fn reject_promise(&mut self, reason: &JsValue, context: &mut Context) { // 1. Assert: The value of promise.[[PromiseState]] is pending. assert_eq!( self.promise_state, @@ -565,6 +605,182 @@ impl Promise { // 2. Return unused. } + /// `Promise.race ( iterable )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-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); + + // 7. Let result be Completion(PerformPromiseRace(iteratorRecord, C, promiseCapability, promiseResolve)). + let result = Self::perform_promise_race( + &iterator_record, + c, + &promise_capability, + &promise_resolve, + 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)). + // TODO: set the [[Done]] field in the IteratorRecord (currently doesn't exist) + + // b. IfAbruptRejectPromise(result, promiseCapability). + if_abrupt_reject_promise!(result, promise_capability, context); + + Ok(result) + } else { + // 9. Return ? result. + result + } + } + + /// `PerformPromiseRace ( iteratorRecord, constructor, resultCapability, promiseResolve )` + /// + /// The abstract operation `PerformPromiseRace` takes arguments `iteratorRecord`, `constructor` + /// (a constructor), `resultCapability` (a [`PromiseCapability`] Record), and `promiseResolve` + /// (a function object) and returns either a normal completion containing an ECMAScript + /// language value or a throw completion. + fn perform_promise_race( + iterator_record: &IteratorRecord, + constructor: &JsValue, + result_capability: &PromiseCapability, + promise_resolve: &JsValue, + context: &mut Context, + ) -> JsResult { + // 1. Repeat, + loop { + // a. Let next be Completion(IteratorStep(iteratorRecord)). + let next = iterator_record.step(context); + + // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. + if next.is_err() { + // TODO: set the [[Done]] field in the IteratorRecord (currently doesn't exist) + } + + // c. ReturnIfAbrupt(next). + let next = next?; + + if let Some(next) = next { + // e. Let nextValue be Completion(IteratorValue(next)). + let next_value = next.value(context); + + // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. + if next_value.is_err() { + // TODO: set the [[Done]] field in the IteratorRecord (currently doesn't exist) + } + + // g. ReturnIfAbrupt(nextValue). + let next_value = next_value?; + + // h. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »). + let next_promise = context.call(promise_resolve, constructor, &[next_value])?; + + // i. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], resultCapability.[[Reject]] »). + next_promise.invoke( + "then", + &[ + result_capability.resolve.clone(), + result_capability.reject.clone(), + ], + context, + )?; + } else { + // d. If next is false, then + // i. Set iteratorRecord.[[Done]] to true. + // TODO: set the [[Done]] field in the IteratorRecord (currently doesn't exist) + + // ii. Return resultCapability.[[Promise]]. + return Ok(result_capability.promise.clone()); + } + } + } + + /// `Promise.reject ( r )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.reject + pub fn reject(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let r = 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. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »). + context.call( + &promise_capability.reject, + &JsValue::undefined(), + &[r.clone()], + )?; + + // 4. Return promiseCapability.[[Promise]]. + Ok(promise_capability.promise.clone()) + } + + /// `Promise.resolve ( x )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.resolve + pub fn resolve(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let x = args.get_or_undefined(0); + + // 1. Let C be the this value. + let c = this; + + if let Some(c) = c.as_object() { + // 3. Return ? PromiseResolve(C, x). + Self::promise_resolve(c.clone(), x.clone(), context) + } else { + // 2. If Type(C) is not Object, throw a TypeError exception. + context.throw_type_error("Promise.resolve() called on a non-object") + } + } + + /// `get Promise [ @@species ]` + /// + /// The `Promise [ @@species ]` accessor property returns the Promise constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-promise-@@species + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/@@species + #[allow(clippy::unnecessary_wraps)] + fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // 1. Return the this value. + Ok(this.clone()) + } + /// `Promise.prototype.catch ( onRejected )` /// /// More information: @@ -894,7 +1110,7 @@ impl Promise { } // 2. Let promiseCapability be ? NewPromiseCapability(C). - let promise_capability = PromiseCapability::new(&JsValue::from(c), context)?; + let promise_capability = PromiseCapability::new(&c.into(), context)?; // 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). context.call(&promise_capability.resolve, &JsValue::undefined(), &[x])?; @@ -902,4 +1118,30 @@ impl Promise { // 4. Return promiseCapability.[[Promise]]. Ok(promise_capability.promise.clone()) } + + /// `GetPromiseResolve ( promiseConstructor )` + /// + /// The abstract operation `GetPromiseResolve` takes argument `promiseConstructor` (a + /// constructor) and returns either a normal completion containing a function object or a throw + /// completion. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-getpromiseresolve + fn get_promise_resolve( + promise_constructor: &JsObject, + context: &mut Context, + ) -> JsResult { + // 1. Let promiseResolve be ? Get(promiseConstructor, "resolve"). + let promise_resolve = promise_constructor.get("resolve", context)?; + + // 2. If IsCallable(promiseResolve) is false, throw a TypeError exception. + if !promise_resolve.is_callable() { + return context.throw_type_error("retrieving a non-callable promise resolver"); + } + + // 3. Return promiseResolve. + Ok(promise_resolve) + } } From 5b8b9d338edb5c64079c5b73ab9c065c25e068cc Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Mon, 13 Jun 2022 21:43:20 +0200 Subject: [PATCH 11/13] Added a bunch of extra documentation links --- boa_engine/src/builtins/promise/mod.rs | 38 ++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index d7caec73113..3e716fd606d 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -27,6 +27,11 @@ use tap::{Conv, Pipe}; /// `IfAbruptRejectPromise ( value, capability )` /// /// `IfAbruptRejectPromise` is a shorthand for a sequence of algorithm steps that use a `PromiseCapability` Record. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ifabruptrejectpromise macro_rules! if_abrupt_reject_promise { ($value:ident, $capability:expr, $context: expr) => { let $value = match $value { @@ -505,6 +510,9 @@ impl Promise { /// `FulfillPromise ( promise, value )` /// + /// The abstract operation `FulfillPromise` takes arguments `promise` and `value` and returns + /// `unused`. + /// /// More information: /// - [ECMAScript reference][spec] /// @@ -542,6 +550,9 @@ impl Promise { /// `RejectPromise ( promise, reason )` /// + /// The abstract operation `RejectPromise` takes arguments `promise` and `reason` and returns + /// `unused`. + /// /// More information: /// - [ECMAScript reference][spec] /// @@ -583,6 +594,13 @@ impl Promise { /// `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] /// @@ -607,10 +625,15 @@ impl 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-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); @@ -663,6 +686,11 @@ impl Promise { /// (a constructor), `resultCapability` (a [`PromiseCapability`] Record), and `promiseResolve` /// (a function object) and returns either a normal completion containing an ECMAScript /// language value or a throw completion. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performpromiserace fn perform_promise_race( iterator_record: &IteratorRecord, constructor: &JsValue, @@ -722,8 +750,10 @@ impl Promise { /// /// More information: /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] /// /// [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(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { let r = args.get_or_undefined(0); @@ -748,8 +778,10 @@ impl Promise { /// /// More information: /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] /// /// [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(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { let x = args.get_or_undefined(0); @@ -785,8 +817,10 @@ impl Promise { /// /// More information: /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] /// /// [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 { let on_rejected = args.get_or_undefined(0); @@ -804,8 +838,10 @@ impl Promise { /// /// More information: /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] /// /// [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(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { // 1. Let promise be the this value. let promise = this; @@ -940,8 +976,10 @@ impl Promise { /// /// More information: /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] /// /// [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 { // 1. Let promise be the this value. let promise = this; From 5222c00b439e8e8014e1bcf820c1cfd1a2385c41 Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Tue, 14 Jun 2022 07:21:57 +0200 Subject: [PATCH 12/13] Using Rc for better performance --- boa_engine/src/builtins/promise/mod.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 3e716fd606d..ace3375b303 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -22,6 +22,7 @@ use crate::{ }; use boa_gc::{Finalize, Gc, Trace}; use boa_profiler::Profiler; +use std::{cell::Cell, rc::Rc}; use tap::{Conv, Pipe}; /// `IfAbruptRejectPromise ( value, capability )` @@ -227,6 +228,7 @@ impl BuiltIn for Promise { .method(Self::then, "then", 1) .method(Self::catch, "catch", 1) .method(Self::finally, "finally", 1) + // .property( WellKnownSymbols::to_string_tag(), Self::NAME, @@ -247,7 +249,8 @@ struct ResolvingFunctionsRecord { #[derive(Debug, Trace, Finalize)] struct RejectResolveCaptures { promise: JsObject, - already_resolved: Gc>, + #[unsafe_ignore_trace] + already_resolved: Rc>, } impl Promise { @@ -329,7 +332,7 @@ impl Promise { context: &mut Context, ) -> ResolvingFunctionsRecord { // 1. Let alreadyResolved be the Record { [[Value]]: false }. - let already_resolved = Gc::new(boa_gc::Cell::new(false)); + let already_resolved = Rc::new(Cell::new(false)); // 5. Set resolve.[[Promise]] to promise. // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. @@ -356,12 +359,12 @@ impl Promise { } = captures; // 5. If alreadyResolved.[[Value]] is true, return undefined. - if *already_resolved.borrow() { + if already_resolved.get() { return Ok(JsValue::Undefined); } // 6. Set alreadyResolved.[[Value]] to true. - *already_resolved.borrow_mut() = true; + already_resolved.set(true); let resolution = args.get_or_undefined(0); @@ -477,12 +480,12 @@ impl Promise { } = captures; // 5. If alreadyResolved.[[Value]] is true, return undefined. - if *already_resolved.borrow() { + if already_resolved.get() { return Ok(JsValue::Undefined); } // 6. Set alreadyResolved.[[Value]] to true. - *already_resolved.borrow_mut() = true; + already_resolved.set(true); // let reason = args.get_or_undefined(0); // 7. Perform RejectPromise(promise, reason). From bde4444779fbaa0c539e271e8dd1f0b0aa0aff4c Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Tue, 14 Jun 2022 22:00:36 +0200 Subject: [PATCH 13/13] Fixed the feedback comments --- boa_engine/src/builtins/promise/mod.rs | 75 +++++++++++++------------- boa_engine/src/context/mod.rs | 8 +-- boa_engine/src/job.rs | 57 +++++++++++++------- 3 files changed, 83 insertions(+), 57 deletions(-) diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index ace3375b303..cca904f395d 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -419,20 +419,23 @@ impl Promise { }; // 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_promise(resolution, context)?; + let then_action = match then_action.as_object() { + Some(then_action) if then_action.is_callable() => then_action, + _ => { + // a. Perform FulfillPromise(promise, resolution). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .fulfill_promise(resolution, context)?; - // b. Return undefined. - return Ok(JsValue::Undefined); - } + // b. Return undefined. + return Ok(JsValue::Undefined); + } + }; // 13. Let thenJobCallback be HostMakeJobCallback(thenAction). - let then_job_callback = JobCallback::make_job_callback(then_action); + let then_job_callback = JobCallback::make_job_callback(then_action.clone()); // 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). let job: JobCallback = PromiseJob::new_promise_resolve_thenable_job( @@ -999,8 +1002,8 @@ impl Promise { // 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(); + let on_fulfilled = args.get_or_undefined(0); + let on_rejected = args.get_or_undefined(1); // 5. Return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability). promise_obj @@ -1019,8 +1022,8 @@ impl Promise { /// [spec]: https://tc39.es/ecma262/#sec-performpromisethen fn perform_promise_then( &mut self, - on_fulfilled: JsValue, - on_rejected: JsValue, + on_fulfilled: &JsValue, + on_rejected: &JsValue, result_capability: Option, context: &mut Context, ) -> JsValue { @@ -1029,27 +1032,27 @@ impl Promise { // 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 - }; + let on_fulfilled_job_callback = match on_fulfilled.as_object() { + // 4. Else, + // a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled). + Some(on_fulfilled) if on_fulfilled.is_callable() => { + Some(JobCallback::make_job_callback(on_fulfilled.clone())) + } + // 3. If IsCallable(onFulfilled) is false, then + // a. Let onFulfilledJobCallback be empty. + _ => None, + }; + + let on_rejected_job_callback = match on_rejected.as_object() { + // 6. Else, + // a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected). + Some(on_rejected) if on_rejected.is_callable() => { + Some(JobCallback::make_job_callback(on_rejected.clone())) + } + // 5. If IsCallable(onRejected) is false, then + // a. Let onRejectedJobCallback be empty. + _ => None, + }; // 7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }. let fulfill_reaction = ReactionRecord { diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index a940483281a..122b0460f6e 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -712,15 +712,17 @@ impl Context { self.realm.set_global_binding_number(); let result = self.run(); self.vm.pop_frame(); - self.run_queued_jobs(); + self.run_queued_jobs()?; let (result, _) = result?; Ok(result) } - fn run_queued_jobs(&mut self) { + /// Runs all the jobs in the job queue. + fn run_queued_jobs(&mut self) -> JsResult<()> { while let Some(job) = self.promise_job_queue.pop_front() { - job.run(self); + job.call_job_callback(&JsValue::Undefined, &[], self)?; } + Ok(()) } /// Return the intrinsic constructors and objects. diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs index c79c1fea684..2a0988b2e57 100644 --- a/boa_engine/src/job.rs +++ b/boa_engine/src/job.rs @@ -1,37 +1,58 @@ -use crate::{Context, JsResult, JsValue}; - +use crate::{prelude::JsObject, Context, JsResult, JsValue}; use gc::{Finalize, Trace}; +/// `JobCallback` records +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-jobcallback-records #[derive(Debug, Clone, Trace, Finalize)] pub struct JobCallback { - callback: JsValue, + callback: JsObject, } impl JobCallback { - pub fn make_job_callback(callback: JsValue) -> Self { + /// `HostMakeJobCallback ( callback )` + /// + /// The host-defined abstract operation `HostMakeJobCallback` takes argument `callback` (a + /// function object) and returns a `JobCallback` Record. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-hostmakejobcallback + pub fn make_job_callback(callback: JsObject) -> Self { + // 1. Return the JobCallback Record { [[Callback]]: callback, [[HostDefined]]: empty }. Self { callback } } + /// `HostCallJobCallback ( jobCallback, V, argumentsList )` + /// + /// The host-defined abstract operation `HostCallJobCallback` takes arguments `jobCallback` (a + /// `JobCallback` Record), `V` (an ECMAScript language value), and `argumentsList` (a `List` of + /// ECMAScript language values) and returns either a normal completion containing an ECMAScript + /// language value or a throw completion. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-hostcalljobcallback pub fn call_job_callback( &self, v: &JsValue, - argument_list: &[JsValue], + arguments_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) - } + // It must perform and return the result of Call(jobCallback.[[Callback]], V, argumentsList). - 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"), - }; + // 1. Assert: IsCallable(jobCallback.[[Callback]]) is true. + assert!( + self.callback.is_callable(), + "the callback of the job callback was not callable" + ); - let _callback_result = callback.__call__(&JsValue::Undefined, &[], context); + // 2. Return ? Call(jobCallback.[[Callback]], V, argumentsList). + self.callback.__call__(v, arguments_list, context) } }