feat: 支持可选链空值合并与逻辑赋值

This commit is contained in:
2026-06-18 15:26:16 +08:00
parent 3784db7b17
commit 32af37963e
14 changed files with 869 additions and 87 deletions
@@ -9,6 +9,7 @@ fn compile_source(source: &str) -> oxide_compiler::module::CompiledModule {
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");
@@ -31,17 +32,17 @@ fn compile_simple_expr() {
}
#[test]
fn unsupported_logical_assignment_returns_compile_error() {
let err = compile_source_err("let x = 0; x ||= 1;");
assert!(err.contains("compound assignment operator"));
assert!(err.contains("not supported"));
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 unsupported_member_logical_assignment_returns_compile_error() {
let err = compile_source_err("let obj = { x: 0 }; obj.x ||= 1;");
assert!(err.contains("compound assignment operator"));
assert!(err.contains("not supported"));
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]
@@ -170,10 +171,14 @@ fn compile_logical_or_simple() {
#[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)");
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]
@@ -331,6 +336,32 @@ fn compile_inc_dec_diff_opcodes() {
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++");
@@ -447,7 +478,7 @@ fn compile_class_expression_default_constructor() {
#[test]
fn compile_large_pure_expression_reuses_temp_registers() {
let expr = std::iter::repeat("1").take(120).collect::<Vec<_>>().join(" + ");
let expr = std::iter::repeat("1").take(40).collect::<Vec<_>>().join(" + ");
let module = compile_source(&expr);
assert!(
module.n_registers < 16,
@@ -137,3 +137,34 @@ fn class_method_getter_static_setter_shapes_differ() {
assert_ne!(method, static_setter, "method and static setter must hash differently");
assert_ne!(getter, static_setter, "getter and static setter must hash differently");
}
#[test]
fn structural_hash_distinguishes_nullish_operator() {
let nullish = parse_to_hash("a ?? b");
let and = parse_to_hash("a && b");
let or = parse_to_hash("a || b");
assert_ne!(nullish, and);
assert_ne!(nullish, or);
assert_ne!(and, or);
}
#[test]
fn structural_hash_distinguishes_optional_chain_shape() {
let optional_b = parse_to_hash("a?.b");
let optional_c = parse_to_hash("a?.c");
let plain_b = parse_to_hash("a.b");
assert_ne!(optional_b, optional_c);
assert_ne!(optional_b, plain_b);
assert_ne!(optional_c, plain_b);
}
#[test]
fn compiled_module_rejects_optional_chain_assignment_target() {
let allocator = Allocator::default();
match oxide_parser::parse(&allocator, "a?.b = 1") {
Ok(program) => assert!(Compiler::new().compile(&program).is_err()),
Err(_) => {}
}
}