#![doc = include_str!("../ABOUT.md")]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg",
html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg"
)]
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#![allow(clippy::print_stdout, clippy::print_stderr)]
mod debug;
mod executor;
mod helper;
mod logger;
use crate::executor::Executor;
use crate::logger::SharedExternalPrinterLogger;
use async_channel::Sender;
use boa_engine::JsValue;
use boa_engine::error::JsErasedError;
use boa_engine::job::NativeAsyncJob;
use boa_engine::{
Context, JsError, Source,
builtins::promise::PromiseState,
context::ContextBuilder,
module::{Module, SimpleModuleLoader},
optimizer::OptimizerOptions,
script::Script,
vm::flowgraph::{Direction, Graph},
};
use boa_parser::source::ReadChar;
use clap::{Parser, ValueEnum, ValueHint};
use color_eyre::{
Result, Section,
eyre::{WrapErr, eyre},
};
use colored::Colorize;
use debug::init_boa_debug_object;
use rustyline::{EditMode, Editor, config::Config, error::ReadlineError};
use std::time::{Duration, Instant};
use std::{
fs::OpenOptions,
io::{self, IsTerminal, Read, Write},
path::{Path, PathBuf},
rc::Rc,
thread,
};
#[cfg(all(
target_arch = "x86_64",
target_os = "linux",
target_env = "gnu",
feature = "dhat"
))]
use jemallocator as _;
#[cfg(all(
target_arch = "x86_64",
target_os = "linux",
target_env = "gnu",
feature = "fast-allocator",
not(feature = "dhat")
))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
#[cfg(all(any(target_os = "windows", target_os = "macos"), feature = "dhat"))]
use mimalloc_safe as _;
#[cfg(all(
any(target_os = "windows", target_os = "macos"),
feature = "fast-allocator",
not(feature = "dhat")
))]
#[global_allocator]
static ALLOC: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc;
#[cfg(feature = "dhat")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
static CLI_HISTORY: &str = ".boa_history";
#[derive(Debug, Parser)]
#[command(author, version, about, name = "boa")]
#[allow(clippy::struct_excessive_bools)] struct Opt {
#[arg(name = "FILE", value_hint = ValueHint::FilePath)]
files: Vec<PathBuf>,
#[arg(long)]
strict: bool,
#[arg(
long,
short = 'a',
value_name = "FORMAT",
ignore_case = true,
value_enum,
conflicts_with = "graph"
)]
#[allow(clippy::option_option)]
dump_ast: Option<Option<DumpFormat>>,
#[arg(long, short, conflicts_with = "graph")]
trace: bool,
#[arg(long = "vi")]
vi_mode: bool,
#[arg(long)]
time: bool,
#[arg(long, short = 'O', group = "optimizer")]
optimize: bool,
#[arg(long, requires = "optimizer")]
optimizer_statistics: bool,
#[arg(
long,
value_name = "FORMAT",
ignore_case = true,
value_enum,
group = "graph"
)]
#[allow(clippy::option_option)]
flowgraph: Option<Option<FlowgraphFormat>>,
#[arg(
long,
value_name = "FORMAT",
ignore_case = true,
value_enum,
requires = "graph"
)]
flowgraph_direction: Option<FlowgraphDirection>,
#[arg(long)]
debug_object: bool,
#[arg(long)]
test262_object: bool,
#[arg(long)]
no_can_block: bool,
#[arg(long, short = 'm', group = "mod")]
module: bool,
#[arg(long, short = 'r', default_value_os_t = PathBuf::from("."), requires = "mod")]
root: PathBuf,
#[arg(long, short = 'e')]
expression: Option<String>,
#[arg(long, short = 'q')]
quiet: bool,
}
impl Opt {
const fn has_dump_flag(&self) -> bool {
self.dump_ast.is_some()
}
}
#[derive(Debug, Copy, Clone, Default, ValueEnum)]
enum DumpFormat {
#[default]
Debug,
Json,
JsonPretty,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum FlowgraphFormat {
Graphviz,
Mermaid,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
enum FlowgraphDirection {
TopToBottom,
BottomToTop,
LeftToRight,
RightToLeft,
}
struct Timer<'a> {
name: &'static str,
start: Instant,
counters: &'a mut Vec<(&'static str, Duration)>,
}
impl Drop for Timer<'_> {
fn drop(&mut self) {
self.counters.push((self.name, self.start.elapsed()));
}
}
struct Counters {
counters: Option<Vec<(&'static str, Duration)>>,
}
impl Counters {
fn new(enabled: bool) -> Self {
Self {
counters: enabled.then_some(Vec::new()),
}
}
fn new_timer(&mut self, name: &'static str) -> Option<Timer<'_>> {
self.counters.as_mut().map(|counters| Timer {
name,
start: Instant::now(),
counters,
})
}
}
impl Drop for Counters {
fn drop(&mut self) {
let Some(counters) = self.counters.take() else {
return;
};
if counters.is_empty() {
return;
}
let max_width = counters
.iter()
.map(|(name, _)| name.len())
.max()
.unwrap_or(0)
.max("Total".len())
+ 1;
let mut total = Duration::ZERO;
eprintln!();
for (name, elapsed) in &counters {
eprintln!(
"{:<width$} {elapsed:.2?}",
format!("{name}:"),
width = max_width
);
total += *elapsed;
}
if counters.len() > 1 {
eprintln!("{:<width$} {total:.2?}", "Total:", width = max_width);
}
}
}
fn dump<R: ReadChar>(
src: Source<'_, R>,
args: &Opt,
is_module: bool,
context: &mut Context,
) -> Result<()> {
if let Some(arg) = args.dump_ast {
let mut counters = Counters::new(args.time);
let arg = arg.unwrap_or_default();
let mut parser = boa_parser::Parser::new(src);
let dump =
if is_module {
let scope = context.realm().scope().clone();
let module = {
let _timer = counters.new_timer("Parsing");
parser
.parse_module(&scope, context.interner_mut())
.map_err(|e| eyre!("Uncaught SyntaxError: {e}"))?
};
let _timer = counters.new_timer("AST generation");
match arg {
DumpFormat::Json => serde_json::to_string(&module)
.expect("could not convert AST to a JSON string"),
DumpFormat::JsonPretty => serde_json::to_string_pretty(&module)
.expect("could not convert AST to a pretty JSON string"),
DumpFormat::Debug => format!("{module:#?}"),
}
} else {
let scope = context.realm().scope().clone();
let mut script = {
let _timer = counters.new_timer("Parsing");
parser
.parse_script(&scope, context.interner_mut())
.map_err(|e| eyre!("Uncaught SyntaxError: {e}"))?
};
if args.optimize {
context.optimize_statement_list(script.statements_mut());
}
let _timer = counters.new_timer("AST generation");
match arg {
DumpFormat::Json => serde_json::to_string(&script)
.expect("could not convert AST to a JSON string"),
DumpFormat::JsonPretty => serde_json::to_string_pretty(&script)
.expect("could not convert AST to a pretty JSON string"),
DumpFormat::Debug => format!("{script:#?}"),
}
};
drop(counters);
println!("{dump}");
}
Ok(())
}
fn generate_flowgraph<R: ReadChar>(
context: &mut Context,
src: Source<'_, R>,
format: FlowgraphFormat,
direction: Option<FlowgraphDirection>,
) -> Result<String> {
let script = Script::parse(src, None, context).map_err(|e| e.into_erased(context))?;
let code = script
.codeblock(context)
.map_err(|e| e.into_erased(context))?;
let direction = match direction {
Some(FlowgraphDirection::TopToBottom) | None => Direction::TopToBottom,
Some(FlowgraphDirection::BottomToTop) => Direction::BottomToTop,
Some(FlowgraphDirection::LeftToRight) => Direction::LeftToRight,
Some(FlowgraphDirection::RightToLeft) => Direction::RightToLeft,
};
let mut graph = Graph::new(direction);
code.to_graph(graph.subgraph(String::default()));
let result = match format {
FlowgraphFormat::Graphviz => graph.to_graphviz_format(),
FlowgraphFormat::Mermaid => graph.to_mermaid_format(),
};
Ok(result)
}
#[must_use]
fn uncaught_error(error: &JsError) -> String {
format!(
"{} {}\n",
"Uncaught Error:".red().bold(),
error.to_string().red()
)
}
#[must_use]
fn uncaught_job_error(error: &JsError) -> String {
format!(
"{} {}\n",
"Uncaught Error (during job evaluation):".red().bold(),
error.to_string().red()
)
}
fn evaluate_expr(
line: &str,
args: &Opt,
context: &mut Context,
printer: &SharedExternalPrinterLogger,
) -> Result<()> {
if args.has_dump_flag() {
dump(Source::from_bytes(line), args, args.module, context)?;
} else if let Some(flowgraph) = args.flowgraph {
match generate_flowgraph(
context,
Source::from_bytes(line),
flowgraph.unwrap_or(FlowgraphFormat::Graphviz),
args.flowgraph_direction,
) {
Ok(v) => println!("{v}"),
Err(v) => eprintln!("{v:?}"),
}
} else {
let mut counters = Counters::new(args.time);
let script = {
let _timer = counters.new_timer("Parsing");
Script::parse(Source::from_bytes(line), None, context)
};
match script {
Ok(script) => {
let result = {
let _timer = counters.new_timer("Execution");
let result = script.evaluate(context);
if let Err(err) = context.run_jobs() {
printer.print(uncaught_job_error(&err));
return Err(eyre!("execution failed"));
}
result
};
match result {
Ok(v) => printer.print(format!("{}\n", v.display())),
Err(v) => {
printer.print(uncaught_error(&v));
return Err(eyre!("execution failed"));
}
}
}
Err(v) => {
printer.print(uncaught_error(&v));
return Err(eyre!("parsing failed"));
}
}
}
Ok(())
}
fn evaluate_file(
file: &Path,
args: &Opt,
context: &mut Context,
loader: &SimpleModuleLoader,
printer: &SharedExternalPrinterLogger,
) -> Result<()> {
let is_module = args.module || file.extension().is_some_and(|ext| ext == "mjs");
if args.has_dump_flag() {
return dump(Source::from_filepath(file)?, args, is_module, context);
}
if let Some(flowgraph) = args.flowgraph {
let flowgraph = generate_flowgraph(
context,
Source::from_filepath(file)?,
flowgraph.unwrap_or(FlowgraphFormat::Graphviz),
args.flowgraph_direction,
)?;
println!("{flowgraph}");
return Ok(());
}
if is_module {
let source = Source::from_filepath(file)?;
let mut counters = Counters::new(args.time);
let module = {
let _timer = counters.new_timer("Parsing");
Module::parse(source, None, context)
};
let module = module.map_err(|e| e.into_erased(context))?;
loader.insert(
file.canonicalize()
.wrap_err("could not canonicalize input file path")?,
module.clone(),
);
let promise = {
let _timer = counters.new_timer("Execution");
let promise = module.load_link_evaluate(context);
context.run_jobs().map_err(|err| err.into_erased(context))?;
Ok::<_, JsErasedError>(promise)
}?;
let result = promise.state();
return match result {
PromiseState::Pending => Err(eyre!("module didn't execute")),
PromiseState::Fulfilled(_) => Ok(()),
PromiseState::Rejected(err) => {
Err(JsError::from_opaque(err).into_erased(context).into())
}
};
}
let source = Source::from_filepath(file)?;
let mut counters = Counters::new(args.time);
let script = {
let _timer = counters.new_timer("Parsing");
Script::parse(source, None, context)
};
let script = script.map_err(|e| e.into_erased(context))?;
let result = {
let _timer = counters.new_timer("Execution");
let result = script.evaluate(context);
context.run_jobs().map_err(|err| err.into_erased(context))?;
result
};
match result {
Ok(v) => {
if !v.is_undefined() {
println!("{}", v.display());
}
}
Err(v) => {
printer.print(uncaught_error(&v));
return Err(eyre!("execution failed"));
}
}
Ok(())
}
fn evaluate_files(
args: &Opt,
context: &mut Context,
loader: &SimpleModuleLoader,
printer: &SharedExternalPrinterLogger,
) -> Result<()> {
for file in &args.files {
evaluate_file(file, args, context, loader, printer)?;
}
Ok(())
}
#[expect(clippy::too_many_lines)]
fn main() -> Result<()> {
color_eyre::config::HookBuilder::default()
.display_location_section(false)
.display_env_section(false)
.install()?;
rustls::crypto::ring::default_provider()
.install_default()
.map_err(|_| eyre!("could not install ring as the default crypto provider"))?;
#[cfg(feature = "dhat")]
let _profiler = dhat::Profiler::new_heap();
let args = Opt::parse();
let (sender, receiver) = async_channel::unbounded();
let printer = SharedExternalPrinterLogger::new();
let executor = Rc::new(Executor::new(printer.clone()));
let loader = Rc::new(SimpleModuleLoader::new(&args.root).map_err(|e| eyre!(e.to_string()))?);
let context = &mut ContextBuilder::new()
.job_executor(executor.clone())
.module_loader(loader.clone())
.can_block(!args.no_can_block)
.build()
.map_err(|e| eyre!(e.to_string()))?;
context.strict(args.strict);
add_runtime(printer.clone(), context);
context.set_trace(args.trace);
if args.debug_object {
init_boa_debug_object(context);
}
if args.test262_object {
boa_runtime::test262::register_js262(
boa_runtime::test262::WorkerHandles::new(),
true, context,
);
context
.eval(Source::from_bytes("var print = console.log.bind(console);"))
.expect("failed to define print");
}
let mut optimizer_options = OptimizerOptions::empty();
optimizer_options.set(OptimizerOptions::STATISTICS, args.optimizer_statistics);
optimizer_options.set(OptimizerOptions::OPTIMIZE_ALL, args.optimize);
context.set_optimizer_options(optimizer_options);
if !args.files.is_empty() {
evaluate_files(&args, context, &loader, &printer)?;
if let Some(ref expr) = args.expression {
evaluate_expr(expr, &args, context, &printer)?;
}
return Ok(());
} else if let Some(ref expr) = args.expression {
evaluate_expr(expr, &args, context, &printer)?;
return Ok(());
} else if !io::stdin().is_terminal() {
let mut input = String::new();
io::stdin()
.read_to_string(&mut input)
.wrap_err("failed to read stdin")?;
return if input.is_empty() {
Ok(())
} else {
evaluate_expr(&input, &args, context, &printer)
};
}
if !args.quiet {
let version = env!("CARGO_PKG_VERSION");
println!("{}", format!("Welcome to Boa v{version}").bold());
println!(
"Type {} for more information, {} to exit.",
"\".help\"".green(),
"Ctrl+D".green()
);
println!();
}
let handle = start_readline_thread(sender, printer.clone(), args.vi_mode, args.strict);
let exec = executor.clone();
let eval_loop = NativeAsyncJob::new(async move |context| {
while let Ok(line) = receiver.recv().await {
let printer_clone = printer.clone();
if let Some(file_path) = line.strip_prefix("__BOA_LOAD_FILE__:") {
let path = Path::new(file_path);
if path.exists() {
let mut context = context.borrow_mut();
if let Err(e) =
evaluate_file(path, &args, &mut context, &loader, &printer_clone)
{
printer_clone.print(format!("{e}\n"));
}
} else {
printer_clone.print(format!(
"{} file '{}' not found\n",
"Error:".red().bold(),
file_path
));
}
continue;
}
let eval_script = NativeAsyncJob::new(async move |context| {
let script =
match Script::parse(Source::from_bytes(&line), None, &mut context.borrow_mut())
{
Ok(script) => script,
Err(err) => {
printer_clone.print(uncaught_error(&err));
return Ok(JsValue::undefined());
}
};
let value = match script.evaluate(&mut context.borrow_mut()) {
Ok(value) => value,
Err(err) => {
printer_clone.print(uncaught_job_error(&err));
return Ok(JsValue::undefined());
}
};
printer_clone.print(format!("{}\n", value.display()));
Ok(JsValue::undefined())
});
context.borrow_mut().enqueue_job(eval_script.into());
}
exec.stop();
Ok(JsValue::undefined())
});
context.enqueue_job(eval_loop.into());
let result = context.run_jobs().map_err(|e| e.into_erased(context));
handle.join().expect("failed to join thread");
Ok(result?)
}
fn readline_thread_main(
sender: &Sender<String>,
printer_out: &SharedExternalPrinterLogger,
vi_mode: bool,
strict: bool,
) -> Result<()> {
let config = Config::builder()
.keyseq_timeout(Some(1))
.edit_mode(if vi_mode {
EditMode::Vi
} else {
EditMode::Emacs
})
.build();
let mut editor =
Editor::with_config(config).wrap_err("failed to set the editor configuration")?;
if let Ok(printer) = editor.create_external_printer() {
printer_out.set(printer);
}
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(CLI_HISTORY)?;
editor
.load_history(CLI_HISTORY)
.wrap_err("failed to read history file `.boa_history`")?;
let readline = ">> ";
editor.set_helper(Some(helper::RLHelper::new(readline, strict)));
loop {
match editor.readline(readline).map(|l| l.trim().to_string()) {
Ok(line) if line == ".exit" => break,
Err(ReadlineError::Eof) => break,
Err(ReadlineError::Interrupted) => {
println!("(To exit, press Ctrl+D or type .exit)");
}
Ok(ref line) if line == ".help" => {
println!("REPL Commands:");
println!(" {} Show this help message", ".help".green());
println!(" {} Exit the REPL", ".exit".green());
println!(" {} Clear the terminal screen", ".clear".green());
println!(
" {} Load and evaluate a JavaScript file",
".load <file>".green()
);
println!();
println!("Press {} to abort the current expression.", "Ctrl+C".bold());
println!("Press {} to exit the REPL.", "Ctrl+D".bold());
}
Ok(ref line) if line == ".clear" => {
print!("\x1B[2J\x1B[3J\x1B[1;1H");
io::stdout().flush().ok();
}
Ok(ref line) if line == ".load" || line.starts_with(".load ") => {
let file = line.strip_prefix(".load").unwrap_or("").trim();
if file.is_empty() {
eprintln!("{}", "Usage: .load <filename>".yellow());
} else {
sender.send_blocking(format!("__BOA_LOAD_FILE__:{file}"))?;
thread::sleep(Duration::from_millis(10));
}
}
Ok(line) => {
editor.add_history_entry(&line).map_err(io::Error::other)?;
sender.send_blocking(line)?;
thread::sleep(Duration::from_millis(10));
}
Err(err) => {
let final_error = eyre!("could not read the next line of the input");
let final_error = if let Err(e) = editor.save_history(CLI_HISTORY) {
final_error.error(e)
} else {
final_error
};
return Err(final_error.error(err));
}
}
}
editor.save_history(CLI_HISTORY)?;
Ok(())
}
fn start_readline_thread(
sender: Sender<String>,
printer_out: SharedExternalPrinterLogger,
vi_mode: bool,
strict: bool,
) -> thread::JoinHandle<()> {
thread::spawn(
move || match readline_thread_main(&sender, &printer_out, vi_mode, strict) {
Ok(()) => {}
Err(e) => eprintln!("readline thread failed: {e}"),
},
)
}
fn add_runtime(printer: SharedExternalPrinterLogger, context: &mut Context) {
boa_runtime::register(
(
boa_runtime::extensions::ConsoleExtension(printer),
#[cfg(feature = "fetch")]
boa_runtime::extensions::FetchExtension(
boa_runtime::fetch::BlockingReqwestFetcher::default(),
),
),
None,
context,
)
.expect("should not fail while registering the runtime");
}