diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index a76172925c..eb7c25e518 100644 --- a/boa_cli/src/main.rs +++ b/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>); + +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}"); + } + } + } + } +} diff --git a/boa_engine/src/builtins/async_generator/mod.rs b/boa_engine/src/builtins/async_generator/mod.rs index 5745f67f3d..612fe83ad0 100644 --- a/boa_engine/src/builtins/async_generator/mod.rs +++ b/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). diff --git a/boa_engine/src/builtins/eval/mod.rs b/boa_engine/src/builtins/eval/mod.rs index 0a1398ca3d..27c0c9e893 100644 --- a/boa_engine/src/builtins/eval/mod.rs +++ b/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. diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 0387d46641..2efcab5c79 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/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, @@ -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, @@ -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(), diff --git a/boa_engine/src/builtins/promise/tests.rs b/boa_engine/src/builtins/promise/tests.rs index f32dcd2ade..9886ab5e87 100644 --- a/boa_engine/src/builtins/promise/tests.rs +++ b/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")); } diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index b9442178ed..02c1f5dc7e 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/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, names_map: FxHashMap, @@ -211,10 +211,10 @@ pub struct ByteCompiler<'b, 'icu> { jump_info: Vec, 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(), diff --git a/boa_engine/src/class.rs b/boa_engine/src/class.rs index 5e465c01a3..186c5a8f6c 100644 --- a/boa_engine/src/class.rs +++ b/boa_engine/src/class.rs @@ -157,12 +157,12 @@ impl 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(context: &'ctx mut Context<'icu>) -> Self +impl<'ctx, 'host> ClassBuilder<'ctx, 'host> { + pub(crate) fn new(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() } } diff --git a/boa_engine/src/context/hooks.rs b/boa_engine/src/context/hooks.rs new file mode 100644 index 0000000000..bafa7963d7 --- /dev/null +++ b/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 { + // 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 {} diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 2b6c7b3bcf..e45c421146 100644 --- a/boa_engine/src/context/mod.rs +++ b/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, - pub(crate) kept_alive: Vec, + + /// 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(&mut self, src: S) -> JsResult 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` 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) -> JsResult { 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, + host_hooks: Option<&'hooks dyn HostHooks>, + job_queue: Option<&'queue dyn JobQueue>, #[cfg(feature = "intl")] icu: Option>, #[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, icu_locid_transform::LocaleTransformError> { + ) -> Result, 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 diff --git a/boa_engine/src/error.rs b/boa_engine/src/error.rs index 1fafa0c140..15053186e9 100644 --- a/boa_engine/src/error.rs +++ b/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), } diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs index 2375a7b058..a4a1cf735d 100644 --- a/boa_engine/src/job.rs +++ b/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) -> JsResult>, } -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, +} + +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(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 { - // 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>); + +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(); + } } } diff --git a/boa_engine/src/native_function.rs b/boa_engine/src/native_function.rs index 35dd26bd79..d0aa955590 100644 --- a/boa_engine/src/native_function.rs +++ b/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: Trace { - fn call(&mut self, args: Args, context: &mut Context<'_>) -> Ret; -} - -#[derive(Trace, Finalize)] -struct GenericClosure -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) -> Ret>>, -} - -impl TraceableGenericClosure for GenericClosure -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 { - inner: GenericInner, -} - -enum GenericInner { - PointerFn(fn(Args, &mut Context<'_>) -> Ret), - Closure(Box>), -} - -impl Finalize for GenericNativeFunction { - 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 Trace for GenericNativeFunction { - custom_trace!(this, { - if let GenericInner::Closure(c) = &this.inner { - mark(c); - } - }); -} - -impl std::fmt::Debug for GenericNativeFunction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("NativeFunction").finish_non_exhaustive() - } -} - -impl GenericNativeFunction { - /// 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(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(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 for a technical explanation - /// on why that is the case. - pub unsafe fn from_closure(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 for a technical explanation - /// on why that is the case. - pub unsafe fn from_closure_with_captures(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), - } - } -} diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index a52def0f98..2fef977757 100644 --- a/boa_engine/src/object/mod.rs +++ b/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; /// 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 NativeObject for T { +impl 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 } diff --git a/boa_engine/src/object/operations.rs b/boa_engine/src/object/operations.rs index 20718bbd7d..68797c8f6e 100644 --- a/boa_engine/src/object/operations.rs +++ b/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. diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index 55cc9dd82b..d3fac43955 100644 --- a/boa_tester/src/exec/mod.rs +++ b/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())