mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
test: 拆分测试文件
This commit is contained in:
@@ -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)"
|
||||
);
|
||||
}
|
||||
+39
-174
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user