diff --git a/crates/oxide_vm/src/lib.rs b/crates/oxide_vm/src/lib.rs index a5d1ab5..48ff300 100644 --- a/crates/oxide_vm/src/lib.rs +++ b/crates/oxide_vm/src/lib.rs @@ -7,6 +7,7 @@ mod dispatch; pub mod native; pub mod vm; mod vm_dispatch_ctrl; +mod vm_dispatch_misc; pub mod vm_pool; mod vm_props; mod vm_runtime; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 37a9a5a..2ac48ef 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -11,10 +11,9 @@ use crate::coercion; use crate::native::{NativeFn, NativeResult}; use oxide_kernel::kernel::OxideKernel; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; -use oxide_kernel::vm_debug; use oxide_types::error::{JsError, JsErrorKind}; use oxide_types::mem::{Epoch, P}; -use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes}; +use oxide_types::object::{JsObject, NativeFnPtr}; use oxide_types::value::JsValue; /// Convert a `NativeFnPtr` to a callable `NativeFn`. @@ -629,171 +628,11 @@ impl Vm { self.dispatch_call_native(rd, a, b)?; } - OpCode::NEW_EXPRESSION => { - let constructor_reg = a; - let first_arg_reg = b as u8; - vm_debug!("NEW_EXPRESSION rd={}", rd); - - let constructor = self.regs[constructor_reg]; - if !constructor.is_object() { - throw_err!(self, TypeError, "NEW_EXPRESSION: constructor is not an object"); - } - let ctor_ptr = constructor.as_js_object_ptr(); - if ctor_ptr.is_null() { - throw_err!(self, TypeError, "NEW_EXPRESSION: constructor is null"); - } - let ctor_obj = unsafe { &*ctor_ptr }; - if !ctor_obj.is_function() { - throw_err!(self, TypeError, "NEW_EXPRESSION: constructor is not a function"); - } - // Arrow functions cannot be used as constructors (D-03) - if ctor_obj.is_arrow() { - throw_err!(self, TypeError, "arrow functions cannot be used as constructors"); - } - - // Read extension word for arg_count - let ext = self.bytecode[self.pc]; - self.pc += 1; - let arg_count = (ext & 0xFF) as usize; - - // Create new empty object - let proto_ptr = &*self.object_prototype as *const JsObject as *mut JsObject; - let new_obj = self - .epoch - .alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr))); - - // Look up constructor.prototype and set as proto of new object - let proto_si = self.kernel.string_forge().intern("prototype").0; - if let Some(proto_val) = self.resolve_property(ctor_obj, proto_si) { - if proto_val.is_object() { - let new_obj_mut = unsafe { &mut *new_obj }; - let proto_obj_ptr = proto_val.as_js_object_ptr(); - let _ = new_obj_mut.set_proto(JsValue::from_js_object(proto_obj_ptr)); - } - } - - // If constructor has native_fn, call it with this=new_obj - if ctor_obj.native_fn().is_some() { - let new_obj_val = JsValue::object(new_obj as *mut u8); - self.regs[255] = new_obj_val; - - let mut args_buf = [0u8; 257]; - args_buf[0] = 255u8; - for i in 0..arg_count.min(256) { - args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8); - } - let args_slice = &args_buf[..arg_count + 1]; - - // SAFETY: native_fn was set via set_native_fn with a valid NativeFn pointer; - // native_fn_ptr_to_fn is the single coercion point for NativeFnPtr → NativeFn. - let func: NativeFn = unsafe { native_fn_ptr_to_fn(ctor_obj.native_fn().unwrap()) }; - self.regs[254] = constructor; - match func(self, args_slice) { - NativeResult::Ok(val) => { - if val.is_object() { - self.regs[rd] = val; - } else { - self.regs[rd] = new_obj_val; - } - } - NativeResult::Err(err_val) => { - self.exception_value = Some(err_val); - self.pending_error_kind = Some("Error"); - match self.unwind() { - Ok(()) => continue, - Err(e) => return Err(e), - } - } - NativeResult::TailCall { .. } => { - throw_err!(self, TypeError, "constructor tail call not supported"); - } - } - } else if ctor_obj.sub_module_index() > 0 { - let sub_idx = ctor_obj.sub_module_index() as usize - 1; - if sub_idx >= self.sub_modules.len() { - return Err(format!( - "NEW_EXPRESSION: sub_module_index {} out of bounds (max {})", - sub_idx, - self.sub_modules.len() - )); - } - - if self.frames.len() >= self.kernel.config.max_call_depth { - return Err("RangeError: Maximum call stack size exceeded".into()); - } - - let new_obj_val = JsValue::object(new_obj as *mut u8); - let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); - let sub_n_args = self.sub_modules[sub_idx].n_args as usize; - let sub_n_registers = self.sub_modules[sub_idx].n_registers; - let sub_constants = self.sub_modules[sub_idx].constants.clone(); - let sub_param_base = self.sub_modules[sub_idx].param_base as usize; - let caller_reg_limit = self.active_reg_limit.max(1); - let saved_regs = self.regs[..caller_reg_limit as usize].to_vec().into_boxed_slice(); - let saved_this = self.regs[254]; - let saved_new_target = self.regs[255]; - - for i in 0..sub_n_args { - let src_reg = first_arg_reg.wrapping_add(i as u8) as usize; - self.regs[sub_param_base + i] = self.regs[src_reg]; - } - self.regs[254] = if ctor_obj.is_derived_constructor() { - JsValue::undefined() - } else { - new_obj_val - }; - self.regs[255] = constructor; - - let converted_sub_constants = self.convert_constants(&sub_constants)?; - - self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); - self.saved_constants_stack.push(std::mem::take(&mut self.constants)); - - self.frames.push(CallFrame { - return_addr: self.pc, - function_name: self.sub_modules[sub_idx] - .function_name - .as_deref() - .map(|name| self.kernel.string_forge().intern(name).0) - .unwrap_or(0), - caller_reg_limit, - saved_regs, - saved_this, - saved_new_target, - callee: constructor, - construct_result_reg: Some(rd as u8), - constructed_this: Some(new_obj_val), - is_derived_constructor: ctor_obj.is_derived_constructor(), - continuation: FrameContinuation::None, - }); - - self.bytecode = sub_bytecode; - self.constants = converted_sub_constants; - - for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { - let si = self.kernel.string_forge().intern(name.as_str()).0; - let global = self.kernel.global_object(); - if let Some(pos) = self.kernel.shape_forge().lookup_position(global.shape_id(), si) { - self.regs[*reg as usize] = global.get_prop_at(pos); - } - } - - self.active_reg_limit = sub_n_registers.max(1); - self.pc = 0; - continue; - } else { - let error = crate::builtins::error::create_error( - self, - "NEW_EXPRESSION: bytecode constructors not yet supported", - ); - self.exception_value = Some(error); - self.pending_error_kind = Some("Error"); - match self.unwind() { - Ok(()) => continue, - Err(e) => return Err(e), - } - } - } + OpCode::NEW_EXPRESSION => match self.dispatch_new_expression(rd, a, b) { + Ok(true) => continue, + Ok(false) => {} + Err(e) => return Err(e), + }, OpCode::SUPER_CALL => { let first_arg_reg = a as u8; @@ -1079,42 +918,7 @@ impl Vm { } OpCode::TEMPLATE_STR => { - // Read header ext word: (segment_count << 16) | (total_len_hint & 0xFFFF) - let header = self.bytecode[self.pc]; - self.pc += 1; - let segment_count = (header >> 16) as usize; - let len_hint = (header & 0xFFFF) as usize; - - // Build result string - let mut result = String::with_capacity(len_hint.max(16)); - for _ in 0..segment_count { - let seg = self.bytecode[self.pc]; - self.pc += 1; - if (seg >> 31) == 1 { - // Expression: register value - let reg = (seg & 0x7F) as u8; - let val = self.regs[reg as usize]; - let s = if val.is_string() { - self.kernel().string_forge().lookup(val.as_string_index()).unwrap_or_default() - } else { - format!("{}", val) - }; - result.push_str(&s); - } else { - // Quasi: constant string - let const_idx = (seg & 0x7FFF_FFFF) as usize; - if const_idx < self.constants.len() { - let val = self.constants[const_idx]; - if val.is_string() { - let s = - self.kernel().string_forge().lookup(val.as_string_index()).unwrap_or_default(); - result.push_str(&s); - } - } - } - } - let si = self.kernel.string_forge().intern(&result).0; - self.regs[rd] = JsValue::string(si, 0); + self.dispatch_template_str(rd); } OpCode::DELETE_PROP_STATIC => { @@ -1130,54 +934,11 @@ impl Vm { } OpCode::INSTANCEOF => { - let lhs_val = self.regs[a]; - let rhs_val = self.regs[b]; - - if !rhs_val.is_object() { - throw_err!(self, TypeError, "INSTANCEOF right-hand side is not callable"); - } - if !lhs_val.is_object() { - self.regs[rd] = JsValue::bool(false); - continue; - } - - let rhs_obj = unsafe { &*rhs_val.as_js_object_ptr() }; - let proto_si = self.kernel.string_forge().intern("prototype").0; - let ctor_proto = self.resolve_property(rhs_obj, proto_si); - - let ctor_proto_ptr = match ctor_proto { - Some(v) if v.is_object() => v.as_js_object_ptr(), - _ => { - self.regs[rd] = JsValue::bool(false); - continue; - } - }; - - let mut proto = unsafe { &*lhs_val.as_js_object_ptr() }.proto(); - loop { - if !proto.is_object() { - self.regs[rd] = JsValue::bool(false); - break; - } - let proto_ptr = proto.as_js_object_ptr(); - if proto_ptr == ctor_proto_ptr { - self.regs[rd] = JsValue::bool(true); - break; - } - proto = unsafe { &*proto_ptr }.proto(); - } + self.dispatch_instanceof(rd, a, b)?; } OpCode::IN => { - let key_val = self.regs[a]; - let obj_ptr = self.regs[b].as_object_ptr() as *mut JsObject; - if obj_ptr.is_null() { - throw_err!(self, TypeError, "IN right-hand side is not an object"); - } - let obj = unsafe { &*obj_ptr }; - let prop_name_si = self.property_key_si(key_val); - let found = self.resolve_property(obj, prop_name_si).is_some(); - self.regs[rd] = JsValue::bool(found); + self.dispatch_in(rd, a, b)?; } OpCode::NOT => { @@ -1232,80 +993,19 @@ impl Vm { } OpCode::FOR_IN_INIT => { - let obj_val = self.regs[a]; - if !obj_val.is_object() { - throw_err!(self, TypeError, "for-in right-hand side is not an object"); - } - - let mut keys_vec = bumpalo::collections::Vec::new_in(self.epoch.bump()); - let mut seen = std::collections::HashSet::new(); - let mut current = obj_val; - - loop { - if !current.is_object() { - break; - } - let cur = unsafe { &*current.as_js_object_ptr() }; - let mut cursor = Some(cur.shape_id()); - while let Some(id) = cursor { - if id == EMPTY_SHAPE_ID { - break; - } - if let Some(shape) = self.kernel.shape_forge().get_shape(id) { - if shape.property_name != u32::MAX && seen.insert(shape.property_name) { - let enumerable = self - .kernel - .shape_forge() - .lookup_position(cur.shape_id(), shape.property_name) - .and_then(|pos| cur.prop_meta_at(pos)) - .map(|meta| meta.attributes.enumerable()) - .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()); - if enumerable { - let hash = - self.kernel.string_forge().get_hash(shape.property_name).unwrap_or(0); - keys_vec.push(JsValue::string(shape.property_name, hash)); - } - } - cursor = shape.parent; - } else { - break; - } - } - current = cur.proto(); - } - - let iter = self.epoch.alloc(ForInIter { keys: keys_vec, index: 0 }); - // SAFETY: ForInIter is arena-allocated and valid until `epoch.reset()`; - // dispatch clears `for_in_iters` before resetting the epoch. - self.for_in_iters.push(iter.cast::>()); + self.dispatch_for_in_init(a)?; } OpCode::FOR_IN_NEXT => { - let iter_ptr = self.for_in_iters.last().copied().unwrap_or(std::ptr::null_mut()); - if iter_ptr.is_null() { - return Err("FOR_IN_NEXT without active iterator".into()); - } - let iter = unsafe { &mut *iter_ptr }; - if iter.index < iter.keys.len() { - self.regs[rd] = iter.keys[iter.index]; - iter.index += 1; - } else { - self.regs[rd] = JsValue::undefined(); - } + self.dispatch_for_in_next(rd)?; } OpCode::FOR_IN_DONE => { - let iter_ptr = self.for_in_iters.last().copied().unwrap_or(std::ptr::null_mut()); - if iter_ptr.is_null() { - self.regs[rd] = JsValue::bool(true); - } else { - let iter = unsafe { &*iter_ptr }; - self.regs[rd] = JsValue::bool(iter.index >= iter.keys.len()); - } + self.dispatch_for_in_done(rd); } OpCode::FOR_IN_CLEANUP => { - self.for_in_iters.pop(); + self.dispatch_for_in_cleanup(); } OpCode::FOR_OF_INIT => { diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs new file mode 100644 index 0000000..cf61967 --- /dev/null +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -0,0 +1,323 @@ +use crate::native::{NativeFn, NativeResult}; +use crate::vm::{native_fn_ptr_to_fn, CallFrame, ForInIter, FrameContinuation, Vm}; +use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::value::JsValue; + +impl Vm { + pub(crate) fn dispatch_new_expression(&mut self, rd: usize, a: usize, b: usize) -> Result { + let constructor_reg = a; + let first_arg_reg = b as u8; + oxide_kernel::vm_debug!("NEW_EXPRESSION rd={}", rd); + + let constructor = self.regs[constructor_reg]; + if !constructor.is_object() { + return self + .raise_type_error("NEW_EXPRESSION: constructor is not an object") + .map(|_| true); + } + let ctor_ptr = constructor.as_js_object_ptr(); + if ctor_ptr.is_null() { + return self.raise_type_error("NEW_EXPRESSION: constructor is null").map(|_| true); + } + let ctor_obj = unsafe { &*ctor_ptr }; + if !ctor_obj.is_function() { + return self + .raise_type_error("NEW_EXPRESSION: constructor is not a function") + .map(|_| true); + } + if ctor_obj.is_arrow() { + return self + .raise_type_error("arrow functions cannot be used as constructors") + .map(|_| true); + } + + let ext = self.bytecode[self.pc]; + self.pc += 1; + let arg_count = (ext & 0xFF) as usize; + + let proto_ptr = &*self.object_prototype as *const JsObject as *mut JsObject; + let new_obj = self.epoch.alloc(JsObject::new_empty( + oxide_kernel::shape_forge::EMPTY_SHAPE_ID, + JsValue::from_js_object(proto_ptr), + )); + + let proto_si = self.kernel.string_forge().intern("prototype").0; + if let Some(proto_val) = self.resolve_property(ctor_obj, proto_si) { + if proto_val.is_object() { + let new_obj_mut = unsafe { &mut *new_obj }; + let proto_obj_ptr = proto_val.as_js_object_ptr(); + let _ = new_obj_mut.set_proto(JsValue::from_js_object(proto_obj_ptr)); + } + } + + if ctor_obj.native_fn().is_some() { + let new_obj_val = JsValue::object(new_obj as *mut u8); + self.regs[255] = new_obj_val; + + let mut args_buf = [0u8; 257]; + args_buf[0] = 255u8; + for i in 0..arg_count.min(256) { + args_buf[i + 1] = first_arg_reg.wrapping_add(i as u8); + } + let args_slice = &args_buf[..arg_count + 1]; + + let func: NativeFn = unsafe { native_fn_ptr_to_fn(ctor_obj.native_fn().unwrap()) }; + self.regs[254] = constructor; + match func(self, args_slice) { + NativeResult::Ok(val) => { + self.regs[rd] = if val.is_object() { val } else { new_obj_val }; + Ok(false) + } + NativeResult::Err(err_val) => { + self.exception_value = Some(err_val); + self.pending_error_kind = Some("Error"); + self.unwind().map(|_| true) + } + NativeResult::TailCall { .. } => { + self.raise_type_error("constructor tail call not supported").map(|_| true) + } + } + } else if ctor_obj.sub_module_index() > 0 { + let sub_idx = ctor_obj.sub_module_index() as usize - 1; + if sub_idx >= self.sub_modules.len() { + return Err(format!( + "NEW_EXPRESSION: sub_module_index {} out of bounds (max {})", + sub_idx, + self.sub_modules.len() + )); + } + + if self.frames.len() >= self.kernel.config.max_call_depth { + return Err("RangeError: Maximum call stack size exceeded".into()); + } + + let new_obj_val = JsValue::object(new_obj as *mut u8); + let sub_bytecode = self.sub_modules[sub_idx].bytecode.clone(); + let sub_n_args = self.sub_modules[sub_idx].n_args as usize; + let sub_n_registers = self.sub_modules[sub_idx].n_registers; + let sub_constants = self.sub_modules[sub_idx].constants.clone(); + let sub_param_base = self.sub_modules[sub_idx].param_base as usize; + let caller_reg_limit = self.active_reg_limit.max(1); + let saved_regs = self.regs[..caller_reg_limit as usize].to_vec().into_boxed_slice(); + let saved_this = self.regs[254]; + let saved_new_target = self.regs[255]; + + for i in 0..sub_n_args { + let src_reg = first_arg_reg.wrapping_add(i as u8) as usize; + self.regs[sub_param_base + i] = self.regs[src_reg]; + } + self.regs[254] = if ctor_obj.is_derived_constructor() { + JsValue::undefined() + } else { + new_obj_val + }; + self.regs[255] = constructor; + + let converted_sub_constants = self.convert_constants(&sub_constants)?; + self.saved_bytecode_stack.push(std::mem::take(&mut self.bytecode)); + self.saved_constants_stack.push(std::mem::take(&mut self.constants)); + + self.frames.push(CallFrame { + return_addr: self.pc, + function_name: self.sub_modules[sub_idx] + .function_name + .as_deref() + .map(|name| self.kernel.string_forge().intern(name).0) + .unwrap_or(0), + caller_reg_limit, + saved_regs, + saved_this, + saved_new_target, + callee: constructor, + construct_result_reg: Some(rd as u8), + constructed_this: Some(new_obj_val), + is_derived_constructor: ctor_obj.is_derived_constructor(), + continuation: FrameContinuation::None, + }); + + self.bytecode = sub_bytecode; + self.constants = converted_sub_constants; + + for (name, reg) in &self.sub_modules[sub_idx].builtin_reg_map { + let si = self.kernel.string_forge().intern(name.as_str()).0; + let global = self.kernel.global_object(); + if let Some(pos) = self.kernel.shape_forge().lookup_position(global.shape_id(), si) { + self.regs[*reg as usize] = global.get_prop_at(pos); + } + } + + self.active_reg_limit = sub_n_registers.max(1); + self.pc = 0; + Ok(true) + } else { + let error = + crate::builtins::error::create_error(self, "NEW_EXPRESSION: bytecode constructors not yet supported"); + self.exception_value = Some(error); + self.pending_error_kind = Some("Error"); + self.unwind().map(|_| true) + } + } + + pub(crate) fn dispatch_template_str(&mut self, rd: usize) { + let header = self.bytecode[self.pc]; + self.pc += 1; + let segment_count = (header >> 16) as usize; + let len_hint = (header & 0xFFFF) as usize; + + let mut result = String::with_capacity(len_hint.max(16)); + for _ in 0..segment_count { + let seg = self.bytecode[self.pc]; + self.pc += 1; + if (seg >> 31) == 1 { + let reg = (seg & 0x7F) as u8; + let val = self.regs[reg as usize]; + let s = if val.is_string() { + self.kernel().string_forge().lookup(val.as_string_index()).unwrap_or_default() + } else { + format!("{}", val) + }; + result.push_str(&s); + } else { + let const_idx = (seg & 0x7FFF_FFFF) as usize; + if const_idx < self.constants.len() { + let val = self.constants[const_idx]; + if val.is_string() { + let s = self.kernel().string_forge().lookup(val.as_string_index()).unwrap_or_default(); + result.push_str(&s); + } + } + } + } + let si = self.kernel.string_forge().intern(&result).0; + self.regs[rd] = JsValue::string(si, 0); + } + + pub(crate) fn dispatch_instanceof(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let lhs_val = self.regs[a]; + let rhs_val = self.regs[b]; + + if !rhs_val.is_object() { + return self.raise_type_error("INSTANCEOF right-hand side is not callable"); + } + if !lhs_val.is_object() { + self.regs[rd] = JsValue::bool(false); + return Ok(()); + } + + let rhs_obj = unsafe { &*rhs_val.as_js_object_ptr() }; + let proto_si = self.kernel.string_forge().intern("prototype").0; + let ctor_proto = self.resolve_property(rhs_obj, proto_si); + + let ctor_proto_ptr = match ctor_proto { + Some(v) if v.is_object() => v.as_js_object_ptr(), + _ => { + self.regs[rd] = JsValue::bool(false); + return Ok(()); + } + }; + + let mut proto = unsafe { &*lhs_val.as_js_object_ptr() }.proto(); + loop { + if !proto.is_object() { + self.regs[rd] = JsValue::bool(false); + break; + } + let proto_ptr = proto.as_js_object_ptr(); + if proto_ptr == ctor_proto_ptr { + self.regs[rd] = JsValue::bool(true); + break; + } + proto = unsafe { &*proto_ptr }.proto(); + } + Ok(()) + } + + pub(crate) fn dispatch_in(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let key_val = self.regs[a]; + let obj_ptr = self.regs[b].as_object_ptr() as *mut JsObject; + if obj_ptr.is_null() { + return self.raise_type_error("IN right-hand side is not an object"); + } + let obj = unsafe { &*obj_ptr }; + let prop_name_si = self.property_key_si(key_val); + let found = self.resolve_property(obj, prop_name_si).is_some(); + self.regs[rd] = JsValue::bool(found); + Ok(()) + } + + pub(crate) fn dispatch_for_in_init(&mut self, a: usize) -> Result<(), String> { + let obj_val = self.regs[a]; + if !obj_val.is_object() { + return self.raise_type_error("for-in right-hand side is not an object"); + } + + let mut keys_vec = bumpalo::collections::Vec::new_in(self.epoch.bump()); + let mut seen = std::collections::HashSet::new(); + let mut current = obj_val; + + loop { + if !current.is_object() { + break; + } + let cur = unsafe { &*current.as_js_object_ptr() }; + let mut cursor = Some(cur.shape_id()); + while let Some(id) = cursor { + if id == oxide_kernel::shape_forge::EMPTY_SHAPE_ID { + break; + } + if let Some(shape) = self.kernel.shape_forge().get_shape(id) { + if shape.property_name != u32::MAX && seen.insert(shape.property_name) { + let enumerable = self + .kernel + .shape_forge() + .lookup_position(cur.shape_id(), shape.property_name) + .and_then(|pos| cur.prop_meta_at(pos)) + .map(|meta| meta.attributes.enumerable()) + .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()); + if enumerable { + let hash = self.kernel.string_forge().get_hash(shape.property_name).unwrap_or(0); + keys_vec.push(JsValue::string(shape.property_name, hash)); + } + } + cursor = shape.parent; + } else { + break; + } + } + current = cur.proto(); + } + + let iter = self.epoch.alloc(ForInIter { keys: keys_vec, index: 0 }); + self.for_in_iters.push(iter.cast::>()); + Ok(()) + } + + pub(crate) fn dispatch_for_in_next(&mut self, rd: usize) -> Result<(), String> { + let iter_ptr = self.for_in_iters.last().copied().unwrap_or(std::ptr::null_mut()); + if iter_ptr.is_null() { + return Err("FOR_IN_NEXT without active iterator".into()); + } + let iter = unsafe { &mut *iter_ptr }; + if iter.index < iter.keys.len() { + self.regs[rd] = iter.keys[iter.index]; + iter.index += 1; + } else { + self.regs[rd] = JsValue::undefined(); + } + Ok(()) + } + + pub(crate) fn dispatch_for_in_done(&mut self, rd: usize) { + let iter_ptr = self.for_in_iters.last().copied().unwrap_or(std::ptr::null_mut()); + if iter_ptr.is_null() { + self.regs[rd] = JsValue::bool(true); + } else { + let iter = unsafe { &*iter_ptr }; + self.regs[rd] = JsValue::bool(iter.index >= iter.keys.len()); + } + } + + pub(crate) fn dispatch_for_in_cleanup(&mut self) { + self.for_in_iters.pop(); + } +}