mirror of https://github.com/boa-dev/boa.git
Browse Source
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 ```pull/2123/head
Iban Eguia
2 years ago
19 changed files with 1670 additions and 26 deletions
File diff suppressed because it is too large
Load Diff
@ -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: <handlerResult is not an abrupt completion> 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, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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")); |
||||||
|
} |
@ -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<JsValue> { |
||||||
|
// 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) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue