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 3 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",
"bitflags",
"boa_engine",
"boa_gc",
"boa_interner",
"colored",
"fxhash",

46
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<bool> {
// 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<Option<IteratorResult>> {
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]

5
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")]

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

27
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<JobCallback>,
}
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

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 context;
pub mod environments;
pub mod job;
pub mod object;
pub mod property;
pub mod realm;

11
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

47
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<DateTimeFormat>),
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<T>(&self) -> bool

13
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 {

1
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"

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(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<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,
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<Cell<Option<bool>>>,
}
/// `print()` function required by the test262 suite.
fn test262_print(_this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
todo!("print() function");
#[allow(clippy::unnecessary_wraps)]
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 {
assert: Box<str>,
sta: Box<str>,
doneprint_handle: 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 = 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<Harness> {
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,
})
}

1
test_ignore.txt

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

Loading…
Cancel
Save