Browse Source

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
```
pull/2123/head
Iban Eguia 2 years ago
parent
commit
0454ddec19
  1. 1
      Cargo.lock
  2. 46
      boa_engine/src/builtins/iterable/mod.rs
  3. 5
      boa_engine/src/builtins/mod.rs
  4. 1191
      boa_engine/src/builtins/promise/mod.rs
  5. 182
      boa_engine/src/builtins/promise/promise_job.rs
  6. 19
      boa_engine/src/builtins/promise/tests.rs
  7. 7
      boa_engine/src/context/intrinsics.rs
  8. 27
      boa_engine/src/context/mod.rs
  9. 58
      boa_engine/src/job.rs
  10. 1
      boa_engine/src/lib.rs
  11. 11
      boa_engine/src/object/jsobject.rs
  12. 47
      boa_engine/src/object/mod.rs
  13. 13
      boa_engine/src/value/mod.rs
  14. 1
      boa_tester/Cargo.toml
  15. 6
      boa_tester/src/exec/js262.rs
  16. 73
      boa_tester/src/exec/mod.rs
  17. 1
      boa_tester/src/main.rs
  18. 6
      boa_tester/src/read.rs
  19. 1
      test_ignore.txt

1
Cargo.lock generated

@ -164,6 +164,7 @@ dependencies = [
"anyhow", "anyhow",
"bitflags", "bitflags",
"boa_engine", "boa_engine",
"boa_gc",
"boa_interner", "boa_interner",
"colored", "colored",
"fxhash", "fxhash",

46
boa_engine/src/builtins/iterable/mod.rs

@ -202,19 +202,26 @@ pub struct IteratorResult {
} }
impl 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: /// More information:
/// - [ECMA reference][spec] /// - [ECMA reference][spec]
/// ///
/// [spec]: https://tc39.es/ecma262/#sec-iteratorclose /// [spec]: https://tc39.es/ecma262/#sec-iteratorcomplete
#[inline] #[inline]
pub fn complete(&self, context: &mut Context) -> JsResult<bool> { pub fn complete(&self, context: &mut Context) -> JsResult<bool> {
// 1. Return ToBoolean(? Get(iterResult, "done")). // 1. Return ToBoolean(? Get(iterResult, "done")).
Ok(self.object.get("done", context)?.to_boolean()) 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: /// More information:
/// - [ECMA reference][spec] /// - [ECMA reference][spec]
@ -226,13 +233,16 @@ impl IteratorResult {
self.object.get("value", context) self.object.get("value", context)
} }
} }
/// Iterator Record
///
/// An Iterator Record is a Record value used to encapsulate an /// 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: /// More information:
/// - [ECMA reference][spec] /// - [ECMA reference][spec]
/// ///
/// [spec]:https://tc39.es/ecma262/#table-iterator-record-fields /// [spec]: https://tc39.es/ecma262/#sec-iterator-records
#[derive(Debug)] #[derive(Debug)]
pub struct IteratorRecord { pub struct IteratorRecord {
/// `[[Iterator]]` /// `[[Iterator]]`
@ -265,7 +275,11 @@ impl IteratorRecord {
&self.next_function &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: /// More information:
/// - [ECMA reference][spec] /// - [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<Option<IteratorResult>> { pub(crate) fn step(&self, context: &mut Context) -> JsResult<Option<IteratorResult>> {
let _timer = Profiler::global().start_event("IteratorRecord::step", "iterator"); let _timer = Profiler::global().start_event("IteratorRecord::step", "iterator");
@ -317,7 +342,12 @@ impl IteratorRecord {
Ok(Some(result)) 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: /// More information:
/// - [ECMA reference][spec] /// - [ECMA reference][spec]

5
boa_engine/src/builtins/mod.rs

@ -20,6 +20,7 @@ pub mod math;
pub mod nan; pub mod nan;
pub mod number; pub mod number;
pub mod object; pub mod object;
pub mod promise;
pub mod proxy; pub mod proxy;
pub mod reflect; pub mod reflect;
pub mod regexp; pub mod regexp;
@ -57,6 +58,7 @@ pub(crate) use self::{
number::Number, number::Number,
object::for_in_iterator::ForInIterator, object::for_in_iterator::ForInIterator,
object::Object as BuiltInObjectObject, object::Object as BuiltInObjectObject,
promise::Promise,
proxy::Proxy, proxy::Proxy,
reflect::Reflect, reflect::Reflect,
regexp::RegExp, regexp::RegExp,
@ -182,7 +184,8 @@ pub fn init(context: &mut Context) {
AggregateError, AggregateError,
Reflect, Reflect,
Generator, Generator,
GeneratorFunction GeneratorFunction,
Promise
}; };
#[cfg(feature = "intl")] #[cfg(feature = "intl")]

1191
boa_engine/src/builtins/promise/mod.rs

File diff suppressed because it is too large Load Diff

182
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: <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,
}
}
}

19
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"));
}

7
boa_engine/src/context/intrinsics.rs

@ -110,6 +110,7 @@ pub struct StandardConstructors {
array_buffer: StandardConstructor, array_buffer: StandardConstructor,
data_view: StandardConstructor, data_view: StandardConstructor,
date_time_format: StandardConstructor, date_time_format: StandardConstructor,
promise: StandardConstructor,
} }
impl Default for StandardConstructors { impl Default for StandardConstructors {
@ -165,6 +166,7 @@ impl Default for StandardConstructors {
array_buffer: StandardConstructor::default(), array_buffer: StandardConstructor::default(),
data_view: StandardConstructor::default(), data_view: StandardConstructor::default(),
date_time_format: StandardConstructor::default(), date_time_format: StandardConstructor::default(),
promise: StandardConstructor::default(),
}; };
// The value of `Array.prototype` is the Array prototype object. // The value of `Array.prototype` is the Array prototype object.
@ -372,6 +374,11 @@ impl StandardConstructors {
pub fn date_time_format(&self) -> &StandardConstructor { pub fn date_time_format(&self) -> &StandardConstructor {
&self.date_time_format &self.date_time_format
} }
#[inline]
pub fn promise(&self) -> &StandardConstructor {
&self.promise
}
} }
/// Cached intrinsic objects /// Cached intrinsic objects

27
boa_engine/src/context/mod.rs

@ -5,6 +5,8 @@ pub mod intrinsics;
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
mod icu; mod icu;
use std::collections::VecDeque;
use intrinsics::{IntrinsicObjects, Intrinsics}; use intrinsics::{IntrinsicObjects, Intrinsics};
#[cfg(feature = "console")] #[cfg(feature = "console")]
@ -13,6 +15,7 @@ use crate::{
builtins::{self, function::NativeFunctionSignature}, builtins::{self, function::NativeFunctionSignature},
bytecompiler::ByteCompiler, bytecompiler::ByteCompiler,
class::{Class, ClassBuilder}, class::{Class, ClassBuilder},
job::JobCallback,
object::{FunctionBuilder, GlobalPropertyMap, JsObject, ObjectData}, object::{FunctionBuilder, GlobalPropertyMap, JsObject, ObjectData},
property::{Attribute, PropertyDescriptor, PropertyKey}, property::{Attribute, PropertyDescriptor, PropertyKey},
realm::Realm, realm::Realm,
@ -97,6 +100,8 @@ pub struct Context {
icu: icu::Icu, icu: icu::Icu,
pub(crate) vm: Vm, pub(crate) vm: Vm,
pub(crate) promise_job_queue: VecDeque<JobCallback>,
} }
impl Default for Context { impl Default for Context {
@ -707,10 +712,19 @@ impl Context {
self.realm.set_global_binding_number(); self.realm.set_global_binding_number();
let result = self.run(); let result = self.run();
self.vm.pop_frame(); self.vm.pop_frame();
self.run_queued_jobs()?;
let (result, _) = result?; let (result, _) = result?;
Ok(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. /// Return the intrinsic constructors and objects.
#[inline] #[inline]
pub fn intrinsics(&self) -> &Intrinsics { pub fn intrinsics(&self) -> &Intrinsics {
@ -728,6 +742,18 @@ impl Context {
pub(crate) fn icu(&self) -> &icu::Icu { pub(crate) fn icu(&self) -> &icu::Icu {
&self.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. /// Builder for the [`Context`] type.
/// ///
@ -795,6 +821,7 @@ impl ContextBuilder {
icu::Icu::new(Box::new(icu_testdata::get_provider())) icu::Icu::new(Box::new(icu_testdata::get_provider()))
.expect("Failed to initialize default icu data.") .expect("Failed to initialize default icu data.")
}), }),
promise_job_queue: VecDeque::new(),
}; };
// Add new builtIns to Context Realm // Add new builtIns to Context Realm

58
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<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)
}
}

1
boa_engine/src/lib.rs

@ -77,6 +77,7 @@ pub mod bytecompiler;
pub mod class; pub mod class;
pub mod context; pub mod context;
pub mod environments; pub mod environments;
pub mod job;
pub mod object; pub mod object;
pub mod property; pub mod property;
pub mod realm; pub mod realm;

11
boa_engine/src/object/jsobject.rs

@ -449,6 +449,17 @@ impl JsObject {
self.borrow().is_typed_array() 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. /// Checks if it's an ordinary object.
/// ///
/// # Panics /// # Panics

47
boa_engine/src/object/mod.rs

@ -40,7 +40,7 @@ use crate::{
set::set_iterator::SetIterator, set::set_iterator::SetIterator,
string::string_iterator::StringIterator, string::string_iterator::StringIterator,
typed_array::integer_indexed_object::IntegerIndexed, typed_array::integer_indexed_object::IntegerIndexed,
DataView, Date, RegExp, DataView, Date, Promise, RegExp,
}, },
context::intrinsics::StandardConstructor, context::intrinsics::StandardConstructor,
property::{Attribute, PropertyDescriptor, PropertyKey}, property::{Attribute, PropertyDescriptor, PropertyKey},
@ -172,6 +172,7 @@ pub enum ObjectKind {
IntegerIndexed(IntegerIndexed), IntegerIndexed(IntegerIndexed),
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
DateTimeFormat(Box<DateTimeFormat>), DateTimeFormat(Box<DateTimeFormat>),
Promise(Promise),
} }
impl ObjectData { 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 /// Create the `ForInIterator` object data
pub fn for_in_iterator(for_in_iterator: ForInIterator) -> Self { pub fn for_in_iterator(for_in_iterator: ForInIterator) -> Self {
Self { Self {
@ -473,6 +482,7 @@ impl Display for ObjectKind {
Self::DataView(_) => "DataView", Self::DataView(_) => "DataView",
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
Self::DateTimeFormat(_) => "DateTimeFormat", 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`. /// Return `true` if it is a native object and the native type is `T`.
#[inline] #[inline]
pub fn is<T>(&self) -> bool pub fn is<T>(&self) -> bool

13
boa_engine/src/value/mod.rs

@ -148,7 +148,7 @@ impl JsValue {
self.as_object().filter(|obj| obj.is_callable()) 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] #[inline]
pub fn is_constructor(&self) -> bool { pub fn is_constructor(&self) -> bool {
matches!(self, Self::Object(obj) if obj.is_constructor()) matches!(self, Self::Object(obj) if obj.is_constructor())
@ -159,6 +159,17 @@ impl JsValue {
self.as_object().filter(|obj| obj.is_constructor()) 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. /// Returns true if the value is a symbol.
#[inline] #[inline]
pub fn is_symbol(&self) -> bool { pub fn is_symbol(&self) -> bool {

1
boa_tester/Cargo.toml

@ -14,6 +14,7 @@ publish = false
[dependencies] [dependencies]
boa_engine = { path = "../boa_engine", features = ["intl"], version = "0.15.0" } boa_engine = { path = "../boa_engine", features = ["intl"], version = "0.15.0" }
boa_interner = { path = "../boa_interner", 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" structopt = "0.3.26"
serde = { version = "1.0.137", features = ["derive"] } serde = { version = "1.0.137", features = ["derive"] }
serde_yaml = "0.8.24" serde_yaml = "0.8.24"

6
boa_tester/src/exec/js262.rs

@ -13,6 +13,7 @@ pub(super) fn init(context: &mut Context) -> JsObject {
.function(create_realm, "createRealm", 0) .function(create_realm, "createRealm", 0)
.function(detach_array_buffer, "detachArrayBuffer", 2) .function(detach_array_buffer, "detachArrayBuffer", 2)
.function(eval_script, "evalScript", 1) .function(eval_script, "evalScript", 1)
.function(gc, "gc", 0)
.property("global", global_obj, Attribute::default()) .property("global", global_obj, Attribute::default())
// .property("agent", agent, Attribute::default()) // .property("agent", agent, Attribute::default())
.build(); .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. /// 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 /// 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. /// 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<JsValue> { fn gc(_this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
todo!() boa_gc::force_collect();
Ok(JsValue::undefined())
} }

73
boa_tester/src/exec/mod.rs

@ -6,7 +6,11 @@ use super::{
Harness, Outcome, Phase, SuiteResult, Test, TestFlags, TestOutcomeResult, TestResult, Harness, Outcome, Phase, SuiteResult, Test, TestFlags, TestOutcomeResult, TestResult,
TestSuite, IGNORED, 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 colored::Colorize;
use rayon::prelude::*; use rayon::prelude::*;
use std::panic; use std::panic;
@ -165,14 +169,16 @@ impl Test {
)) { )) {
let res = panic::catch_unwind(|| match self.expected_outcome { let res = panic::catch_unwind(|| match self.expected_outcome {
Outcome::Positive => { Outcome::Positive => {
// TODO: implement async and add `harness/doneprintHandle.js` to the includes.
let mut context = Context::default(); 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(_) => { Ok(_) => {
let res = context.eval(&test_content); 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 { let text = match res {
Ok(val) => val.display().to_string(), Ok(val) => val.display().to_string(),
Err(e) => format!("Uncaught {}", e.display()), 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) { if let Err(e) = Parser::new(test_content.as_bytes()).parse_all(&mut context) {
(false, format!("Uncaught {e}")) (false, format!("Uncaught {e}"))
} else { } 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(_) => match context.eval(&test_content) {
Ok(res) => (false, res.display().to_string()), Ok(res) => (false, res.display().to_string()),
Err(e) => { Err(e) => {
@ -306,9 +313,14 @@ impl Test {
} }
/// Sets the environment up to run the 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. // Register the print() function.
context.register_global_function("print", 1, test262_print); Self::register_print_fn(context, callback_obj);
// add the $262 object. // add the $262 object.
let _js262 = js262::init(context); let _js262 = js262::init(context);
@ -318,12 +330,18 @@ impl Test {
} }
context context
.eval(&harness.assert.as_ref()) .eval(harness.assert.as_ref())
.map_err(|e| format!("could not run assert.js:\n{}", e.display()))?; .map_err(|e| format!("could not run assert.js:\n{}", e.display()))?;
context context
.eval(&harness.sta.as_ref()) .eval(harness.sta.as_ref())
.map_err(|e| format!("could not run sta.js:\n{}", e.display()))?; .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() { for include in self.includes.iter() {
context context
.eval( .eval(
@ -343,9 +361,42 @@ impl Test {
Ok(()) 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<Cell<Option<bool>>>,
} }
/// `print()` function required by the test262 suite. /// `print()` function required by the test262 suite.
fn test262_print(_this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult<JsValue> { #[allow(clippy::unnecessary_wraps)]
todo!("print() function"); fn test262_print(
_this: &JsValue,
args: &[JsValue],
captures: &mut CallbackObject,
_context: &mut Context,
) -> JsResult<JsValue> {
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())
} }

1
boa_tester/src/main.rs

@ -342,6 +342,7 @@ fn run_test_suite(
struct Harness { struct Harness {
assert: Box<str>, assert: Box<str>,
sta: Box<str>, sta: Box<str>,
doneprint_handle: Box<str>,
includes: FxHashMap<Box<str>, Box<str>>, includes: FxHashMap<Box<str>, Box<str>>,
} }

6
boa_tester/src/read.rs

@ -84,7 +84,7 @@ pub(super) fn read_harness(test262_path: &Path) -> anyhow::Result<Harness> {
let file_name = entry.file_name(); let file_name = entry.file_name();
let file_name = file_name.to_string_lossy(); 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; continue;
} }
@ -102,10 +102,14 @@ pub(super) fn read_harness(test262_path: &Path) -> anyhow::Result<Harness> {
let sta = fs::read_to_string(test262_path.join("harness/sta.js")) let sta = fs::read_to_string(test262_path.join("harness/sta.js"))
.context("error reading harnes/sta.js")? .context("error reading harnes/sta.js")?
.into_boxed_str(); .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 { Ok(Harness {
assert, assert,
sta, sta,
doneprint_handle,
includes, includes,
}) })
} }

1
test_ignore.txt

@ -1,6 +1,5 @@
// Not implemented yet: // Not implemented yet:
flag:module flag:module
flag:async
// Non-implemented features: // Non-implemented features:
feature:json-modules feature:json-modules

Loading…
Cancel
Save