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
3 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