From 2f1d666ed62bd416095f5ca2a460eeffe7f3fab2 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Fri, 5 Jun 2026 02:59:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E5=99=A8=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=E4=B8=8E=20CLI=20?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/oxide_cli/Cargo.toml | 1 + crates/oxide_cli/src/main.rs | 36 +++++ crates/oxide_compiler/tests/compiler_tests.rs | 138 ++++++++++++++++++ 4 files changed, 176 insertions(+) create mode 100644 crates/oxide_compiler/tests/compiler_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 36aabbae..12999547 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 04449d37..f3eeef95 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 59011273..56843736 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 00000000..141b9d25 --- /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" + ); +}