diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index e6a2979..f8ddc56 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -1,4 +1,6 @@ -use oxide_parser::{Expression, ForStatementInit, SimpleAssignmentTarget, Statement}; +use oxide_parser::{ + AssignmentOperator, Expression, ForStatementInit, SimpleAssignmentTarget, Statement, +}; use crate::compiler::{is_side_effect_free, CompileCtx, Compiler, Label}; @@ -210,8 +212,12 @@ impl Compiler { { self.count_expression(&member.object, ctx); self.count_expression(&assign.right, ctx); + if assign.operator != AssignmentOperator::Assign { + ctx.alloc_reg(); + ctx.projected_pc += 1; + } ctx.alloc_reg(); - ctx.projected_pc += 1; // IC_SET_PROP + ctx.projected_pc += 1; // IC_SET_PROP or COMPOUND_MEMBER_* } else if let oxide_parser::AssignmentTarget::ComputedMemberExpression(member) = &assign.left { @@ -319,6 +325,19 @@ impl Compiler { ctx.alloc_reg(); ctx.projected_pc += 1; } + SimpleAssignmentTarget::StaticMemberExpression(member) => { + self.count_expression(&member.object, ctx); + ctx.alloc_reg(); + ctx.projected_pc += 1; + ctx.alloc_reg(); + ctx.projected_pc += 1; + } + SimpleAssignmentTarget::ComputedMemberExpression(member) => { + self.count_expression(&member.object, ctx); + self.count_expression(&member.expression, ctx); + ctx.alloc_reg(); + ctx.projected_pc += 1; + } _ => { ctx.alloc_reg(); } diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 3c894a9..49515c2 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -606,14 +606,29 @@ impl Compiler { (idx & 0xFF) as u8, ((idx >> 8) & 0xFF) as u8, )); - ctx.emit(opcode::encode( - OpCode::IC_SET_PROP, - obj_reg, - val_reg, - key_reg, - )); - ctx.emit(0); - Ok(val_reg) + if assign.operator != AssignmentOperator::Assign { + let op = match assign.operator { + AssignmentOperator::Addition => OpCode::COMPOUND_MEMBER_ADD, + AssignmentOperator::Subtraction => OpCode::COMPOUND_MEMBER_SUB, + AssignmentOperator::Multiplication => OpCode::COMPOUND_MEMBER_MUL, + AssignmentOperator::Division => OpCode::COMPOUND_MEMBER_DIV, + AssignmentOperator::Remainder => OpCode::COMPOUND_MEMBER_MOD, + AssignmentOperator::Exponential => OpCode::COMPOUND_MEMBER_EXP, + _ => unreachable!(), + }; + ctx.emit(opcode::encode(op, obj_reg, val_reg, key_reg)); + ctx.emit(0); + Ok(val_reg) + } else { + ctx.emit(opcode::encode( + OpCode::IC_SET_PROP, + obj_reg, + val_reg, + key_reg, + )); + ctx.emit(0); + Ok(val_reg) + } } else if let oxide_parser::AssignmentTarget::ComputedMemberExpression(member) = &assign.left { @@ -670,6 +685,37 @@ impl Compiler { ctx.emit(opcode::encode(op, var_reg, result_reg, result_reg)); Ok(result_reg) } + SimpleAssignmentTarget::StaticMemberExpression(member) => { + let obj_reg = self.emit_expression(&member.object, ctx)?; + let prop_name = member.property.name.as_str(); + let key_idx = ctx.add_constant(Constant::String(prop_name.to_string())); + let key_reg = ctx.alloc_reg(); + ctx.emit(opcode::encode( + OpCode::LOAD_CONST, + key_reg, + (key_idx & 0xFF) as u8, + ((key_idx >> 8) & 0xFF) as u8, + )); + let val_reg = ctx.alloc_reg(); + let op = match update.operator { + UpdateOperator::Increment => OpCode::MEMBER_INC, + UpdateOperator::Decrement => OpCode::MEMBER_DEC, + }; + ctx.emit(opcode::encode(op, obj_reg, val_reg, key_reg)); + ctx.emit(0); + Ok(val_reg) + } + SimpleAssignmentTarget::ComputedMemberExpression(member) => { + let obj_reg = self.emit_expression(&member.object, ctx)?; + let key_reg = self.emit_expression(&member.expression, ctx)?; + let val_reg = ctx.alloc_reg(); + let op = match update.operator { + UpdateOperator::Increment => OpCode::DYN_MEMBER_INC, + UpdateOperator::Decrement => OpCode::DYN_MEMBER_DEC, + }; + ctx.emit(opcode::encode(op, obj_reg, key_reg, val_reg)); + Ok(val_reg) + } _ => Err("member update not yet supported".into()), }, Expression::Identifier(ident) => { diff --git a/crates/oxide_compiler/src/opcode.rs b/crates/oxide_compiler/src/opcode.rs index 8a63b56..3c20d22 100644 --- a/crates/oxide_compiler/src/opcode.rs +++ b/crates/oxide_compiler/src/opcode.rs @@ -75,11 +75,23 @@ pub enum OpCode { NEW_ARRAY = 0x57, SET_ELEM = 0x58, - // ── Profiling — placeholders (0x60-0x6F) ── - PROFILE_TYPE = 0x60, - PROFILE_SHAPE = 0x61, - PROFILE_BRANCH = 0x62, - PROFILE_CALL = 0x63, + // -- Member Update (0x59-0x62) -- + MEMBER_INC = 0x59, + MEMBER_DEC = 0x5A, + DYN_MEMBER_INC = 0x5B, + DYN_MEMBER_DEC = 0x5C, + COMPOUND_MEMBER_ADD = 0x5D, + COMPOUND_MEMBER_SUB = 0x5E, + COMPOUND_MEMBER_MUL = 0x5F, + COMPOUND_MEMBER_DIV = 0x60, + COMPOUND_MEMBER_MOD = 0x61, + COMPOUND_MEMBER_EXP = 0x62, + + // -- Profiling - placeholders (0x63-0x6F) -- + PROFILE_TYPE = 0x63, + PROFILE_SHAPE = 0x64, + PROFILE_BRANCH = 0x65, + PROFILE_CALL = 0x66, // ── Parallel — placeholders (0x70-0x7F) ── FORK = 0x70, @@ -162,10 +174,20 @@ impl TryFrom for OpCode { 0x56 => Ok(OpCode::NEW_OBJECT), 0x57 => Ok(OpCode::NEW_ARRAY), 0x58 => Ok(OpCode::SET_ELEM), - 0x60 => Ok(OpCode::PROFILE_TYPE), - 0x61 => Ok(OpCode::PROFILE_SHAPE), - 0x62 => Ok(OpCode::PROFILE_BRANCH), - 0x63 => Ok(OpCode::PROFILE_CALL), + 0x59 => Ok(OpCode::MEMBER_INC), + 0x5A => Ok(OpCode::MEMBER_DEC), + 0x5B => Ok(OpCode::DYN_MEMBER_INC), + 0x5C => Ok(OpCode::DYN_MEMBER_DEC), + 0x5D => Ok(OpCode::COMPOUND_MEMBER_ADD), + 0x5E => Ok(OpCode::COMPOUND_MEMBER_SUB), + 0x5F => Ok(OpCode::COMPOUND_MEMBER_MUL), + 0x60 => Ok(OpCode::COMPOUND_MEMBER_DIV), + 0x61 => Ok(OpCode::COMPOUND_MEMBER_MOD), + 0x62 => Ok(OpCode::COMPOUND_MEMBER_EXP), + 0x63 => Ok(OpCode::PROFILE_TYPE), + 0x64 => Ok(OpCode::PROFILE_SHAPE), + 0x65 => Ok(OpCode::PROFILE_BRANCH), + 0x66 => Ok(OpCode::PROFILE_CALL), 0x70 => Ok(OpCode::FORK), 0x71 => Ok(OpCode::JOIN), 0xF0 => Ok(OpCode::NOP), @@ -231,6 +253,16 @@ impl fmt::Display for OpCode { OpCode::NEW_OBJECT => "NEW_OBJECT", OpCode::NEW_ARRAY => "NEW_ARRAY", OpCode::SET_ELEM => "SET_ELEM", + OpCode::MEMBER_INC => "MEMBER_INC", + OpCode::MEMBER_DEC => "MEMBER_DEC", + OpCode::DYN_MEMBER_INC => "DYN_MEMBER_INC", + OpCode::DYN_MEMBER_DEC => "DYN_MEMBER_DEC", + OpCode::COMPOUND_MEMBER_ADD => "COMPOUND_MEMBER_ADD", + OpCode::COMPOUND_MEMBER_SUB => "COMPOUND_MEMBER_SUB", + OpCode::COMPOUND_MEMBER_MUL => "COMPOUND_MEMBER_MUL", + OpCode::COMPOUND_MEMBER_DIV => "COMPOUND_MEMBER_DIV", + OpCode::COMPOUND_MEMBER_MOD => "COMPOUND_MEMBER_MOD", + OpCode::COMPOUND_MEMBER_EXP => "COMPOUND_MEMBER_EXP", OpCode::PROFILE_TYPE => "PROFILE_TYPE", OpCode::PROFILE_SHAPE => "PROFILE_SHAPE", OpCode::PROFILE_BRANCH => "PROFILE_BRANCH", diff --git a/crates/oxide_compiler/tests/compile_expr_tests.rs b/crates/oxide_compiler/tests/compile_expr_tests.rs index 6d9fc39..c2611fb 100644 --- a/crates/oxide_compiler/tests/compile_expr_tests.rs +++ b/crates/oxide_compiler/tests/compile_expr_tests.rs @@ -298,3 +298,63 @@ fn compile_inc_dec_diff_opcodes() { assert!(has_inc_pre, "++x should emit INC_PRE"); assert!(has_dec_pre, "--x should emit DEC_PRE"); } + +#[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"); +} diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 84daa1c..6772270 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -108,6 +108,44 @@ impl Vm { None } + fn set_member_prop( + &mut self, + obj: &mut JsObject, + prop_name_si: u32, + val: JsValue, + ) -> Result<(), String> { + if let Some(offset) = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + { + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.set_prop(offset, val); + } else { + let new_offset = obj.prop_count(); + let new_shape_id = self + .kernel + .shape_forge() + .make_shape(obj.shape_id(), prop_name_si); + let new_ext = (new_shape_id & 0x00FF_FFFF) | ((new_offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.set_shape_id(new_shape_id); + obj.set_prop_count(new_offset + 1); + obj.set_prop_expand(new_offset, val, self.epoch.bump()); + obj.bump_generation(); + self.kernel.prop_forge().upsert( + new_shape_id, + PropTemplate { + shape_id: new_shape_id, + offset: new_offset, + generation: obj.generation(), + }, + ); + } + Ok(()) + } + pub fn rerun(&mut self) -> Result { self.pc = 0; self.regs = [JsValue::undefined(); 256]; @@ -661,6 +699,531 @@ impl Vm { self.regs[rd] = JsValue::float(n - 1.0); } + OpCode::MEMBER_INC => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("MEMBER_INC on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + let ext = self.bytecode[self.pc]; + self.pc += 1; + let cached_shape_id = ext & 0x00FF_FFFF; + let cached_offset = ((ext >> 24) & 0xFF) as u8; + + let prop_val = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + obj.get_prop(cached_offset) + } else if let Some(template) = + self.kernel.prop_forge().get_template(obj.shape_id()) + { + let new_ext = + (obj.shape_id() & 0x00FF_FFFF) | ((template.offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.get_prop(template.offset) + } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + let offset = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + .unwrap_or(0); + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + val + } else { + JsValue::undefined() + }; + + let n = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let new_val = JsValue::float(n + 1.0); + + let offset = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + cached_offset + } else if let Some(off) = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + { + off + } else { + let new_offset = obj.prop_count(); + let new_shape_id = self + .kernel + .shape_forge() + .make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.set_prop_count(new_offset + 1); + obj.set_prop_expand(new_offset, new_val, self.epoch.bump()); + obj.bump_generation(); + let new_ext = (new_shape_id & 0x00FF_FFFF) | ((new_offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + self.kernel.prop_forge().upsert( + new_shape_id, + PropTemplate { + shape_id: new_shape_id, + offset: new_offset, + generation: obj.generation(), + }, + ); + self.regs[a] = new_val; + continue; + }; + obj.set_prop(offset, new_val); + self.regs[a] = new_val; + } + + OpCode::MEMBER_DEC => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("MEMBER_DEC on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + let ext = self.bytecode[self.pc]; + self.pc += 1; + let cached_shape_id = ext & 0x00FF_FFFF; + let cached_offset = ((ext >> 24) & 0xFF) as u8; + + let prop_val = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + obj.get_prop(cached_offset) + } else if let Some(template) = + self.kernel.prop_forge().get_template(obj.shape_id()) + { + let new_ext = + (obj.shape_id() & 0x00FF_FFFF) | ((template.offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.get_prop(template.offset) + } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + let offset = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + .unwrap_or(0); + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + val + } else { + JsValue::undefined() + }; + + let n = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let new_val = JsValue::float(n - 1.0); + + let offset = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + cached_offset + } else if let Some(off) = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + { + off + } else { + let new_offset = obj.prop_count(); + let new_shape_id = self + .kernel + .shape_forge() + .make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.set_prop_count(new_offset + 1); + obj.set_prop_expand(new_offset, new_val, self.epoch.bump()); + obj.bump_generation(); + let new_ext = (new_shape_id & 0x00FF_FFFF) | ((new_offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + self.kernel.prop_forge().upsert( + new_shape_id, + PropTemplate { + shape_id: new_shape_id, + offset: new_offset, + generation: obj.generation(), + }, + ); + self.regs[a] = new_val; + continue; + }; + obj.set_prop(offset, new_val); + self.regs[a] = new_val; + } + + OpCode::DYN_MEMBER_INC => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("DYN_MEMBER_INC on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let key_val = self.regs[a]; + if !key_val.is_string() { + return Err("DYN_MEMBER_INC key not a string".into()); + } + let prop_name_si = key_val.as_string_index(); + let prop_val = self + .resolve_property(obj, prop_name_si) + .unwrap_or(JsValue::undefined()); + let n = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let new_val = JsValue::float(n + 1.0); + + if let Some(offset) = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + { + obj.set_prop(offset, new_val); + } else { + let new_offset = obj.prop_count(); + let new_shape_id = self + .kernel + .shape_forge() + .make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.set_prop_count(new_offset + 1); + obj.set_prop_expand(new_offset, new_val, self.epoch.bump()); + obj.bump_generation(); + }; + self.regs[b] = new_val; + } + + OpCode::DYN_MEMBER_DEC => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("DYN_MEMBER_DEC on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let key_val = self.regs[a]; + if !key_val.is_string() { + return Err("DYN_MEMBER_DEC key not a string".into()); + } + let prop_name_si = key_val.as_string_index(); + let prop_val = self + .resolve_property(obj, prop_name_si) + .unwrap_or(JsValue::undefined()); + let n = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let new_val = JsValue::float(n - 1.0); + + if let Some(offset) = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + { + obj.set_prop(offset, new_val); + } else { + let new_offset = obj.prop_count(); + let new_shape_id = self + .kernel + .shape_forge() + .make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.set_prop_count(new_offset + 1); + obj.set_prop_expand(new_offset, new_val, self.epoch.bump()); + obj.bump_generation(); + }; + self.regs[b] = new_val; + } + + OpCode::COMPOUND_MEMBER_ADD => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("COMPOUND_MEMBER_ADD on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + let ext = self.bytecode[self.pc]; + self.pc += 1; + let cached_shape_id = ext & 0x00FF_FFFF; + let cached_offset = ((ext >> 24) & 0xFF) as u8; + + let prop_val = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + obj.get_prop(cached_offset) + } else if let Some(template) = + self.kernel.prop_forge().get_template(obj.shape_id()) + { + let new_ext = + (obj.shape_id() & 0x00FF_FFFF) | ((template.offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.get_prop(template.offset) + } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + let offset = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + .unwrap_or(0); + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + val + } else { + JsValue::undefined() + }; + + let rhs = self.regs[a]; + let new_val = if prop_val.is_string() || rhs.is_string() { + let ls = coercion::to_string(self.kernel.string_forge().as_ref(), prop_val); + let rs = coercion::to_string(self.kernel.string_forge().as_ref(), rhs); + let concat = format!("{ls}{rs}"); + self.intern(&concat) + } else { + let ln = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let rn = coercion::to_number(rhs, self.kernel.string_forge().as_ref()); + JsValue::float(ln + rn) + }; + + if let Some(offset) = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + { + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.set_prop(offset, new_val); + } else { + let new_offset = obj.prop_count(); + let new_shape_id = self + .kernel + .shape_forge() + .make_shape(obj.shape_id(), prop_name_si); + let new_ext = (new_shape_id & 0x00FF_FFFF) | ((new_offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.set_shape_id(new_shape_id); + obj.set_prop_count(new_offset + 1); + obj.set_prop_expand(new_offset, new_val, self.epoch.bump()); + obj.bump_generation(); + self.kernel.prop_forge().upsert( + new_shape_id, + PropTemplate { + shape_id: new_shape_id, + offset: new_offset, + generation: obj.generation(), + }, + ); + } + self.regs[a] = new_val; + } + + OpCode::COMPOUND_MEMBER_SUB => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("COMPOUND_MEMBER_SUB on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + let ext = self.bytecode[self.pc]; + self.pc += 1; + let cached_shape_id = ext & 0x00FF_FFFF; + let cached_offset = ((ext >> 24) & 0xFF) as u8; + + let prop_val = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + obj.get_prop(cached_offset) + } else if let Some(template) = + self.kernel.prop_forge().get_template(obj.shape_id()) + { + let new_ext = + (obj.shape_id() & 0x00FF_FFFF) | ((template.offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.get_prop(template.offset) + } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + let offset = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + .unwrap_or(0); + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + val + } else { + JsValue::undefined() + }; + + let ln = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let rn = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + let new_val = JsValue::float(ln - rn); + self.regs[a] = new_val; + if let Some(offset) = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + { + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.set_prop(offset, new_val); + } else { + let new_offset = obj.prop_count(); + let new_shape_id = self + .kernel + .shape_forge() + .make_shape(obj.shape_id(), prop_name_si); + let new_ext = (new_shape_id & 0x00FF_FFFF) | ((new_offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.set_shape_id(new_shape_id); + obj.set_prop_count(new_offset + 1); + obj.set_prop_expand(new_offset, new_val, self.epoch.bump()); + obj.bump_generation(); + self.kernel.prop_forge().upsert( + new_shape_id, + PropTemplate { + shape_id: new_shape_id, + offset: new_offset, + generation: obj.generation(), + }, + ); + } + } + + OpCode::COMPOUND_MEMBER_MUL => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("COMPOUND_MEMBER_MUL on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + let ext = self.bytecode[self.pc]; + self.pc += 1; + let cached_shape_id = ext & 0x00FF_FFFF; + + let prop_val = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + obj.get_prop(((ext >> 24) & 0xFF) as u8) + } else if let Some(template) = + self.kernel.prop_forge().get_template(obj.shape_id()) + { + let new_ext = + (obj.shape_id() & 0x00FF_FFFF) | ((template.offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.get_prop(template.offset) + } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + let offset = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + .unwrap_or(0); + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + val + } else { + JsValue::undefined() + }; + + let ln = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let rn = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + let new_val = JsValue::float(ln * rn); + self.regs[a] = new_val; + self.set_member_prop(obj, prop_name_si, new_val)?; + } + + OpCode::COMPOUND_MEMBER_DIV => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("COMPOUND_MEMBER_DIV on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + let ext = self.bytecode[self.pc]; + self.pc += 1; + let cached_shape_id = ext & 0x00FF_FFFF; + + let prop_val = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + obj.get_prop(((ext >> 24) & 0xFF) as u8) + } else if let Some(template) = + self.kernel.prop_forge().get_template(obj.shape_id()) + { + let new_ext = + (obj.shape_id() & 0x00FF_FFFF) | ((template.offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.get_prop(template.offset) + } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + let offset = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + .unwrap_or(0); + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + val + } else { + JsValue::undefined() + }; + + let ln = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let rn = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + let new_val = JsValue::float(ln / rn); + self.regs[a] = new_val; + self.set_member_prop(obj, prop_name_si, new_val)?; + } + + OpCode::COMPOUND_MEMBER_MOD => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("COMPOUND_MEMBER_MOD on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + let ext = self.bytecode[self.pc]; + self.pc += 1; + let cached_shape_id = ext & 0x00FF_FFFF; + + let prop_val = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + obj.get_prop(((ext >> 24) & 0xFF) as u8) + } else if let Some(template) = + self.kernel.prop_forge().get_template(obj.shape_id()) + { + let new_ext = + (obj.shape_id() & 0x00FF_FFFF) | ((template.offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.get_prop(template.offset) + } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + let offset = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + .unwrap_or(0); + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + val + } else { + JsValue::undefined() + }; + + let ln = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let rn = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + let new_val = JsValue::float(ln % rn); + self.regs[a] = new_val; + self.set_member_prop(obj, prop_name_si, new_val)?; + } + + OpCode::COMPOUND_MEMBER_EXP => { + let obj_ptr = self.regs[rd].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return Err("COMPOUND_MEMBER_EXP on non-object".into()); + } + let obj = unsafe { &mut *obj_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + let ext = self.bytecode[self.pc]; + self.pc += 1; + let cached_shape_id = ext & 0x00FF_FFFF; + + let prop_val = if cached_shape_id != 0 && cached_shape_id == obj.shape_id() { + obj.get_prop(((ext >> 24) & 0xFF) as u8) + } else if let Some(template) = + self.kernel.prop_forge().get_template(obj.shape_id()) + { + let new_ext = + (obj.shape_id() & 0x00FF_FFFF) | ((template.offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + obj.get_prop(template.offset) + } else if let Some(val) = self.resolve_property(obj, prop_name_si) { + let offset = self + .kernel + .shape_forge() + .lookup_offset(obj.shape_id(), prop_name_si) + .unwrap_or(0); + let new_ext = (obj.shape_id() & 0x00FF_FFFF) | ((offset as u32) << 24); + self.bytecode[self.pc - 1] = new_ext; + val + } else { + JsValue::undefined() + }; + + let ln = coercion::to_number(prop_val, self.kernel.string_forge().as_ref()); + let rn = coercion::to_number(self.regs[a], self.kernel.string_forge().as_ref()); + let new_val = JsValue::float(ln.powf(rn)); + self.regs[a] = new_val; + self.set_member_prop(obj, prop_name_si, new_val)?; + } + OpCode::FOR_IN_INIT => { let obj_val = self.regs[a]; if !obj_val.is_object() { diff --git a/crates/oxide_vm/tests/object_tests.rs b/crates/oxide_vm/tests/object_tests.rs index 3f6494e..66158c6 100644 --- a/crates/oxide_vm/tests/object_tests.rs +++ b/crates/oxide_vm/tests/object_tests.rs @@ -42,3 +42,78 @@ fn eval_object_property_read() { fn eval_object_missing_property() { assert_eq!(eval("({a:1}).b"), "undefined"); } + +#[test] +fn eval_member_inc_post() { + assert_eq!(eval("var obj={x:1}; obj.x++; obj.x"), "2"); +} + +#[test] +fn eval_member_inc_post_expr() { + assert_eq!(eval("var obj={x:1}; obj.x++"), "2"); +} + +#[test] +fn eval_member_dec() { + assert_eq!(eval("var obj={x:5}; obj.x--; obj.x"), "4"); +} + +#[test] +fn eval_member_dec_expr() { + assert_eq!(eval("var obj={x:5}; obj.x--"), "4"); +} + +#[test] +fn eval_member_inc_pre() { + assert_eq!(eval("var obj={x:5}; ++obj.x; obj.x"), "6"); +} + +#[test] +fn eval_dyn_member_inc() { + assert_eq!(eval("var obj={a:3}; var k='a'; obj[k]++"), "4"); +} + +#[test] +fn eval_dyn_member_inc_var() { + assert_eq!(eval("var obj={a:3}; var k='a'; obj[k]++; obj.a"), "4"); +} + +#[test] +fn eval_compound_member_add() { + assert_eq!(eval("var obj={x:5}; obj.x+=3; obj.x"), "8"); +} + +#[test] +fn eval_compound_member_sub() { + assert_eq!(eval("var obj={x:10}; obj.x-=2; obj.x"), "8"); +} + +#[test] +fn eval_compound_member_mul() { + assert_eq!(eval("var obj={x:2}; obj.x*=3; obj.x"), "6"); +} + +#[test] +fn eval_compound_member_div() { + assert_eq!(eval("var obj={x:10}; obj.x/=2; obj.x"), "5"); +} + +#[test] +fn eval_compound_member_mod() { + assert_eq!(eval("var obj={x:7}; obj.x%=3; obj.x"), "1"); +} + +#[test] +fn eval_compound_member_exp() { + assert_eq!(eval("var obj={x:2}; obj.x**=3; obj.x"), "8"); +} + +#[test] +fn eval_compound_member_expr_val() { + assert_eq!(eval("var obj={x:5}; var y=obj.x+=3; y"), "8"); +} + +#[test] +fn eval_member_multi_inc() { + assert_eq!(eval("var obj={}; obj.x=0; obj.x++; obj.x++; obj.x"), "2"); +}