From bb309561c734754be8c95c313e8d85e4425c5682 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 6 Jun 2026 15:34:59 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=8B=86=E5=88=86=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/compile_expr_tests.rs | 133 +++++++++++ ...ompiler_tests.rs => compile_stmt_tests.rs} | 213 ++++-------------- .../tests/structural_hash_tests.rs | 99 ++++++++ .../tests/symbol_table_tests.rs | 63 ++++++ 4 files changed, 334 insertions(+), 174 deletions(-) create mode 100644 crates/oxide_compiler/tests/compile_expr_tests.rs rename crates/oxide_compiler/tests/{compiler_tests.rs => compile_stmt_tests.rs} (50%) create mode 100644 crates/oxide_compiler/tests/structural_hash_tests.rs create mode 100644 crates/oxide_compiler/tests/symbol_table_tests.rs diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs new file mode 100644 index 00000000..68fe3a56 --- /dev/null +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -0,0 +1,133 @@ +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") +} + +#[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::Int(42) + ); +} + +#[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 compile_ternary_emits_jumps() { + let module = compile_source("true ? 1 : 2"); + assert!( + module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE), + "ternary should contain JMP_IF_FALSE" + ); +} + +#[test] +fn compile_logical_not() { + let module = compile_source("!true"); + assert!( + module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == OpCode::NOT), + "!true should emit NOT opcode" + ); +} + +#[test] +fn compile_logical_and_simple() { + let module = compile_source("1 && 2"); + assert!( + module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == OpCode::AND), + "simple && should emit AND opcode" + ); +} + +#[test] +fn compile_logical_or_simple() { + let module = compile_source("0 || 1"); + assert!( + module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == OpCode::OR), + "simple || should emit OR opcode" + ); +} + +#[test] +fn regression_coalesce_consistency() { + let result = std::panic::catch_unwind(|| { + compile_source("a ?? b"); + }); + assert!( + result.is_err(), + "a ?? b should produce an error (not silently misbehave)" + ); +} diff --git a/crates/oxide_compiler/tests/compiler_tests.rs b/crates/oxide_compiler/tests/compile_stmt_tests.rs similarity index 50% rename from crates/oxide_compiler/tests/compiler_tests.rs rename to crates/oxide_compiler/tests/compile_stmt_tests.rs index d49281c1..da76e2c1 100644 --- a/crates/oxide_compiler/tests/compiler_tests.rs +++ b/crates/oxide_compiler/tests/compile_stmt_tests.rs @@ -1,6 +1,5 @@ use oxide_compiler::compiler::Compiler; use oxide_compiler::opcode::{self, OpCode}; -use oxide_kernel::code_forge::CodeForge; use oxide_parser::Allocator; fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule { @@ -10,131 +9,6 @@ fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule { 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::compiler::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::Int(42) - ); -} - -#[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_hash_hit() { - let forge = CodeForge::new(); - let compiler = Compiler::new(); - let allocator = Allocator::default(); - let program = oxide_parser::parse(&allocator, "1 + 2").expect("parse failed"); - - let m1 = forge - .get_or_compile(&program, &compiler) - .expect("first compile"); - let m2 = forge - .get_or_compile(&program, &compiler) - .expect("second compile"); - - assert_eq!( - (m1.bytecode.len(), m1.n_registers), - (m2.bytecode.len(), m2.n_registers) - ); -} - -#[test] -fn structural_hash_same_shape() { - let hash_a = parse_to_hash("var x = 1 + 2; var y = x;"); - let hash_b = parse_to_hash("var a = 1 + 2; var b = a;"); - assert_eq!(hash_a, hash_b); -} - -#[test] -fn structural_hash_different_init_values() { - let hash_a = parse_to_hash("var x = 1 + 2;"); - let hash_b = parse_to_hash("var y = 3 + 4;"); - assert_ne!(hash_a, hash_b); -} - -#[test] -fn structural_hash_different_string_literals() { - let hash_a = parse_to_hash(r#"var x = "hello";"#); - let hash_b = parse_to_hash(r#"var x = "world";"#); - assert_ne!(hash_a, hash_b); -} - -#[test] -fn structural_hash_different_booleans() { - let hash_a = parse_to_hash("var x = true;"); - let hash_b = parse_to_hash("var x = false;"); - assert_ne!(hash_a, hash_b); -} - -#[test] -fn structural_hash_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;"); @@ -210,18 +84,6 @@ fn compile_for_emits_jumps() { ); } -#[test] -fn compile_ternary_emits_jumps() { - let module = compile_source("true ? 1 : 2"); - assert!( - module - .bytecode - .iter() - .any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE), - "ternary should contain JMP_IF_FALSE" - ); -} - #[test] fn compile_break_in_loop() { let module = compile_source("while (true) { break; }"); @@ -256,42 +118,6 @@ fn compile_continue_outside_loop_errors() { assert!(result.is_err(), "continue outside loop should error"); } -#[test] -fn compile_logical_not() { - let module = compile_source("!true"); - assert!( - module - .bytecode - .iter() - .any(|&i| opcode::opcode(i) == OpCode::NOT), - "!true should emit NOT opcode" - ); -} - -#[test] -fn compile_logical_and_simple() { - let module = compile_source("1 && 2"); - assert!( - module - .bytecode - .iter() - .any(|&i| opcode::opcode(i) == OpCode::AND), - "simple && should emit AND opcode" - ); -} - -#[test] -fn compile_logical_or_simple() { - let module = compile_source("0 || 1"); - assert!( - module - .bytecode - .iter() - .any(|&i| opcode::opcode(i) == OpCode::OR), - "simple || should emit OR opcode" - ); -} - #[test] fn compile_nested_if() { let module = compile_source("var a=1,b=0; if (a) { if (b) { 1 } else { 2 } }"); @@ -311,3 +137,42 @@ fn compile_empty_while_body() { let module = compile_source("while (false) {}"); assert!(!module.bytecode.is_empty(), "empty while should compile"); } + +#[test] +fn regression_for_var_init() { + let module = compile_source("for (var i = 0; i < 3; i = i + 1) {}"); + assert!(!module.bytecode.is_empty(), "for(var) init should compile"); + assert!( + module + .bytecode + .iter() + .any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE), + "for(var) init should contain JMP_IF_FALSE" + ); +} + +#[test] +fn regression_var_decl_counter_no_init() { + let result = std::panic::catch_unwind(|| { + compile_source("for (var i; i < 3; i = i + 1) {}"); + }); + assert!( + result.is_err(), + "for(var) without initializer should fail - accessing TDZ variable in test expression" + ); +} + +#[test] +fn regression_redundant_jmp_if_without_else() { + let module = compile_source("if (true) { 1 }"); + assert!( + !module.bytecode.is_empty(), + "if without else should compile" + ); + let last = opcode::opcode(*module.bytecode.last().unwrap()); + assert_eq!( + last, + OpCode::HALT, + "final opcode should be HALT, not dangling JMP" + ); +} diff --git a/crates/oxide_compiler/tests/structural_hash_tests.rs b/crates/oxide_compiler/tests/structural_hash_tests.rs new file mode 100644 index 00000000..bd25bdc7 --- /dev/null +++ b/crates/oxide_compiler/tests/structural_hash_tests.rs @@ -0,0 +1,99 @@ +use oxide_compiler::compiler::Compiler; +use oxide_kernel::code_forge::CodeForge; +use oxide_parser::Allocator; + +#[allow(dead_code)] +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::compiler::structural_hash(&program) +} + +#[test] +fn structural_hash_hit() { + let forge = CodeForge::new(); + let compiler = Compiler::new(); + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, "1 + 2").expect("parse failed"); + + let m1 = forge + .get_or_compile(&program, &compiler) + .expect("first compile"); + let m2 = forge + .get_or_compile(&program, &compiler) + .expect("second compile"); + + assert_eq!( + (m1.bytecode.len(), m1.n_registers), + (m2.bytecode.len(), m2.n_registers) + ); +} + +#[test] +fn structural_hash_same_shape() { + let hash_a = parse_to_hash("var x = 1 + 2; var y = x;"); + let hash_b = parse_to_hash("var a = 1 + 2; var b = a;"); + assert_eq!(hash_a, hash_b); +} + +#[test] +fn structural_hash_different_init_values() { + let hash_a = parse_to_hash("var x = 1 + 2;"); + let hash_b = parse_to_hash("var y = 3 + 4;"); + assert_ne!(hash_a, hash_b); +} + +#[test] +fn structural_hash_different_string_literals() { + let hash_a = parse_to_hash(r#"var x = "hello";"#); + let hash_b = parse_to_hash(r#"var x = "world";"#); + assert_ne!(hash_a, hash_b); +} + +#[test] +fn structural_hash_different_booleans() { + let hash_a = parse_to_hash("var x = true;"); + let hash_b = parse_to_hash("var x = false;"); + assert_ne!(hash_a, hash_b); +} + +#[test] +fn structural_hash_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 regression_forstatement_hash() { + let hash_a = parse_to_hash("for(i=0;i<3;i++){1}"); + let hash_b = parse_to_hash("for(;;){1}"); + assert_ne!( + hash_a, hash_b, + "different for-loop init/test/update must produce different hashes" + ); +} + +#[test] +fn regression_conditionalexpression_hash() { + let hash_a = parse_to_hash("a?1:2"); + let hash_b = parse_to_hash("a?3:4"); + assert_ne!( + hash_a, hash_b, + "different ternary branches must produce different hashes" + ); + + let hash_c = parse_to_hash("a?1:2"); + let hash_d = parse_to_hash("a?1:2||3"); + assert_ne!( + hash_c, hash_d, + "different expression types with same test must produce different hashes" + ); +} diff --git a/crates/oxide_compiler/tests/symbol_table_tests.rs b/crates/oxide_compiler/tests/symbol_table_tests.rs new file mode 100644 index 00000000..e6890d29 --- /dev/null +++ b/crates/oxide_compiler/tests/symbol_table_tests.rs @@ -0,0 +1,63 @@ +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") +} + +#[test] +fn symbol_table_declare_and_lookup() { + let module = compile_source("var x = 42;"); + assert!(!module.bytecode.is_empty()); + assert_eq!( + module.constants[0], + oxide_compiler::compiler::Constant::Int(42) + ); +} + +#[test] +fn symbol_table_nested_scopes() { + let module = compile_source("var x = 1; if (true) { var y = 2; }"); + assert!(!module.bytecode.is_empty(), "nested scopes should compile"); + let jmp_count = module + .bytecode + .iter() + .filter(|&&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE) + .count(); + assert!(jmp_count >= 1, "nested if should have JMP_IF_FALSE"); +} + +#[test] +fn symbol_table_tdz_global_shadow() { + let result = std::panic::catch_unwind(|| { + compile_source("var x = 1; { var x = x; }"); + }); + assert!( + result.is_err(), + "TDZ: accessing x in its own initializer inside a block should error" + ); +} + +#[test] +fn symbol_table_duplicate_var() { + let result = std::panic::catch_unwind(|| { + compile_source("var x = 1; var x = 2;"); + }); + assert!( + result.is_err(), + "duplicate var declaration in same scope should error (strict-mode semantics)" + ); +} + +#[test] +fn symbol_table_undeclared_auto_global() { + let module = compile_source("x = 5;"); + assert!( + !module.bytecode.is_empty(), + "undeclared assignment should auto-create global" + ); +}