Browse Source

Implement host hooks and job queues APIs (#2529)

Follows from #2528, and should complement #2411 to implement the module import hooks.

~~Similarly to the Intl/ICU4X PR (#2478), this has a lot of trivial changes caused by the new lifetimes. I thought about passing the queue and the hooks by value, but it was very painful having to wrap everything with `Rc` in order to be accessible by the host.
In contrast, `&dyn` can be easily provided by the host and has the advantage of not requiring additional allocations, with the downside of adding two more lifetimes to our `Context`, but I think it's worth.~~ I was able to unify all lifetimes into the shortest one of the three, making our API just like before!

Changes:
- Added a new `HostHooks` trait and a `&dyn HostHooks` field to `Context`. This allows hosts to implement the trait for their custom type, then pass it to the context.
- Added a new `JobQueue` trait and a `&dyn JobQueue` field to our `Context`, allowing custom event loops and other fun things.
- Added two simple implementations of `JobQueue`: `IdleJobQueue` which does nothing and `SimpleJobQueue` which runs all jobs until all successfully complete or until any of them throws an error.
- Modified `boa_cli` to run all jobs until the queue is empty, even if a job returns `Err`. This also prints all errors to the user.
pull/2544/head
José Julián Espina 2 years ago
parent
commit
5ab0aa21cc
  1. 36
      boa_cli/src/main.rs
  2. 18
      boa_engine/src/builtins/async_generator/mod.rs
  3. 10
      boa_engine/src/builtins/eval/mod.rs
  4. 252
      boa_engine/src/builtins/promise/mod.rs
  5. 12
      boa_engine/src/builtins/promise/tests.rs
  6. 10
      boa_engine/src/bytecompiler/mod.rs
  7. 10
      boa_engine/src/class.rs
  8. 149
      boa_engine/src/context/hooks.rs
  9. 164
      boa_engine/src/context/mod.rs
  10. 10
      boa_engine/src/error.rs
  11. 195
      boa_engine/src/job.rs
  12. 168
      boa_engine/src/native_function.rs
  13. 47
      boa_engine/src/object/mod.rs
  14. 22
      boa_engine/src/object/operations.rs
  15. 23
      boa_tester/src/exec/mod.rs

36
boa_cli/src/main.rs

@ -62,13 +62,15 @@ mod helper;
use boa_ast::StatementList; use boa_ast::StatementList;
use boa_engine::{ use boa_engine::{
context::ContextBuilder,
job::{JobQueue, NativeJob},
vm::flowgraph::{Direction, Graph}, vm::flowgraph::{Direction, Graph},
Context, JsResult, Context, JsResult,
}; };
use clap::{Parser, ValueEnum, ValueHint}; use clap::{Parser, ValueEnum, ValueHint};
use colored::{Color, Colorize}; use colored::{Color, Colorize};
use rustyline::{config::Config, error::ReadlineError, EditMode, Editor}; use rustyline::{config::Config, error::ReadlineError, EditMode, Editor};
use std::{fs::read, fs::OpenOptions, io, path::PathBuf}; use std::{cell::RefCell, collections::VecDeque, fs::read, fs::OpenOptions, io, path::PathBuf};
#[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] #[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))]
#[cfg_attr( #[cfg_attr(
@ -253,7 +255,8 @@ fn generate_flowgraph(
fn main() -> Result<(), io::Error> { fn main() -> Result<(), io::Error> {
let args = Opt::parse(); let args = Opt::parse();
let mut context = Context::default(); let queue = Jobs::default();
let mut context = ContextBuilder::new().job_queue(&queue).build();
// Trace Output // Trace Output
context.set_trace(args.trace); context.set_trace(args.trace);
@ -280,6 +283,7 @@ fn main() -> Result<(), io::Error> {
Ok(v) => println!("{}", v.display()), Ok(v) => println!("{}", v.display()),
Err(v) => eprintln!("Uncaught {v}"), Err(v) => eprintln!("Uncaught {v}"),
} }
context.run_jobs();
} }
} }
@ -333,11 +337,14 @@ fn main() -> Result<(), io::Error> {
} }
} else { } else {
match context.eval(line.trim_end()) { match context.eval(line.trim_end()) {
Ok(v) => println!("{}", v.display()), Ok(v) => {
println!("{}", v.display());
}
Err(v) => { Err(v) => {
eprintln!("{}: {}", "Uncaught".red(), v.to_string().red()); eprintln!("{}: {}", "Uncaught".red(), v.to_string().red());
} }
} }
context.run_jobs();
} }
} }
@ -355,3 +362,26 @@ fn main() -> Result<(), io::Error> {
Ok(()) Ok(())
} }
#[derive(Default)]
struct Jobs(RefCell<VecDeque<NativeJob>>);
impl JobQueue for Jobs {
fn enqueue_promise_job(&self, job: NativeJob, _: &mut Context<'_>) {
self.0.borrow_mut().push_front(job);
}
fn run_jobs(&self, context: &mut Context<'_>) {
loop {
let jobs = std::mem::take(&mut *self.0.borrow_mut());
if jobs.is_empty() {
return;
}
for job in jobs {
if let Err(e) = job.call(context) {
eprintln!("Uncaught {e}");
}
}
}
}
}

18
boa_engine/src/builtins/async_generator/mod.rs

@ -632,20 +632,22 @@ impl AsyncGenerator {
context, context,
NativeFunction::from_copy_closure_with_captures( NativeFunction::from_copy_closure_with_captures(
|_this, args, generator, context| { |_this, args, generator, context| {
let mut generator_borrow_mut = generator.borrow_mut(); let next = {
let gen = generator_borrow_mut let mut generator_borrow_mut = generator.borrow_mut();
.as_async_generator_mut() let gen = generator_borrow_mut
.expect("already checked before"); .as_async_generator_mut()
.expect("already checked before");
// a. Set generator.[[AsyncGeneratorState]] to completed. // a. Set generator.[[AsyncGeneratorState]] to completed.
gen.state = AsyncGeneratorState::Completed; gen.state = AsyncGeneratorState::Completed;
gen.queue.pop_front().expect("must have one entry")
};
// b. Let result be NormalCompletion(value). // b. Let result be NormalCompletion(value).
let result = Ok(args.get_or_undefined(0).clone()); let result = Ok(args.get_or_undefined(0).clone());
// c. Perform AsyncGeneratorCompleteStep(generator, result, true). // c. Perform AsyncGeneratorCompleteStep(generator, result, true).
let next = gen.queue.pop_front().expect("must have one entry");
drop(generator_borrow_mut);
Self::complete_step(&next, result, true, context); Self::complete_step(&next, result, true, context);
// d. Perform AsyncGeneratorDrainQueue(generator). // d. Perform AsyncGeneratorDrainQueue(generator).

10
boa_engine/src/builtins/eval/mod.rs

@ -117,15 +117,17 @@ impl Eval {
// Because of implementation details the following code differs from the spec. // Because of implementation details the following code differs from the spec.
// 5. Perform ? HostEnsureCanCompileStrings(evalRealm). // 5. Perform ? HostEnsureCanCompileStrings(evalRealm).
let mut parser = Parser::new(x.as_bytes()); context.host_hooks().ensure_can_compile_strings(context)?;
if strict {
parser.set_strict();
}
// 11. Perform the following substeps in an implementation-defined order, possibly interleaving parsing and error detection: // 11. Perform the following substeps in an implementation-defined order, possibly interleaving parsing and error detection:
// a. Let script be ParseText(StringToCodePoints(x), Script). // a. Let script be ParseText(StringToCodePoints(x), Script).
// b. If script is a List of errors, throw a SyntaxError exception. // b. If script is a List of errors, throw a SyntaxError exception.
// c. If script Contains ScriptBody is false, return undefined. // c. If script Contains ScriptBody is false, return undefined.
// d. Let body be the ScriptBody of script. // d. Let body be the ScriptBody of script.
let mut parser = Parser::new(x.as_bytes());
if strict {
parser.set_strict();
}
let body = parser.parse_eval(direct, context.interner_mut())?; let body = parser.parse_eval(direct, context.interner_mut())?;
// 6. Let inFunction be false. // 6. Let inFunction be false.

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

@ -62,7 +62,7 @@ pub(crate) enum PromiseState {
} }
/// The internal representation of a `Promise` object. /// The internal representation of a `Promise` object.
#[derive(Debug, Clone, Trace, Finalize)] #[derive(Debug, Trace, Finalize)]
pub struct Promise { pub struct Promise {
state: PromiseState, state: PromiseState,
fulfill_reactions: Vec<ReactionRecord>, fulfill_reactions: Vec<ReactionRecord>,
@ -76,7 +76,7 @@ pub struct Promise {
/// - [ECMAScript reference][spec] /// - [ECMAScript reference][spec]
/// ///
/// [spec]: https://tc39.es/ecma262/#sec-promisereaction-records /// [spec]: https://tc39.es/ecma262/#sec-promisereaction-records
#[derive(Debug, Clone, Trace, Finalize)] #[derive(Debug, Trace, Finalize)]
pub(crate) struct ReactionRecord { pub(crate) struct ReactionRecord {
/// The `[[Capability]]` field. /// The `[[Capability]]` field.
promise_capability: Option<PromiseCapability>, promise_capability: Option<PromiseCapability>,
@ -101,6 +101,25 @@ enum ReactionType {
Reject, Reject,
} }
/// The operation type of the [`HostPromiseRejectionTracker`][fn] abstract operation.
///
/// # Note
///
/// Per the spec:
///
/// > If operation is "handle", an implementation should not hold a reference to promise in a way
/// that would interfere with garbage collection. An implementation may hold a reference to promise
/// if operation is "reject", since it is expected that rejections will be rare and not on hot code paths.
///
/// [fn]: https://tc39.es/ecma262/#sec-host-promise-rejection-tracker
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OperationType {
/// A promise was rejected without any handlers.
Reject,
/// A handler was added to a rejected promise for the first time.
Handle,
}
/// The internal `PromiseCapability` data type. /// The internal `PromiseCapability` data type.
/// ///
/// More information: /// More information:
@ -133,99 +152,94 @@ impl PromiseCapability {
resolve: JsValue, resolve: JsValue,
} }
match c.as_constructor() { // 1. If IsConstructor(C) is false, throw a TypeError exception.
// 1. If IsConstructor(C) is false, throw a TypeError exception. let c = c.as_constructor().ok_or_else(|| {
None => Err(JsNativeError::typ() JsNativeError::typ().with_message("PromiseCapability: expected constructor")
.with_message("PromiseCapability: expected constructor") })?;
.into()),
Some(c) => { let c = c.clone();
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).
// 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 }.
// 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }. let promise_capability = Gc::new(GcCell::new(RejectResolve {
let promise_capability = Gc::new(GcCell::new(RejectResolve { reject: JsValue::undefined(),
reject: JsValue::undefined(), resolve: 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:
// 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, "", « »).
// 5. Let executor be CreateBuiltinFunction(executorClosure, 2, "", « »). let executor = FunctionObjectBuilder::new(
let executor = FunctionObjectBuilder::new( context,
context, NativeFunction::from_copy_closure_with_captures(
NativeFunction::from_copy_closure_with_captures( |_this, args: &[JsValue], captures, _| {
|_this, args: &[JsValue], captures, _| { let mut promise_capability = captures.borrow_mut();
let mut promise_capability = captures.borrow_mut(); // a. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception.
// a. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception. if !promise_capability.resolve.is_undefined() {
if !promise_capability.resolve.is_undefined() { return Err(JsNativeError::typ()
return Err(JsNativeError::typ() .with_message("promiseCapability.[[Resolve]] is not undefined")
.with_message("promiseCapability.[[Resolve]] is not undefined") .into());
.into()); }
}
// b. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception.
// b. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception. if !promise_capability.reject.is_undefined() {
if !promise_capability.reject.is_undefined() { return Err(JsNativeError::typ()
return Err(JsNativeError::typ() .with_message("promiseCapability.[[Reject]] is not undefined")
.with_message("promiseCapability.[[Reject]] is not undefined") .into());
.into()); }
}
let resolve = args.get_or_undefined(0);
let resolve = args.get_or_undefined(0); let reject = args.get_or_undefined(1);
let reject = args.get_or_undefined(1);
// c. Set promiseCapability.[[Resolve]] to resolve.
// c. Set promiseCapability.[[Resolve]] to resolve. promise_capability.resolve = resolve.clone();
promise_capability.resolve = resolve.clone();
// d. Set promiseCapability.[[Reject]] to reject.
// d. Set promiseCapability.[[Reject]] to reject. promise_capability.reject = reject.clone();
promise_capability.reject = reject.clone();
// e. Return undefined.
// e. Return undefined. Ok(JsValue::Undefined)
Ok(JsValue::Undefined) },
}, promise_capability.clone(),
promise_capability.clone(), ),
), )
) .name("")
.name("") .length(2)
.length(2) .build()
.build() .into();
.into();
// 6. Let promise be ? Construct(C, « executor »).
// 6. Let promise be ? Construct(C, « executor »). let promise = c.construct(&[executor], Some(&c), context)?;
let promise = c.construct(&[executor], Some(&c), context)?;
let promise_capability = promise_capability.borrow();
let promise_capability = promise_capability.borrow();
let resolve = promise_capability.resolve.clone();
let resolve = promise_capability.resolve.clone(); let reject = promise_capability.reject.clone();
let reject = promise_capability.reject.clone();
// 7. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception.
// 7. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception. let resolve = resolve
let resolve = resolve .as_object()
.as_object() .cloned()
.cloned() .and_then(JsFunction::from_object)
.and_then(JsFunction::from_object) .ok_or_else(|| {
.ok_or_else(|| { JsNativeError::typ().with_message("promiseCapability.[[Resolve]] is not callable")
JsNativeError::typ() })?;
.with_message("promiseCapability.[[Resolve]] is not callable")
})?; // 8. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception.
let reject = reject
// 8. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception. .as_object()
let reject = reject .cloned()
.as_object() .and_then(JsFunction::from_object)
.cloned() .ok_or_else(|| {
.and_then(JsFunction::from_object) JsNativeError::typ().with_message("promiseCapability.[[Reject]] is not callable")
.ok_or_else(|| { })?;
JsNativeError::typ()
.with_message("promiseCapability.[[Reject]] is not callable") // 9. Set promiseCapability.[[Promise]] to promise.
})?; // 10. Return promiseCapability.
Ok(PromiseCapability {
// 9. Set promiseCapability.[[Promise]] to promise. promise,
// 10. Return promiseCapability. resolve,
Ok(PromiseCapability { reject,
promise, })
resolve,
reject,
})
}
}
} }
/// Returns the promise object. /// Returns the promise object.
@ -1313,13 +1327,13 @@ impl Promise {
} }
let Some(then) = resolution.as_object() else { let Some(then) = resolution.as_object() else {
// 8. If Type(resolution) is not Object, then // 8. If Type(resolution) is not Object, then
// a. Perform FulfillPromise(promise, resolution). // a. Perform FulfillPromise(promise, resolution).
Self::fulfill_promise(promise, resolution.clone(), context); Self::fulfill_promise(promise, resolution.clone(), context);
// b. Return undefined. // b. Return undefined.
return Ok(JsValue::Undefined); return Ok(JsValue::Undefined);
}; };
// 9. Let then be Completion(Get(resolution, "then")). // 9. Let then be Completion(Get(resolution, "then")).
let then_action = match then.get("then", context) { let then_action = match then.get("then", context) {
@ -1345,7 +1359,7 @@ impl Promise {
}; };
// 13. Let thenJobCallback be HostMakeJobCallback(thenAction). // 13. Let thenJobCallback be HostMakeJobCallback(thenAction).
let then_job_callback = JobCallback::make_job_callback(then_action); let then_job_callback = context.host_hooks().make_job_callback(then_action, context);
// 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). // 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback).
let job = new_promise_resolve_thenable_job( let job = new_promise_resolve_thenable_job(
@ -1355,7 +1369,7 @@ impl Promise {
); );
// 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). // 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
context.host_enqueue_promise_job(job); context.job_queue().enqueue_promise_job(job, context);
// 16. Return undefined. // 16. Return undefined.
Ok(JsValue::Undefined) Ok(JsValue::Undefined)
@ -1401,7 +1415,6 @@ impl Promise {
// 6. Set alreadyResolved.[[Value]] to true. // 6. Set alreadyResolved.[[Value]] to true.
already_resolved.set(true); already_resolved.set(true);
// let reason = args.get_or_undefined(0);
// 7. Perform RejectPromise(promise, reason). // 7. Perform RejectPromise(promise, reason).
Self::reject_promise(promise, args.get_or_undefined(0).clone(), context); Self::reject_promise(promise, args.get_or_undefined(0).clone(), context);
@ -1511,7 +1524,9 @@ impl Promise {
// 7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject"). // 7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject").
if !handled { if !handled {
// TODO context
.host_hooks()
.promise_rejection_tracker(promise, OperationType::Reject, context);
} }
// 9. Return unused. // 9. Return unused.
@ -1541,7 +1556,7 @@ impl Promise {
let job = new_promise_reaction_job(reaction, argument.clone()); let job = new_promise_reaction_job(reaction, argument.clone());
// b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). // b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
context.host_enqueue_promise_job(job); context.job_queue().enqueue_promise_job(job, context);
} }
// 2. Return unused. // 2. Return unused.
@ -1996,7 +2011,7 @@ impl Promise {
.and_then(JsFunction::from_object) .and_then(JsFunction::from_object)
// 4. Else, // 4. Else,
// a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled). // a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled).
.map(JobCallback::make_job_callback); .map(|f| context.host_hooks().make_job_callback(f, context));
// 5. If IsCallable(onRejected) is false, then // 5. If IsCallable(onRejected) is false, then
// a. Let onRejectedJobCallback be empty. // a. Let onRejectedJobCallback be empty.
@ -2006,7 +2021,7 @@ impl Promise {
.and_then(JsFunction::from_object) .and_then(JsFunction::from_object)
// 6. Else, // 6. Else,
// a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected). // a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected).
.map(JobCallback::make_job_callback); .map(|f| context.host_hooks().make_job_callback(f, context));
// 7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }. // 7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }.
let fulfill_reaction = ReactionRecord { let fulfill_reaction = ReactionRecord {
@ -2027,6 +2042,7 @@ impl Promise {
let promise = promise.as_promise().expect("IsPromise(promise) is false"); let promise = promise.as_promise().expect("IsPromise(promise) is false");
(promise.state.clone(), promise.handled) (promise.state.clone(), promise.handled)
}; };
match state { match state {
// 9. If promise.[[PromiseState]] is pending, then // 9. If promise.[[PromiseState]] is pending, then
PromiseState::Pending => { PromiseState::Pending => {
@ -2048,7 +2064,9 @@ impl Promise {
let fulfill_job = new_promise_reaction_job(fulfill_reaction, value.clone()); let fulfill_job = new_promise_reaction_job(fulfill_reaction, value.clone());
// c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]). // c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]).
context.host_enqueue_promise_job(fulfill_job); context
.job_queue()
.enqueue_promise_job(fulfill_job, context);
} }
// 11. Else, // 11. Else,
@ -2057,14 +2075,18 @@ impl Promise {
PromiseState::Rejected(ref reason) => { PromiseState::Rejected(ref reason) => {
// c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle"). // c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle").
if !handled { if !handled {
// TODO context.host_hooks().promise_rejection_tracker(
promise,
OperationType::Handle,
context,
);
} }
// d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason). // d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason).
let reject_job = new_promise_reaction_job(reject_reaction, reason.clone()); let reject_job = new_promise_reaction_job(reject_reaction, reason.clone());
// e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]). // e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]).
context.host_enqueue_promise_job(reject_job); context.job_queue().enqueue_promise_job(reject_job, context);
// 12. Set promise.[[PromiseIsHandled]] to true. // 12. Set promise.[[PromiseIsHandled]] to true.
promise promise
@ -2175,8 +2197,9 @@ fn new_promise_reaction_job(mut reaction: ReactionRecord, argument: JsValue) ->
} }
}, },
// e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)). // e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)).
Some(handler) => handler Some(handler) => context
.call_job_callback(&JsValue::Undefined, &[argument.clone()], context) .host_hooks()
.call_job_callback(handler, &JsValue::Undefined, &[argument.clone()], context)
.map_err(|e| e.to_opaque(context)), .map_err(|e| e.to_opaque(context)),
}; };
@ -2242,7 +2265,8 @@ fn new_promise_resolve_thenable_job(
let resolving_functions = Promise::create_resolving_functions(&promise_to_resolve, context); let resolving_functions = Promise::create_resolving_functions(&promise_to_resolve, context);
// b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)). // b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)).
let then_call_result = then.call_job_callback( let then_call_result = context.host_hooks().call_job_callback(
then,
&thenable, &thenable,
&[ &[
resolving_functions.resolve.clone().into(), resolving_functions.resolve.clone().into(),

12
boa_engine/src/builtins/promise/tests.rs

@ -1,19 +1,21 @@
use crate::{forward, Context}; use crate::{context::ContextBuilder, forward, job::SimpleJobQueue};
#[test] #[test]
fn promise() { fn promise() {
let mut context = Context::default(); let queue = SimpleJobQueue::new();
let mut context = ContextBuilder::new().job_queue(&queue).build();
let init = r#" let init = r#"
let count = 0; let count = 0;
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
count += 1; count += 1;
resolve(undefined); resolve(undefined);
}).then((_) => (count += 1)); }).then((_) => (count += 1));
count += 1; count += 1;
count; count;
"#; "#;
let result = context.eval(init).unwrap(); let result = context.eval(init).unwrap();
assert_eq!(result.as_number(), Some(2_f64)); assert_eq!(result.as_number(), Some(2_f64));
context.run_jobs();
let after_completion = forward(&mut context, "count"); let after_completion = forward(&mut context, "count");
assert_eq!(after_completion, String::from("3")); assert_eq!(after_completion, String::from("3"));
} }

10
boa_engine/src/bytecompiler/mod.rs

@ -202,7 +202,7 @@ impl Access<'_> {
/// The [`ByteCompiler`] is used to compile ECMAScript AST from [`boa_ast`] to bytecode. /// The [`ByteCompiler`] is used to compile ECMAScript AST from [`boa_ast`] to bytecode.
#[derive(Debug)] #[derive(Debug)]
pub struct ByteCompiler<'b, 'icu> { pub struct ByteCompiler<'b, 'host> {
code_block: CodeBlock, code_block: CodeBlock,
literals_map: FxHashMap<Literal, u32>, literals_map: FxHashMap<Literal, u32>,
names_map: FxHashMap<Identifier, u32>, names_map: FxHashMap<Identifier, u32>,
@ -211,10 +211,10 @@ pub struct ByteCompiler<'b, 'icu> {
jump_info: Vec<JumpControlInfo>, jump_info: Vec<JumpControlInfo>,
in_async_generator: bool, in_async_generator: bool,
json_parse: bool, json_parse: bool,
context: &'b mut Context<'icu>, context: &'b mut Context<'host>,
} }
impl<'b, 'icu> ByteCompiler<'b, 'icu> { impl<'b, 'host> ByteCompiler<'b, 'host> {
/// Represents a placeholder address that will be patched later. /// Represents a placeholder address that will be patched later.
const DUMMY_ADDRESS: u32 = u32::MAX; const DUMMY_ADDRESS: u32 = u32::MAX;
@ -224,8 +224,8 @@ impl<'b, 'icu> ByteCompiler<'b, 'icu> {
name: Sym, name: Sym,
strict: bool, strict: bool,
json_parse: bool, json_parse: bool,
context: &'b mut Context<'icu>, context: &'b mut Context<'host>,
) -> ByteCompiler<'b, 'icu> { ) -> ByteCompiler<'b, 'host> {
Self { Self {
code_block: CodeBlock::new(name, 0, strict), code_block: CodeBlock::new(name, 0, strict),
literals_map: FxHashMap::default(), literals_map: FxHashMap::default(),

10
boa_engine/src/class.rs

@ -157,12 +157,12 @@ impl<T: Class> ClassConstructor for T {
/// Class builder which allows adding methods and static methods to the class. /// Class builder which allows adding methods and static methods to the class.
#[derive(Debug)] #[derive(Debug)]
pub struct ClassBuilder<'ctx, 'icu> { pub struct ClassBuilder<'ctx, 'host> {
builder: ConstructorBuilder<'ctx, 'icu>, builder: ConstructorBuilder<'ctx, 'host>,
} }
impl<'ctx, 'icu> ClassBuilder<'ctx, 'icu> { impl<'ctx, 'host> ClassBuilder<'ctx, 'host> {
pub(crate) fn new<T>(context: &'ctx mut Context<'icu>) -> Self pub(crate) fn new<T>(context: &'ctx mut Context<'host>) -> Self
where where
T: ClassConstructor, T: ClassConstructor,
{ {
@ -292,7 +292,7 @@ impl<'ctx, 'icu> ClassBuilder<'ctx, 'icu> {
/// Return the current context. /// Return the current context.
#[inline] #[inline]
pub fn context(&mut self) -> &mut Context<'icu> { pub fn context(&mut self) -> &mut Context<'host> {
self.builder.context() self.builder.context()
} }
} }

149
boa_engine/src/context/hooks.rs

@ -0,0 +1,149 @@
use crate::{
builtins::promise::OperationType,
job::JobCallback,
object::{JsFunction, JsObject},
Context, JsResult, JsValue,
};
/// [`Host Hooks`] customizable by the host code or engine.
///
/// Every hook contains on its `Requirements` section the spec requirements
/// that the hook must abide to for spec compliance.
///
/// # Usage
///
/// Implement the trait for a custom struct (maybe with additional state), overriding the methods that
/// need to be redefined:
///
/// ```
/// use boa_engine::{JsNativeError, JsResult, context::{Context, ContextBuilder, HostHooks}};
///
/// struct Hooks;
///
/// impl HostHooks for Hooks {
/// fn ensure_can_compile_strings(
/// &self,
/// context: &mut Context<'_>,
/// ) -> JsResult<()> {
/// Err(JsNativeError::typ().with_message("eval calls not available").into())
/// }
/// }
/// let hooks = Hooks; // Can have additional state.
/// let context = &mut ContextBuilder::new().host_hooks(&hooks).build();
/// let result = context.eval(r#"eval("let a = 5")"#);
/// assert_eq!(result.unwrap_err().to_string(), "TypeError: eval calls not available");
/// ```
///
/// [`Host Hooks`]: https://tc39.es/ecma262/#sec-host-hooks-summary
pub trait HostHooks {
/// [`HostMakeJobCallback ( callback )`][spec]
///
/// # Requirements
///
/// - It must return a `JobCallback` Record whose `[[Callback]]` field is `callback`.
///
/// [spec]: https://tc39.es/ecma262/#sec-hostmakejobcallback
fn make_job_callback(&self, callback: JsFunction, _context: &mut Context<'_>) -> JobCallback {
// The default implementation of HostMakeJobCallback performs the following steps when called:
// 1. Return the JobCallback Record { [[Callback]]: callback, [[HostDefined]]: empty }.
JobCallback::new(callback, ())
}
/// [`HostCallJobCallback ( jobCallback, V, argumentsList )`][spec]
///
/// # Requirements
///
/// - It must perform and return the result of `Call(jobCallback.[[Callback]], V, argumentsList)`.
///
/// [spec]: https://tc39.es/ecma262/#sec-hostcalljobcallback
fn call_job_callback(
&self,
job: JobCallback,
this: &JsValue,
args: &[JsValue],
context: &mut Context<'_>,
) -> JsResult<JsValue> {
// The default implementation of HostCallJobCallback performs the following steps when called:
// 1. Assert: IsCallable(jobCallback.[[Callback]]) is true.
// already asserted by `Call`.
// 2. Return ? Call(jobCallback.[[Callback]], V, argumentsList).
job.callback().call(this, args, context)
}
/// [`HostPromiseRejectionTracker ( promise, operation )`][spec]
///
/// # Requirements
///
/// - It must complete normally (i.e. not return an abrupt completion). This is already
/// ensured by the return type.
///
/// [spec]: https://tc39.es/ecma262/#sec-host-promise-rejection-tracker
fn promise_rejection_tracker(
&self,
_promise: &JsObject,
_operation: OperationType,
_context: &mut Context<'_>,
) {
// The default implementation of HostPromiseRejectionTracker is to return unused.
}
/// [`HostEnsureCanCompileStrings ( calleeRealm )`][spec]
///
/// # Requirements
///
/// - If the returned Completion Record is a normal completion, it must be a normal completion
/// containing unused. This is already ensured by the return type.
///
/// [spec]: https://tc39.es/ecma262/#sec-hostensurecancompilestrings
fn ensure_can_compile_strings(
&self,
/* Realm (WIP), */ _context: &mut Context<'_>,
) -> JsResult<()> {
// The default implementation of HostEnsureCanCompileStrings is to return NormalCompletion(unused).
Ok(())
}
/// [`HostHasSourceTextAvailable ( func )`][spec]
///
/// # Requirements
///
/// - It must be deterministic with respect to its parameters. Each time it is called with a
/// specific `func` as its argument, it must return the same result.
///
/// [spec]: https://tc39.es/ecma262/#sec-hosthassourcetextavailable
fn has_source_text_available(
&self,
_function: &JsFunction,
_context: &mut Context<'_>,
) -> bool {
// The default implementation of HostHasSourceTextAvailable is to return true.
true
}
/// [`HostEnsureCanAddPrivateElement ( O )`][spec]
///
/// # Requirements
///
/// - If `O` is not a host-defined exotic object, this abstract operation must return
/// `NormalCompletion(unused)` and perform no other steps.
/// - Any two calls of this abstract operation with the same argument must return the same kind
/// of *Completion Record*.
/// - This abstract operation should only be overriden by ECMAScript hosts that are web browsers.
///
/// [spec]: https://tc39.es/ecma262/#sec-hostensurecanaddprivateelement
fn ensure_can_add_private_element(
&self,
_o: &JsObject,
_context: &mut Context<'_>,
) -> JsResult<()> {
Ok(())
}
}
/// Default implementation of [`HostHooks`], which doesn't carry any state.
#[derive(Debug, Clone, Copy)]
pub(crate) struct DefaultHooks;
impl HostHooks for DefaultHooks {}

164
boa_engine/src/context/mod.rs

@ -1,6 +1,9 @@
//! The ECMAScript context. //! The ECMAScript context.
mod hooks;
pub mod intrinsics; pub mod intrinsics;
use hooks::DefaultHooks;
pub use hooks::HostHooks;
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
pub(crate) mod icu; pub(crate) mod icu;
@ -9,7 +12,6 @@ pub(crate) mod icu;
pub use icu::BoaProvider; pub use icu::BoaProvider;
use intrinsics::{IntrinsicObjects, Intrinsics}; use intrinsics::{IntrinsicObjects, Intrinsics};
use std::collections::VecDeque;
#[cfg(not(feature = "intl"))] #[cfg(not(feature = "intl"))]
pub use std::marker::PhantomData; pub use std::marker::PhantomData;
@ -20,7 +22,7 @@ use crate::{
builtins, builtins,
bytecompiler::ByteCompiler, bytecompiler::ByteCompiler,
class::{Class, ClassBuilder}, class::{Class, ClassBuilder},
job::NativeJob, job::{IdleJobQueue, JobQueue, NativeJob},
native_function::NativeFunction, native_function::NativeFunction,
object::{FunctionObjectBuilder, GlobalPropertyMap, JsObject}, object::{FunctionObjectBuilder, GlobalPropertyMap, JsObject},
property::{Attribute, PropertyDescriptor, PropertyKey}, property::{Attribute, PropertyDescriptor, PropertyKey},
@ -76,7 +78,7 @@ use boa_profiler::Profiler;
/// ///
/// assert_eq!(value.as_number(), Some(12.0)) /// assert_eq!(value.as_number(), Some(12.0))
/// ``` /// ```
pub struct Context<'icu> { pub struct Context<'host> {
/// realm holds both the global object and the environment /// realm holds both the global object and the environment
pub(crate) realm: Realm, pub(crate) realm: Realm,
@ -90,22 +92,21 @@ pub struct Context<'icu> {
/// Intrinsic objects /// Intrinsic objects
intrinsics: Intrinsics, intrinsics: Intrinsics,
/// ICU related utilities
#[cfg(feature = "intl")]
icu: icu::Icu<'icu>,
#[cfg(not(feature = "intl"))]
icu: PhantomData<&'icu ()>,
/// Number of instructions remaining before a forced exit /// Number of instructions remaining before a forced exit
#[cfg(feature = "fuzz")] #[cfg(feature = "fuzz")]
pub(crate) instructions_remaining: usize, pub(crate) instructions_remaining: usize,
pub(crate) vm: Vm, pub(crate) vm: Vm,
pub(crate) promise_job_queue: VecDeque<NativeJob>,
pub(crate) kept_alive: Vec<JsObject>, pub(crate) kept_alive: Vec<JsObject>,
/// ICU related utilities
#[cfg(feature = "intl")]
icu: icu::Icu<'host>,
host_hooks: &'host dyn HostHooks,
job_queue: &'host dyn JobQueue,
} }
impl std::fmt::Debug for Context<'_> { impl std::fmt::Debug for Context<'_> {
@ -114,16 +115,15 @@ impl std::fmt::Debug for Context<'_> {
debug debug
.field("realm", &self.realm) .field("realm", &self.realm)
.field("interner", &self.interner); .field("interner", &self.interner)
.field("intrinsics", &self.intrinsics)
.field("vm", &self.vm)
.field("promise_job_queue", &"JobQueue")
.field("hooks", &"HostHooks");
#[cfg(feature = "console")] #[cfg(feature = "console")]
debug.field("console", &self.console); debug.field("console", &self.console);
debug
.field("intrinsics", &self.intrinsics)
.field("vm", &self.vm)
.field("promise_job_queue", &self.promise_job_queue);
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
debug.field("icu", &self.icu); debug.field("icu", &self.icu);
@ -143,7 +143,7 @@ impl Context<'_> {
/// Create a new [`ContextBuilder`] to specify the [`Interner`] and/or /// Create a new [`ContextBuilder`] to specify the [`Interner`] and/or
/// the icu data provider. /// the icu data provider.
#[must_use] #[must_use]
pub fn builder() -> ContextBuilder<'static> { pub fn builder() -> ContextBuilder<'static, 'static, 'static> {
ContextBuilder::default() ContextBuilder::default()
} }
@ -159,6 +159,9 @@ impl Context<'_> {
/// assert!(value.is_number()); /// assert!(value.is_number());
/// assert_eq!(value.as_number().unwrap(), 4.0); /// assert_eq!(value.as_number().unwrap(), 4.0);
/// ``` /// ```
///
/// Note that this won't run any scheduled promise jobs; you need to call [`Context::run_jobs`]
/// on the context or [`JobQueue::run_jobs`] on the provided queue to run them.
#[allow(clippy::unit_arg, clippy::drop_copy)] #[allow(clippy::unit_arg, clippy::drop_copy)]
pub fn eval<S>(&mut self, src: S) -> JsResult<JsValue> pub fn eval<S>(&mut self, src: S) -> JsResult<JsValue>
where where
@ -202,6 +205,9 @@ impl Context<'_> {
/// just a pointer copy. Therefore, if you'd like to execute the same `CodeBlock` multiple /// just a pointer copy. Therefore, if you'd like to execute the same `CodeBlock` multiple
/// times, there is no need to re-compile it, and you can just call `clone()` on the /// times, there is no need to re-compile it, and you can just call `clone()` on the
/// `Gc<CodeBlock>` returned by the [`Self::compile()`] function. /// `Gc<CodeBlock>` returned by the [`Self::compile()`] function.
///
/// Note that this won't run any scheduled promise jobs; you need to call [`Context::run_jobs`]
/// on the context or [`JobQueue::run_jobs`] on the provided queue to run them.
pub fn execute(&mut self, code_block: Gc<CodeBlock>) -> JsResult<JsValue> { pub fn execute(&mut self, code_block: Gc<CodeBlock>) -> JsResult<JsValue> {
let _timer = Profiler::global().start_event("Execution", "Main"); let _timer = Profiler::global().start_event("Execution", "Main");
@ -211,9 +217,8 @@ impl Context<'_> {
let result = self.run(); let result = self.run();
self.vm.pop_frame(); self.vm.pop_frame();
self.clear_kept_objects(); self.clear_kept_objects();
self.run_queued_jobs()?;
let (result, _) = result?; result.map(|r| r.0)
Ok(result)
} }
/// Register a global property. /// Register a global property.
@ -375,16 +380,15 @@ impl Context<'_> {
self.vm.trace = trace; self.vm.trace = trace;
} }
/// More information: /// Enqueues a [`NativeJob`] on the [`JobQueue`].
/// - [ECMAScript reference][spec] pub fn enqueue_job(&mut self, job: NativeJob) {
/// self.job_queue.enqueue_promise_job(job, self);
/// [spec]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob }
pub fn host_enqueue_promise_job(&mut self, job: NativeJob /* , realm: Realm */) {
// If realm is not null ... /// Runs all the jobs in the job queue.
// TODO pub fn run_jobs(&mut self) {
// Let scriptOrModule be ... self.job_queue.run_jobs(self);
// TODO self.clear_kept_objects();
self.promise_job_queue.push_back(job);
} }
/// Abstract operation [`ClearKeptObjects`][clear]. /// Abstract operation [`ClearKeptObjects`][clear].
@ -450,21 +454,22 @@ impl Context<'_> {
// Create intrinsics, add global objects here // Create intrinsics, add global objects here
builtins::init(self); builtins::init(self);
} }
}
impl<'host> Context<'host> {
/// Get the host hooks.
pub(crate) fn host_hooks(&self) -> &'host dyn HostHooks {
self.host_hooks
}
/// Runs all the jobs in the job queue. /// Get the job queue.
fn run_queued_jobs(&mut self) -> JsResult<()> { pub(crate) fn job_queue(&mut self) -> &'host dyn JobQueue {
while let Some(job) = self.promise_job_queue.pop_front() { self.job_queue
job.call(self)?;
self.clear_kept_objects();
}
Ok(())
} }
}
#[cfg(feature = "intl")]
impl<'icu> Context<'icu> {
/// Get the ICU related utilities /// Get the ICU related utilities
pub(crate) const fn icu(&self) -> &icu::Icu<'icu> { #[cfg(feature = "intl")]
pub(crate) const fn icu(&self) -> &icu::Icu<'host> {
&self.icu &self.icu
} }
} }
@ -480,9 +485,11 @@ impl<'icu> Context<'icu> {
feature = "intl", feature = "intl",
doc = "The required data in a valid provider is specified in [`BoaProvider`]" doc = "The required data in a valid provider is specified in [`BoaProvider`]"
)] )]
#[derive(Default, Debug)] #[derive(Default)]
pub struct ContextBuilder<'icu> { pub struct ContextBuilder<'icu, 'hooks, 'queue> {
interner: Option<Interner>, interner: Option<Interner>,
host_hooks: Option<&'hooks dyn HostHooks>,
job_queue: Option<&'queue dyn JobQueue>,
#[cfg(feature = "intl")] #[cfg(feature = "intl")]
icu: Option<icu::Icu<'icu>>, icu: Option<icu::Icu<'icu>>,
#[cfg(not(feature = "intl"))] #[cfg(not(feature = "intl"))]
@ -491,7 +498,31 @@ pub struct ContextBuilder<'icu> {
instructions_remaining: usize, instructions_remaining: usize,
} }
impl<'a> ContextBuilder<'a> { impl std::fmt::Debug for ContextBuilder<'_, '_, '_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut out = f.debug_struct("ContextBuilder");
out.field("interner", &self.interner)
.field("host_hooks", &"HostHooks");
#[cfg(feature = "intl")]
out.field("icu", &self.icu);
#[cfg(feature = "fuzz")]
out.field("instructions_remaining", &self.instructions_remaining);
out.finish()
}
}
impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> {
/// Creates a new [`ContextBuilder`] with a default empty [`Interner`]
/// and a default [`BoaProvider`] if the `intl` feature is enabled.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Initializes the context [`Interner`] to the provided interner. /// Initializes the context [`Interner`] to the provided interner.
/// ///
/// This is useful when you want to initialize an [`Interner`] with /// This is useful when you want to initialize an [`Interner`] with
@ -520,13 +551,33 @@ impl<'a> ContextBuilder<'a> {
pub fn icu_provider( pub fn icu_provider(
self, self,
provider: BoaProvider<'_>, provider: BoaProvider<'_>,
) -> Result<ContextBuilder<'_>, icu_locid_transform::LocaleTransformError> { ) -> Result<ContextBuilder<'_, 'hooks, 'queue>, icu_locid_transform::LocaleTransformError> {
Ok(ContextBuilder { Ok(ContextBuilder {
icu: Some(icu::Icu::new(provider)?), icu: Some(icu::Icu::new(provider)?),
..self ..self
}) })
} }
/// Initializes the [`HostHooks`] for the context.
///
/// [`Host Hooks`]: https://tc39.es/ecma262/#sec-host-hooks-summary
#[must_use]
pub fn host_hooks(self, host_hooks: &dyn HostHooks) -> ContextBuilder<'icu, '_, 'queue> {
ContextBuilder {
host_hooks: Some(host_hooks),
..self
}
}
/// Initializes the [`JobQueue`] for the context.
#[must_use]
pub fn job_queue(self, job_queue: &dyn JobQueue) -> ContextBuilder<'icu, 'hooks, '_> {
ContextBuilder {
job_queue: Some(job_queue),
..self
}
}
/// Specifies the number of instructions remaining to the [`Context`]. /// Specifies the number of instructions remaining to the [`Context`].
/// ///
/// This function is only available if the `fuzz` feature is enabled. /// This function is only available if the `fuzz` feature is enabled.
@ -537,17 +588,15 @@ impl<'a> ContextBuilder<'a> {
self self
} }
/// Creates a new [`ContextBuilder`] with a default empty [`Interner`]
/// and a default [`BoaProvider`] if the `intl` feature is enabled.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Builds a new [`Context`] with the provided parameters, and defaults /// Builds a new [`Context`] with the provided parameters, and defaults
/// all missing parameters to their default values. /// all missing parameters to their default values.
#[must_use] #[must_use]
pub fn build(self) -> Context<'a> { pub fn build<'host>(self) -> Context<'host>
where
'icu: 'host,
'hooks: 'host,
'queue: 'host,
{
let intrinsics = Intrinsics::default(); let intrinsics = Intrinsics::default();
let mut context = Context { let mut context = Context {
realm: Realm::create(intrinsics.constructors().object().prototype().into()), realm: Realm::create(intrinsics.constructors().object().prototype().into()),
@ -561,12 +610,11 @@ impl<'a> ContextBuilder<'a> {
let provider = BoaProvider::Buffer(boa_icu_provider::buffer()); let provider = BoaProvider::Buffer(boa_icu_provider::buffer());
icu::Icu::new(provider).expect("Failed to initialize default icu data.") icu::Icu::new(provider).expect("Failed to initialize default icu data.")
}), }),
#[cfg(not(feature = "intl"))]
icu: PhantomData,
#[cfg(feature = "fuzz")] #[cfg(feature = "fuzz")]
instructions_remaining: self.instructions_remaining, instructions_remaining: self.instructions_remaining,
promise_job_queue: VecDeque::new(),
kept_alive: Vec::new(), kept_alive: Vec::new(),
host_hooks: self.host_hooks.unwrap_or(&DefaultHooks),
job_queue: self.job_queue.unwrap_or(&IdleJobQueue),
}; };
// Add new builtIns to Context Realm // Add new builtIns to Context Realm

10
boa_engine/src/error.rs

@ -66,15 +66,15 @@ enum Repr {
/// The error type returned by the [`JsError::try_native`] method. /// The error type returned by the [`JsError::try_native`] method.
#[derive(Debug, Clone, Error)] #[derive(Debug, Clone, Error)]
pub enum TryNativeError { pub enum TryNativeError {
/// This error is returned when a property of the error object has an invalid type. /// A property of the error object has an invalid type.
#[error("invalid type of property `{0}`")] #[error("invalid type of property `{0}`")]
InvalidPropertyType(&'static str), InvalidPropertyType(&'static str),
/// This error is returned when the message of the error object could not be decoded. /// The message of the error object could not be decoded.
#[error("property `message` cannot contain unpaired surrogates")] #[error("property `message` cannot contain unpaired surrogates")]
InvalidMessageEncoding, InvalidMessageEncoding,
/// This error is returned when a property of the error object is not accessible. /// A property of the error object is not accessible.
#[error("could not access property `{property}`")] #[error("could not access property `{property}`")]
InaccessibleProperty { InaccessibleProperty {
/// The name of the property that could not be accessed. /// The name of the property that could not be accessed.
@ -84,7 +84,7 @@ pub enum TryNativeError {
source: JsError, source: JsError,
}, },
/// This error is returned when any inner error of an aggregate error is not accessible. /// An inner error of an aggregate error is not accessible.
#[error("could not get element `{index}` of property `errors`")] #[error("could not get element `{index}` of property `errors`")]
InvalidErrorsIndex { InvalidErrorsIndex {
/// The index of the error that could not be accessed. /// The index of the error that could not be accessed.
@ -94,7 +94,7 @@ pub enum TryNativeError {
source: JsError, source: JsError,
}, },
/// This error is returned when the error value not an error object. /// The error value is not an error object.
#[error("opaque error of type `{:?}` is not an Error object", .0.get_type())] #[error("opaque error of type `{:?}` is not an Error object", .0.get_type())]
NotAnErrorObject(JsValue), NotAnErrorObject(JsValue),
} }

195
boa_engine/src/job.rs

@ -1,12 +1,34 @@
//! Data structures for the microtask job queue. //! Boa's API to create and customize `ECMAScript` jobs and job queues.
//!
//! [`NativeJob`] is an ECMAScript [Job], or a closure that runs an `ECMAScript` computation when
//! there's no other computation running.
//!
//! [`JobCallback`] is an ECMAScript [`JobCallback`] record, containing an `ECMAScript` function
//! that is executed when a promise is either fulfilled or rejected.
//!
//! [`JobQueue`] is a trait encompassing the required functionality for a job queue; this allows
//! implementing custom event loops, custom handling of Jobs or other fun things.
//! This trait is also accompanied by two implementors of the trait:
//! - [`IdleJobQueue`], which is a queue that does nothing, and the default queue if no queue is
//! provided. Useful for hosts that want to disable promises.
//! - [`SimpleJobQueue`], which is a simple FIFO queue that runs all jobs to completion, bailing
//! on the first error encountered.
//!
//! [Job]: https://tc39.es/ecma262/#sec-jobs
//! [JobCallback]: https://tc39.es/ecma262/#sec-jobcallback-records
use crate::{object::JsFunction, Context, JsResult, JsValue}; use std::{any::Any, cell::RefCell, collections::VecDeque, fmt::Debug};
use crate::{
object::{JsFunction, NativeObject},
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Trace};
/// An ECMAScript [Job] closure. /// An ECMAScript [Job] closure.
/// ///
/// The specification allows scheduling any [`NativeJob`] closure by the host into the job queue. /// The specification allows scheduling any [`NativeJob`] closure by the host into the job queue.
/// However, custom jobs must abide to a list of requirements. /// However, host-defined jobs must abide to a set of requirements.
/// ///
/// ### Requirements /// ### Requirements
/// ///
@ -19,21 +41,21 @@ use boa_gc::{Finalize, Trace};
/// - Once evaluation of a Job starts, it must run to completion before evaluation of any other Job starts. /// - Once evaluation of a Job starts, it must run to completion before evaluation of any other Job starts.
/// - The Abstract Closure must return a normal completion, implementing its own handling of errors. /// - The Abstract Closure must return a normal completion, implementing its own handling of errors.
/// ///
/// `NativeJob`'s API differs slightly on the last requirement, since it allows closures returning /// `NativeJob`s API differs slightly on the last requirement, since it allows closures returning
/// [`JsResult`], but it's okay because `NativeJob`s are handled by the host anyways; a host could /// [`JsResult`], but it's okay because `NativeJob`s are handled by the host anyways; a host could
/// pass a closure returning `Err` and handle the error on `JobQueue::run_jobs`, making the closure /// pass a closure returning `Err` and handle the error on [`JobQueue::run_jobs`], making the closure
/// effectively run as if it never returned `Err`. /// effectively run as if it never returned `Err`.
/// ///
/// ## [`Trace`]? /// ## [`Trace`]?
/// ///
/// `NativeJob` doesn't implement `Trace` because it doesn't need to; all jobs can only be run once /// `NativeJob` doesn't implement `Trace` because it doesn't need to; all jobs can only be run once
/// and putting a `JobQueue` on a garbage collected object should definitely be discouraged. /// and putting a [`JobQueue`] on a garbage collected object should definitely be discouraged.
/// ///
/// On the other hand, it seems like this type breaks all the safety checks of the /// On the other hand, it seems like this type breaks all the safety checks of the
/// [`NativeFunction`] API, since you can capture any `Trace` variable into the closure... but it /// [`NativeFunction`] API, since you can capture any `Trace` variable into the closure... but it
/// doesn't! /// doesn't!
/// The garbage collector doesn't need to trace the captured variables because the closures /// The garbage collector doesn't need to trace the captured variables because the closures
/// are always stored on the `JobQueue`, which is always rooted, which means the captured variables /// are always stored on the [`JobQueue`], which is always rooted, which means the captured variables
/// are also rooted, allowing us to capture any variable in the closure for free! /// are also rooted, allowing us to capture any variable in the closure for free!
/// ///
/// [Job]: https://tc39.es/ecma262/#sec-jobs /// [Job]: https://tc39.es/ecma262/#sec-jobs
@ -43,7 +65,7 @@ pub struct NativeJob {
f: Box<dyn FnOnce(&mut Context<'_>) -> JsResult<JsValue>>, f: Box<dyn FnOnce(&mut Context<'_>) -> JsResult<JsValue>>,
} }
impl std::fmt::Debug for NativeJob { impl Debug for NativeJob {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NativeJob").field("f", &"Closure").finish() f.debug_struct("NativeJob").field("f", &"Closure").finish()
} }
@ -64,56 +86,135 @@ impl NativeJob {
} }
} }
/// `JobCallback` records /// [`JobCallback`][spec] records
///
/// More information:
/// - [ECMAScript reference][spec]
/// ///
/// [spec]: https://tc39.es/ecma262/#sec-jobcallback-records /// [spec]: https://tc39.es/ecma262/#sec-jobcallback-records
#[derive(Debug, Clone, Trace, Finalize)] #[derive(Trace, Finalize)]
pub struct JobCallback { pub struct JobCallback {
callback: JsFunction, callback: JsFunction,
host_defined: Box<dyn NativeObject>,
}
impl Debug for JobCallback {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JobCallback")
.field("callback", &self.callback)
.field("host_defined", &"dyn NativeObject")
.finish()
}
} }
impl JobCallback { impl JobCallback {
/// `HostMakeJobCallback ( callback )` /// Creates a new `JobCallback`.
/// pub fn new<T: Any + Trace>(callback: JsFunction, host_defined: T) -> Self {
/// The host-defined abstract operation `HostMakeJobCallback` takes argument `callback` (a JobCallback {
/// function object) and returns a `JobCallback` Record. callback,
/// host_defined: Box::new(host_defined),
/// More information: }
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-hostmakejobcallback
pub fn make_job_callback(callback: JsFunction) -> Self {
// 1. Return the JobCallback Record { [[Callback]]: callback, [[HostDefined]]: empty }.
Self { callback }
} }
/// `HostCallJobCallback ( jobCallback, V, argumentsList )` /// Gets the inner callback of the job.
/// pub const fn callback(&self) -> &JsFunction {
/// The host-defined abstract operation `HostCallJobCallback` takes arguments `jobCallback` (a &self.callback
/// `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. /// Gets a reference to the host defined additional field as an `Any` trait object.
/// pub fn host_defined(&self) -> &dyn Any {
/// More information: self.host_defined.as_any()
/// - [ECMAScript reference][spec] }
/// Gets a mutable reference to the host defined additional field as an `Any` trait object.
pub fn host_defined_mut(&mut self) -> &mut dyn Any {
self.host_defined.as_mut_any()
}
}
/// A queue of `ECMAscript` [Jobs].
///
/// This is the main API that allows creating custom event loops with custom job queues.
///
/// [Jobs]: https://tc39.es/ecma262/#sec-jobs
pub trait JobQueue {
/// [`HostEnqueuePromiseJob ( job, realm )`][spec].
/// ///
/// [spec]: https://tc39.es/ecma262/#sec-hostcalljobcallback /// Enqueues a [`NativeJob`] on the job queue. Note that host-defined [Jobs] need to satisfy
/// a set of requirements for them to be spec-compliant.
/// ///
/// # Panics /// [spec]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob
/// [Jobs]: https://tc39.es/ecma262/#sec-jobs
fn enqueue_promise_job(&self, job: NativeJob, context: &mut Context<'_>);
/// Runs all jobs in the queue.
/// ///
/// Panics if the `JobCallback` is not callable. /// Running a job could enqueue more jobs in the queue. The implementor of the trait
pub fn call_job_callback( /// determines if the method should loop until there are no more queued jobs or if
&self, /// it should only run one iteration of the queue.
v: &JsValue, fn run_jobs(&self, context: &mut Context<'_>);
arguments_list: &[JsValue], }
context: &mut Context<'_>,
) -> JsResult<JsValue> { /// A job queue that does nothing.
// It must perform and return the result of Call(jobCallback.[[Callback]], V, argumentsList). ///
// 1. Assert: IsCallable(jobCallback.[[Callback]]) is true. /// This is the default job queue for the [`Context`], and is useful if you want to disable
// 2. Return ? Call(jobCallback.[[Callback]], V, argumentsList). /// the promise capabilities of the engine.
self.callback.call(v, arguments_list, context) ///
/// If you want to enable running promise jobs, see [`SimpleJobQueue`].
#[derive(Debug, Clone, Copy)]
pub struct IdleJobQueue;
impl JobQueue for IdleJobQueue {
fn enqueue_promise_job(&self, _: NativeJob, _: &mut Context<'_>) {}
fn run_jobs(&self, _: &mut Context<'_>) {}
}
/// A simple FIFO job queue that bails on the first error.
///
/// To enable running promise jobs on the engine, you need to pass it to the [`ContextBuilder`]:
///
/// ```
/// use boa_engine::{context::ContextBuilder, job::SimpleJobQueue};
///
/// let queue = SimpleJobQueue::new();
/// let context = ContextBuilder::new().job_queue(&queue).build();
/// ```
///
/// [`ContextBuilder`]: crate::context::ContextBuilder
#[derive(Default)]
pub struct SimpleJobQueue(RefCell<VecDeque<NativeJob>>);
impl Debug for SimpleJobQueue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SimpleQueue").field(&"..").finish()
}
}
impl SimpleJobQueue {
/// Creates an empty `SimpleJobQueue`.
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl JobQueue for SimpleJobQueue {
fn enqueue_promise_job(&self, job: NativeJob, _: &mut Context<'_>) {
// If realm is not null ...
// TODO
// Let scriptOrModule be ...
// TODO
self.0.borrow_mut().push_back(job);
}
fn run_jobs(&self, context: &mut Context<'_>) {
// Yeah, I have no idea why Rust extends the lifetime of a `RefCell` that should be immediately
// dropped after calling `pop_front`.
let mut next_job = self.0.borrow_mut().pop_front();
while let Some(job) = next_job {
if job.call(context).is_err() {
self.0.borrow_mut().clear();
return;
};
next_job = self.0.borrow_mut().pop_front();
}
} }
} }

168
boa_engine/src/native_function.rs

@ -3,8 +3,6 @@
//! [`NativeFunction`] is the main type of this module, providing APIs to create native callables //! [`NativeFunction`] is the main type of this module, providing APIs to create native callables
//! from native Rust functions and closures. //! from native Rust functions and closures.
use std::marker::PhantomData;
use boa_gc::{custom_trace, Finalize, Gc, Trace}; use boa_gc::{custom_trace, Finalize, Gc, Trace};
use crate::{Context, JsResult, JsValue}; use crate::{Context, JsResult, JsValue};
@ -197,169 +195,3 @@ impl NativeFunction {
} }
} }
} }
trait TraceableGenericClosure<Ret, Args>: Trace {
fn call(&mut self, args: Args, context: &mut Context<'_>) -> Ret;
}
#[derive(Trace, Finalize)]
struct GenericClosure<Ret, Args, F, T>
where
F: FnMut(Args, &mut T, &mut Context<'_>) -> Ret,
T: Trace,
{
// SAFETY: `GenericNativeFunction`'s safe API ensures only `Copy` closures are stored; its unsafe API,
// on the other hand, explains the invariants to hold in order for this to be safe, shifting
// the responsibility to the caller.
#[unsafe_ignore_trace]
f: F,
captures: T,
#[allow(clippy::type_complexity)]
phantom: PhantomData<Box<dyn FnMut(Args, &mut T, &mut Context<'_>) -> Ret>>,
}
impl<Ret, Args, F, T> TraceableGenericClosure<Ret, Args> for GenericClosure<Ret, Args, F, T>
where
F: FnMut(Args, &mut T, &mut Context<'_>) -> Ret,
T: Trace,
{
fn call(&mut self, args: Args, context: &mut Context<'_>) -> Ret {
(self.f)(args, &mut self.captures, context)
}
}
/// A callable generic Rust function that can be invoked by the engine.
///
/// This is a more general struct of the [`NativeFunction`] API, useful for callbacks defined in the
/// host that are useful to the engine, such as [`HostCallJobCallback`] or [`HostEnqueuePromiseJob`].
///
/// `GenericNativeFunction` functions are divided in two:
/// - Function pointers a.k.a common functions.
/// - Closure functions that can capture the current environment.
///
/// # Caveats
///
/// - Since the Rust language doesn't support [**variadic generics**], all functions requiring
/// more than 1 argument (excluding the required [`Context`]), will define its generic parameter
/// `Args` as a tuple instead, which slightly worsens the API. We hope this can improve when
/// variadic generics arrive.
///
/// - By limitations of the Rust language, the garbage collector currently cannot inspect closures
/// in order to trace their captured variables. This means that only [`Copy`] closures are 100% safe
/// to use. All other closures can also be stored in a `GenericNativeFunction`, albeit by using an
/// `unsafe` API, but note that passing closures implicitly capturing traceable types could cause
/// **Undefined Behaviour**.
///
/// [`HostCallJobCallback`]: https://tc39.es/ecma262/#sec-hostcalljobcallback
/// [`HostEnqueuePromiseJob`]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob
/// [**variadic generics**]: https://github.com/rust-lang/rfcs/issues/376
pub struct GenericNativeFunction<Ret: 'static, Args: 'static> {
inner: GenericInner<Ret, Args>,
}
enum GenericInner<Ret: 'static, Args: 'static> {
PointerFn(fn(Args, &mut Context<'_>) -> Ret),
Closure(Box<dyn TraceableGenericClosure<Ret, Args>>),
}
impl<Ret, Args> Finalize for GenericNativeFunction<Ret, Args> {
fn finalize(&self) {
if let GenericInner::Closure(c) = &self.inner {
c.finalize();
}
}
}
// Manual implementation because deriving `Trace` triggers the `single_use_lifetimes` lint.
// SAFETY: Only closures can contain `Trace` captures, so this implementation is safe.
unsafe impl<Ret, Args> Trace for GenericNativeFunction<Ret, Args> {
custom_trace!(this, {
if let GenericInner::Closure(c) = &this.inner {
mark(c);
}
});
}
impl<Ret, Args> std::fmt::Debug for GenericNativeFunction<Ret, Args> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NativeFunction").finish_non_exhaustive()
}
}
impl<Ret, Args> GenericNativeFunction<Ret, Args> {
/// Creates a `GenericNativeFunction` from a function pointer.
#[inline]
pub fn from_fn_ptr(function: fn(Args, &mut Context<'_>) -> Ret) -> Self {
Self {
inner: GenericInner::PointerFn(function),
}
}
/// Creates a `GenericNativeFunction` from a `Copy` closure.
pub fn from_copy_closure<F>(closure: F) -> Self
where
F: FnMut(Args, &mut Context<'_>) -> Ret + Copy + 'static,
{
// SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
unsafe { Self::from_closure(closure) }
}
/// Creates a `GenericNativeFunction` from a `Copy` closure and a list of traceable captures.
pub fn from_copy_closure_with_captures<F, T>(closure: F, captures: T) -> Self
where
F: FnMut(Args, &mut T, &mut Context<'_>) -> Ret + Copy + 'static,
T: Trace + 'static,
{
// SAFETY: The `Copy` bound ensures there are no traceable types inside the closure.
unsafe { Self::from_closure_with_captures(closure, captures) }
}
/// Creates a new `GenericNativeFunction` from a closure.
///
/// # Safety
///
/// Passing a closure that contains a captured variable that needs to be traced by the garbage
/// collector could cause an use after free, memory corruption or other kinds of **Undefined
/// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
/// on why that is the case.
pub unsafe fn from_closure<F>(mut closure: F) -> Self
where
F: FnMut(Args, &mut Context<'_>) -> Ret + 'static,
{
// SAFETY: The caller must ensure the invariants of the closure hold.
unsafe {
Self::from_closure_with_captures(move |args, _, context| closure(args, context), ())
}
}
/// Create a new `GenericNativeFunction` from a closure and a list of traceable captures.
///
/// # Safety
///
/// Passing a closure that contains a captured variable that needs to be traced by the garbage
/// collector could cause an use after free, memory corruption or other kinds of **Undefined
/// Behaviour**. See <https://github.com/Manishearth/rust-gc/issues/50> for a technical explanation
/// on why that is the case.
pub unsafe fn from_closure_with_captures<F, T>(closure: F, captures: T) -> Self
where
F: FnMut(Args, &mut T, &mut Context<'_>) -> Ret + 'static,
T: Trace + 'static,
{
Self {
inner: GenericInner::Closure(Box::new(GenericClosure {
f: closure,
captures,
phantom: PhantomData,
})),
}
}
/// Calls this `GenericNativeFunction`, forwarding the arguments to the corresponding function.
#[inline]
pub fn call(&mut self, args: Args, context: &mut Context<'_>) -> Ret {
match self.inner {
GenericInner::PointerFn(f) => f(args, context),
GenericInner::Closure(ref mut c) => c.call(args, context),
}
}
}

47
boa_engine/src/object/mod.rs

@ -58,7 +58,7 @@ use crate::{
use boa_gc::{custom_trace, Finalize, GcCell, Trace, WeakGc}; use boa_gc::{custom_trace, Finalize, GcCell, Trace, WeakGc};
use std::{ use std::{
any::Any, any::Any,
fmt::{self, Debug, Display}, fmt::{self, Debug},
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
@ -92,8 +92,8 @@ pub type JsPrototype = Option<JsObject>;
/// This trait allows Rust types to be passed around as objects. /// This trait allows Rust types to be passed around as objects.
/// ///
/// This is automatically implemented, when a type implements `Debug`, `Any` and `Trace`. /// This is automatically implemented when a type implements `Any` and `Trace`.
pub trait NativeObject: Debug + Any + Trace { pub trait NativeObject: Any + Trace {
/// Convert the Rust type which implements `NativeObject` to a `&dyn Any`. /// Convert the Rust type which implements `NativeObject` to a `&dyn Any`.
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
@ -101,7 +101,7 @@ pub trait NativeObject: Debug + Any + Trace {
fn as_mut_any(&mut self) -> &mut dyn Any; fn as_mut_any(&mut self) -> &mut dyn Any;
} }
impl<T: Any + Debug + Trace> NativeObject for T { impl<T: Any + Trace> NativeObject for T {
fn as_any(&self) -> &dyn Any { fn as_any(&self) -> &dyn Any {
self self
} }
@ -164,7 +164,7 @@ pub struct ObjectData {
} }
/// Defines the different types of objects. /// Defines the different types of objects.
#[derive(Debug, Finalize)] #[derive(Finalize)]
pub enum ObjectKind { pub enum ObjectKind {
/// The `AsyncFromSyncIterator` object kind. /// The `AsyncFromSyncIterator` object kind.
AsyncFromSyncIterator(AsyncFromSyncIterator), AsyncFromSyncIterator(AsyncFromSyncIterator),
@ -683,7 +683,7 @@ impl ObjectData {
} }
} }
impl Display for ObjectKind { impl Debug for ObjectKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self { f.write_str(match self {
Self::AsyncFromSyncIterator(_) => "AsyncFromSyncIterator", Self::AsyncFromSyncIterator(_) => "AsyncFromSyncIterator",
@ -736,8 +736,7 @@ impl Debug for ObjectData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ObjectData") f.debug_struct("ObjectData")
.field("kind", &self.kind) .field("kind", &self.kind)
.field("internal_methods", &"internal_methods") .finish_non_exhaustive()
.finish()
} }
} }
@ -1866,17 +1865,17 @@ where
/// Builder for creating native function objects /// Builder for creating native function objects
#[derive(Debug)] #[derive(Debug)]
pub struct FunctionObjectBuilder<'ctx, 'icu> { pub struct FunctionObjectBuilder<'ctx, 'host> {
context: &'ctx mut Context<'icu>, context: &'ctx mut Context<'host>,
function: Function, function: Function,
name: JsString, name: JsString,
length: usize, length: usize,
} }
impl<'ctx, 'icu> FunctionObjectBuilder<'ctx, 'icu> { impl<'ctx, 'host> FunctionObjectBuilder<'ctx, 'host> {
/// Create a new `FunctionBuilder` for creating a native function. /// Create a new `FunctionBuilder` for creating a native function.
#[inline] #[inline]
pub fn new(context: &'ctx mut Context<'icu>, function: NativeFunction) -> Self { pub fn new(context: &'ctx mut Context<'host>, function: NativeFunction) -> Self {
Self { Self {
context, context,
function: Function::Native { function: Function::Native {
@ -1998,15 +1997,15 @@ impl<'ctx, 'icu> FunctionObjectBuilder<'ctx, 'icu> {
/// } /// }
/// ``` /// ```
#[derive(Debug)] #[derive(Debug)]
pub struct ObjectInitializer<'ctx, 'icu> { pub struct ObjectInitializer<'ctx, 'host> {
context: &'ctx mut Context<'icu>, context: &'ctx mut Context<'host>,
object: JsObject, object: JsObject,
} }
impl<'ctx, 'icu> ObjectInitializer<'ctx, 'icu> { impl<'ctx, 'host> ObjectInitializer<'ctx, 'host> {
/// Create a new `ObjectBuilder`. /// Create a new `ObjectBuilder`.
#[inline] #[inline]
pub fn new(context: &'ctx mut Context<'icu>) -> Self { pub fn new(context: &'ctx mut Context<'host>) -> Self {
let object = JsObject::with_object_proto(context); let object = JsObject::with_object_proto(context);
Self { context, object } Self { context, object }
} }
@ -2063,8 +2062,8 @@ impl<'ctx, 'icu> ObjectInitializer<'ctx, 'icu> {
} }
/// Builder for creating constructors objects, like `Array`. /// Builder for creating constructors objects, like `Array`.
pub struct ConstructorBuilder<'ctx, 'icu> { pub struct ConstructorBuilder<'ctx, 'host> {
context: &'ctx mut Context<'icu>, context: &'ctx mut Context<'host>,
function: NativeFunctionPointer, function: NativeFunctionPointer,
object: JsObject, object: JsObject,
has_prototype_property: bool, has_prototype_property: bool,
@ -2092,13 +2091,13 @@ impl Debug for ConstructorBuilder<'_, '_> {
} }
} }
impl<'ctx, 'icu> ConstructorBuilder<'ctx, 'icu> { impl<'ctx, 'host> ConstructorBuilder<'ctx, 'host> {
/// Create a new `ConstructorBuilder`. /// Create a new `ConstructorBuilder`.
#[inline] #[inline]
pub fn new( pub fn new(
context: &'ctx mut Context<'icu>, context: &'ctx mut Context<'host>,
function: NativeFunctionPointer, function: NativeFunctionPointer,
) -> ConstructorBuilder<'ctx, 'icu> { ) -> ConstructorBuilder<'ctx, 'host> {
Self { Self {
context, context,
function, function,
@ -2115,10 +2114,10 @@ impl<'ctx, 'icu> ConstructorBuilder<'ctx, 'icu> {
} }
pub(crate) fn with_standard_constructor( pub(crate) fn with_standard_constructor(
context: &'ctx mut Context<'icu>, context: &'ctx mut Context<'host>,
function: NativeFunctionPointer, function: NativeFunctionPointer,
standard_constructor: StandardConstructor, standard_constructor: StandardConstructor,
) -> ConstructorBuilder<'ctx, 'icu> { ) -> ConstructorBuilder<'ctx, 'host> {
Self { Self {
context, context,
function, function,
@ -2350,7 +2349,7 @@ impl<'ctx, 'icu> ConstructorBuilder<'ctx, 'icu> {
/// Return the current context. /// Return the current context.
#[inline] #[inline]
pub fn context(&mut self) -> &mut Context<'icu> { pub fn context(&mut self) -> &mut Context<'host> {
self.context self.context
} }

22
boa_engine/src/object/operations.rs

@ -706,7 +706,9 @@ impl JsObject {
) -> JsResult<()> { ) -> JsResult<()> {
// 1. If the host is a web browser, then // 1. If the host is a web browser, then
// a. Perform ? HostEnsureCanAddPrivateElement(O). // a. Perform ? HostEnsureCanAddPrivateElement(O).
self.host_ensure_can_add_private_element(context)?; context
.host_hooks()
.ensure_can_add_private_element(self, context)?;
// 2. Let entry be PrivateElementFind(O, P). // 2. Let entry be PrivateElementFind(O, P).
let entry = self.private_element_find(name, false, false); let entry = self.private_element_find(name, false, false);
@ -754,7 +756,9 @@ impl JsObject {
// 2. If the host is a web browser, then // 2. If the host is a web browser, then
// a. Perform ? HostEnsureCanAddPrivateElement(O). // a. Perform ? HostEnsureCanAddPrivateElement(O).
self.host_ensure_can_add_private_element(context)?; context
.host_hooks()
.ensure_can_add_private_element(self, context)?;
// 3. Let entry be PrivateElementFind(O, method.[[Key]]). // 3. Let entry be PrivateElementFind(O, method.[[Key]]).
let entry = self.private_element_find(name, getter, setter); let entry = self.private_element_find(name, getter, setter);
@ -774,20 +778,6 @@ impl JsObject {
Ok(()) Ok(())
} }
/// Abstract operation `HostEnsureCanAddPrivateElement ( O )`
///
/// Ensure private elements can be added to an object.
///
/// More information:
/// - [ECMAScript specification][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-hostensurecanaddprivateelement
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
fn host_ensure_can_add_private_element(&self, _context: &mut Context<'_>) -> JsResult<()> {
// TODO: Make this operation host-defined.
Ok(())
}
/// Abstract operation `PrivateGet ( O, P )` /// Abstract operation `PrivateGet ( O, P )`
/// ///
/// Get the value of a private element. /// Get the value of a private element.

23
boa_tester/src/exec/mod.rs

@ -7,8 +7,9 @@ use super::{
}; };
use crate::read::ErrorType; use crate::read::ErrorType;
use boa_engine::{ use boa_engine::{
builtins::JsArgs, native_function::NativeFunction, object::FunctionObjectBuilder, builtins::JsArgs, context::ContextBuilder, job::SimpleJobQueue,
property::Attribute, Context, JsNativeErrorKind, JsValue, native_function::NativeFunction, object::FunctionObjectBuilder, property::Attribute, Context,
JsNativeErrorKind, JsValue,
}; };
use boa_parser::Parser; use boa_parser::Parser;
use colored::Colorize; use colored::Colorize;
@ -162,10 +163,11 @@ impl Test {
let result = std::panic::catch_unwind(|| match self.expected_outcome { let result = std::panic::catch_unwind(|| match self.expected_outcome {
Outcome::Positive => { Outcome::Positive => {
let mut context = Context::default(); let queue = SimpleJobQueue::new();
let context = &mut ContextBuilder::new().job_queue(&queue).build();
let async_result = AsyncResult::default(); let async_result = AsyncResult::default();
if let Err(e) = self.set_up_env(harness, &mut context, async_result.clone()) { if let Err(e) = self.set_up_env(harness, context, async_result.clone()) {
return (false, e); return (false, e);
} }
@ -175,6 +177,8 @@ impl Test {
Err(e) => return (false, format!("Uncaught {e}")), Err(e) => return (false, format!("Uncaught {e}")),
}; };
context.run_jobs();
if let Err(e) = async_result.inner.borrow().as_ref() { if let Err(e) = async_result.inner.borrow().as_ref() {
return (false, format!("Uncaught {e}")); return (false, format!("Uncaught {e}"));
} }
@ -209,8 +213,8 @@ impl Test {
phase: Phase::Runtime, phase: Phase::Runtime,
error_type, error_type,
} => { } => {
let mut context = Context::default(); let context = &mut Context::default();
if let Err(e) = self.set_up_env(harness, &mut context, AsyncResult::default()) { if let Err(e) = self.set_up_env(harness, context, AsyncResult::default()) {
return (false, e); return (false, e);
} }
let code = match Parser::new(test_content.as_bytes()) let code = match Parser::new(test_content.as_bytes())
@ -222,12 +226,11 @@ impl Test {
Err(e) => return (false, format!("Uncaught {e}")), Err(e) => return (false, format!("Uncaught {e}")),
}; };
// TODO: timeout
let e = match context.execute(code) { let e = match context.execute(code) {
Ok(res) => return (false, res.display().to_string()), Ok(res) => return (false, res.display().to_string()),
Err(e) => e, Err(e) => e,
}; };
if let Ok(e) = e.try_native(&mut context) { if let Ok(e) = e.try_native(context) {
match &e.kind { match &e.kind {
JsNativeErrorKind::Syntax if error_type == ErrorType::SyntaxError => {} JsNativeErrorKind::Syntax if error_type == ErrorType::SyntaxError => {}
JsNativeErrorKind::Reference if error_type == ErrorType::ReferenceError => { JsNativeErrorKind::Reference if error_type == ErrorType::ReferenceError => {
@ -242,10 +245,10 @@ impl Test {
.as_opaque() .as_opaque()
.expect("try_native cannot fail if e is not opaque") .expect("try_native cannot fail if e is not opaque")
.as_object() .as_object()
.and_then(|o| o.get("constructor", &mut context).ok()) .and_then(|o| o.get("constructor", context).ok())
.as_ref() .as_ref()
.and_then(JsValue::as_object) .and_then(JsValue::as_object)
.and_then(|o| o.get("name", &mut context).ok()) .and_then(|o| o.get("name", context).ok())
.as_ref() .as_ref()
.and_then(JsValue::as_string) .and_then(JsValue::as_string)
.map(|s| s == error_type.as_str()) .map(|s| s == error_type.as_str())

Loading…
Cancel
Save