This repository has been archived on 2026-09-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
OxideJS/crates/oxide_compiler/tests/compile_stmt_tests.rs
T

442 lines
14 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use oxide_bytecode::module::{CompiledModule, Constant};
use oxide_bytecode::opcode::{self, OpCode};
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
fn compile_source(source: &str) -> 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_var_declaration() {
let module = compile_source("var x = 42;");
assert!(!module.bytecode.is_empty());
assert_eq!(module.constants[0], Constant::Int(42));
}
#[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");
}
#[test]
fn compile_if_else_emits_jmp_if_false() {
let module = compile_source("if (true) { 1 } else { 2 }");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE),
"if/else should contain JMP_IF_FALSE"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP),
"if/else should contain JMP"
);
}
#[test]
fn compile_while_emits_jump_back() {
let module = compile_source("while (true) { 1 }");
let jmp_ops: Vec<_> = module.bytecode.iter().filter(|&&i| opcode::opcode(i) == OpCode::JMP).collect();
assert!(!jmp_ops.is_empty(), "while should contain JMP (backward)");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE),
"while should contain JMP_IF_FALSE"
);
}
#[test]
fn compile_for_emits_jumps() {
let module = compile_source("for (i=0; i<3; i=i+1) { 1 }");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE),
"for should contain JMP_IF_FALSE"
);
}
#[test]
fn compile_break_in_loop() {
let module = compile_source("while (true) { break; }");
assert!(!module.bytecode.is_empty(), "break should compile without error");
}
#[test]
fn compile_continue_in_loop() {
let module = compile_source("while (true) { continue; }");
assert!(!module.bytecode.is_empty(), "continue should compile without error");
}
#[test]
fn compile_break_outside_loop_errors() {
let result = std::panic::catch_unwind(|| {
compile_source("break;");
});
assert!(result.is_err(), "break outside loop should error");
}
#[test]
fn compile_continue_outside_loop_errors() {
let result = std::panic::catch_unwind(|| {
compile_source("continue;");
});
assert!(result.is_err(), "continue outside loop should error");
}
#[test]
fn compile_nested_if() {
let module = compile_source("var a=1,b=0; if (a) { if (b) { 1 } else { 2 } }");
let jmp_if_false_count = module
.bytecode
.iter()
.filter(|&&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE)
.count();
assert!(jmp_if_false_count >= 2, "nested if should have 2+ JMP_IF_FALSE");
}
#[test]
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() {
// `for (var i; ...)` 把 `i` 初始化为 undefined(var 语义而非 TDZ),且
// 编译器对无初始化声明符的指令数保持一致,因此可编译为良构循环头
// 而非 panic。
let module = compile_source("for (var i; i < 3; i = i + 1) {}");
assert!(!module.bytecode.is_empty(), "for(var) no-init should compile");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_FALSE),
"for(var) no-init should contain JMP_IF_FALSE"
);
}
#[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");
}
#[test]
fn compile_do_while_emits_jmp_if_true() {
let module = compile_source("do { 1 } while (true)");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_TRUE),
"do-while should contain JMP_IF_TRUE"
);
}
#[test]
fn compile_do_while_emits_body_before_test() {
let module = compile_source("var x = true; do { 1 } while (x)");
let has_opcodes_before_jmp = module
.bytecode
.iter()
.take_while(|&&i| opcode::opcode(i) != OpCode::JMP_IF_TRUE)
.count()
> 1;
assert!(has_opcodes_before_jmp, "body should emit before test");
}
#[test]
fn compile_do_while_break() {
let module = compile_source("do { break; } while (true)");
assert!(!module.bytecode.is_empty(), "do-while with break should compile");
}
#[test]
fn compile_do_while_continue() {
let module = compile_source("var x = 0; do { continue; } while (x < 5)");
assert!(!module.bytecode.is_empty(), "do-while with continue should compile");
}
#[test]
fn compile_for_in_emits_opcodes() {
let module = compile_source("var obj={a:1}; for (k in obj) {}");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::FOR_IN_INIT),
"for-in should emit FOR_IN_INIT"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::FOR_IN_NEXT),
"for-in should emit FOR_IN_NEXT"
);
}
#[test]
fn compile_for_in_var_decl() {
let module = compile_source("var obj={a:1}; for (var k in obj) { k }");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_for_in_let_decl() {
let module = compile_source("var obj={a:1}; for (let k in obj) { k }");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_switch_emits_jmp_if_true() {
let module = compile_source("var x=0; switch(x){case 1:1;case 2:2;}");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_TRUE),
"switch should emit JMP_IF_TRUE"
);
}
#[test]
fn compile_switch_default() {
let module = compile_source("var x=0; switch(x){default:0;}");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_switch_break() {
let module = compile_source("var x=0; switch(x){case 1:break;}");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_try_finally_resolves_finally_offset() {
let module = compile_source("try { 1; } finally { 2; }");
let try_finally_begin = module
.bytecode
.iter()
.copied()
.find(|&instr| opcode::opcode(instr) == OpCode::TRY_FINALLY_BEGIN)
.expect("try/finally should emit TRY_FINALLY_BEGIN");
assert_ne!(opcode::offset16(try_finally_begin), 0, "finally target offset should be resolved");
}
#[test]
fn compile_continue_in_switch_errors() {
let result = std::panic::catch_unwind(|| {
compile_source("var x=0; switch(x){case 1:continue;}");
});
assert!(result.is_err(), "continue inside switch should error");
}
#[test]
fn compile_large_adjacent_switches_do_not_collide_labels() {
let mut source = String::from("var r=0;switch(256){");
for i in 0..=256 {
source.push_str(&format!("case {i}:"));
if i == 256 {
source.push_str("r=1;break;");
}
}
source.push_str("}switch(0){case 0:r=r+1;break;}r");
let module = compile_source(&source);
assert!(
module
.bytecode
.iter()
.filter(|&&i| opcode::opcode(i) == OpCode::JMP_IF_TRUE)
.count()
>= 258,
"expected both large switches to compile with distinct case labels"
);
}
#[test]
fn compile_class_declaration_emits_constructor_and_prototype_setup() {
let module = compile_source("class A { m() { return 1; } }");
assert_eq!(module.sub_modules.len(), 2, "expected constructor + method submodules");
assert!(
module
.bytecode
.iter()
.filter(|&&i| opcode::opcode(i) == OpCode::NEW_OBJECT)
.count()
>= 1,
"class should allocate a prototype object"
);
assert!(
module
.bytecode
.iter()
.filter(|&&i| opcode::opcode(i) == OpCode::DEFINE_PROP_ATTRS)
.count()
>= 3,
"class should assign method, constructor, and prototype properties"
);
}
#[test]
fn compile_derived_constructor_inserts_instance_fields_after_super() {
let module = compile_source("class Base {} class Derived extends Base { value = 1; constructor() { super(); } }");
assert!(!module.sub_modules.is_empty(), "derived class should compile its constructor");
}
#[test]
fn compile_class_declaration_default_constructor_creates_submodule() {
let module = compile_source("class A {}");
assert_eq!(module.sub_modules.len(), 1, "expected synthesized default constructor");
assert!(
module.sub_modules[0].is_class_constructor,
"default constructor submodule should be marked as class constructor"
);
}
#[test]
fn compile_class_instance_getter_emits_define_accessor() {
let module = compile_source("class A { get x() { return 1; } }");
assert!(
module
.bytecode
.iter()
.any(|&i| opcode::opcode(i) == OpCode::DEFINE_ACCESSOR_ATTRS),
"class getter should emit DEFINE_ACCESSOR_ATTRS"
);
}
#[test]
fn compile_class_static_setter_emits_define_accessor() {
let module = compile_source("class A { static set x(v) { this.y = v; } }");
assert!(
module
.bytecode
.iter()
.any(|&i| opcode::opcode(i) == OpCode::DEFINE_ACCESSOR_ATTRS),
"static class setter should emit DEFINE_ACCESSOR_ATTRS"
);
}
#[test]
fn compile_labeled_loop_emits_body() {
let module = compile_source("outer: while (true) { break outer; }");
assert!(!module.bytecode.is_empty(), "labeled loop should produce bytecode");
}
#[test]
fn compile_empty_statements_are_skipped() {
let module = compile_source("; ; var x = 1;");
assert!(
module.constants.iter().any(|c| matches!(c, Constant::Int(1))),
"var after empty statements should still compile"
);
}
#[test]
fn compile_for_of_emits_iterator_protocol() {
let module = compile_source("let arr = [1]; for (const v of arr) { v; }");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::FOR_OF_INIT),
"for-of should emit FOR_OF_INIT"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::FOR_OF_NEXT),
"for-of should emit FOR_OF_NEXT"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::FOR_OF_DONE),
"for-of should emit FOR_OF_DONE"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::FOR_OF_CLOSE),
"for-of should emit FOR_OF_CLOSE"
);
}
#[test]
fn compile_try_catch_emits_try_begin_and_throw() {
let module = compile_source("try { throw new Error('x'); } catch (e) { e; }");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::THROW),
"throw should emit THROW"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::TRY_BEGIN),
"try-catch should emit TRY_BEGIN"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::TRY_END),
"try-catch should emit TRY_END"
);
}
fn has_opcode(module: &CompiledModule, target: OpCode) -> bool {
if module.bytecode.iter().any(|&i| opcode::opcode(i) == target) {
return true;
}
module.sub_modules.iter().any(|m| has_opcode(m, target))
}
#[test]
fn compile_closure_capture_emits_upvalue_load() {
let module = compile_source("function outer() { var x = 1; return function() { return x; }; }");
assert!(
has_opcode(&module, OpCode::LOAD_UPVALUE),
"inner function should emit LOAD_UPVALUE somewhere in nested sub_modules"
);
}
#[test]
fn compile_closure_capture_emits_make_cell_for_captured_var() {
let module = compile_source("function outer() { var x = 1; return function() { return x; }; }");
assert!(
has_opcode(&module, OpCode::MAKE_CELL),
"captured outer var should emit MAKE_CELL so the inner closure can read it"
);
}
#[test]
fn compile_closure_capture_arrow_emits_make_cell() {
let module = compile_source("function outer() { let x = 1; return () => x; }");
assert!(
has_opcode(&module, OpCode::MAKE_CELL),
"captured let in arrow closure should emit MAKE_CELL"
);
}
#[test]
fn compile_adjacent_for_of_lexical_bindings_in_distinct_scopes() {
let module = compile_source(
"for (const [arg, descr] of [[1, 'a']]) { descr; } for (const [arg, descr] of [[2, 'b']]) { descr; }",
);
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_compound_assignment_complement_ops() {
let module = compile_source("let x = 10; x -= 1; x *= 2; x /= 3; x %= 4;");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::COMPOUND_SUB),
"x -= should emit COMPOUND_SUB"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::COMPOUND_MUL),
"x *= should emit COMPOUND_MUL"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::COMPOUND_DIV),
"x /= should emit COMPOUND_DIV"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::COMPOUND_MOD),
"x %= should emit COMPOUND_MOD"
);
}