Files
OxideJS-New/crates/oxide_compiler/tests/compile_expr_tests.rs
T
pjh123 27b7f28726 test: compile_multiple_stmts 锚按 D-11 放宽为存在性断言
DCE 默认开启删除死 '1; 2' 链的 LOAD_CONST,仅剩顶层值 '3'。
精确计数断言 load_count >= 3 实测失败,放宽为 any(LOAD_CONST)。
2026-08-06 12:16:51 +08:00

534 lines
16 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")
}
#[allow(dead_code)]
fn compile_source_err(source: &str) -> String {
let allocator = Allocator::default();
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
let compiler = Compiler::new();
match compiler.compile(&program) {
Ok(_) => panic!("compile should fail"),
Err(err) => err,
}
}
#[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 test_constant_pool_overflow_range_error() {
let expr = (0..70_000).map(|i| i.to_string()).collect::<Vec<_>>().join(",");
let source = format!("var a=[{expr}]; a");
let err = compile_source_err(&source);
assert!(err.contains("too many constants"), "unexpected error: {err}");
}
#[test]
fn compile_logical_assignment_vars() {
compile_source("let x = 0; x ||= 1;");
compile_source("let x = 1; x &&= 2;");
compile_source("let x = null; x ??= 3;");
}
#[test]
fn compile_logical_assignment_members() {
compile_source("let obj = { x: 0 }; obj.x ||= 1;");
compile_source("let obj = { x: 1 }; obj.x &&= 2;");
compile_source("let obj = { x: null }; obj.x ??= 3;");
}
#[test]
fn compile_constants() {
let module = compile_source("42");
assert!(!module.constants.is_empty());
assert_eq!(module.constants[0], Constant::Int(42));
}
#[test]
fn compile_dedups_identical_string_constants() {
let module = compile_source("var a = 'hello'; var b = 'hello';");
let count = module
.constants
.iter()
.filter(|c| matches!(c, Constant::String(s) if s == "hello"))
.count();
assert_eq!(count, 1, "identical string constants should be interned once per module");
}
#[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;");
// D-11 放宽:Compiler 默认 DCE 开启,死 `1; 2` 链的 LOAD_CONST 被删除,
// 仅剩顶层值 `3` 的 LOAD_CONST(HALT 经 reg0 返回)。精确计数断言放宽为存在性断言。
let has_load = module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::LOAD_CONST);
assert!(has_load, "expected at least one LOAD_CONST for the final value");
}
#[test]
fn compile_object_literal_getter_emits_define_accessor() {
let module = compile_source("var o = { get x() { return 1; } };");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::DEFINE_ACCESSOR),
"object literal getter should emit DEFINE_ACCESSOR"
);
}
#[test]
fn compile_object_literal_setter_emits_define_accessor() {
let module = compile_source("var o = { set x(v) { this.y = v; } };");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::DEFINE_ACCESSOR),
"object literal setter should emit DEFINE_ACCESSOR"
);
}
#[test]
fn compile_binary_ops() {
let tests = [
("3 * 4", OpCode::MUL),
("10 / 2", OpCode::DIV),
("7 % 3", OpCode::MOD),
("5 - 2", OpCode::SUB),
("5 & 3", OpCode::BIT_AND),
("5 | 2", OpCode::BIT_OR),
("5 ^ 1", OpCode::BIT_XOR),
("1 << 3", OpCode::SHL),
("-8 >> 1", OpCode::SHR),
("-1 >>> 0", OpCode::USHR),
];
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_bitwise_not() {
let module = compile_source("~0");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::BIT_NOT),
"~0 should emit BIT_NOT opcode"
);
}
#[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 module = compile_source("var a = null; var b = 1; a ?? b");
assert!(
module
.bytecode
.iter()
.any(|&instr| opcode::opcode(instr) == OpCode::NULLISH || opcode::opcode(instr) == OpCode::JMP_IF_NULLISH),
"a ?? b should emit nullish bytecode"
);
}
#[test]
fn compile_strict_eq() {
let module = compile_source("1 === 2");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::STRICT_EQ),
"1 === 2 should emit STRICT_EQ opcode"
);
}
#[test]
fn compile_strict_neq() {
let module = compile_source("1 !== 2");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::STRICT_NEQ),
"1 !== 2 should emit STRICT_NEQ opcode"
);
}
#[test]
fn compile_unary_plus() {
let module = compile_source("+'hello'");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::UNARY_PLUS),
"+'hello' should emit UNARY_PLUS opcode"
);
}
#[test]
fn compile_typeof_strict_eq() {
let module = compile_source("typeof 42 === 'number'");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::TYPEOF),
"should emit TYPEOF opcode"
);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::STRICT_EQ),
"should emit STRICT_EQ opcode"
);
}
#[test]
fn compile_strict_eq_no_error() {
let result = std::panic::catch_unwind(|| {
compile_source("1 === 2");
});
assert!(result.is_ok(), "1 === 2 should compile without error");
}
#[test]
fn compile_unary_plus_no_error() {
let result = std::panic::catch_unwind(|| {
compile_source("+'hello'");
});
assert!(result.is_ok(), "+'hello' should compile without error");
}
#[test]
fn compile_compound_add() {
let module = compile_source("var x=0; x+=1");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::COMPOUND_ADD),
"x+=1 should emit COMPOUND_ADD opcode"
);
}
#[test]
fn compile_compound_exp() {
let module = compile_source("var x=0; x**=2");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::COMPOUND_EXP),
"x**=2 should emit COMPOUND_EXP opcode"
);
}
#[test]
fn compile_compound_bitwise_ops() {
let tests = [
("var x=5; x&=3", OpCode::COMPOUND_AND),
("var x=5; x|=2", OpCode::COMPOUND_OR),
("var x=5; x^=1", OpCode::COMPOUND_XOR),
("var x=1; x<<=5", OpCode::COMPOUND_SHL),
("var x=-8; x>>=1", OpCode::COMPOUND_SHR),
("var x=-1; x>>>=0", OpCode::COMPOUND_USHR),
];
for (src, expected_op) in tests {
let module = compile_source(src);
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == expected_op),
"expected {:?} in '{src}'",
expected_op
);
}
}
#[test]
fn compile_compound_assign_no_error() {
let result = std::panic::catch_unwind(|| {
compile_source("x+=1");
});
assert!(result.is_ok(), "x+=1 should compile without error");
}
#[test]
fn compile_compound_exp_no_error() {
let result = std::panic::catch_unwind(|| {
compile_source("x**=2");
});
assert!(result.is_ok(), "x**=2 should compile without error");
}
#[test]
fn compile_inc_pre() {
let module = compile_source("++x");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::INC_PRE),
"++x should emit INC_PRE opcode"
);
}
#[test]
fn compile_inc_post() {
let module = compile_source("x++");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::INC_POST),
"x++ should emit INC_POST opcode"
);
}
#[test]
fn compile_inc_no_error() {
let result = std::panic::catch_unwind(|| {
compile_source("x++");
});
assert!(result.is_ok(), "x++ should compile without error");
}
#[test]
fn compile_dec_no_error() {
let result = std::panic::catch_unwind(|| {
compile_source("x--");
});
assert!(result.is_ok(), "x-- should compile without error");
}
#[test]
fn compile_inc_dec_diff_opcodes() {
let m1 = compile_source("++x");
let m2 = compile_source("--x");
let has_inc_pre = m1.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::INC_PRE);
let has_dec_pre = m2.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::DEC_PRE);
assert!(has_inc_pre, "++x should emit INC_PRE");
assert!(has_dec_pre, "--x should emit DEC_PRE");
}
#[test]
fn nullish_opcodes_round_trip() {
for op in [OpCode::NULLISH, OpCode::JMP_IF_NULLISH] {
assert_eq!(OpCode::try_from(op as u8), Ok(op));
assert_eq!(format!("{op}"), format!("{op:?}"));
}
}
#[test]
fn compile_nullish_fast_path_opcode() {
let module = compile_source("null ?? 5");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::NULLISH),
"side-effect-free ?? should emit NULLISH"
);
}
#[test]
fn compile_nullish_short_circuit_opcode() {
let module = compile_source("var x = 0; x ?? (x = 5)");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::JMP_IF_NULLISH),
"side-effecting ?? should emit JMP_IF_NULLISH"
);
}
#[test]
fn compile_member_inc() {
let module = compile_source("var obj={x:1}; obj.x++");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::MEMBER_INC),
"obj.x++ should emit MEMBER_INC"
);
}
#[test]
fn compile_dyn_member_inc() {
let module = compile_source("var obj={a:3}; var k='a'; obj[k]++");
assert!(
module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::DYN_MEMBER_INC),
"obj[k]++ should emit DYN_MEMBER_INC"
);
}
#[test]
fn compile_compound_member_add() {
let module = compile_source("var obj={x:1}; obj.x+=1");
assert!(
module
.bytecode
.iter()
.any(|&i| opcode::opcode(i) == OpCode::COMPOUND_MEMBER_ADD),
"obj.x+=1 should emit COMPOUND_MEMBER_ADD"
);
}
#[test]
fn compile_compound_member_assign_ok() {
let result = std::panic::catch_unwind(|| {
compile_source("var obj={x:1}; obj.x+=1");
});
assert!(result.is_ok(), "obj.x+=1 should compile without error");
}
#[test]
fn compile_prefix_member_inc_ok() {
let result = std::panic::catch_unwind(|| {
compile_source("var obj={x:1}; ++obj.x");
});
assert!(result.is_ok(), "++obj.x should compile without error");
}
#[test]
fn compile_compound_member_exp_ok() {
let result = std::panic::catch_unwind(|| {
compile_source("var obj={x:1}; obj.x**=2");
});
assert!(result.is_ok(), "obj.x**=2 should compile without error");
}
#[test]
fn compile_this_in_function_ok() {
let module = compile_source("function f() { return this; }");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_this_member_assign_ok() {
let module = compile_source("function f() { this.x = 1; }");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_this_member_read_ok() {
let module = compile_source("function f() { return this.x; }");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_infinity_ok() {
let module = compile_source("Infinity");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_global_this_ok() {
let module = compile_source("globalThis");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_neg_infinity_ok() {
let module = compile_source("1 / -Infinity");
assert!(!module.bytecode.is_empty());
}
#[test]
fn compile_class_expression_emits_constructor_value() {
let module = compile_source("const C = class Foo { method() { return 2; } }; C");
assert_eq!(module.sub_modules.len(), 2, "expected constructor + method submodules");
assert!(
module
.bytecode
.iter()
.filter(|&&i| opcode::opcode(i) == OpCode::SET_PROP)
.count()
>= 3,
"class expression should wire method/constructor/prototype properties"
);
}
#[test]
fn compile_class_expression_default_constructor() {
let module = compile_source("class A {}; (class B {})");
assert!(
module.sub_modules.iter().any(|m| m.is_class_constructor),
"expected at least one class constructor submodule"
);
}
#[test]
fn compile_large_pure_expression_reuses_temp_registers() {
let expr = std::iter::repeat("1").take(40).collect::<Vec<_>>().join(" + ");
let module = compile_source(&expr);
assert!(
module.n_registers < 16,
"pure expression should reuse temp registers, got {}",
module.n_registers
);
}
#[test]
fn compile_builtin_globals_are_registered_lazily() {
let module = compile_source("Object; Array; Math; JSON");
assert_eq!(module.builtin_reg_map.len(), 4, "only referenced builtins should allocate registers");
}
#[test]
fn compile_comparison_complement_ops() {
let module = compile_source("let a = 1, b = 2; a > b; a <= b; a >= b; a != b;");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::GT), "a > b should emit GT");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::LTE), "a <= b should emit LTE");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::GTE), "a >= b should emit GTE");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::NEQ), "a != b should emit NEQ");
}
#[test]
fn compile_template_literal_emits_template_str() {
let module = compile_source("let name = 'x'; `hello ${name}`;");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::TEMPLATE_STR), "template literal should emit TEMPLATE_STR");
}
#[test]
fn compile_dynamic_member_ops() {
let module = compile_source("let obj = {}, k = 'x', arr = [1]; obj[k]; obj[k] = 1; arr[0];");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::NEW_ARRAY), "array literal should emit NEW_ARRAY");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::GET_PROP_DYNAMIC), "obj[k] should emit GET_PROP_DYNAMIC");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::SET_PROP_DYNAMIC), "obj[k] = should emit SET_PROP_DYNAMIC");
}
#[test]
fn compile_delete_new_instanceof_in() {
let module = compile_source("let obj = { x: 1 }, B = function(){}; delete obj.x; new B(); obj instanceof B; 'x' in obj;");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::DELETE_PROP_STATIC), "delete obj.x should emit DELETE_PROP_STATIC");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::NEW_EXPRESSION), "new B() should emit NEW_EXPRESSION");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::INSTANCEOF), "instanceof should emit INSTANCEOF");
assert!(module.bytecode.iter().any(|&i| opcode::opcode(i) == OpCode::IN), "in should emit IN");
}