diff --git a/Cargo.lock b/Cargo.lock index 36aabba..1299954 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -755,6 +755,7 @@ dependencies = [ "ansi_term", "clap", "oxide_api", + "oxide_compiler", "oxide_parser", "rustyline", ] diff --git a/crates/oxide_cli/Cargo.toml b/crates/oxide_cli/Cargo.toml index 04449d3..f3eeef9 100644 --- a/crates/oxide_cli/Cargo.toml +++ b/crates/oxide_cli/Cargo.toml @@ -12,6 +12,7 @@ path = "src/main.rs" [dependencies] oxide_parser = { path = "../oxide_parser" } +oxide_compiler = { path = "../oxide_compiler" } oxide_api = { path = "../oxide_api" } clap.workspace = true rustyline.workspace = true diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index 5901127..5684373 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -3,6 +3,7 @@ use std::process::ExitCode; use ansi_term::Colour::Red; use clap::{Parser, Subcommand}; +use oxide_compiler::compiler::Compiler; use oxide_parser::Allocator; #[derive(Parser)] @@ -22,6 +23,7 @@ struct Cli { enum Commands { Eval { code: String }, Run { file: String }, + Compile { file: String }, Bench { suite: Option }, Test { suite: Option }, } @@ -32,6 +34,7 @@ fn main() -> ExitCode { match cli.command { Some(Commands::Eval { code }) => eval(&code), Some(Commands::Run { file }) => run(&file), + Some(Commands::Compile { file }) => compile(&file), Some(Commands::Bench { .. }) => not_implemented("bench"), Some(Commands::Test { .. }) => not_implemented("test"), None => repl(), @@ -64,6 +67,39 @@ fn run(file: &str) -> ExitCode { } } +fn compile(file: &str) -> ExitCode { + let source = match fs::read_to_string(file) { + Ok(s) => s, + Err(err) => { + eprintln!("{}", Red.paint(format!("Cannot read {file}: {err}"))); + return ExitCode::FAILURE; + } + }; + + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, &source) { + Ok(p) => p, + Err(errors) => { + for err in &errors { + eprintln!("{}", Red.paint(err.to_string())); + } + return ExitCode::FAILURE; + } + }; + + let compiler = Compiler::new(); + match compiler.compile(&program) { + Ok(module) => { + print!("{module}"); + ExitCode::SUCCESS + } + Err(err) => { + eprintln!("{}", Red.paint(err)); + ExitCode::FAILURE + } + } +} + fn repl() -> ExitCode { use rustyline::error::ReadlineError; use rustyline::DefaultEditor; diff --git a/crates/oxide_compiler/tests/compiler_tests.rs b/crates/oxide_compiler/tests/compiler_tests.rs new file mode 100644 index 0000000..141b9d2 --- /dev/null +++ b/crates/oxide_compiler/tests/compiler_tests.rs @@ -0,0 +1,138 @@ +use oxide_compiler::cache::CodeCache; +use oxide_compiler::compiler::Compiler; +use oxide_compiler::opcode::{self, OpCode}; +use oxide_parser::Allocator; + +fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + let compiler = Compiler::new(); + compiler.compile(&program).expect("compile failed") +} + +fn parse_to_hash(source: &str) -> u64 { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + oxide_compiler::cache::structural_hash(&program) +} + +#[test] +fn compile_simple_expr() { + let module = compile_source("1 + 2"); + + assert!(!module.bytecode.is_empty()); + assert!(module.bytecode.len() >= 4); + + let last = opcode::opcode(*module.bytecode.last().unwrap()); + assert_eq!(last, OpCode::HALT); +} + +#[test] +fn compile_constants() { + let module = compile_source("42"); + + assert!(!module.constants.is_empty()); + assert_eq!( + module.constants[0], + oxide_compiler::compiler::Constant::Number(42.0) + ); +} + +#[test] +fn compile_negation() { + let module = compile_source("-5"); + + let has_neg = module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == OpCode::NEG); + assert!(has_neg); +} + +#[test] +fn compile_multiple_stmts() { + let module = compile_source("1; 2; 3;"); + + let load_count = module + .bytecode + .iter() + .filter(|&&i| opcode::opcode(i) == OpCode::LOAD_CONST) + .count(); + assert!(load_count >= 3); +} + +#[test] +fn compile_binary_ops() { + let tests = [ + ("3 * 4", OpCode::MUL), + ("10 / 2", OpCode::DIV), + ("7 % 3", OpCode::MOD), + ("5 - 2", OpCode::SUB), + ]; + + for (src, expected_op) in tests { + let module = compile_source(src); + let has_op = module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == expected_op); + assert!(has_op, "expected {:?} in '{src}'", expected_op); + } +} + +#[test] +fn structural_cache_hit() { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, "1 + 2").expect("parse failed"); + let compiler = Compiler::new(); + let mut cache = CodeCache::new(); + + let m1 = cache + .get_or_compile(&program, &compiler) + .expect("compile failed"); + let m2 = cache + .get_or_compile(&program, &compiler) + .expect("compile failed"); + + assert_eq!(m1.bytecode, m2.bytecode); +} + +#[test] +fn structural_cache_same_shape() { + let hash_a = parse_to_hash("var x = 1 + 2; var y = x;"); + let hash_b = parse_to_hash("var a = 3 + 4; var b = a;"); + assert_eq!(hash_a, hash_b); +} + +#[test] +fn structural_cache_different_shape() { + let hash_a = parse_to_hash("1 + 2"); + let hash_b = parse_to_hash("1 + 2 * 3"); + assert_ne!(hash_a, hash_b); +} + +#[test] +fn compile_var_declaration() { + let module = compile_source("var x = 42;"); + assert!(!module.bytecode.is_empty()); + assert_eq!( + module.constants[0], + oxide_compiler::compiler::Constant::Number(42.0) + ); +} + +#[test] +fn compile_return_nothing() { + let module = compile_source("function f() { return; }"); + let last = opcode::opcode(*module.bytecode.last().unwrap()); + assert_eq!(last, OpCode::HALT); +} + +#[test] +fn compile_return_value() { + let module = compile_source("function f() { return 42; }"); + assert!( + !module.bytecode.is_empty(), + "function declaration should produce bytecode" + ); +}