diff --git a/nova_cli/Cargo.toml b/nova_cli/Cargo.toml index cc61d17c9..ebf5c1c6c 100644 --- a/nova_cli/Cargo.toml +++ b/nova_cli/Cargo.toml @@ -12,6 +12,14 @@ readme.workspace = true keywords.workspace = true categories = ["development-tools", "command-line-utilities"] +[lib] +name = "nova_cli" +path = "src/lib/lib.rs" + +[[bin]] +name = "nova_cli" +path = "src/main.rs" + [dependencies] clap = { workspace = true } cliclack = { workspace = true } diff --git a/nova_cli/src/lib/child_hooks.rs b/nova_cli/src/lib/child_hooks.rs new file mode 100644 index 000000000..b827707ea --- /dev/null +++ b/nova_cli/src/lib/child_hooks.rs @@ -0,0 +1,111 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! The [`HostHooks`] implementation for macrotasks and promise jobs, i.e. +//! everything but the main thread. + +use std::{ + cell::RefCell, + collections::VecDeque, + sync::{atomic::AtomicBool, mpsc}, + thread, + time::Duration, +}; + +use nova_vm::ecmascript::execution::agent::{HostHooks, Job}; + +use crate::{ChildToHostMessage, HostToChildMessage}; + +pub struct CliChildHooks { + promise_job_queue: RefCell>, + macrotask_queue: RefCell>, + pub(crate) receiver: mpsc::Receiver, + pub(crate) host_sender: mpsc::SyncSender, + ready_to_leave: AtomicBool, +} + +// RefCell doesn't implement Debug +impl std::fmt::Debug for CliChildHooks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CliHostHooks") + //.field("promise_job_queue", &*self.promise_job_queue.borrow()) + .finish() + } +} + +impl CliChildHooks { + pub fn new( + host_sender: mpsc::SyncSender, + ) -> (Self, mpsc::SyncSender) { + let (sender, receiver) = mpsc::sync_channel(1); + ( + Self { + promise_job_queue: Default::default(), + macrotask_queue: Default::default(), + receiver, + host_sender, + ready_to_leave: Default::default(), + }, + sender, + ) + } + + pub fn is_ready_to_leave(&self) -> bool { + self.ready_to_leave + .load(std::sync::atomic::Ordering::Relaxed) + } + + pub fn mark_ready_to_leave(&self) { + self.ready_to_leave + .store(true, std::sync::atomic::Ordering::Relaxed) + } + + pub fn has_promise_jobs(&self) -> bool { + !self.promise_job_queue.borrow().is_empty() + } + + pub fn pop_promise_job(&self) -> Option { + self.promise_job_queue.borrow_mut().pop_front() + } + + pub fn has_macrotasks(&self) -> bool { + !self.macrotask_queue.borrow().is_empty() + } + + pub fn pop_macrotask(&self) -> Option { + let mut off_thread_job_queue = self.macrotask_queue.borrow_mut(); + let mut counter = 0u8; + while !off_thread_job_queue.is_empty() { + counter = counter.wrapping_add(1); + for (i, job) in off_thread_job_queue.iter().enumerate() { + if job.is_finished() { + let job = off_thread_job_queue.swap_remove(i); + return Some(job); + } + } + if counter == 0 { + thread::sleep(Duration::from_millis(5)); + } else { + core::hint::spin_loop(); + } + } + None + } +} + +impl HostHooks for CliChildHooks { + fn enqueue_generic_job(&self, job: Job) { + self.macrotask_queue.borrow_mut().push(job); + } + + fn enqueue_promise_job(&self, job: Job) { + self.promise_job_queue.borrow_mut().push_back(job); + } + + fn enqueue_timeout_job(&self, _timeout_job: Job, _milliseconds: u64) {} + + fn get_host_data(&self) -> &dyn std::any::Any { + self + } +} diff --git a/nova_cli/src/lib/fmt.rs b/nova_cli/src/lib/fmt.rs new file mode 100644 index 000000000..ec3bda8c9 --- /dev/null +++ b/nova_cli/src/lib/fmt.rs @@ -0,0 +1,62 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Formatting values and errors. + +use nova_vm::{ + ecmascript::{ + execution::{Agent, JsResult}, + types::Value, + }, + engine::context::{Bindable, GcScope}, +}; +use oxc_diagnostics::OxcDiagnostic; + +pub fn print_result(agent: &mut Agent, result: JsResult, verbose: bool, gc: GcScope) { + match result { + Ok(result) => { + if verbose { + println!("{result:?}"); + } + } + Err(error) => { + eprintln!( + "Uncaught exception: {}", + error + .value() + .unbind() + .string_repr(agent, gc) + .as_wtf8(agent) + .to_string_lossy() + ); + std::process::exit(1); + } + } +} + +/// Exit the program with parse errors. +pub fn exit_with_parse_errors(errors: Vec, source_path: &str, source: &str) -> ! { + assert!(!errors.is_empty()); + + // This seems to be needed for color and Unicode output. + miette::set_hook(Box::new(|_| { + Box::new(oxc_diagnostics::GraphicalReportHandler::new()) + })) + .unwrap(); + + eprintln!("Parse errors:"); + + // SAFETY: This function never returns, so `source`'s lifetime must last for + // the duration of the program. + let source: &'static str = unsafe { std::mem::transmute(source) }; + let named_source = miette::NamedSource::new(source_path, source); + + for error in errors { + let report = error.with_source_code(named_source.clone()); + eprint!("{report:?}"); + } + eprintln!(); + + std::process::exit(1); +} diff --git a/nova_cli/src/helper.rs b/nova_cli/src/lib/globals.rs similarity index 84% rename from nova_cli/src/helper.rs rename to nova_cli/src/lib/globals.rs index 3c305ef79..27eb83dcd 100644 --- a/nova_cli/src/helper.rs +++ b/nova_cli/src/lib/globals.rs @@ -2,14 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use std::{ - cell::RefCell, - collections::VecDeque, - ops::Deref, - sync::{LazyLock, atomic::AtomicBool, mpsc}, - thread, - time::Duration, -}; +//! Initializing the global object with builtin functions. + +use std::{ops::Deref, sync::LazyLock, thread, time::Duration}; // Record the start time of the program. // To be used for the `now` function for time measurement. @@ -23,7 +18,7 @@ use nova_vm::{ }, execution::{ Agent, JsResult, - agent::{ExceptionType, GcAgent, HostHooks, Job, Options, unwrap_try}, + agent::{ExceptionType, GcAgent, Options, unwrap_try}, }, scripts_and_modules::script::{parse_script, script_evaluation}, types::{ @@ -36,97 +31,8 @@ use nova_vm::{ rootable::Scopable, }, }; -use oxc_diagnostics::OxcDiagnostic; - -use crate::{ChildToHostMessage, CliHostHooks, HostToChildMessage}; - -struct CliChildHooks { - promise_job_queue: RefCell>, - macrotask_queue: RefCell>, - receiver: mpsc::Receiver, - host_sender: mpsc::SyncSender, - ready_to_leave: AtomicBool, -} - -// RefCell doesn't implement Debug -impl std::fmt::Debug for CliChildHooks { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CliHostHooks") - //.field("promise_job_queue", &*self.promise_job_queue.borrow()) - .finish() - } -} - -impl CliChildHooks { - fn new( - host_sender: mpsc::SyncSender, - ) -> (Self, mpsc::SyncSender) { - let (sender, receiver) = mpsc::sync_channel(1); - ( - Self { - promise_job_queue: Default::default(), - macrotask_queue: Default::default(), - receiver, - host_sender, - ready_to_leave: Default::default(), - }, - sender, - ) - } - - fn is_ready_to_leave(&self) -> bool { - self.ready_to_leave - .load(std::sync::atomic::Ordering::Relaxed) - } - - fn has_promise_jobs(&self) -> bool { - !self.promise_job_queue.borrow().is_empty() - } - - fn pop_promise_job(&self) -> Option { - self.promise_job_queue.borrow_mut().pop_front() - } - - fn has_macrotasks(&self) -> bool { - !self.macrotask_queue.borrow().is_empty() - } - - fn pop_macrotask(&self) -> Option { - let mut off_thread_job_queue = self.macrotask_queue.borrow_mut(); - let mut counter = 0u8; - while !off_thread_job_queue.is_empty() { - counter = counter.wrapping_add(1); - for (i, job) in off_thread_job_queue.iter().enumerate() { - if job.is_finished() { - let job = off_thread_job_queue.swap_remove(i); - return Some(job); - } - } - if counter == 0 { - thread::sleep(Duration::from_millis(5)); - } else { - core::hint::spin_loop(); - } - } - None - } -} - -impl HostHooks for CliChildHooks { - fn enqueue_generic_job(&self, job: Job) { - self.macrotask_queue.borrow_mut().push(job); - } - - fn enqueue_promise_job(&self, job: Job) { - self.promise_job_queue.borrow_mut().push_back(job); - } - - fn enqueue_timeout_job(&self, _timeout_job: Job, _milliseconds: u64) {} - fn get_host_data(&self) -> &dyn std::any::Any { - self - } -} +use crate::{ChildToHostMessage, CliChildHooks, CliHostHooks, HostToChildMessage}; /// Initialize the global object with the built-in functions. pub fn initialize_global_object(agent: &mut Agent, global: Object, gc: GcScope) { @@ -714,9 +620,7 @@ fn initialize_child_global_object(agent: &mut Agent, global: Object, mut gc: GcS .get_host_data() .downcast_ref::() .unwrap(); - hooks - .ready_to_leave - .store(true, std::sync::atomic::Ordering::Relaxed); + hooks.mark_ready_to_leave(); Ok(Value::Undefined) } @@ -765,29 +669,3 @@ fn initialize_child_global_object(agent: &mut Agent, global: Object, mut gc: GcS create_obj_func(agent, agent_obj, "sleep", sleep, 1, gc); create_obj_func(agent, agent_obj, "monotonicNow", monotonic_now, 0, gc); } - -/// Exit the program with parse errors. -pub fn exit_with_parse_errors(errors: Vec, source_path: &str, source: &str) -> ! { - assert!(!errors.is_empty()); - - // This seems to be needed for color and Unicode output. - miette::set_hook(Box::new(|_| { - Box::new(oxc_diagnostics::GraphicalReportHandler::new()) - })) - .unwrap(); - - eprintln!("Parse errors:"); - - // SAFETY: This function never returns, so `source`'s lifetime must last for - // the duration of the program. - let source: &'static str = unsafe { std::mem::transmute(source) }; - let named_source = miette::NamedSource::new(source_path, source); - - for error in errors { - let report = error.with_source_code(named_source.clone()); - eprint!("{report:?}"); - } - eprintln!(); - - std::process::exit(1); -} diff --git a/nova_cli/src/lib/host_hooks.rs b/nova_cli/src/lib/host_hooks.rs new file mode 100644 index 000000000..b276cd06f --- /dev/null +++ b/nova_cli/src/lib/host_hooks.rs @@ -0,0 +1,216 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! The [`HostHooks`] implementation for the main thread. + +use std::{ + cell::RefCell, collections::VecDeque, fmt::Debug, ops::Deref, path::PathBuf, rc::Rc, + sync::mpsc, thread, time::Duration, +}; + +use nova_vm::{ + ecmascript::{ + execution::{ + Agent, + agent::{ExceptionType, HostHooks, Job}, + }, + scripts_and_modules::{ + module::module_semantics::{ + ModuleRequest, Referrer, cyclic_module_records::GraphLoadingStateRecord, + finish_loading_imported_module, source_text_module_records::parse_module, + }, + script::HostDefined, + }, + types::{SharedDataBlock, String as JsString}, + }, + engine::{ + Global, + context::{Bindable, NoGcScope}, + }, +}; + +pub enum HostToChildMessage { + Broadcast(SharedDataBlock), +} + +pub enum ChildToHostMessage { + Joined, + Report(String), +} + +pub struct CliHostHooks { + promise_job_queue: RefCell>, + macrotask_queue: RefCell>, + pub(crate) receiver: mpsc::Receiver, + pub(crate) own_sender: mpsc::SyncSender, + pub(crate) child_senders: RefCell>>, +} + +// RefCell doesn't implement Debug +impl Debug for CliHostHooks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CliHostHooks") + //.field("promise_job_queue", &*self.promise_job_queue.borrow()) + .finish() + } +} + +impl Default for CliHostHooks { + fn default() -> Self { + Self::new() + } +} + +impl CliHostHooks { + pub fn new() -> Self { + let (sender, receiver) = mpsc::sync_channel(10); + Self { + promise_job_queue: Default::default(), + macrotask_queue: Default::default(), + receiver, + own_sender: sender, + child_senders: Default::default(), + } + } + + pub fn add_child(&self, child_sender: mpsc::SyncSender) { + self.child_senders.borrow_mut().push(child_sender); + } + + pub fn has_promise_jobs(&self) -> bool { + !self.promise_job_queue.borrow().is_empty() + } + + pub fn pop_promise_job(&self) -> Option { + self.promise_job_queue.borrow_mut().pop_front() + } + + pub fn has_macrotasks(&self) -> bool { + !self.macrotask_queue.borrow().is_empty() + } + + pub fn pop_macrotask(&self) -> Option { + let mut off_thread_job_queue = self.macrotask_queue.borrow_mut(); + let mut counter = 0u8; + while !off_thread_job_queue.is_empty() { + counter = counter.wrapping_add(1); + for (i, job) in off_thread_job_queue.iter().enumerate() { + if job.is_finished() { + let job = off_thread_job_queue.swap_remove(i); + return Some(job); + } + } + if counter == 0 { + thread::sleep(Duration::from_millis(5)); + } else { + core::hint::spin_loop(); + } + } + None + } +} + +impl HostHooks for CliHostHooks { + fn enqueue_generic_job(&self, job: Job) { + self.macrotask_queue.borrow_mut().push(job); + } + + fn enqueue_promise_job(&self, job: Job) { + self.promise_job_queue.borrow_mut().push_back(job); + } + + fn enqueue_timeout_job(&self, _timeout_job: Job, _milliseconds: u64) {} + + fn load_imported_module<'gc>( + &self, + agent: &mut Agent, + referrer: Referrer<'gc>, + module_request: ModuleRequest<'gc>, + _host_defined: Option, + payload: &mut GraphLoadingStateRecord<'gc>, + gc: NoGcScope<'gc, '_>, + ) { + let specifier = module_request.specifier(agent); + let specifier = specifier.to_string_lossy(agent); + let specifier_target = if let Some(specifier) = specifier.strip_prefix("./") { + let referrer_path = referrer + .host_defined(agent) + .unwrap() + .downcast::() + .unwrap(); + let parent = referrer_path + .parent() + .expect("Attempted to get sibling file of root"); + parent.join(specifier) + } else if specifier.starts_with("../") { + let referrer_path = referrer + .host_defined(agent) + .unwrap() + .downcast::() + .unwrap(); + referrer_path + .join(specifier.deref()) + .canonicalize() + .expect("Failed to canonicalize target path") + } else { + match specifier { + std::borrow::Cow::Borrowed(str) => PathBuf::from(str), + std::borrow::Cow::Owned(string) => PathBuf::from(string), + } + }; + let realm = referrer.realm(agent, gc); + let module_map = realm + .host_defined(agent) + .expect("No referrer realm [[HostDefined]] slot") + .downcast::() + .expect("No referrer realm ModuleMap"); + if let Some(module) = module_map.get(agent, &specifier_target, gc) { + finish_loading_imported_module( + agent, + referrer, + module_request, + payload, + Ok(module), + gc, + ); + return; + } + let file = match std::fs::read_to_string(&specifier_target) { + Ok(file) => file, + Err(err) => { + let result = Err(agent.throw_exception(ExceptionType::Error, err.to_string(), gc)); + finish_loading_imported_module( + agent, + referrer, + module_request, + payload, + result, + gc, + ); + return; + } + }; + let source_text = JsString::from_string(agent, file, gc); + let result = parse_module( + agent, + source_text, + referrer.realm(agent, gc), + Some(Rc::new(specifier_target.clone())), + gc, + ) + .map(|m| { + let global_m = Global::new(agent, m.unbind().into()); + module_map.add(specifier_target, global_m); + m.into() + }) + .map_err(|err| { + agent.throw_exception(ExceptionType::Error, err.first().unwrap().to_string(), gc) + }); + finish_loading_imported_module(agent, referrer, module_request, payload, result, gc); + } + + fn get_host_data(&self) -> &dyn std::any::Any { + self + } +} diff --git a/nova_cli/src/lib/lib.rs b/nova_cli/src/lib/lib.rs new file mode 100644 index 000000000..1b251e53a --- /dev/null +++ b/nova_cli/src/lib/lib.rs @@ -0,0 +1,193 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Utilities for nova cli program. +//! +//! > [!IMPORTANT] +//! > This library is currently mainly aimed at internal use and might not +//! > adhere to semver versioning. + +mod child_hooks; +mod fmt; +mod globals; +mod host_hooks; +mod module_map; + +pub use child_hooks::CliChildHooks; +pub use fmt::{exit_with_parse_errors, print_result}; +pub use host_hooks::{ChildToHostMessage, CliHostHooks, HostToChildMessage}; +pub use module_map::ModuleMap; + +use globals::{initialize_global_object, initialize_global_object_with_internals}; +use nova_vm::ecmascript::execution::agent::RealmRoot; +use nova_vm::{ + ecmascript::{ + execution::{ + Agent, JsResult, + agent::{GcAgent, Job, Options}, + }, + types::{Object, Value}, + }, + engine::context::{Bindable, GcScope, NoGcScope}, +}; +use std::rc::Rc; + +pub fn run_microtask_queue<'gc>( + agent: &mut Agent, + host_hooks: &CliHostHooks, + mut gc: GcScope<'gc, '_>, +) -> JsResult<'gc, ()> { + while let Some(job) = host_hooks.pop_promise_job() { + job.run(agent, gc.reborrow()).unbind()?.bind(gc.nogc()); + } + Ok(()) +} + +pub struct InstanceConfig { + /// Whether to enable garbage collection. Default `true`. + pub enable_gc: bool, + /// Whether to enable verbose logging. Default `false`. + pub verbose: bool, + /// Whether the main thread is allowed to block. Default `true`. + pub block: bool, + /// Whether to expose some internal functions like a function to run garbage collector. Default `false`. + pub expose_internals: bool, + /// Whether all scripts should be interpreted in strict mode. Default `false`. + pub strict: bool, +} + +impl Default for InstanceConfig { + fn default() -> Self { + Self { + enable_gc: true, + verbose: false, + block: true, + expose_internals: false, + strict: false, + } + } +} + +// SAFETY: Rust has a well-defined drop order; the fields drop in declaration order. `host_hooks` must be dropped after `realm`! +pub struct Instance { + config: InstanceConfig, + realm: InstanceRealm, + // SAFETY: drop last + host_hooks: Box, +} + +pub struct InstanceRealm { + realm: RealmRoot, + agent: GcAgent, +} + +impl InstanceRealm { + pub fn run_in(&mut self, func: F) -> R + where + F: for<'agent, 'gc, 'scope> FnOnce(&'agent mut Agent, GcScope<'gc, 'scope>) -> R, + { + self.agent.run_in_realm(&self.realm, func) + } + + pub fn initialize_module_map(&mut self, module_map: ModuleMap) { + let host_defined = Rc::new(module_map); + self.realm + .initialize_host_defined(&mut self.agent, host_defined); + } + + pub fn run_job(&mut self, job: Job, then: F) -> R + where + F: for<'agent, 'gc, 'scope> FnOnce( + &'agent mut Agent, + JsResult<'_, ()>, + GcScope<'gc, 'scope>, + ) -> R, + { + self.agent.run_job(job, then) + } + + pub fn run_gc(&mut self) { + self.agent.gc(); + } +} + +/// # Safety +/// The user is responsible to ensure that for the duration that the resulting reference exists, `reference` is valid for reads of type `T`. +#[allow(clippy::needless_lifetimes)] +unsafe fn extend_lifetime<'a, 'b, T>(reference: &'a T) -> &'b T { + unsafe { &*std::ptr::from_ref(reference) } +} + +impl Instance { + pub fn new(config: InstanceConfig) -> Self { + let host_hooks = Box::new(CliHostHooks::new()); + let mut agent = GcAgent::new( + Options { + disable_gc: !config.enable_gc, + print_internals: config.verbose, + no_block: !config.block, + }, + // SAFETY: We keep the host hooks alive for at least as long as the agent + unsafe { extend_lifetime(&*host_hooks) as &'static _ }, + ); + + let create_global_object: Option fn(&mut Agent, GcScope<'a, '_>) -> Object<'a>> = + None; + let create_global_this_value: Option< + for<'a> fn(&mut Agent, GcScope<'a, '_>) -> Object<'a>, + > = None; + let initialize_global: Option = if config.expose_internals + { + Some(initialize_global_object_with_internals) + } else { + Some(initialize_global_object) + }; + let realm = agent.create_realm( + create_global_object, + create_global_this_value, + initialize_global, + ); + + Self { + config, + host_hooks, + realm: InstanceRealm { realm, agent }, + } + } + + pub fn split_mut(&mut self) -> (&InstanceConfig, &mut CliHostHooks, &mut InstanceRealm) { + (&self.config, &mut self.host_hooks, &mut self.realm) + } + + pub fn initialize_module_map(&mut self, module_map: ModuleMap) { + self.realm.initialize_module_map(module_map) + } + + pub fn run_tasks(&mut self) { + let (_, host, realm) = self.split_mut(); + if host.has_macrotasks() { + while let Some(job) = host.pop_macrotask() { + realm.run_job(job, |agent, result, mut gc| { + let result = if result.is_ok() && { host.has_promise_jobs() } { + run_microtask_queue(agent, host, gc.reborrow()) + .unbind() + .bind(gc.nogc()) + } else { + result + }; + print_result(agent, result.map(|_| Value::Undefined).unbind(), false, gc); + }); + } + } + } +} + +pub fn get_module_map(agent: &Agent, nogc: NoGcScope) -> Rc { + agent + .current_realm(nogc) + .host_defined(agent) + .unwrap() + .downcast() + .unwrap() +} diff --git a/nova_cli/src/lib/module_map.rs b/nova_cli/src/lib/module_map.rs new file mode 100644 index 000000000..66f555e4c --- /dev/null +++ b/nova_cli/src/lib/module_map.rs @@ -0,0 +1,39 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! A datastructure for keeping track of the loaded modules. + +use std::{cell::RefCell, collections::HashMap, path::PathBuf}; + +use nova_vm::{ + ecmascript::{ + execution::Agent, + scripts_and_modules::module::module_semantics::abstract_module_records::AbstractModule, + }, + engine::{Global, context::NoGcScope}, +}; + +#[derive(Default)] +pub struct ModuleMap { + map: RefCell>>>, +} + +impl ModuleMap { + pub fn new() -> Self { + Default::default() + } + + pub fn add(&self, path: PathBuf, module: Global>) { + self.map.borrow_mut().insert(path, module); + } + + pub fn get<'a>( + &self, + agent: &Agent, + path: &PathBuf, + gc: NoGcScope<'a, '_>, + ) -> Option> { + self.map.borrow().get(path).map(|g| g.get(agent, gc)) + } +} diff --git a/nova_cli/src/main.rs b/nova_cli/src/main.rs index e05d28c51..139999287 100644 --- a/nova_cli/src/main.rs +++ b/nova_cli/src/main.rs @@ -1,48 +1,22 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -mod helper; mod theme; -use std::{ - cell::RefCell, - collections::{HashMap, VecDeque}, - fmt::Debug, - ops::Deref, - path::PathBuf, - ptr::NonNull, - rc::Rc, - sync::mpsc, - thread, - time::Duration, -}; +use std::{fmt::Debug, rc::Rc}; use clap::{Parser as ClapParser, Subcommand}; use cliclack::{input, intro, set_theme}; -use helper::{ - exit_with_parse_errors, initialize_global_object, initialize_global_object_with_internals, -}; +use nova_cli::{self as lib, Instance, InstanceConfig, ModuleMap}; use nova_vm::{ ecmascript::{ - execution::{ - Agent, JsResult, - agent::{ExceptionType, GcAgent, HostHooks, Job, Options}, - }, scripts_and_modules::{ - module::module_semantics::{ - ModuleRequest, Referrer, abstract_module_records::AbstractModule, - cyclic_module_records::GraphLoadingStateRecord, finish_loading_imported_module, - source_text_module_records::parse_module, - }, - script::{HostDefined, parse_script, script_evaluation}, + module::module_semantics::source_text_module_records::parse_module, + script::{parse_script, script_evaluation}, }, - types::{Object, SharedDataBlock, String as JsString, Value}, - }, - engine::{ - Global, - context::{Bindable, GcScope, NoGcScope}, - rootable::Scopable, + types::String as JsString, }, + engine::{Global, context::Bindable, rootable::Scopable}, register_probes, }; use oxc_parser::Parser; @@ -111,210 +85,6 @@ enum Command { }, } -enum HostToChildMessage { - Broadcast(SharedDataBlock), -} - -enum ChildToHostMessage { - Joined, - Report(String), -} - -struct CliHostHooks { - promise_job_queue: RefCell>, - macrotask_queue: RefCell>, - receiver: mpsc::Receiver, - own_sender: mpsc::SyncSender, - child_senders: RefCell>>, -} - -// RefCell doesn't implement Debug -impl Debug for CliHostHooks { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("CliHostHooks") - //.field("promise_job_queue", &*self.promise_job_queue.borrow()) - .finish() - } -} - -impl CliHostHooks { - fn new() -> Self { - let (sender, receiver) = mpsc::sync_channel(10); - Self { - promise_job_queue: Default::default(), - macrotask_queue: Default::default(), - receiver, - own_sender: sender, - child_senders: Default::default(), - } - } - - fn add_child(&self, child_sender: mpsc::SyncSender) { - self.child_senders.borrow_mut().push(child_sender); - } - - fn has_promise_jobs(&self) -> bool { - !self.promise_job_queue.borrow().is_empty() - } - - fn pop_promise_job(&self) -> Option { - self.promise_job_queue.borrow_mut().pop_front() - } - - fn has_macrotasks(&self) -> bool { - !self.macrotask_queue.borrow().is_empty() - } - - fn pop_macrotask(&self) -> Option { - let mut off_thread_job_queue = self.macrotask_queue.borrow_mut(); - let mut counter = 0u8; - while !off_thread_job_queue.is_empty() { - counter = counter.wrapping_add(1); - for (i, job) in off_thread_job_queue.iter().enumerate() { - if job.is_finished() { - let job = off_thread_job_queue.swap_remove(i); - return Some(job); - } - } - if counter == 0 { - thread::sleep(Duration::from_millis(5)); - } else { - core::hint::spin_loop(); - } - } - None - } -} - -impl HostHooks for CliHostHooks { - fn enqueue_generic_job(&self, job: Job) { - self.macrotask_queue.borrow_mut().push(job); - } - - fn enqueue_promise_job(&self, job: Job) { - self.promise_job_queue.borrow_mut().push_back(job); - } - - fn enqueue_timeout_job(&self, _timeout_job: Job, _milliseconds: u64) {} - - fn load_imported_module<'gc>( - &self, - agent: &mut Agent, - referrer: Referrer<'gc>, - module_request: ModuleRequest<'gc>, - _host_defined: Option, - payload: &mut GraphLoadingStateRecord<'gc>, - gc: NoGcScope<'gc, '_>, - ) { - let specifier = module_request.specifier(agent); - let specifier = specifier.to_string_lossy(agent); - let specifier_target = if let Some(specifier) = specifier.strip_prefix("./") { - let referrer_path = referrer - .host_defined(agent) - .unwrap() - .downcast::() - .unwrap(); - let parent = referrer_path - .parent() - .expect("Attempted to get sibling file of root"); - parent.join(specifier) - } else if specifier.starts_with("../") { - let referrer_path = referrer - .host_defined(agent) - .unwrap() - .downcast::() - .unwrap(); - referrer_path - .join(specifier.deref()) - .canonicalize() - .expect("Failed to canonicalize target path") - } else { - match specifier { - std::borrow::Cow::Borrowed(str) => PathBuf::from(str), - std::borrow::Cow::Owned(string) => PathBuf::from(string), - } - }; - let realm = referrer.realm(agent, gc); - let module_map = realm - .host_defined(agent) - .expect("No referrer realm [[HostDefined]] slot") - .downcast::() - .expect("No referrer realm ModuleMap"); - if let Some(module) = module_map.get(agent, &specifier_target, gc) { - finish_loading_imported_module( - agent, - referrer, - module_request, - payload, - Ok(module), - gc, - ); - return; - } - let file = match std::fs::read_to_string(&specifier_target) { - Ok(file) => file, - Err(err) => { - let result = Err(agent.throw_exception(ExceptionType::Error, err.to_string(), gc)); - finish_loading_imported_module( - agent, - referrer, - module_request, - payload, - result, - gc, - ); - return; - } - }; - let source_text = JsString::from_string(agent, file, gc); - let result = parse_module( - agent, - source_text, - referrer.realm(agent, gc), - Some(Rc::new(specifier_target.clone())), - gc, - ) - .map(|m| { - let global_m = Global::new(agent, m.unbind().into()); - module_map.add(specifier_target, global_m); - m.into() - }) - .map_err(|err| { - agent.throw_exception(ExceptionType::Error, err.first().unwrap().to_string(), gc) - }); - finish_loading_imported_module(agent, referrer, module_request, payload, result, gc); - } - - fn get_host_data(&self) -> &dyn std::any::Any { - self - } -} - -struct ModuleMap { - map: RefCell>>>, -} - -impl ModuleMap { - fn new() -> Self { - Self { - map: Default::default(), - } - } - - fn add(&self, path: PathBuf, module: Global>) { - self.map.borrow_mut().insert(path, module); - } - - fn get<'a>( - &self, - agent: &Agent, - path: &PathBuf, - gc: NoGcScope<'a, '_>, - ) -> Option> { - self.map.borrow().get(path).map(|g| g.get(agent, gc)) - } -} - fn main() -> Result<(), Box> { let args = Cli::parse(); @@ -329,7 +99,7 @@ fn main() -> Result<(), Box> { let result = parser.parse(); if !result.errors.is_empty() { - exit_with_parse_errors(result.errors, &path, &file); + lib::exit_with_parse_errors(result.errors, &path, &file); } let SemanticBuilderReturn { errors, .. } = SemanticBuilder::new() @@ -337,7 +107,7 @@ fn main() -> Result<(), Box> { .build(&result.program); if !errors.is_empty() { - exit_with_parse_errors(result.errors, &path, &file); + lib::exit_with_parse_errors(result.errors, &path, &file); } println!("{:?}", result.program); @@ -351,209 +121,106 @@ fn main() -> Result<(), Box> { expose_internals, paths, } => { - fn run_microtask_queue<'gc>( - agent: &mut Agent, - host_hooks: &CliHostHooks, - mut gc: GcScope<'gc, '_>, - ) -> JsResult<'gc, ()> { - while let Some(job) = host_hooks.pop_promise_job() { - job.run(agent, gc.reborrow()).unbind()?.bind(gc.nogc()); - } - Ok(()) - } - - fn print_result( - agent: &mut Agent, - result: JsResult, - verbose: bool, - gc: GcScope, - ) { - match result { - Ok(result) => { - if verbose { - println!("{result:?}"); - } - } - Err(error) => { - eprintln!( - "Uncaught exception: {}", - error - .value() - .unbind() - .string_repr(agent, gc) - .as_wtf8(agent) - .to_string_lossy() - ); - std::process::exit(1); - } - } - } + let config = InstanceConfig { + block: !no_block, + enable_gc: !nogc, + verbose, + expose_internals, + strict: !no_strict, + }; + let mut instance = Instance::new(config); - let host_hooks: NonNull = - NonNull::from(Box::leak(Box::new(CliHostHooks::new()))); - let mut agent = GcAgent::new( - Options { - disable_gc: nogc, - print_internals: verbose, - no_block, - }, - // SAFETY: Host hooks is a valid pointer. - unsafe { host_hooks.as_ref() }, - ); assert!(!paths.is_empty()); - let create_global_object: Option< - for<'a> fn(&mut Agent, GcScope<'a, '_>) -> Object<'a>, - > = None; - let create_global_this_value: Option< - for<'a> fn(&mut Agent, GcScope<'a, '_>) -> Object<'a>, - > = None; - let initialize_global: Option = if expose_internals { - Some(initialize_global_object_with_internals) - } else { - Some(initialize_global_object) - }; - let realm = agent.create_realm( - create_global_object, - create_global_this_value, - initialize_global, - ); - let module_map = Rc::new(ModuleMap::new()); - realm.initialize_host_defined(&mut agent, module_map.clone()); + let module_map = ModuleMap::new(); + instance.initialize_module_map(module_map); + let (config, host_hooks, realm) = instance.split_mut(); let last_index = paths.len() - 1; for (index, path) in paths.into_iter().enumerate() { - // SAFETY: Still valid. - let host_hooks = unsafe { host_hooks.as_ref() }; - agent.run_in_realm( - &realm, - |agent, mut gc| -> Result<(), Box> { - let absolute_path = std::fs::canonicalize(&path)?; - let file = std::fs::read_to_string(&absolute_path)?; - let source_text = JsString::from_string(agent, file, gc.nogc()); - let realm = agent.current_realm(gc.nogc()); - let result = if module && last_index == index { - let module = match parse_module( - agent, - source_text.unbind(), - realm, - Some(Rc::new(absolute_path.clone())), - gc.nogc(), - ) { - Ok(module) => module, - Err(errors) => { - // Borrow the string data from the Agent - let source_text = source_text.to_string_lossy(agent); - exit_with_parse_errors(errors, &path, &source_text) - } - }; - module_map - .add(absolute_path, Global::new(agent, module.unbind().into())); - agent - .run_parsed_module( - module.unbind(), - Some(module_map.clone()), - gc.reborrow(), - ) - .unbind() - .bind(gc.nogc()) - } else { - let script = match parse_script( - agent, - source_text, - realm, - !no_strict, - Some(Rc::new(absolute_path.clone())), - gc.nogc(), - ) { - Ok(script) => script, - Err(errors) => { - // Borrow the string data from the Agent - let source_text = source_text.to_string_lossy(agent); - exit_with_parse_errors(errors, &path, &source_text) - } - }; - script_evaluation(agent, script.unbind(), gc.reborrow()) - .unbind() - .bind(gc.nogc()) + realm.run_in(|agent, mut gc| -> Result<(), Box> { + let absolute_path = std::fs::canonicalize(&path)?; + let file = std::fs::read_to_string(&absolute_path)?; + let source_text = JsString::from_string(agent, file, gc.nogc()); + let realm = agent.current_realm(gc.nogc()); + let result = if module && last_index == index { + let module = match parse_module( + agent, + source_text.unbind(), + realm, + Some(Rc::new(absolute_path.clone())), + gc.nogc(), + ) { + Ok(module) => module, + Err(errors) => { + // Borrow the string data from the Agent + let source_text = source_text.to_string_lossy(agent); + lib::exit_with_parse_errors(errors, &path, &source_text) + } }; - - let result = if let Ok(result) = result - && host_hooks.has_promise_jobs() - { - let result = result.scope(agent, gc.nogc()); - let microtask_result = - run_microtask_queue(agent, host_hooks, gc.reborrow()) - .unbind() - .bind(gc.nogc()); - // SAFETY: not shared. - microtask_result.map(|_| unsafe { result.take(agent) }.bind(gc.nogc())) - } else { - result + let module_map: Rc = lib::get_module_map(agent, gc.nogc()); + module_map.add(absolute_path, Global::new(agent, module.unbind().into())); + agent + .run_parsed_module( + module.unbind(), + Some(module_map.clone()), + gc.reborrow(), + ) + .unbind() + .bind(gc.nogc()) + } else { + let script = match parse_script( + agent, + source_text, + realm, + config.strict, + Some(Rc::new(absolute_path.clone())), + gc.nogc(), + ) { + Ok(script) => script, + Err(errors) => { + // Borrow the string data from the Agent + let source_text = source_text.to_string_lossy(agent); + lib::exit_with_parse_errors(errors, &path, &source_text) + } }; - - print_result(agent, result.unbind(), verbose, gc); - Ok(()) - }, - )?; - } - { - // SAFETY: Still valid. - let host_hooks = unsafe { host_hooks.as_ref() }; - if host_hooks.has_macrotasks() { - while let Some(job) = host_hooks.pop_macrotask() { - agent.run_job(job, |agent, result, mut gc| { - let result = if result.is_ok() && host_hooks.has_promise_jobs() { - run_microtask_queue(agent, host_hooks, gc.reborrow()) - .unbind() - .bind(gc.nogc()) - } else { - result - }; - print_result( - agent, - result.map(|_| Value::Undefined).unbind(), - false, - gc, - ); - }); - } - } + script_evaluation(agent, script.unbind(), gc.reborrow()) + .unbind() + .bind(gc.nogc()) + }; + + let result = if let Ok(result) = result + && host_hooks.has_promise_jobs() + { + let result = result.scope(agent, gc.nogc()); + let microtask_result = + lib::run_microtask_queue(agent, host_hooks, gc.reborrow()) + .unbind() + .bind(gc.nogc()); + // SAFETY: not shared. + microtask_result.map(|_| unsafe { result.take(agent) }.bind(gc.nogc())) + } else { + result + }; + + lib::print_result(agent, result.unbind(), verbose, gc); + Ok(()) + })?; } - agent.remove_realm(realm); - drop(agent); - // SAFETY: Host hooks are no longer used as agent is dropped. - drop(unsafe { Box::from_raw(host_hooks.as_ptr()) }); + instance.run_tasks(); } Command::Repl { expose_internals, print_internals, disable_gc, } => { - let host_hooks: &CliHostHooks = &*Box::leak(Box::new(CliHostHooks::new())); - let mut agent = GcAgent::new( - Options { - disable_gc, - print_internals, - // Never allow blocking in the REPL. - no_block: true, - }, - host_hooks, - ); - let create_global_object: Option< - for<'a> fn(&mut Agent, GcScope<'a, '_>) -> Object<'a>, - > = None; - let create_global_this_value: Option< - for<'a> fn(&mut Agent, GcScope<'a, '_>) -> Object<'a>, - > = None; - let initialize_global: Option = if expose_internals { - Some(initialize_global_object_with_internals) - } else { - Some(initialize_global_object) + let config = InstanceConfig { + enable_gc: !disable_gc, + verbose: print_internals, + expose_internals, + // Never allow blocking in the REPL. + block: false, + ..Default::default() }; - let realm = agent.create_realm( - create_global_object, - create_global_this_value, - initialize_global, - ); + let mut instance = Instance::new(config); set_theme(DefaultTheme); println!("\n"); @@ -563,6 +230,8 @@ fn main() -> Result<(), Box> { let _ = ctrlc::set_handler(|| { std::process::exit(0); }); + + let (_config, _host_hooks, realm) = instance.split_mut(); loop { intro("Nova Repl")?; let input: String = input("").placeholder(&placeholder).interact()?; @@ -570,18 +239,18 @@ fn main() -> Result<(), Box> { if input.matches("exit").count() == 1 { std::process::exit(0); } else if input.matches("gc").count() == 1 { - agent.gc(); + realm.run_gc(); continue; } placeholder = input.to_string(); - agent.run_in_realm(&realm, |agent, mut gc| { + realm.run_in(|agent, mut gc| { let realm = agent.current_realm(gc.nogc()); let source_text = JsString::from_string(agent, input, gc.nogc()); let script = match parse_script(agent, source_text, realm, true, None, gc.nogc()) { Ok(script) => script, Err(errors) => { - exit_with_parse_errors(errors, "", &placeholder); + lib::exit_with_parse_errors(errors, "", &placeholder); } }; let result = script_evaluation(agent, script.unbind(), gc.reborrow());