diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index 627cdea1..59011273 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -1,3 +1,116 @@ -fn main() { - println!("OxideJS CLI - not yet implemented"); +use std::fs; +use std::process::ExitCode; + +use ansi_term::Colour::Red; +use clap::{Parser, Subcommand}; +use oxide_parser::Allocator; + +#[derive(Parser)] +#[command(name = "oxide", version, about = "OxideJS - Rust JavaScript engine")] +struct Cli { + #[command(subcommand)] + command: Option, + + #[arg(short, long, global = true)] + verbose: bool, + + #[arg(short, long, global = true)] + quiet: bool, +} + +#[derive(Subcommand)] +enum Commands { + Eval { code: String }, + Run { file: String }, + Bench { suite: Option }, + Test { suite: Option }, +} + +fn main() -> ExitCode { + let cli = Cli::parse(); + + match cli.command { + Some(Commands::Eval { code }) => eval(&code), + Some(Commands::Run { file }) => run(&file), + Some(Commands::Bench { .. }) => not_implemented("bench"), + Some(Commands::Test { .. }) => not_implemented("test"), + None => repl(), + } +} + +fn eval(code: &str) -> ExitCode { + let allocator = Allocator::default(); + match oxide_parser::parse(&allocator, code) { + Ok(program) => { + println!("{program:#?}"); + ExitCode::SUCCESS + } + Err(errors) => { + for err in &errors { + eprintln!("{}", Red.paint(err.to_string())); + } + ExitCode::FAILURE + } + } +} + +fn run(file: &str) -> ExitCode { + match fs::read_to_string(file) { + Ok(source) => eval(&source), + Err(err) => { + eprintln!("{}", Red.paint(format!("Cannot read {file}: {err}"))); + ExitCode::FAILURE + } + } +} + +fn repl() -> ExitCode { + use rustyline::error::ReadlineError; + use rustyline::DefaultEditor; + + let mut rl = match DefaultEditor::new() { + Ok(editor) => editor, + Err(err) => { + eprintln!("{}", Red.paint(format!("Failed to start REPL: {err}"))); + return ExitCode::FAILURE; + } + }; + + loop { + match rl.readline("oxide> ") { + Ok(line) => { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + if trimmed == ".exit" || trimmed == ".quit" { + println!("exit"); + return ExitCode::SUCCESS; + } + rl.add_history_entry(trimmed).ok(); + eval(trimmed); + } + Err(ReadlineError::Interrupted) => { + println!("^C"); + return ExitCode::SUCCESS; + } + Err(ReadlineError::Eof) => { + println!("exit"); + return ExitCode::SUCCESS; + } + Err(err) => { + eprintln!("{}", Red.paint(format!("REPL error: {err}"))); + return ExitCode::FAILURE; + } + } + } +} + +fn not_implemented(command: &str) -> ExitCode { + use ansi_term::Colour::Yellow; + eprintln!( + "{}", + Yellow.paint(format!("'{command}' is not yet implemented")) + ); + ExitCode::SUCCESS } diff --git a/crates/oxide_cli/tests/cli_smoke.rs b/crates/oxide_cli/tests/cli_smoke.rs new file mode 100644 index 00000000..30459e21 --- /dev/null +++ b/crates/oxide_cli/tests/cli_smoke.rs @@ -0,0 +1,64 @@ +use std::process::Command; + +#[test] +fn eval_simple_expression() { + let output = Command::new("cargo") + .args(["run", "--", "eval", "1 + 2"]) + .output() + .expect("failed to run oxide eval"); + + assert!(output.status.success(), "eval 1+2 should exit 0"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Program"), + "output should contain AST node: {stdout}" + ); +} + +#[test] +fn eval_syntax_error() { + let output = Command::new("cargo") + .args(["run", "--", "eval", "function("]) + .output() + .expect("failed to run oxide eval"); + + assert!( + !output.status.success(), + "syntax error should exit non-zero" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.is_empty(), + "syntax error should produce stderr output" + ); +} + +#[test] +fn run_file() { + let fixture = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/hello.js"); + let output = Command::new("cargo") + .args(["run", "--", "run", fixture]) + .output() + .expect("failed to run oxide run"); + + assert!(output.status.success(), "run hello.js should exit 0"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("Program"), + "run output should contain AST node" + ); +} + +#[test] +fn bench_not_implemented() { + let output = Command::new("cargo") + .args(["run", "--", "bench"]) + .output() + .expect("failed to run oxide bench"); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("not yet implemented"), + "bench should print not-implemented message" + ); +} diff --git a/crates/oxide_cli/tests/fixtures/hello.js b/crates/oxide_cli/tests/fixtures/hello.js new file mode 100644 index 00000000..e0ef5840 --- /dev/null +++ b/crates/oxide_cli/tests/fixtures/hello.js @@ -0,0 +1 @@ +1 + 2