From 8c3f4aabe61e3322b2cd6d7490f2bafa7ab61bdb Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 6 Jun 2026 20:20:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=A4=8D=E5=90=88?= =?UTF-8?q?=E8=BF=90=E7=AE=97=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_compiler/src/counter.rs | 2 +- crates/oxide_compiler/src/emitter.rs | 26 ++++++++--- crates/oxide_compiler/src/hash.rs | 5 +++ crates/oxide_compiler/src/opcode.rs | 20 ++++++++- .../tests/compile_expr_tests.rs | 40 +++++++++++++++++ .../tests/structural_hash_tests.rs | 7 +++ crates/oxide_vm/src/vm.rs | 45 +++++++++++++++++++ crates/oxide_vm/tests/arithmetic_tests.rs | 40 +++++++++++++++++ 8 files changed, 177 insertions(+), 8 deletions(-) diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index e66e27ca..9aa421be 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -223,7 +223,7 @@ impl Compiler { } else { self.count_expression(&assign.right, ctx); ctx.alloc_reg(); - ctx.projected_pc += 1; // STORE_VAR + ctx.projected_pc += 1; } } Expression::ConditionalExpression(cond) => { diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 4923b269..2a265c46 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -630,13 +630,27 @@ impl Compiler { &assign.left { if assign.operator != AssignmentOperator::Assign { - return Err("compound assignment not yet supported".into()); + let rhs = self.emit_expression(&assign.right, ctx)?; + let name = id_ref.name.as_str(); + let var_reg = ctx.lookup_or_global(name); + let op = match assign.operator { + AssignmentOperator::Addition => OpCode::COMPOUND_ADD, + AssignmentOperator::Subtraction => OpCode::COMPOUND_SUB, + AssignmentOperator::Multiplication => OpCode::COMPOUND_MUL, + AssignmentOperator::Division => OpCode::COMPOUND_DIV, + AssignmentOperator::Remainder => OpCode::COMPOUND_MOD, + AssignmentOperator::Exponential => OpCode::COMPOUND_EXP, + _ => unreachable!(), + }; + ctx.emit(opcode::encode(op, var_reg, rhs, 0)); + Ok(var_reg) + } else { + let val_reg = self.emit_expression(&assign.right, ctx)?; + let name = id_ref.name.as_str(); + let var_reg = ctx.lookup_or_global(name); + ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, 0)); + Ok(val_reg) } - let val_reg = self.emit_expression(&assign.right, ctx)?; - let name = id_ref.name.as_str(); - let var_reg = ctx.lookup_or_global(name); - ctx.emit(opcode::encode(OpCode::STORE_VAR, var_reg, val_reg, 0)); - Ok(val_reg) } else { Err("assignment target not supported".into()) } diff --git a/crates/oxide_compiler/src/hash.rs b/crates/oxide_compiler/src/hash.rs index c487b70e..ac97f301 100644 --- a/crates/oxide_compiler/src/hash.rs +++ b/crates/oxide_compiler/src/hash.rs @@ -155,6 +155,11 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { 8u8.hash(h); b.value.hash(h); } + Expression::AssignmentExpression(assign) => { + 11u8.hash(h); + std::mem::discriminant(&assign.operator).hash(h); + hash_expression(&assign.right, h); + } _ => { std::mem::discriminant(expr).hash(h); } diff --git a/crates/oxide_compiler/src/opcode.rs b/crates/oxide_compiler/src/opcode.rs index b28b11d3..55e76952 100644 --- a/crates/oxide_compiler/src/opcode.rs +++ b/crates/oxide_compiler/src/opcode.rs @@ -16,8 +16,14 @@ pub enum OpCode { DIV = 0x03, MOD = 0x04, NEG = 0x05, + COMPOUND_ADD = 0x06, + COMPOUND_SUB = 0x07, + COMPOUND_MUL = 0x08, + COMPOUND_DIV = 0x09, + COMPOUND_MOD = 0x0A, + COMPOUND_EXP = 0x0B, - // ── Comparison (0x10-0x1F) ── + // -- Comparison (0x10-0x1F) -- EQ = 0x10, NEQ = 0x11, LT = 0x12, @@ -104,6 +110,12 @@ impl TryFrom for OpCode { 0x03 => Ok(OpCode::DIV), 0x04 => Ok(OpCode::MOD), 0x05 => Ok(OpCode::NEG), + 0x06 => Ok(OpCode::COMPOUND_ADD), + 0x07 => Ok(OpCode::COMPOUND_SUB), + 0x08 => Ok(OpCode::COMPOUND_MUL), + 0x09 => Ok(OpCode::COMPOUND_DIV), + 0x0A => Ok(OpCode::COMPOUND_MOD), + 0x0B => Ok(OpCode::COMPOUND_EXP), 0x10 => Ok(OpCode::EQ), 0x11 => Ok(OpCode::NEQ), 0x12 => Ok(OpCode::LT), @@ -163,6 +175,12 @@ impl fmt::Display for OpCode { OpCode::DIV => "DIV", OpCode::MOD => "MOD", OpCode::NEG => "NEG", + OpCode::COMPOUND_ADD => "COMPOUND_ADD", + OpCode::COMPOUND_SUB => "COMPOUND_SUB", + OpCode::COMPOUND_MUL => "COMPOUND_MUL", + OpCode::COMPOUND_DIV => "COMPOUND_DIV", + OpCode::COMPOUND_MOD => "COMPOUND_MOD", + OpCode::COMPOUND_EXP => "COMPOUND_EXP", OpCode::EQ => "EQ", OpCode::NEQ => "NEQ", OpCode::LT => "LT", diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs index 1c54eb76..ad90156d 100644 --- a/crates/oxide_compiler/tests/compile_expr_tests.rs +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -202,3 +202,43 @@ fn compile_unary_plus_no_error() { }); 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_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"); +} diff --git a/crates/oxide_compiler/tests/structural_hash_tests.rs b/crates/oxide_compiler/tests/structural_hash_tests.rs index bd25bdc7..1d963dcd 100644 --- a/crates/oxide_compiler/tests/structural_hash_tests.rs +++ b/crates/oxide_compiler/tests/structural_hash_tests.rs @@ -97,3 +97,10 @@ fn regression_conditionalexpression_hash() { "different expression types with same test must produce different hashes" ); } + +#[test] +fn hash_compound_vs_simple_assign() { + let hash_a = parse_to_hash("x=1"); + let hash_b = parse_to_hash("x+=1"); + assert_ne!(hash_a, hash_b, "x=1 and x+=1 must produce different hashes"); +} diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index a9c7a96b..1de0fbac 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -527,6 +527,51 @@ impl Vm { } } + OpCode::COMPOUND_ADD => { + let lhs = self.regs[rd]; + let rhs = self.regs[a]; + if lhs.is_string() || rhs.is_string() { + let ls = coercion::to_string(self.kernel.string_forge().as_ref(), lhs); + let rs = coercion::to_string(self.kernel.string_forge().as_ref(), rhs); + let concat = format!("{ls}{rs}"); + self.regs[rd] = self.intern(&concat); + } else { + let ln = coercion::to_number(lhs, self.kernel.string_forge().as_ref()); + let rn = coercion::to_number(rhs, self.kernel.string_forge().as_ref()); + self.regs[rd] = JsValue::float(ln + rn); + } + } + + OpCode::COMPOUND_SUB => { + let l = coercion::to_number(self.regs[rd], self.kernel.string_forge().as_ref()); + let r = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + self.regs[rd] = JsValue::float(l - r); + } + + OpCode::COMPOUND_MUL => { + let l = coercion::to_number(self.regs[rd], self.kernel.string_forge().as_ref()); + let r = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + self.regs[rd] = JsValue::float(l * r); + } + + OpCode::COMPOUND_DIV => { + let l = coercion::to_number(self.regs[rd], self.kernel.string_forge().as_ref()); + let r = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + self.regs[rd] = JsValue::float(l / r); + } + + OpCode::COMPOUND_MOD => { + let l = coercion::to_number(self.regs[rd], self.kernel.string_forge().as_ref()); + let r = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + self.regs[rd] = JsValue::float(l % r); + } + + OpCode::COMPOUND_EXP => { + let l = coercion::to_number(self.regs[rd], self.kernel.string_forge().as_ref()); + let r = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + self.regs[rd] = JsValue::float(l.powf(r)); + } + OpCode::TYPEOF => { let val = self.regs[a]; let result = if val.is_undefined() { diff --git a/crates/oxide_vm/tests/arithmetic_tests.rs b/crates/oxide_vm/tests/arithmetic_tests.rs index fc9ff6a7..9f569a1a 100644 --- a/crates/oxide_vm/tests/arithmetic_tests.rs +++ b/crates/oxide_vm/tests/arithmetic_tests.rs @@ -63,3 +63,43 @@ fn eval_division_by_zero() { fn eval_string_concat() { assert_eq!(eval("1 + 2 + 3"), "6"); } + +#[test] +fn eval_compound_add() { + assert_eq!(eval("var x=5; x+=3; x"), "8"); +} + +#[test] +fn eval_compound_sub() { + assert_eq!(eval("var x=10; x-=4; x"), "6"); +} + +#[test] +fn eval_compound_mul() { + assert_eq!(eval("var x=2; x*=3; x"), "6"); +} + +#[test] +fn eval_compound_div() { + assert_eq!(eval("var x=10; x/=2; x"), "5"); +} + +#[test] +fn eval_compound_mod() { + assert_eq!(eval("var x=7; x%=3; x"), "1"); +} + +#[test] +fn eval_compound_exp() { + assert_eq!(eval("var x=2; x**=3; x"), "8"); +} + +#[test] +fn eval_compound_add_expr_value() { + assert_eq!(eval("var x=5; x+=3"), "8"); +} + +#[test] +fn eval_compound_undefined() { + assert_eq!(eval("var x; x+=1; x"), "NaN"); +}