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_engine::{
context::ContextBuilder,
job::{JobQueue, NativeJob},
vm::flowgraph::{Direction, Graph},
Context, JsResult,
};
use clap::{Parser, ValueEnum, ValueHint};
use colored::{Color, Colorize};
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_attr(
@ -253,7 +255,8 @@ fn generate_flowgraph(
fn main() -> Result<(), io::Error> {
let args = Opt::parse();
let mut context = Context::default();
let queue = Jobs::default();
let mut context = ContextBuilder::new().job_queue(&queue).build();
// Trace Output
context.set_trace(args.trace);
@ -280,6 +283,7 @@ fn main() -> Result<(), io::Error> {
Ok(v) => println!("{}", v.display()),
Err(v) => eprintln!("Uncaught {v}"),
}
context.run_jobs();
}
}
@ -333,11 +337,14 @@ fn main() -> Result<(), io::Error> {
}
} else {
match context.eval(line.trim_end()) {
Ok(v) => println!("{}", v.display()),
Ok(v) => {
println!("{}", v.display());
}
Err(v) => {
eprintln!("{}: {}", "Uncaught".red(), v.to_string().red());
}
}
context.run_jobs();
}
}
@ -355,3 +362,26 @@ fn main() -> Result<(), io::Error> {
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,
NativeFunction::from_copy_closure_with_captures(
|_this, args, generator, context| {
let mut generator_borrow_mut = generator.borrow_mut();
let gen = generator_borrow_mut
.as_async_generator_mut()
.expect("already checked before");
let next = {
let mut generator_borrow_mut = generator.borrow_mut();
let gen = generator_borrow_mut
.as_async_generator_mut()
.expect("already checked before");
// a. Set generator.[[AsyncGeneratorState]] to completed.
gen.state = AsyncGeneratorState::Completed;
// a. Set generator.[[AsyncGeneratorState]] to completed.
gen.state = AsyncGeneratorState::Completed;
gen.queue.pop_front().expect("must have one entry")
};
// b. Let result be NormalCompletion(value).
let result = Ok(args.get_or_undefined(0).clone());
// 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);
// 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.
// 5. Perform ? HostEnsureCanCompileStrings(evalRealm).
let mut parser = Parser::new(x.as_bytes());
if strict {
parser.set_strict();
}
context.host_hooks().ensure_can_compile_strings(context)?;
// 11. Perform the following substeps in an implementation-defined order, possibly interleaving parsing and error detection:
// a. Let script be ParseText(StringToCodePoints(x), Script).
// b. If script is a List of errors, throw a SyntaxError exception.
// c. If script Contains ScriptBody is false, return undefined.
// 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())?;
// 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.
#[derive(Debug, Clone, Trace, Finalize)]
#[derive(Debug, Trace, Finalize)]
pub struct Promise {
state: PromiseState,
fulfill_reactions: Vec<ReactionRecord>,
@ -76,7 +76,7 @@ pub struct Promise {
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-promisereaction-records
#[derive(Debug, Clone, Trace, Finalize)]
#[derive(Debug, Trace, Finalize)]
pub(crate) struct ReactionRecord {
/// The `[[Capability]]` field.
promise_capability: Option<PromiseCapability>,
@ -101,6 +101,25 @@ enum ReactionType {
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.
///
/// More information:
@ -133,99 +152,94 @@ impl PromiseCapability {
resolve: JsValue,
}
match c.as_constructor() {
// 1. If IsConstructor(C) is false, throw a TypeError exception.
None => Err(JsNativeError::typ()
.with_message("PromiseCapability: expected constructor")
.into()),
Some(c) => {
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).
// 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }.
let promise_capability = Gc::new(GcCell::new(RejectResolve {
reject: 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:
// 5. Let executor be CreateBuiltinFunction(executorClosure, 2, "", « »).
let executor = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_this, args: &[JsValue], captures, _| {
let mut promise_capability = captures.borrow_mut();
// a. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception.
if !promise_capability.resolve.is_undefined() {
return Err(JsNativeError::typ()
.with_message("promiseCapability.[[Resolve]] is not undefined")
.into());
}
// b. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception.
if !promise_capability.reject.is_undefined() {
return Err(JsNativeError::typ()
.with_message("promiseCapability.[[Reject]] is not undefined")
.into());
}
let resolve = args.get_or_undefined(0);
let reject = args.get_or_undefined(1);
// c. Set promiseCapability.[[Resolve]] to resolve.
promise_capability.resolve = resolve.clone();
// d. Set promiseCapability.[[Reject]] to reject.
promise_capability.reject = reject.clone();
// e. Return undefined.
Ok(JsValue::Undefined)
},
promise_capability.clone(),
),
)
.name("")
.length(2)
.build()
.into();
// 6. Let promise be ? Construct(C, « executor »).
let promise = c.construct(&[executor], Some(&c), context)?;
let promise_capability = promise_capability.borrow();
let resolve = promise_capability.resolve.clone();
let reject = promise_capability.reject.clone();
// 7. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception.
let resolve = resolve
.as_object()
.cloned()
.and_then(JsFunction::from_object)
.ok_or_else(|| {
JsNativeError::typ()
.with_message("promiseCapability.[[Resolve]] is not callable")
})?;
// 8. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception.
let reject = reject
.as_object()
.cloned()
.and_then(JsFunction::from_object)
.ok_or_else(|| {
JsNativeError::typ()
.with_message("promiseCapability.[[Reject]] is not callable")
})?;
// 9. Set promiseCapability.[[Promise]] to promise.
// 10. Return promiseCapability.
Ok(PromiseCapability {
promise,
resolve,
reject,
})
}
}
// 1. If IsConstructor(C) is false, throw a TypeError exception.
let c = c.as_constructor().ok_or_else(|| {
JsNativeError::typ().with_message("PromiseCapability: expected constructor")
})?;
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).
// 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }.
let promise_capability = Gc::new(GcCell::new(RejectResolve {
reject: 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:
// 5. Let executor be CreateBuiltinFunction(executorClosure, 2, "", « »).
let executor = FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
|_this, args: &[JsValue], captures, _| {
let mut promise_capability = captures.borrow_mut();
// a. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception.
if !promise_capability.resolve.is_undefined() {
return Err(JsNativeError::typ()
.with_message("promiseCapability.[[Resolve]] is not undefined")
.into());
}
// b. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception.
if !promise_capability.reject.is_undefined() {
return Err(JsNativeError::typ()
.with_message("promiseCapability.[[Reject]] is not undefined")
.into());
}
let resolve = args.get_or_undefined(0);
let reject = args.get_or_undefined(1);
// c. Set promiseCapability.[[Resolve]] to resolve.
promise_capability.resolve = resolve.clone();
// d. Set promiseCapability.[[Reject]] to reject.
promise_capability.reject = reject.clone();
// e. Return undefined.
Ok(JsValue::Undefined)
},
promise_capability.clone(),
),
)
.name("")
.length(2)
.build()
.into();
// 6. Let promise be ? Construct(C, « executor »).
let promise = c.construct(&[executor], Some(&c), context)?;
let promise_capability = promise_capability.borrow();
let resolve = promise_capability.resolve.clone();
let reject = promise_capability.reject.clone();
// 7. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception.
let resolve = resolve
.as_object()
.cloned()
.and_then(JsFunction::from_object)
.ok_or_else(|| {
JsNativeError::typ().with_message("promiseCapability.[[Resolve]] is not callable")
})?;
// 8. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception.
let reject = reject
.as_object()
.cloned()
.and_then(JsFunction::from_object)
.ok_or_else(|| {
JsNativeError::typ().with_message("promiseCapability.[[Reject]] is not callable")
})?;
// 9. Set promiseCapability.[[Promise]] to promise.
// 10. Return promiseCapability.
Ok(PromiseCapability {
promise,
resolve,
reject,
})
}
/// Returns the promise object.
@ -1313,13 +1327,13 @@ impl Promise {
}
let Some(then) = resolution.as_object() else {
// 8. If Type(resolution) is not Object, then
// a. Perform FulfillPromise(promise, resolution).
Self::fulfill_promise(promise, resolution.clone(), context);
// 8. If Type(resolution) is not Object, then
// a. Perform FulfillPromise(promise, resolution).
Self::fulfill_promise(promise, resolution.clone(), context);
// b. Return undefined.
return Ok(JsValue::Undefined);
};
// b. Return undefined.
return Ok(JsValue::Undefined);
};
// 9. Let then be Completion(Get(resolution, "then")).
let then_action = match then.get("then", context) {
@ -1345,7 +1359,7 @@ impl Promise {
};
// 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).
let job = new_promise_resolve_thenable_job(
@ -1355,7 +1369,7 @@ impl Promise {
);
// 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
context.host_enqueue_promise_job(job);
context.job_queue().enqueue_promise_job(job, context);
// 16. Return undefined.
Ok(JsValue::Undefined)
@ -1401,7 +1415,6 @@ impl Promise {
// 6. Set alreadyResolved.[[Value]] to true.
already_resolved.set(true);
// let reason = args.get_or_undefined(0);
// 7. Perform RejectPromise(promise, reason).
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").
if !handled {
// TODO
context
.host_hooks()
.promise_rejection_tracker(promise, OperationType::Reject, context);
}
// 9. Return unused.
@ -1541,7 +1556,7 @@ impl Promise {
let job = new_promise_reaction_job(reaction, argument.clone());
// b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
context.host_enqueue_promise_job(job);
context.job_queue().enqueue_promise_job(job, context);
}
// 2. Return unused.
@ -1996,7 +2011,7 @@ impl Promise {
.and_then(JsFunction::from_object)
// 4. Else,
// 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
// a. Let onRejectedJobCallback be empty.
@ -2006,7 +2021,7 @@ impl Promise {
.and_then(JsFunction::from_object)
// 6. Else,
// 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 }.
let fulfill_reaction = ReactionRecord {
@ -2027,6 +2042,7 @@ impl Promise {
let promise = promise.as_promise().expect("IsPromise(promise) is false");
(promise.state.clone(), promise.handled)
};
match state {
// 9. If promise.[[PromiseState]] is pending, then
PromiseState::Pending => {
@ -2048,7 +2064,9 @@ impl Promise {
let fulfill_job = new_promise_reaction_job(fulfill_reaction, value.clone());
// 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,
@ -2057,14 +2075,18 @@ impl Promise {
PromiseState::Rejected(ref reason) => {
// c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle").
if !handled {
// TODO
context.host_hooks().promise_rejection_tracker(
promise,
OperationType::Handle,
context,
);
}
// d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason).
let reject_job = new_promise_reaction_job(reject_reaction, reason.clone());
// 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.
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 »)).
Some(handler) => handler
.call_job_callback(&JsValue::Undefined, &[argument.clone()], context)
Some(handler) => context
.host_hooks()
.call_job_callback(handler, &JsValue::Undefined, &[argument.clone()], 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);
// 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,
&[
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]
fn promise() {
let mut context = Context::default();
let queue = SimpleJobQueue::new();
let mut context = ContextBuilder::new().job_queue(&queue).build();
let init = r#"
let count = 0;
const promise = new Promise((resolve, reject) => {
count += 1;
resolve(undefined);
count += 1;
resolve(undefined);
}).then((_) => (count += 1));
count += 1;
count;
"#;
"#;
let result = context.eval(init).unwrap();
assert_eq!(result.as_number(), Some(2_f64));
context.run_jobs();
let after_completion = forward(&mut context, "count");
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.
#[derive(Debug)]
pub struct ByteCompiler<'b, 'icu> {
pub struct ByteCompiler<'b, 'host> {
code_block: CodeBlock,
literals_map: FxHashMap<Literal, u32>,
names_map: FxHashMap<Identifier, u32>,
@ -211,10 +211,10 @@ pub struct ByteCompiler<'b, 'icu> {
jump_info: Vec<JumpControlInfo>,
in_async_generator: 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.
const DUMMY_ADDRESS: u32 = u32::MAX;
@ -224,8 +224,8 @@ impl<'b, 'icu> ByteCompiler<'b, 'icu> {
name: Sym,
strict: bool,
json_parse: bool,
context: &'b mut Context<'icu>,
) -> ByteCompiler<'b, 'icu> {
context: &'b mut Context<'host>,
) -> ByteCompiler<'b, 'host> {
Self {
code_block: CodeBlock::new(name, 0, strict),
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.
#[derive(Debug)]
pub struct ClassBuilder<'ctx, 'icu> {
builder: ConstructorBuilder<'ctx, 'icu>,
pub struct ClassBuilder<'ctx, 'host> {
builder: ConstructorBuilder<'ctx, 'host>,
}
impl<'ctx, 'icu> ClassBuilder<'ctx, 'icu> {
pub(crate) fn new<T>(context: &'ctx mut Context<'icu>) -> Self
impl<'ctx, 'host> ClassBuilder<'ctx, 'host> {
pub(crate) fn new<T>(context: &'ctx mut Context<'host>) -> Self
where
T: ClassConstructor,
{
@ -292,7 +292,7 @@ impl<'ctx, 'icu> ClassBuilder<'ctx, 'icu> {
/// Return the current context.
#[inline]
pub fn context(&mut self) -> &mut Context<'icu> {
pub fn context(&mut self) -> &mut Context<'host> {
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.
mod hooks;
pub mod intrinsics;
use hooks::DefaultHooks;
pub use hooks::HostHooks;
#[cfg(feature = "intl")]
pub(crate) mod icu;
@ -9,7 +12,6 @@ pub(crate) mod icu;
pub use icu::BoaProvider;
use intrinsics::{IntrinsicObjects, Intrinsics};
use std::collections::VecDeque;
#[cfg(not(feature = "intl"))]
pub use std::marker::PhantomData;
@ -20,7 +22,7 @@ use crate::{
builtins,
bytecompiler::ByteCompiler,
class::{Class, ClassBuilder},
job::NativeJob,
job::{IdleJobQueue, JobQueue, NativeJob},
native_function::NativeFunction,
object::{FunctionObjectBuilder, GlobalPropertyMap, JsObject},
property::{Attribute, PropertyDescriptor, PropertyKey},
@ -76,7 +78,7 @@ use boa_profiler::Profiler;
///
/// 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
pub(crate) realm: Realm,
@ -90,22 +92,21 @@ pub struct Context<'icu> {
/// Intrinsic objects
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
#[cfg(feature = "fuzz")]
pub(crate) instructions_remaining: usize,
pub(crate) vm: Vm,
pub(crate) promise_job_queue: VecDeque<NativeJob>,
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<'_> {
@ -114,16 +115,15 @@ impl std::fmt::Debug for Context<'_> {
debug
.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")]
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")]
debug.field("icu", &self.icu);
@ -143,7 +143,7 @@ impl Context<'_> {
/// Create a new [`ContextBuilder`] to specify the [`Interner`] and/or
/// the icu data provider.
#[must_use]
pub fn builder() -> ContextBuilder<'static> {
pub fn builder() -> ContextBuilder<'static, 'static, 'static> {
ContextBuilder::default()
}
@ -159,6 +159,9 @@ impl Context<'_> {
/// assert!(value.is_number());
/// 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)]
pub fn eval<S>(&mut self, src: S) -> JsResult<JsValue>
where
@ -202,6 +205,9 @@ impl Context<'_> {
/// 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
/// `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> {
let _timer = Profiler::global().start_event("Execution", "Main");
@ -211,9 +217,8 @@ impl Context<'_> {
let result = self.run();
self.vm.pop_frame();
self.clear_kept_objects();
self.run_queued_jobs()?;
let (result, _) = result?;
Ok(result)
result.map(|r| r.0)
}
/// Register a global property.
@ -375,16 +380,15 @@ impl Context<'_> {
self.vm.trace = trace;
}
/// More information:
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob
pub fn host_enqueue_promise_job(&mut self, job: NativeJob /* , realm: Realm */) {
// If realm is not null ...
// TODO
// Let scriptOrModule be ...
// TODO
self.promise_job_queue.push_back(job);
/// Enqueues a [`NativeJob`] on the [`JobQueue`].
pub fn enqueue_job(&mut self, job: NativeJob) {
self.job_queue.enqueue_promise_job(job, self);
}
/// Runs all the jobs in the job queue.
pub fn run_jobs(&mut self) {
self.job_queue.run_jobs(self);
self.clear_kept_objects();
}
/// Abstract operation [`ClearKeptObjects`][clear].
@ -450,21 +454,22 @@ impl Context<'_> {
// Create intrinsics, add global objects here
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.
fn run_queued_jobs(&mut self) -> JsResult<()> {
while let Some(job) = self.promise_job_queue.pop_front() {
job.call(self)?;
self.clear_kept_objects();
}
Ok(())
/// Get the job queue.
pub(crate) fn job_queue(&mut self) -> &'host dyn JobQueue {
self.job_queue
}
}
#[cfg(feature = "intl")]
impl<'icu> Context<'icu> {
/// 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
}
}
@ -480,9 +485,11 @@ impl<'icu> Context<'icu> {
feature = "intl",
doc = "The required data in a valid provider is specified in [`BoaProvider`]"
)]
#[derive(Default, Debug)]
pub struct ContextBuilder<'icu> {
#[derive(Default)]
pub struct ContextBuilder<'icu, 'hooks, 'queue> {
interner: Option<Interner>,
host_hooks: Option<&'hooks dyn HostHooks>,
job_queue: Option<&'queue dyn JobQueue>,
#[cfg(feature = "intl")]
icu: Option<icu::Icu<'icu>>,
#[cfg(not(feature = "intl"))]
@ -491,7 +498,31 @@ pub struct ContextBuilder<'icu> {
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.
///
/// This is useful when you want to initialize an [`Interner`] with
@ -520,13 +551,33 @@ impl<'a> ContextBuilder<'a> {
pub fn icu_provider(
self,
provider: BoaProvider<'_>,
) -> Result<ContextBuilder<'_>, icu_locid_transform::LocaleTransformError> {
) -> Result<ContextBuilder<'_, 'hooks, 'queue>, icu_locid_transform::LocaleTransformError> {
Ok(ContextBuilder {
icu: Some(icu::Icu::new(provider)?),
..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`].
///
/// This function is only available if the `fuzz` feature is enabled.
@ -537,17 +588,15 @@ impl<'a> ContextBuilder<'a> {
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
/// all missing parameters to their default values.
#[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 mut context = Context {
realm: Realm::create(intrinsics.constructors().object().prototype().into()),
@ -561,12 +610,11 @@ impl<'a> ContextBuilder<'a> {
let provider = BoaProvider::Buffer(boa_icu_provider::buffer());
icu::Icu::new(provider).expect("Failed to initialize default icu data.")
}),
#[cfg(not(feature = "intl"))]
icu: PhantomData,
#[cfg(feature = "fuzz")]
instructions_remaining: self.instructions_remaining,
promise_job_queue: VecDeque::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

10
boa_engine/src/error.rs

@ -66,15 +66,15 @@ enum Repr {
/// The error type returned by the [`JsError::try_native`] method.
#[derive(Debug, Clone, Error)]
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}`")]
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")]
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}`")]
InaccessibleProperty {
/// The name of the property that could not be accessed.
@ -84,7 +84,7 @@ pub enum TryNativeError {
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`")]
InvalidErrorsIndex {
/// The index of the error that could not be accessed.
@ -94,7 +94,7 @@ pub enum TryNativeError {
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())]
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};
/// An ECMAScript [Job] closure.
///
/// 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
///
@ -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.
/// - 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
/// 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`.
///
/// ## [`Trace`]?
///
/// `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
/// [`NativeFunction`] API, since you can capture any `Trace` variable into the closure... but it
/// doesn't!
/// 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!
///
/// [Job]: https://tc39.es/ecma262/#sec-jobs
@ -43,7 +65,7 @@ pub struct NativeJob {
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 {
f.debug_struct("NativeJob").field("f", &"Closure").finish()
}
@ -64,56 +86,135 @@ impl NativeJob {
}
}
/// `JobCallback` records
///
/// More information:
/// - [ECMAScript reference][spec]
/// [`JobCallback`][spec] records
///
/// [spec]: https://tc39.es/ecma262/#sec-jobcallback-records
#[derive(Debug, Clone, Trace, Finalize)]
#[derive(Trace, Finalize)]
pub struct JobCallback {
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 {
/// `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: JsFunction) -> Self {
// 1. Return the JobCallback Record { [[Callback]]: callback, [[HostDefined]]: empty }.
Self { callback }
/// Creates a new `JobCallback`.
pub fn new<T: Any + Trace>(callback: JsFunction, host_defined: T) -> Self {
JobCallback {
callback,
host_defined: Box::new(host_defined),
}
}
/// `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]
/// Gets the inner callback of the job.
pub const fn callback(&self) -> &JsFunction {
&self.callback
}
/// Gets a reference to the host defined additional field as an `Any` trait object.
pub fn host_defined(&self) -> &dyn Any {
self.host_defined.as_any()
}
/// 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.
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.
// 2. Return ? Call(jobCallback.[[Callback]], V, argumentsList).
self.callback.call(v, arguments_list, context)
/// Running a job could enqueue more jobs in the queue. The implementor of the trait
/// determines if the method should loop until there are no more queued jobs or if
/// it should only run one iteration of the queue.
fn run_jobs(&self, context: &mut Context<'_>);
}
/// A job queue that does nothing.
///
/// This is the default job queue for the [`Context`], and is useful if you want to disable
/// the promise capabilities of the engine.
///
/// 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
//! from native Rust functions and closures.
use std::marker::PhantomData;
use boa_gc::{custom_trace, Finalize, Gc, Trace};
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 std::{
any::Any,
fmt::{self, Debug, Display},
fmt::{self, Debug},
ops::{Deref, DerefMut},
};
@ -92,8 +92,8 @@ pub type JsPrototype = Option<JsObject>;
/// This trait allows Rust types to be passed around as objects.
///
/// This is automatically implemented, when a type implements `Debug`, `Any` and `Trace`.
pub trait NativeObject: Debug + Any + Trace {
/// This is automatically implemented when a type implements `Any` and `Trace`.
pub trait NativeObject: Any + Trace {
/// Convert the Rust type which implements `NativeObject` to a `&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;
}
impl<T: Any + Debug + Trace> NativeObject for T {
impl<T: Any + Trace> NativeObject for T {
fn as_any(&self) -> &dyn Any {
self
}
@ -164,7 +164,7 @@ pub struct ObjectData {
}
/// Defines the different types of objects.
#[derive(Debug, Finalize)]
#[derive(Finalize)]
pub enum ObjectKind {
/// The `AsyncFromSyncIterator` object kind.
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 {
f.write_str(match self {
Self::AsyncFromSyncIterator(_) => "AsyncFromSyncIterator",
@ -736,8 +736,7 @@ impl Debug for ObjectData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ObjectData")
.field("kind", &self.kind)
.field("internal_methods", &"internal_methods")
.finish()
.finish_non_exhaustive()
}
}
@ -1866,17 +1865,17 @@ where
/// Builder for creating native function objects
#[derive(Debug)]
pub struct FunctionObjectBuilder<'ctx, 'icu> {
context: &'ctx mut Context<'icu>,
pub struct FunctionObjectBuilder<'ctx, 'host> {
context: &'ctx mut Context<'host>,
function: Function,
name: JsString,
length: usize,
}
impl<'ctx, 'icu> FunctionObjectBuilder<'ctx, 'icu> {
impl<'ctx, 'host> FunctionObjectBuilder<'ctx, 'host> {
/// Create a new `FunctionBuilder` for creating a native function.
#[inline]
pub fn new(context: &'ctx mut Context<'icu>, function: NativeFunction) -> Self {
pub fn new(context: &'ctx mut Context<'host>, function: NativeFunction) -> Self {
Self {
context,
function: Function::Native {
@ -1998,15 +1997,15 @@ impl<'ctx, 'icu> FunctionObjectBuilder<'ctx, 'icu> {
/// }
/// ```
#[derive(Debug)]
pub struct ObjectInitializer<'ctx, 'icu> {
context: &'ctx mut Context<'icu>,
pub struct ObjectInitializer<'ctx, 'host> {
context: &'ctx mut Context<'host>,
object: JsObject,
}
impl<'ctx, 'icu> ObjectInitializer<'ctx, 'icu> {
impl<'ctx, 'host> ObjectInitializer<'ctx, 'host> {
/// Create a new `ObjectBuilder`.
#[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);
Self { context, object }
}
@ -2063,8 +2062,8 @@ impl<'ctx, 'icu> ObjectInitializer<'ctx, 'icu> {
}
/// Builder for creating constructors objects, like `Array`.
pub struct ConstructorBuilder<'ctx, 'icu> {
context: &'ctx mut Context<'icu>,
pub struct ConstructorBuilder<'ctx, 'host> {
context: &'ctx mut Context<'host>,
function: NativeFunctionPointer,
object: JsObject,
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`.
#[inline]
pub fn new(
context: &'ctx mut Context<'icu>,
context: &'ctx mut Context<'host>,
function: NativeFunctionPointer,
) -> ConstructorBuilder<'ctx, 'icu> {
) -> ConstructorBuilder<'ctx, 'host> {
Self {
context,
function,
@ -2115,10 +2114,10 @@ impl<'ctx, 'icu> ConstructorBuilder<'ctx, 'icu> {
}
pub(crate) fn with_standard_constructor(
context: &'ctx mut Context<'icu>,
context: &'ctx mut Context<'host>,
function: NativeFunctionPointer,
standard_constructor: StandardConstructor,
) -> ConstructorBuilder<'ctx, 'icu> {
) -> ConstructorBuilder<'ctx, 'host> {
Self {
context,
function,
@ -2350,7 +2349,7 @@ impl<'ctx, 'icu> ConstructorBuilder<'ctx, 'icu> {
/// Return the current context.
#[inline]
pub fn context(&mut self) -> &mut Context<'icu> {
pub fn context(&mut self) -> &mut Context<'host> {
self.context
}

22
boa_engine/src/object/operations.rs

@ -706,7 +706,9 @@ impl JsObject {
) -> JsResult<()> {
// 1. If the host is a web browser, then
// 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).
let entry = self.private_element_find(name, false, false);
@ -754,7 +756,9 @@ impl JsObject {
// 2. If the host is a web browser, then
// 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]]).
let entry = self.private_element_find(name, getter, setter);
@ -774,20 +778,6 @@ impl JsObject {
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 )`
///
/// 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 boa_engine::{
builtins::JsArgs, native_function::NativeFunction, object::FunctionObjectBuilder,
property::Attribute, Context, JsNativeErrorKind, JsValue,
builtins::JsArgs, context::ContextBuilder, job::SimpleJobQueue,
native_function::NativeFunction, object::FunctionObjectBuilder, property::Attribute, Context,
JsNativeErrorKind, JsValue,
};
use boa_parser::Parser;
use colored::Colorize;
@ -162,10 +163,11 @@ impl Test {
let result = std::panic::catch_unwind(|| match self.expected_outcome {
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();
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);
}
@ -175,6 +177,8 @@ impl Test {
Err(e) => return (false, format!("Uncaught {e}")),
};
context.run_jobs();
if let Err(e) = async_result.inner.borrow().as_ref() {
return (false, format!("Uncaught {e}"));
}
@ -209,8 +213,8 @@ impl Test {
phase: Phase::Runtime,
error_type,
} => {
let mut context = Context::default();
if let Err(e) = self.set_up_env(harness, &mut context, AsyncResult::default()) {
let context = &mut Context::default();
if let Err(e) = self.set_up_env(harness, context, AsyncResult::default()) {
return (false, e);
}
let code = match Parser::new(test_content.as_bytes())
@ -222,12 +226,11 @@ impl Test {
Err(e) => return (false, format!("Uncaught {e}")),
};
// TODO: timeout
let e = match context.execute(code) {
Ok(res) => return (false, res.display().to_string()),
Err(e) => e,
};
if let Ok(e) = e.try_native(&mut context) {
if let Ok(e) = e.try_native(context) {
match &e.kind {
JsNativeErrorKind::Syntax if error_type == ErrorType::SyntaxError => {}
JsNativeErrorKind::Reference if error_type == ErrorType::ReferenceError => {
@ -242,10 +245,10 @@ impl Test {
.as_opaque()
.expect("try_native cannot fail if e is not opaque")
.as_object()
.and_then(|o| o.get("constructor", &mut context).ok())
.and_then(|o| o.get("constructor", context).ok())
.as_ref()
.and_then(JsValue::as_object)
.and_then(|o| o.get("name", &mut context).ok())
.and_then(|o| o.get("name", context).ok())
.as_ref()
.and_then(JsValue::as_string)
.map(|s| s == error_type.as_str())

Loading…
Cancel
Save