From 87a7224c2dd71e9dc74e7b15a006508884a8e372 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 6 Jun 2026 20:08:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=A5=E6=A0=BC?= =?UTF-8?q?=E6=AF=94=E8=BE=83=E8=BF=90=E7=AE=97=E7=AC=A6=E4=B8=8E=E4=B8=80?= =?UTF-8?q?=E5=85=83=E5=8A=A0=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/emitter.rs | 7 ++ crates/oxide_compiler/src/opcode.rs | 9 +++ .../tests/compile_expr_tests.rs | 71 +++++++++++++++++++ crates/oxide_vm/src/coercion.rs | 42 +++++++++++ crates/oxide_vm/src/vm.rs | 15 ++++ crates/oxide_vm/tests/coercion_tests.rs | 55 ++++++++++++++ crates/oxide_vm/tests/comparison_tests.rs | 64 +++++++++++++++++ 7 files changed, 263 insertions(+) diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 732886a0..4923b269 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -399,6 +399,8 @@ impl Compiler { BinaryOperator::LessEqualThan => OpCode::LTE, BinaryOperator::GreaterEqualThan => OpCode::GTE, BinaryOperator::In => OpCode::IN, + BinaryOperator::StrictEquality => OpCode::STRICT_EQ, + BinaryOperator::StrictInequality => OpCode::STRICT_NEQ, _ => return Err(format!("unsupported binary operator: {:?}", bin.operator)), }; let r = ctx.alloc_reg(); @@ -428,6 +430,11 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::NOT, r, arg, 0)); Ok(r) } + UnaryOperator::UnaryPlus => { + let r = ctx.alloc_reg(); + ctx.emit(opcode::encode(OpCode::UNARY_PLUS, r, arg, 0)); + Ok(r) + } _ => Err(format!("unsupported unary operator: {:?}", un.operator)), } } diff --git a/crates/oxide_compiler/src/opcode.rs b/crates/oxide_compiler/src/opcode.rs index 98aee825..b28b11d3 100644 --- a/crates/oxide_compiler/src/opcode.rs +++ b/crates/oxide_compiler/src/opcode.rs @@ -28,6 +28,9 @@ pub enum OpCode { NOT = 0x17, AND = 0x18, OR = 0x19, + STRICT_EQ = 0x1A, + STRICT_NEQ = 0x1C, + UNARY_PLUS = 0x1D, // -- Control Flow (0x20-0x2F) -- JMP = 0x20, @@ -111,6 +114,9 @@ impl TryFrom for OpCode { 0x17 => Ok(OpCode::NOT), 0x18 => Ok(OpCode::AND), 0x19 => Ok(OpCode::OR), + 0x1A => Ok(OpCode::STRICT_EQ), + 0x1C => Ok(OpCode::STRICT_NEQ), + 0x1D => Ok(OpCode::UNARY_PLUS), 0x20 => Ok(OpCode::JMP), 0x21 => Ok(OpCode::JMP_IF_FALSE), 0x22 => Ok(OpCode::JMP_IF_TRUE), @@ -167,6 +173,9 @@ impl fmt::Display for OpCode { OpCode::NOT => "NOT", OpCode::AND => "AND", OpCode::OR => "OR", + OpCode::STRICT_EQ => "STRICT_EQ", + OpCode::STRICT_NEQ => "STRICT_NEQ", + OpCode::UNARY_PLUS => "UNARY_PLUS", OpCode::JMP => "JMP", OpCode::JMP_IF_FALSE => "JMP_IF_FALSE", OpCode::JMP_IF_TRUE => "JMP_IF_TRUE", diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs index 68fe3a56..1c54eb76 100644 --- a/crates/oxide_compiler/tests/compile_expr_tests.rs +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -131,3 +131,74 @@ fn regression_coalesce_consistency() { "a ?? b should produce an error (not silently misbehave)" ); } + +#[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"); +} diff --git a/crates/oxide_vm/src/coercion.rs b/crates/oxide_vm/src/coercion.rs index d62f39b0..f01d0834 100644 --- a/crates/oxide_vm/src/coercion.rs +++ b/crates/oxide_vm/src/coercion.rs @@ -214,6 +214,48 @@ fn panic_to_object() -> ! { panic!("ToObject failed: null or undefined cannot be converted to object") } +pub fn same_value(lhs: JsValue, rhs: JsValue) -> bool { + if lhs.is_double() && rhs.is_double() { + let a = lhs.as_double(); + let b = rhs.as_double(); + if a.is_nan() && b.is_nan() { + return true; + } + if a == 0.0 && b == 0.0 { + let a_neg = a.is_sign_negative(); + let b_neg = b.is_sign_negative(); + return a_neg == b_neg; + } + return a == b; + } + if lhs.is_int() && rhs.is_int() { + return lhs.as_int() == rhs.as_int(); + } + if lhs.is_bool() && rhs.is_bool() { + return lhs.as_bool() == rhs.as_bool(); + } + if lhs.is_null() && rhs.is_null() { + return true; + } + if lhs.is_undefined() && rhs.is_undefined() { + return true; + } + if lhs.is_string() && rhs.is_string() { + return lhs == rhs; + } + if lhs.is_object() && rhs.is_object() { + return lhs.as_ptr() == rhs.as_ptr(); + } + false +} + +pub fn strict_equality(lhs: JsValue, rhs: JsValue) -> bool { + if lhs.is_double() && rhs.is_double() { + return strict_double_eq(lhs.as_double(), rhs.as_double()); + } + same_value(lhs, rhs) +} + pub fn to_object(val: JsValue, vm: &mut Vm) -> Result { if val.is_object() { return Ok(val); diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 750a6783..a9c7a96b 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -262,6 +262,21 @@ impl Vm { self.regs[rd] = JsValue::bool(!rel.unwrap_or(true)); } + OpCode::STRICT_EQ => { + let eq = coercion::strict_equality(self.regs[a], self.regs[b]); + self.regs[rd] = JsValue::bool(eq); + } + + OpCode::STRICT_NEQ => { + let ne = !coercion::strict_equality(self.regs[a], self.regs[b]); + self.regs[rd] = JsValue::bool(ne); + } + + OpCode::UNARY_PLUS => { + let v = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + self.regs[rd] = JsValue::float(v); + } + OpCode::JMP => { let offset = opcode::offset16(instr) as isize; self.pc = ((self.pc as isize) + offset - 1) as usize; diff --git a/crates/oxide_vm/tests/coercion_tests.rs b/crates/oxide_vm/tests/coercion_tests.rs index e1d38c8e..1e741bc0 100644 --- a/crates/oxide_vm/tests/coercion_tests.rs +++ b/crates/oxide_vm/tests/coercion_tests.rs @@ -1,5 +1,7 @@ use oxide_compiler::compiler::Compiler; use oxide_parser::Allocator; +use oxide_types::value::JsValue; +use oxide_vm::coercion; use oxide_vm::vm::Vm; fn eval(source: &str) -> String { @@ -33,3 +35,56 @@ fn eval_coercion_bool_equals_int() { fn eval_not_falsy() { assert_eq!(eval("!0"), "true"); } + +#[test] +fn test_same_value_signed_zero() { + assert!(!coercion::same_value( + JsValue::float(0.0), + JsValue::float(-0.0) + )); +} + +#[test] +fn test_strict_equality_signed_zero() { + assert!(coercion::strict_equality( + JsValue::float(0.0), + JsValue::float(-0.0) + )); +} + +#[test] +fn test_same_value_nan() { + assert!(coercion::same_value( + JsValue::float(f64::NAN), + JsValue::float(f64::NAN) + )); +} + +#[test] +fn test_same_value_type_mismatch() { + assert!(!coercion::same_value(JsValue::int(1), JsValue::float(1.0))); +} + +#[test] +fn test_strict_equality_nan() { + assert!(!coercion::strict_equality( + JsValue::float(f64::NAN), + JsValue::float(f64::NAN) + )); +} + +#[test] +fn test_strict_equality_type_mismatch() { + assert!(!coercion::strict_equality( + JsValue::int(1), + JsValue::float(1.0) + )); +} + +#[test] +fn test_strict_equality_null_undefined() { + assert!(!coercion::strict_equality( + JsValue::null(), + JsValue::undefined() + )); +} diff --git a/crates/oxide_vm/tests/comparison_tests.rs b/crates/oxide_vm/tests/comparison_tests.rs index 815d91ef..c7072ed1 100644 --- a/crates/oxide_vm/tests/comparison_tests.rs +++ b/crates/oxide_vm/tests/comparison_tests.rs @@ -58,3 +58,67 @@ fn eval_comparison_gte() { fn eval_string_number_eq() { assert_eq!(eval("5 == 5"), "true"); } + +#[test] +fn eval_strict_eq_true() { + assert_eq!(eval("1 === 1"), "true"); +} + +#[test] +fn eval_strict_eq_false_type() { + assert_eq!(eval("1 === '1'"), "false"); +} + +#[test] +fn eval_strict_eq_null_undefined() { + let source = r#" + var undef; + undef === null + "#; + assert_eq!(eval(source), "false"); +} + +#[test] +fn eval_strict_eq_bool_number() { + assert_eq!(eval("true === 1"), "false"); +} + +#[test] +fn eval_strict_eq_signed_zero() { + assert_eq!(eval("+0 === -0"), "true"); +} + +#[test] +fn eval_strict_neq_true() { + assert_eq!(eval("1 !== '1'"), "true"); +} + +#[test] +fn eval_strict_neq_false() { + assert_eq!(eval("1 !== 1"), "false"); +} + +#[test] +fn eval_unary_plus_number() { + assert_eq!(eval("+'42'"), "42"); +} + +#[test] +fn eval_unary_plus_bool() { + assert_eq!(eval("+true"), "1"); +} + +#[test] +fn eval_unary_plus_null() { + assert_eq!(eval("+null"), "0"); +} + +#[test] +fn eval_unary_plus_nan() { + assert_eq!(eval("+'hello'"), "NaN"); +} + +#[test] +fn eval_unary_plus_false() { + assert_eq!(eval("+false"), "0"); +}