From 0454ddec195fcf46a72a59adabc45b754d925176 Mon Sep 17 00:00:00 2001 From: Iban Eguia Date: Wed, 15 Jun 2022 01:25:46 +0000 Subject: [PATCH] Execution stack & promises (#2107) This PR overrides #1923. It also removes the `queues` dependency added there, and rebases it to the latest `main` branch state. It adds the following: - A job queue (in `Context`) - The constructor [`Promise`](https://tc39.es/ecma262/#sec-promise-executor) - [`Promise.race`](https://tc39.es/ecma262/#sec-promise.race) - [`Promise.reject`](https://tc39.es/ecma262/#sec-promise.reject) - [`Promise.resolve`](https://tc39.es/ecma262/#sec-promise.resolve) - [`get Promise [ @@species ]`](https://tc39.es/ecma262/#sec-get-promise-@@species) - [`Promise.prototype [ @@toStringTag ]`](https://tc39.es/ecma262/#sec-promise.prototype-@@tostringtag) - [`Promise.prototype.then`](https://tc39.es/ecma262/#sec-promise.prototype.then) - [`Promise.prototype.finally`](https://tc39.es/ecma262/#sec-promise.prototype.finally) - [`Promise.prototype.catch`](https://tc39.es/ecma262/#sec-promise.prototype.catch) - The additional needed infrastructure - [`PerformPromiseThen ( promise, onFulfilled, onRejected [ , resultCapability ] )`](https://tc39.es/ecma262/#sec-performpromisethen) - [`TriggerPromiseReactions ( reactions, argument )`](https://tc39.es/ecma262/#sec-triggerpromisereactions) - [`PerformPromiseRace ( iteratorRecord, constructor, resultCapability, promiseResolve )`](https://tc39.es/ecma262/#sec-performpromiserace) - [`RejectPromise ( promise, reason )`](https://tc39.es/ecma262/#sec-rejectpromise) - [`FulfillPromise ( promise, value )`](https://tc39.es/ecma262/#sec-fulfillpromise) - [`IfAbruptRejectPromise ( value, capability )`](https://tc39.es/ecma262/#sec-ifabruptrejectpromise) - [`CreateResolvingFunctions ( promise )`](https://tc39.es/ecma262/#sec-createresolvingfunctions) - [`NewPromiseCapability ( C )`](https://tc39.es/ecma262/#sec-newpromisecapability) - [`NewPromiseReactionJob ( reaction, argument )`](https://tc39.es/ecma262/#sec-newpromisereactionjob) - [`NewPromiseResolveThenableJob ( promiseToResolve, thenable, then )`](https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob) - [`PromiseResolve ( C, x )`](https://tc39.es/ecma262/#sec-promise-resolve) - A test case showcasing the run-to-completion semantics. An example program that shows the control flow with this addition is: ```javascript new Promise((res, rej) => { console.log("A"); res(undefined); }).then((_) => console.log("B")); console.log("C"); ``` Which would output: ``` A C B ``` --- Cargo.lock | 1 + boa_engine/src/builtins/iterable/mod.rs | 46 +- boa_engine/src/builtins/mod.rs | 5 +- boa_engine/src/builtins/promise/mod.rs | 1191 +++++++++++++++++ .../src/builtins/promise/promise_job.rs | 182 +++ boa_engine/src/builtins/promise/tests.rs | 19 + boa_engine/src/context/intrinsics.rs | 7 + boa_engine/src/context/mod.rs | 27 + boa_engine/src/job.rs | 58 + boa_engine/src/lib.rs | 1 + boa_engine/src/object/jsobject.rs | 11 + boa_engine/src/object/mod.rs | 47 +- boa_engine/src/value/mod.rs | 13 +- boa_tester/Cargo.toml | 1 + boa_tester/src/exec/js262.rs | 6 +- boa_tester/src/exec/mod.rs | 73 +- boa_tester/src/main.rs | 1 + boa_tester/src/read.rs | 6 +- test_ignore.txt | 1 - 19 files changed, 1670 insertions(+), 26 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 ea54e6d4fa..c16c715b04 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_engine/src/builtins/iterable/mod.rs b/boa_engine/src/builtins/iterable/mod.rs index 08eebd96ea..ee55c6b762 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/mod.rs b/boa_engine/src/builtins/mod.rs index 938c4b7c3b..2d4bcd5596 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 0000000000..cca904f395 --- /dev/null +++ b/boa_engine/src/builtins/promise/mod.rs @@ -0,0 +1,1191 @@ +//! This module implements the global `Promise` object. + +#[cfg(test)] +mod tests; + +mod promise_job; + +use self::promise_job::PromiseJob; +use super::{iterable::IteratorRecord, JsArgs}; +use crate::{ + builtins::BuiltIn, + context::intrinsics::StandardConstructors, + job::JobCallback, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsObject, ObjectData, + }, + property::Attribute, + symbol::WellKnownSymbols, + value::JsValue, + Context, JsResult, +}; +use boa_gc::{Finalize, Gc, Trace}; +use boa_profiler::Profiler; +use std::{cell::Cell, rc::Rc}; +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 { + // 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, + Fulfilled, + Rejected, +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct Promise { + promise_result: Option, + #[unsafe_ignore_trace] + promise_state: PromiseState, + promise_fulfill_reactions: Vec, + promise_reject_reactions: Vec, + promise_is_handled: bool, +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct ReactionRecord { + promise_capability: Option, + #[unsafe_ignore_trace] + reaction_type: ReactionType, + handler: Option, +} + +#[derive(Debug, Clone, Copy)] +enum ReactionType { + Fulfill, + Reject, +} + +#[derive(Debug, Clone, Trace, Finalize)] +struct PromiseCapability { + promise: JsValue, + resolve: JsValue, + reject: JsValue, +} + +#[derive(Debug, Trace, Finalize)] +struct PromiseCapabilityCaptures { + promise_capability: Gc>, +} + +#[derive(Debug, Trace, Finalize)] +struct ReactionJobCaptures { + reaction: ReactionRecord, + argument: JsValue, +} + +impl PromiseCapability { + /// `NewPromiseCapability ( C )` + /// + /// 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"); + + let get_species = FunctionBuilder::native(context, Self::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().promise().clone(), + ) + .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) + } +} + +#[derive(Debug)] +struct ResolvingFunctionsRecord { + resolve: JsValue, + reject: JsValue, +} + +#[derive(Debug, Trace, Finalize)] +struct RejectResolveCaptures { + promise: JsObject, + #[unsafe_ignore_trace] + already_resolved: Rc>, +} + +impl Promise { + const LENGTH: usize = 1; + + /// `Promise ( executor )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise-executor + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return context.throw_type_error("Promise NewTarget cannot be undefined"); + } + + let executor = args.get_or_undefined(0); + + // 2. If IsCallable(executor) is false, throw a TypeError exception. + if !executor.is_callable() { + return context.throw_type_error("Promise executor is not callable"); + } + + // 3. Let promise be ? OrdinaryCreateFromConstructor(NewTarget, "%Promise.prototype%", « [[PromiseState]], [[PromiseResult]], [[PromiseFulfillReactions]], [[PromiseRejectReactions]], [[PromiseIsHandled]] »). + let promise = + get_prototype_from_constructor(new_target, StandardConstructors::promise, context)?; + + let promise = JsObject::from_proto_and_data( + promise, + ObjectData::promise(Self { + promise_result: None, + // 4. Set promise.[[PromiseState]] to pending. + promise_state: PromiseState::Pending, + // 5. Set promise.[[PromiseFulfillReactions]] to a new empty List. + promise_fulfill_reactions: Vec::new(), + // 6. Set promise.[[PromiseRejectReactions]] to a new empty List. + promise_reject_reactions: Vec::new(), + // 7. Set promise.[[PromiseIsHandled]] to false. + promise_is_handled: false, + }), + ); + + // // 8. Let resolvingFunctions be CreateResolvingFunctions(promise). + let resolving_functions = Self::create_resolving_functions(&promise, context); + + // // 9. Let completion Completion(Call(executor, undefined, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)be ). + let completion = context.call( + executor, + &JsValue::Undefined, + &[ + resolving_functions.resolve, + resolving_functions.reject.clone(), + ], + ); + + // 10. If completion is an abrupt completion, then + if let Err(value) = completion { + // a. Perform ? Call(resolvingFunctions.[[Reject]], undefined, « completion.[[Value]] »). + context.call(&resolving_functions.reject, &JsValue::Undefined, &[value])?; + } + + // 11. Return promise. + promise.conv::().pipe(Ok) + } + + /// `CreateResolvingFunctions ( promise )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createresolvingfunctions + fn create_resolving_functions( + promise: &JsObject, + context: &mut Context, + ) -> ResolvingFunctionsRecord { + // 1. Let alreadyResolved be the Record { [[Value]]: false }. + let already_resolved = Rc::new(Cell::new(false)); + + // 5. Set resolve.[[Promise]] to promise. + // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. + let resolve_captures = RejectResolveCaptures { + already_resolved: already_resolved.clone(), + promise: promise.clone(), + }; + + // 2. Let stepsResolve be the algorithm steps defined in Promise Resolve Functions. + // 3. Let lengthResolve be the number of non-optional parameters of the function definition in Promise Resolve Functions. + // 4. Let resolve be CreateBuiltinFunction(stepsResolve, lengthResolve, "", « [[Promise]], [[AlreadyResolved]] »). + let resolve = 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() { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + already_resolved.set(true); + + let resolution = args.get_or_undefined(0); + + // 7. If SameValue(resolution, promise) is true, then + if JsValue::same_value(resolution, &promise.clone().into()) { + // a. Let selfResolutionError be a newly created TypeError object. + let self_resolution_error = + 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_promise(&self_resolution_error, context); + + // c. Return undefined. + return Ok(JsValue::Undefined); + } + + 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() + .expect("Expected promise to be a Promise") + .fulfill_promise(resolution, context)?; + + // b. Return undefined. + return Ok(JsValue::Undefined); + }; + + let then_action = 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_promise(&value, context); + + // b. Return undefined. + return Ok(JsValue::Undefined); + } + // 11. Let thenAction be then.[[Value]]. + Ok(then) => then, + }; + + // 12. If IsCallable(thenAction) is false, then + let 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); + } + }; + + // 13. Let thenJobCallback be HostMakeJobCallback(thenAction). + 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( + promise.clone(), + resolution.clone(), + then_job_callback, + context, + ); + + // 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + context.host_enqueue_promise_job(job); + + // 16. Return undefined. + Ok(JsValue::Undefined) + }, + resolve_captures, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // 10. Set reject.[[Promise]] to promise. + // 11. Set reject.[[AlreadyResolved]] to alreadyResolved. + let reject_captures = RejectResolveCaptures { + promise: promise.clone(), + already_resolved, + }; + + // 7. Let stepsReject be the algorithm steps defined in Promise Reject Functions. + // 8. Let lengthReject be the number of non-optional parameters of the function definition in Promise Reject Functions. + // 9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »). + let reject = FunctionBuilder::closure_with_captures( + context, + |_this, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise-reject-functions + + // 1. Let F be the active function object. + // 2. Assert: F has a [[Promise]] internal slot whose value is an Object. + // 3. Let promise be F.[[Promise]]. + // 4. Let alreadyResolved be F.[[AlreadyResolved]]. + let RejectResolveCaptures { + promise, + already_resolved, + } = captures; + + // 5. If alreadyResolved.[[Value]] is true, return undefined. + if already_resolved.get() { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + already_resolved.set(true); + + // let reason = args.get_or_undefined(0); + // 7. Perform RejectPromise(promise, reason). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .reject_promise(args.get_or_undefined(0), context); + + // 8. Return undefined. + Ok(JsValue::Undefined) + }, + reject_captures, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }. + let resolve = resolve.conv::(); + let reject = reject.conv::(); + ResolvingFunctionsRecord { resolve, reject } + } + + /// `FulfillPromise ( promise, value )` + /// + /// The abstract operation `FulfillPromise` takes arguments `promise` and `value` and returns + /// `unused`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-fulfillpromise + 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, + PromiseState::Pending, + "promise was not pending" + ); + + // 2. Let reactions be promise.[[PromiseFulfillReactions]]. + let reactions = &self.promise_fulfill_reactions; + + // 7. Perform TriggerPromiseReactions(reactions, value). + Self::trigger_promise_reactions(reactions, value, context); + // reordering this statement does not affect the semantics + + // 3. Set promise.[[PromiseResult]] to value. + self.promise_result = Some(value.clone()); + + // 4. Set promise.[[PromiseFulfillReactions]] to undefined. + self.promise_fulfill_reactions = Vec::new(); + + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + self.promise_reject_reactions = Vec::new(); + + // 6. Set promise.[[PromiseState]] to fulfilled. + self.promise_state = PromiseState::Fulfilled; + + // 8. Return unused. + Ok(()) + } + + /// `RejectPromise ( promise, reason )` + /// + /// The abstract operation `RejectPromise` takes arguments `promise` and `reason` and returns + /// `unused`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-rejectpromise + 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, + PromiseState::Pending, + "Expected promise.[[PromiseState]] to be pending" + ); + + // 2. Let reactions be promise.[[PromiseRejectReactions]]. + let reactions = &self.promise_reject_reactions; + + // 8. Perform TriggerPromiseReactions(reactions, reason). + Self::trigger_promise_reactions(reactions, reason, context); + // reordering this statement does not affect the semantics + + // 3. Set promise.[[PromiseResult]] to reason. + self.promise_result = Some(reason.clone()); + + // 4. Set promise.[[PromiseFulfillReactions]] to undefined. + self.promise_fulfill_reactions = Vec::new(); + + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + self.promise_reject_reactions = Vec::new(); + + // 6. Set promise.[[PromiseState]] to rejected. + self.promise_state = PromiseState::Rejected; + + // 7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject"). + if !self.promise_is_handled { + // TODO + } + + // 9. Return unused. + } + + /// `TriggerPromiseReactions ( reactions, argument )` + /// + /// The abstract operation `TriggerPromiseReactions` takes arguments `reactions` (a `List` of + /// `PromiseReaction` Records) and `argument` and returns unused. It enqueues a new `Job` for + /// each record in `reactions`. Each such `Job` processes the `[[Type]]` and `[[Handler]]` of + /// the `PromiseReaction` Record, and if the `[[Handler]]` is not `empty`, calls it passing the + /// given argument. If the `[[Handler]]` is `empty`, the behaviour is determined by the + /// `[[Type]]`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-triggerpromisereactions + pub fn trigger_promise_reactions( + reactions: &[ReactionRecord], + argument: &JsValue, + context: &mut Context, + ) { + // 1. For each element reaction of reactions, do + for reaction in reactions { + // a. Let job be NewPromiseReactionJob(reaction, argument). + let job = + PromiseJob::new_promise_reaction_job(reaction.clone(), argument.clone(), context); + + // b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + context.host_enqueue_promise_job(job); + } + + // 2. Return unused. + } + + /// `Promise.race ( iterable )` + /// + /// The `race` function returns a new promise which is settled in the same way as the first + /// passed promise to settle. It resolves all elements of the passed `iterable` to promises. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.race + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race + pub fn race(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let iterable = args.get_or_undefined(0); + + // 1. Let C be the this value. + let c = this; + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(c, context)?; + + // 3. Let promiseResolve be Completion(GetPromiseResolve(C)). + let promise_resolve = + Self::get_promise_resolve(c.as_object().expect("this was not an object"), context); + + // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). + if_abrupt_reject_promise!(promise_resolve, promise_capability, context); + + // 5. Let iteratorRecord be Completion(GetIterator(iterable)). + let iterator_record = iterable.get_iterator(context, None, None); + + // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability). + if_abrupt_reject_promise!(iterator_record, promise_capability, context); + + // 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. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performpromiserace + 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] + /// - [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); + + // 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] + /// - [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); + + // 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: + /// - [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); + + // 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: + /// - [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; + + // 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] + /// - [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; + + // 2. If IsPromise(promise) is false, throw a TypeError exception. + let promise_obj = match promise.as_promise() { + 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); + let on_rejected = args.get_or_undefined(1); + + // 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) + } + + /// `PerformPromiseThen ( promise, onFulfilled, onRejected [ , resultCapability ] )` + /// + /// 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 = 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 { + promise_capability: result_capability.clone(), + reaction_type: ReactionType::Fulfill, + handler: on_fulfilled_job_callback, + }; + + // 8. Let rejectReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Reject, [[Handler]]: onRejectedJobCallback }. + let reject_reaction = ReactionRecord { + promise_capability: result_capability.clone(), + reaction_type: ReactionType::Reject, + handler: on_rejected_job_callback, + }; + + match self.promise_state { + // 9. If promise.[[PromiseState]] is pending, then + PromiseState::Pending => { + // a. Append fulfillReaction as the last element of the List that is promise.[[PromiseFulfillReactions]]. + self.promise_fulfill_reactions.push(fulfill_reaction); + + // b. Append rejectReaction as the last element of the List that is promise.[[PromiseRejectReactions]]. + self.promise_reject_reactions.push(reject_reaction); + } + + // 10. Else if promise.[[PromiseState]] is fulfilled, then + PromiseState::Fulfilled => { + // a. Let value be promise.[[PromiseResult]]. + let value = self + .promise_result + .clone() + .expect("promise.[[PromiseResult]] cannot be empty"); + + // b. Let fulfillJob be NewPromiseReactionJob(fulfillReaction, value). + let fulfill_job = + PromiseJob::new_promise_reaction_job(fulfill_reaction, value, context); + + // c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]). + context.host_enqueue_promise_job(fulfill_job); + } + + // 11. Else, + // a. Assert: The value of promise.[[PromiseState]] is rejected. + PromiseState::Rejected => { + // b. Let reason be promise.[[PromiseResult]]. + let reason = self + .promise_result + .clone() + .expect("promise.[[PromiseResult]] cannot be empty"); + + // c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle"). + if !self.promise_is_handled { + // HostPromiseRejectionTracker(promise, "handle") + // TODO + } + + // d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason). + let reject_job = + PromiseJob::new_promise_reaction_job(reject_reaction, reason, context); + + // e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]). + context.host_enqueue_promise_job(reject_job); + + // 12. Set promise.[[PromiseIsHandled]] to true. + self.promise_is_handled = true; + } + } + + match result_capability { + // 13. If resultCapability is undefined, then + // a. Return undefined. + None => JsValue::Undefined, + + // 14. Else, + // a. Return resultCapability.[[Promise]]. + Some(result_capability) => result_capability.promise.clone(), + } + } + + /// `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(&c.into(), 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()) + } + + /// `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) + } +} 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 0000000000..093b3cba46 --- /dev/null +++ b/boa_engine/src/builtins/promise/promise_job.rs @@ -0,0 +1,182 @@ +use super::{Promise, PromiseCapability, ReactionJobCaptures}; +use crate::{ + builtins::promise::{ReactionRecord, ReactionType}, + job::JobCallback, + object::{FunctionBuilder, JsObject}, + Context, JsValue, +}; +use boa_gc::{Finalize, Trace}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct PromiseJob; + +impl PromiseJob { + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-newpromisereactionjob + pub(crate) fn new_promise_reaction_job( + reaction: ReactionRecord, + argument: JsValue, + context: &mut Context, + ) -> JobCallback { + // 1. Let job be a new Job Abstract Closure with no parameters that captures reaction and argument and performs the following steps when called: + let job = FunctionBuilder::closure_with_captures( + context, + |_this, _args, captures, context| { + let ReactionJobCaptures { reaction, argument } = captures; + + let ReactionRecord { + // a. Let promiseCapability be reaction.[[Capability]]. + promise_capability, + // b. Let type be reaction.[[Type]]. + reaction_type, + // c. Let handler be reaction.[[Handler]]. + handler, + } = reaction; + + let handler_result = match handler { + // d. If handler is empty, then + None => match reaction_type { + // i. If type is Fulfill, let handlerResult be NormalCompletion(argument). + ReactionType::Fulfill => Ok(argument.clone()), + // ii. Else, + // 1. Assert: type is Reject. + ReactionType::Reject => { + // 2. Let handlerResult be ThrowCompletion(argument). + Err(argument.clone()) + } + }, + // e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)). + Some(handler) => { + handler.call_job_callback(&JsValue::Undefined, &[argument.clone()], context) + } + }; + + match promise_capability { + None => { + // f. If promiseCapability is undefined, then + // i. Assert: handlerResult is not an abrupt completion. + assert!( + handler_result.is_ok(), + "Assertion: failed" + ); + + // ii. Return empty. + Ok(JsValue::Undefined) + } + Some(promise_capability_record) => { + // g. Assert: promiseCapability is a PromiseCapability Record. + let PromiseCapability { + promise: _, + resolve, + reject, + } = promise_capability_record; + + match handler_result { + // h. If handlerResult is an abrupt completion, then + Err(value) => { + // i. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »). + context.call(reject, &JsValue::Undefined, &[value]) + } + + // i. Else, + Ok(value) => { + // i. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »). + context.call(resolve, &JsValue::Undefined, &[value]) + } + } + } + } + }, + ReactionJobCaptures { reaction, argument }, + ) + .build() + .into(); + + // 2. Let handlerRealm be null. + // 3. If reaction.[[Handler]] is not empty, then + // a. Let getHandlerRealmResult be Completion(GetFunctionRealm(reaction.[[Handler]].[[Callback]])). + // b. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]]. + // c. Else, set handlerRealm to the current Realm Record. + // d. NOTE: handlerRealm is never null unless the handler is undefined. When the handler is a revoked Proxy and no ECMAScript code runs, handlerRealm is used to create error objects. + // 4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }. + JobCallback::make_job_callback(job) + } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob + pub(crate) fn new_promise_resolve_thenable_job( + promise_to_resolve: JsObject, + thenable: JsValue, + then: JobCallback, + context: &mut Context, + ) -> JobCallback { + // 1. Let job be a new Job Abstract Closure with no parameters that captures promiseToResolve, thenable, and then and performs the following steps when called: + let job = FunctionBuilder::closure_with_captures( + context, + |_this: &JsValue, _args: &[JsValue], captures, context: &mut Context| { + let JobCapture { + promise_to_resolve, + thenable, + then, + } = captures; + + // a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve). + let resolving_functions = + Promise::create_resolving_functions(promise_to_resolve, context); + + // b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)). + let then_call_result = then.call_job_callback( + thenable, + &[ + resolving_functions.resolve, + resolving_functions.reject.clone(), + ], + context, + ); + + // c. If thenCallResult is an abrupt completion, then + if let Err(value) = then_call_result { + // i. Return ? Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »). + return context.call( + &resolving_functions.reject, + &JsValue::Undefined, + &[value], + ); + } + + // d. Return ? thenCallResult. + then_call_result + }, + JobCapture::new(promise_to_resolve, thenable, then), + ) + .build(); + + // 2. Let getThenRealmResult be Completion(GetFunctionRealm(then.[[Callback]])). + // 3. If getThenRealmResult is a normal completion, let thenRealm be getThenRealmResult.[[Value]]. + // 4. Else, let thenRealm be the current Realm Record. + // 5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked Proxy and no code runs, thenRealm is used to create error objects. + // 6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }. + JobCallback::make_job_callback(job.into()) + } +} + +#[derive(Debug, Trace, Finalize)] +struct JobCapture { + promise_to_resolve: JsObject, + thenable: JsValue, + then: JobCallback, +} + +impl JobCapture { + fn new(promise_to_resolve: JsObject, thenable: JsValue, then: JobCallback) -> Self { + Self { + promise_to_resolve, + thenable, + then, + } + } +} diff --git a/boa_engine/src/builtins/promise/tests.rs b/boa_engine/src/builtins/promise/tests.rs new file mode 100644 index 0000000000..f32dcd2ade --- /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 43f4887cb3..eec70548f5 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 e95bc92e09..122b0460f6 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -5,6 +5,8 @@ pub mod intrinsics; #[cfg(feature = "intl")] mod icu; +use std::collections::VecDeque; + use intrinsics::{IntrinsicObjects, Intrinsics}; #[cfg(feature = "console")] @@ -13,6 +15,7 @@ use crate::{ builtins::{self, function::NativeFunctionSignature}, bytecompiler::ByteCompiler, class::{Class, ClassBuilder}, + job::JobCallback, object::{FunctionBuilder, GlobalPropertyMap, JsObject, ObjectData}, property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, @@ -97,6 +100,8 @@ pub struct Context { icu: icu::Icu, pub(crate) vm: Vm, + + pub(crate) promise_job_queue: VecDeque, } impl Default for Context { @@ -707,10 +712,19 @@ 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) } + /// 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.call_job_callback(&JsValue::Undefined, &[], self)?; + } + Ok(()) + } + /// Return the intrinsic constructors and objects. #[inline] pub fn intrinsics(&self) -> &Intrinsics { @@ -728,6 +742,18 @@ impl Context { pub(crate) fn icu(&self) -> &icu::Icu { &self.icu } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob + pub fn host_enqueue_promise_job(&mut self, job: JobCallback /* , realm: Realm */) { + // If realm is not null ... + // TODO + // Let scriptOrModule be ... + // TODO + self.promise_job_queue.push_back(job); + } } /// Builder for the [`Context`] type. /// @@ -795,6 +821,7 @@ impl ContextBuilder { icu::Icu::new(Box::new(icu_testdata::get_provider())) .expect("Failed to initialize default icu data.") }), + promise_job_queue: VecDeque::new(), }; // Add new builtIns to Context Realm diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs new file mode 100644 index 0000000000..2a0988b2e5 --- /dev/null +++ b/boa_engine/src/job.rs @@ -0,0 +1,58 @@ +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: JsObject, +} + +impl JobCallback { + /// `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, + arguments_list: &[JsValue], + context: &mut Context, + ) -> JsResult { + // It must perform and return the result of Call(jobCallback.[[Callback]], V, argumentsList). + + // 1. Assert: IsCallable(jobCallback.[[Callback]]) is true. + assert!( + self.callback.is_callable(), + "the callback of the job callback was not callable" + ); + + // 2. Return ? Call(jobCallback.[[Callback]], V, argumentsList). + self.callback.__call__(v, arguments_list, context) + } +} diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 8f550f5f82..626db69561 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/jsobject.rs b/boa_engine/src/object/jsobject.rs index cf75a8b3a0..e144897504 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/object/mod.rs b/boa_engine/src/object/mod.rs index 5eb4d6e972..2bd8d95c37 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -40,7 +40,7 @@ use crate::{ set::set_iterator::SetIterator, string::string_iterator::StringIterator, typed_array::integer_indexed_object::IntegerIndexed, - DataView, Date, RegExp, + DataView, Date, Promise, RegExp, }, context::intrinsics::StandardConstructor, property::{Attribute, PropertyDescriptor, PropertyKey}, @@ -172,6 +172,7 @@ pub enum ObjectKind { IntegerIndexed(IntegerIndexed), #[cfg(feature = "intl")] DateTimeFormat(Box), + Promise(Promise), } impl ObjectData { @@ -255,6 +256,14 @@ impl ObjectData { } } + /// Create the `Promise` object data + pub fn promise(promise: Promise) -> Self { + Self { + kind: ObjectKind::Promise(promise), + internal_methods: &ORDINARY_INTERNAL_METHODS, + } + } + /// Create the `ForInIterator` object data pub fn for_in_iterator(for_in_iterator: ForInIterator) -> Self { Self { @@ -473,6 +482,7 @@ impl Display for ObjectKind { Self::DataView(_) => "DataView", #[cfg(feature = "intl")] Self::DateTimeFormat(_) => "DateTimeFormat", + Self::Promise(_) => "Promise", }) } } @@ -1203,6 +1213,41 @@ impl Object { } } + /// Checks if it is a `Promise` object. + #[inline] + pub fn is_promise(&self) -> bool { + matches!( + self.data, + ObjectData { + kind: ObjectKind::Promise(_), + .. + } + ) + } + + /// Gets the promise data if the object is a promise. + #[inline] + pub fn as_promise(&self) -> Option<&Promise> { + match self.data { + ObjectData { + kind: ObjectKind::Promise(ref promise), + .. + } => Some(promise), + _ => None, + } + } + + #[inline] + pub fn as_promise_mut(&mut self) -> Option<&mut Promise> { + match self.data { + ObjectData { + kind: ObjectKind::Promise(ref mut promise), + .. + } => Some(promise), + _ => None, + } + } + /// Return `true` if it is a native object and the native type is `T`. #[inline] pub fn is(&self) -> bool diff --git a/boa_engine/src/value/mod.rs b/boa_engine/src/value/mod.rs index 509249529f..9b2c900ecb 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 { diff --git a/boa_tester/Cargo.toml b/boa_tester/Cargo.toml index 83075293cd..83313984c1 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 91bb507ed2..f6ad73dd21 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 ff5748972a..3c51964521 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); @@ -318,12 +330,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( @@ -343,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()) } diff --git a/boa_tester/src/main.rs b/boa_tester/src/main.rs index 8b33aa9062..d2f18c12c2 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 092737ed19..86cadb7221 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, }) } diff --git a/test_ignore.txt b/test_ignore.txt index 9fa40b450e..321c3f7fdb 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