diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 6954f46..f1761a2 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -109,6 +109,7 @@ pub struct ErrorMethods { } pub struct StringMethods { + pub from_char_code: *const (), pub index_of: *const (), pub includes: *const (), pub char_at: *const (), @@ -938,6 +939,10 @@ impl BuiltinWorld { } pub fn bind_string_methods(&self, methods: &StringMethods, string_forge: &StringForge, shape_forge: &ShapeForge) { + let ctor_ptr = P::as_ptr(&self.string_constructor) as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + bind_methods!(self, ctor, string_forge, shape_forge, ("fromCharCode", methods.from_char_code, 1),); + let proto_ptr = P::as_ptr(&self.string_proto) as *mut JsObject; let proto = unsafe { &mut *proto_ptr }; bind_methods!( diff --git a/crates/oxide_vm/src/bindings/bind_string.rs b/crates/oxide_vm/src/bindings/bind_string.rs index 6dfea76..39dc721 100644 --- a/crates/oxide_vm/src/bindings/bind_string.rs +++ b/crates/oxide_vm/src/bindings/bind_string.rs @@ -7,6 +7,7 @@ use oxide_types::value::JsValue; pub fn bind_string(core: &Arc, session: &KernelSession, global: &mut JsObject) { let string_methods = StringMethods { + from_char_code: crate::builtins::string::string_from_char_code as *const (), index_of: crate::builtins::string::string_index_of as *const (), includes: crate::builtins::string::string_includes as *const (), char_at: crate::builtins::string::string_char_at as *const (), diff --git a/crates/oxide_vm/src/builtins/array.rs b/crates/oxide_vm/src/builtins/array.rs index 57e261d..016d6ca 100644 --- a/crates/oxide_vm/src/builtins/array.rs +++ b/crates/oxide_vm/src/builtins/array.rs @@ -3,8 +3,8 @@ use oxide_types::object::JsObject; use oxide_types::value::JsValue; use crate::coercion; -use crate::native::{NativeFn, NativeResult}; -use crate::vm::{native_fn_ptr_to_fn, Vm}; +use crate::native::NativeResult; +use crate::vm::Vm; macro_rules! array_ptr { ($vm:expr, $args:expr) => {{ @@ -46,31 +46,44 @@ fn create_new_array(vm: &mut Vm, n: usize) -> *mut JsObject { fn invoke_native_callback(vm: &mut Vm, callback_val: JsValue, this_val: JsValue, cb_args: &[JsValue]) -> NativeResult { if !callback_val.is_object() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(crate::builtins::error::create_type_error(vm, "callback is not a function")); } let cb_ptr = callback_val.as_js_object_ptr(); if cb_ptr.is_null() { - return NativeResult::Err(JsValue::undefined()); + return NativeResult::Err(crate::builtins::error::create_type_error(vm, "callback is not a function")); } let cb = unsafe { &*cb_ptr }; - if !cb.is_function() || cb.native_fn().is_none() { - return NativeResult::Err(JsValue::undefined()); + if !cb.is_function() { + return NativeResult::Err(crate::builtins::error::create_type_error(vm, "callback is not a function")); } - // 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(cb.native_fn().unwrap()) }; - - let base = 240u8; - let n = cb_args.len().min(15); - let mut args_buf = [0u8; 17]; - vm.set_reg(base, this_val); - args_buf[0] = base; - for i in 0..n { - vm.set_reg(base + 1 + i as u8, cb_args[i]); - args_buf[i + 1] = base + 1 + i as u8; + match vm.call_function_sync(callback_val, this_val, cb_args) { + Ok(value) => NativeResult::Ok(value), + Err(err) => NativeResult::Err(callback_error_from_text(vm, &err)), } +} - func(vm, &args_buf[..n + 1]) +fn callback_error_from_text(vm: &mut Vm, err: &str) -> JsValue { + let err = err.strip_prefix("uncaught ").unwrap_or(err); + if let Some(msg) = err.strip_prefix("TypeError: ") { + return crate::builtins::error::create_type_error(vm, msg); + } + if let Some(msg) = err.strip_prefix("ReferenceError: ") { + return crate::builtins::error::create_reference_error(vm, msg); + } + if let Some(msg) = err.strip_prefix("RangeError: ") { + return crate::builtins::error::create_range_error(vm, msg); + } + if let Some(msg) = err.strip_prefix("SyntaxError: ") { + return crate::builtins::error::create_syntax_error(vm, msg); + } + if let Some(msg) = err.strip_prefix("Error: ") { + return crate::builtins::error::create_error(vm, msg); + } + crate::builtins::error::create_error(vm, err) +} + +fn unexpected_tail_call_error(vm: &mut Vm) -> NativeResult { + NativeResult::Err(crate::builtins::error::create_type_error(vm, "unexpected tail call in array callback")) } pub fn array_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { @@ -410,7 +423,11 @@ pub fn array_for_each(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; for i in 0..n { let elem = unsafe { (*arr_ptr).get_prop_at(i) }; - let _ = invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i as i32), vm.reg(args[0])]); + match invoke_native_callback(vm, callback_val, this_val, &[elem, JsValue::int(i as i32), vm.reg(args[0])]) { + NativeResult::Ok(_) => {} + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), + } } NativeResult::Ok(JsValue::undefined()) } @@ -432,7 +449,8 @@ pub fn array_map(vm: &mut Vm, args: &[u8]) -> NativeResult { NativeResult::Ok(mapped) => unsafe { (*new_arr).set_prop_at(i, mapped); }, - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } unsafe { @@ -461,7 +479,8 @@ pub fn array_filter(vm: &mut Vm, args: &[u8]) -> NativeResult { kept.push(elem); } } - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } let new_arr = create_new_array(vm, kept.len()); @@ -506,7 +525,8 @@ pub fn array_reduce(vm: &mut Vm, args: &[u8]) -> NativeResult { &[accumulator, elem, JsValue::int(i as i32), vm.reg(args[0])], ) { NativeResult::Ok(result) => accumulator = result, - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } NativeResult::Ok(accumulator) @@ -531,7 +551,8 @@ pub fn array_find(vm: &mut Vm, args: &[u8]) -> NativeResult { return NativeResult::Ok(elem); } } - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } NativeResult::Ok(JsValue::undefined()) @@ -556,7 +577,8 @@ pub fn array_some(vm: &mut Vm, args: &[u8]) -> NativeResult { return NativeResult::Ok(JsValue::bool(true)); } } - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } NativeResult::Ok(JsValue::bool(false)) @@ -581,7 +603,8 @@ pub fn array_every(vm: &mut Vm, args: &[u8]) -> NativeResult { return NativeResult::Ok(JsValue::bool(false)); } } - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } NativeResult::Ok(JsValue::bool(true)) @@ -617,7 +640,8 @@ pub fn array_flat_map(vm: &mut Vm, args: &[u8]) -> NativeResult { } flat.push(result); } - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } let new_arr = create_new_array(vm, flat.len()); @@ -765,7 +789,8 @@ pub fn array_find_index(vm: &mut Vm, args: &[u8]) -> NativeResult { return NativeResult::Ok(JsValue::int(i as i32)); } } - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } NativeResult::Ok(JsValue::int(-1)) @@ -789,7 +814,8 @@ pub fn array_find_last(vm: &mut Vm, args: &[u8]) -> NativeResult { return NativeResult::Ok(elem); } } - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } NativeResult::Ok(JsValue::undefined()) @@ -818,26 +844,73 @@ pub fn array_reduce_right(vm: &mut Vm, args: &[u8]) -> NativeResult { &[acc, elem, JsValue::int(i), vm.reg(args[0])], ) { NativeResult::Ok(r) => acc = r, - NativeResult::Err(_) | NativeResult::TailCall { .. } => return NativeResult::Err(JsValue::undefined()), + NativeResult::Err(err) => return NativeResult::Err(err), + NativeResult::TailCall { .. } => return unexpected_tail_call_error(vm), } } NativeResult::Ok(acc) } -pub fn array_sort(vm: &mut Vm, _args: &[u8]) -> NativeResult { - let arr_ptr = array_ptr!(vm, _args); - let arr = unsafe { &mut *arr_ptr }; - let len = arr.prop_count() as usize; - let mut vals: Vec = (0..len).map(|i| arr.get_prop_at(i)).collect(); +pub fn array_sort(vm: &mut Vm, args: &[u8]) -> NativeResult { + let arr_ptr = array_ptr!(vm, args); + let len = unsafe { (*arr_ptr).prop_count() as usize }; + let mut vals: Vec = (0..len).map(|i| unsafe { (*arr_ptr).get_prop_at(i) }).collect(); + let comparator = if args.len() > 1 { + let candidate = vm.reg(args[1]); + if candidate.is_object() { + let ptr = candidate.as_js_object_ptr(); + if !ptr.is_null() && unsafe { &*ptr }.is_function() { + Some(candidate) + } else { + None + } + } else { + None + } + } else { + None + }; + let mut sort_error = None; vals.sort_by(|a, b| { - let sa = coercion::to_string(vm.kernel_core().string_forge().as_ref(), *a); - let sb = coercion::to_string(vm.kernel_core().string_forge().as_ref(), *b); - sa.cmp(&sb) + if sort_error.is_some() { + return std::cmp::Ordering::Equal; + } + if let Some(callback) = comparator { + match invoke_native_callback(vm, callback, JsValue::undefined(), &[*a, *b]) { + NativeResult::Ok(result) => { + let n = coercion::to_number(result, vm.kernel_core().string_forge().as_ref()); + if n.is_nan() || n == 0.0 { + std::cmp::Ordering::Equal + } else if n < 0.0 { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Greater + } + } + NativeResult::Err(err) => { + sort_error = Some(err); + std::cmp::Ordering::Equal + } + NativeResult::TailCall { .. } => { + sort_error = + Some(crate::builtins::error::create_type_error(vm, "unexpected tail call in array callback")); + std::cmp::Ordering::Equal + } + } + } else { + let sa = coercion::to_string(vm.kernel_core().string_forge().as_ref(), *a); + let sb = coercion::to_string(vm.kernel_core().string_forge().as_ref(), *b); + sa.cmp(&sb) + } }); + if let Some(err) = sort_error { + return NativeResult::Err(err); + } + let arr = unsafe { &mut *arr_ptr }; for (i, &v) in vals.iter().enumerate() { arr.set_prop_at(i, v); } - NativeResult::Ok(vm.reg(_args[0])) + NativeResult::Ok(vm.reg(args[0])) } pub fn array_values(vm: &mut Vm, args: &[u8]) -> NativeResult { diff --git a/crates/oxide_vm/src/builtins/boolean.rs b/crates/oxide_vm/src/builtins/boolean.rs index 7860d2b..56f4164 100644 --- a/crates/oxide_vm/src/builtins/boolean.rs +++ b/crates/oxide_vm/src/builtins/boolean.rs @@ -57,6 +57,9 @@ pub fn boolean_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn boolean_prototype_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if this_val.is_bool() { + return NativeResult::Ok(this_val); + } if !this_val.is_object() { return NativeResult::Err(crate::builtins::error::create_type_error( vm, @@ -76,6 +79,13 @@ pub fn boolean_prototype_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult { pub fn boolean_prototype_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if this_val.is_bool() { + return if this_val.as_bool() { + NativeResult::Ok(vm.intern("true")) + } else { + NativeResult::Ok(vm.intern("false")) + }; + } if !this_val.is_object() { return NativeResult::Err(crate::builtins::error::create_type_error( vm, diff --git a/crates/oxide_vm/src/builtins/string.rs b/crates/oxide_vm/src/builtins/string.rs index 5cbb7e5..8438bd1 100644 --- a/crates/oxide_vm/src/builtins/string.rs +++ b/crates/oxide_vm/src/builtins/string.rs @@ -11,6 +11,19 @@ fn this_string(vm: &Vm, args: &[u8]) -> String { coercion::to_string(vm.kernel_core().string_forge().as_ref(), vm.reg(args[0])) } +pub fn string_from_char_code(vm: &mut Vm, args: &[u8]) -> NativeResult { + let mut out = String::new(); + for &arg_reg in args.iter().skip(1) { + let code = coercion::to_uint32(vm.reg(arg_reg), vm.kernel_core().string_forge().as_ref()) & 0xFFFF; + if let Some(ch) = char::from_u32(code) { + out.push(ch); + } else { + out.push('\u{FFFD}'); + } + } + NativeResult::Ok(vm.intern(&out)) +} + pub fn string_value_of(vm: &mut Vm, args: &[u8]) -> NativeResult { let this_val = vm.reg(args[0]); if this_val.is_string() { diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index b46deb7..ad38d53 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -59,6 +59,36 @@ impl Vm { make_private_name_id(local_id) } + fn primitive_property_get(&mut self, val: JsValue, prop_name_si: u32) -> Result, String> { + if val.is_string() { + let length_si = self.kernel_core.string_forge().intern("length").0; + if prop_name_si == length_si { + let len = self + .kernel_core + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default() + .encode_utf16() + .count(); + return Ok(Some(JsValue::int(len as i32))); + } + let proto_ptr = self.session.builtin_world().string_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &*proto_ptr }; + return self.ordinary_get(proto, prop_name_si, val).map(Some); + } + if val.is_int() || val.is_double() { + let proto_ptr = self.session.builtin_world().number_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &*proto_ptr }; + return self.ordinary_get(proto, prop_name_si, val).map(Some); + } + if val.is_bool() { + let proto_ptr = self.session.builtin_world().boolean_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &*proto_ptr }; + return self.ordinary_get(proto, prop_name_si, val).map(Some); + } + Ok(None) + } + fn private_slot(&self, obj: &JsObject, private_key: u32) -> Option { self.kernel_core.shape_forge().lookup_position(obj.shape_id(), private_key) } @@ -138,27 +168,11 @@ impl Vm { let val = self.regs[a]; let obj_ptr = val.as_object_ptr() as *mut JsObject; if obj_ptr.is_null() { - if val.is_string() { - let proto_ptr = self.session.builtin_world().string_proto.as_ptr() as *mut JsObject; - let proto = unsafe { &*proto_ptr }; - let prop_name_si = self.property_key_si(self.regs[b]); - let resolved = self.ordinary_get(proto, prop_name_si, val)?; - if !resolved.is_undefined() { - self.regs[a] = resolved; - self.pc += 3; - return Ok(()); - } - } - if val.is_int() || val.is_double() { - let proto_ptr = self.session.builtin_world().number_proto.as_ptr() as *mut JsObject; - let proto = unsafe { &*proto_ptr }; - let prop_name_si = self.property_key_si(self.regs[b]); - let resolved = self.ordinary_get(proto, prop_name_si, val)?; - if !resolved.is_undefined() { - self.regs[a] = resolved; - self.pc += 3; - return Ok(()); - } + let prop_name_si = self.property_key_si(self.regs[b]); + if let Some(resolved) = self.primitive_property_get(val, prop_name_si)? { + self.regs[a] = resolved; + self.pc += 3; + return Ok(()); } self.raise_type_error("IC_GET_PROP on non-object")?; return Ok(()); @@ -222,7 +236,7 @@ impl Vm { } fn dispatch_ic_set_prop(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { - let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "IC_SET_PROP on non-object")? else { + let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "Cannot create property on non-object")? else { return Ok(()); }; @@ -286,11 +300,15 @@ impl Vm { } fn dispatch_get_prop(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let prop_name_si = self.property_key_si(self.regs[b]); + if let Some(value) = self.primitive_property_get(self.regs[rd], prop_name_si)? { + self.regs[a] = value; + return Ok(()); + } let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "GET_PROP on non-object")? else { return Ok(()); }; let obj = unsafe { &*obj_ptr }; - let prop_name_si = self.property_key_si(self.regs[b]); let val = self.ordinary_get_with_target(obj, prop_name_si, self.regs[rd], a as u8)?; if self.accessor_frame_target_reg.take().is_none() { self.regs[a] = val; @@ -299,7 +317,7 @@ impl Vm { } fn dispatch_set_prop(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { - let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "SET_PROP on non-object")? else { + let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "Cannot create property on non-object")? else { return Ok(()); }; let prop_name_si = self.property_key_si(self.regs[b]); @@ -320,11 +338,15 @@ impl Vm { } fn dispatch_get_prop_dynamic(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { + let prop_name_si = self.property_key_si(self.regs[a]); + if let Some(value) = self.primitive_property_get(self.regs[rd], prop_name_si)? { + self.regs[b] = value; + return Ok(()); + } let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "GET_PROP_DYNAMIC on non-object")? else { return Ok(()); }; let obj = unsafe { &*obj_ptr }; - let prop_name_si = self.property_key_si(self.regs[a]); let val = self.ordinary_get_with_target(obj, prop_name_si, self.regs[rd], b as u8)?; if self.accessor_frame_target_reg.take().is_none() { self.regs[b] = val; @@ -333,7 +355,7 @@ impl Vm { } fn dispatch_set_prop_dynamic(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { - let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "SET_PROP_DYNAMIC on non-object")? else { + let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "Cannot create property on non-object")? else { return Ok(()); }; let prop_name_si = self.property_key_si(self.regs[a]); @@ -354,7 +376,7 @@ impl Vm { } fn dispatch_set_elem(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { - let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "SET_ELEM on non-object")? else { + let Some(obj_ptr) = self.checked_object_ptr(self.regs[rd], "Cannot create property on non-object")? else { return Ok(()); }; let idx = if self.regs[a].is_int() { diff --git a/crates/oxide_vm/tests/builtins_array_tests.rs b/crates/oxide_vm/tests/builtins_array_tests.rs index 1804e6a..9f3adae 100644 --- a/crates/oxide_vm/tests/builtins_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_array_tests.rs @@ -12,6 +12,18 @@ fn eval(source: &str) -> Result<(Vm, JsValue), String> { Ok((vm, result)) } +fn to_str(vm: &Vm, val: JsValue) -> String { + vm.kernel_core() + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default() +} + +fn assert_num_eq(val: JsValue, expected: f64) { + let actual = if val.is_int() { val.as_int() as f64 } else { val.as_double() }; + assert_eq!(actual, expected); +} + #[test] fn array_push_adds_element() { let (_vm, result) = eval("var a = [1,2]; a.push(3); a").unwrap(); @@ -246,6 +258,66 @@ fn array_sort_default() { assert_eq!(obj.prop_count(), 3); } +#[test] +fn array_map_filter_reduce_accept_bytecode_callbacks() { + let (vm, result) = eval("[1,2,3].map(function(x){return x+1}).join(',')").unwrap(); + assert_eq!(to_str(&vm, result), "2,3,4"); + + let (vm, result) = eval("[1,2,3].map(x => x * 2).join(',')").unwrap(); + assert_eq!(to_str(&vm, result), "2,4,6"); + + let (vm, result) = eval("[1,2,3,4].filter(x => x % 2 === 0).join(',')").unwrap(); + assert_eq!(to_str(&vm, result), "2,4"); + + let (_vm, result) = eval("[1,2,3].reduce((a,b)=>a+b,0)").unwrap(); + assert_num_eq(result, 6.0); +} + +#[test] +fn array_iteration_predicates_accept_bytecode_callbacks() { + let (_vm, result) = eval("[1,2,3].forEach(function(x){return x+1})").unwrap(); + assert!(result.is_undefined()); + + let (_vm, result) = eval("[1,2,3].find(x => x > 1)").unwrap(); + assert_num_eq(result, 2.0); + + let (_vm, result) = eval("[1,2,3].some(x => x === 2)").unwrap(); + assert_eq!(result, JsValue::bool(true)); + + let (_vm, result) = eval("[1,2,3].every(function(x){return x > 0})").unwrap(); + assert_eq!(result, JsValue::bool(true)); +} + +#[test] +fn array_callback_exceptions_propagate() { + let (vm, result) = eval("try{[1].forEach(()=>{throw new TypeError('boom')})}catch(e){e.message}").unwrap(); + assert_eq!(to_str(&vm, result), "boom"); + + let (vm, result) = eval("try{[1].map(()=>{throw new Error('x')})}catch(e){e.message}").unwrap(); + assert_eq!(to_str(&vm, result), "x"); + + let (vm, result) = eval("try{[1].filter(()=>{throw new Error('f')})}catch(e){e.message}").unwrap(); + assert_eq!(to_str(&vm, result), "f"); +} + +#[test] +fn array_sort_uses_user_comparator() { + let (vm, result) = eval("[3,1,2].sort((a,b)=>a-b).join(',')").unwrap(); + assert_eq!(to_str(&vm, result), "1,2,3"); + + let (vm, result) = eval("[3,1,2].sort((a,b)=>b-a).join(',')").unwrap(); + assert_eq!(to_str(&vm, result), "3,2,1"); + + let (vm, result) = eval("[10,9,1].sort().join(',')").unwrap(); + assert_eq!(to_str(&vm, result), "1,10,9"); +} + +#[test] +fn array_sort_propagates_comparator_exception() { + let (vm, result) = eval("try{[1,2,3].sort(()=>{throw new Error('cmp')})}catch(e){e.message}").unwrap(); + assert_eq!(to_str(&vm, result), "cmp"); +} + #[test] fn array_copy_within_copies() { let (_vm, result) = eval("[1,2,3,4,5].copyWithin(0,3)").unwrap(); diff --git a/crates/oxide_vm/tests/builtins_boolean_tests.rs b/crates/oxide_vm/tests/builtins_boolean_tests.rs index b61b52e..bd706b7 100644 --- a/crates/oxide_vm/tests/builtins_boolean_tests.rs +++ b/crates/oxide_vm/tests/builtins_boolean_tests.rs @@ -106,3 +106,23 @@ fn boolean_new_to_string() { let r = eval(&mut vm, "new Boolean(false).toString()").unwrap(); assert_eq!(str_val(&vm, r), "false"); } + +#[test] +fn boolean_primitive_to_string_autoboxes() { + let mut vm = Vm::new(); + let r = eval(&mut vm, "true.toString()").unwrap(); + assert_eq!(str_val(&vm, r), "true"); + + let r = eval(&mut vm, "false.toString()").unwrap(); + assert_eq!(str_val(&vm, r), "false"); +} + +#[test] +fn boolean_primitive_value_of_autoboxes() { + let mut vm = Vm::new(); + let r = eval(&mut vm, "false.valueOf()").unwrap(); + assert_eq!(r, JsValue::bool(false)); + + let r = eval(&mut vm, "(true).constructor === Boolean").unwrap(); + assert_eq!(r, JsValue::bool(true)); +} diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index 1b992ad..9f60597 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -16,6 +16,11 @@ fn to_str(vm: &Vm, val: JsValue) -> String { .unwrap_or_default() } +fn assert_num_eq(val: JsValue, expected: f64) { + let actual = if val.is_int() { val.as_int() as f64 } else { val.as_double() }; + assert_eq!(actual, expected); +} + #[test] fn string_index_of_found() { let mut vm = Vm::new(); @@ -23,6 +28,13 @@ fn string_index_of_found() { assert_eq!(result.as_int(), 1); } +#[test] +fn string_from_char_code_static() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "String.fromCharCode(65,66,67)").unwrap(); + assert_eq!(to_str(&vm, result), "ABC"); +} + #[test] fn string_index_of_not_found() { let mut vm = Vm::new(); @@ -86,6 +98,26 @@ fn string_to_upper_case() { assert_eq!(to_str(&vm, s), "HELLO"); } +#[test] +fn string_primitive_length_autoboxes() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'abc'.length").unwrap(); + assert_num_eq(result, 3.0); + + let result = eval(&mut vm, "''.length").unwrap(); + assert_num_eq(result, 0.0); + + let result = eval(&mut vm, "'abc'.length + 'de'.length").unwrap(); + assert_num_eq(result, 5.0); +} + +#[test] +fn string_length_does_not_break_methods() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'hello'.toUpperCase()").unwrap(); + assert_eq!(to_str(&vm, s), "HELLO"); +} + #[test] fn string_to_lower_case() { let mut vm = Vm::new(); diff --git a/crates/oxide_vm/tests/coercion_tests.rs b/crates/oxide_vm/tests/coercion_tests.rs index 10ee803..757d3a0 100644 --- a/crates/oxide_vm/tests/coercion_tests.rs +++ b/crates/oxide_vm/tests/coercion_tests.rs @@ -105,3 +105,17 @@ fn test_to_int32_and_to_uint32() { assert_eq!(coercion::to_int32(JsValue::float(f64::INFINITY), sf), 0); assert_eq!(coercion::to_uint32(JsValue::int(-1), sf), 4_294_967_295); } + +#[test] +fn primitive_property_write_throws_clean_type_error() { + for source in ["'abc'.x = 1", "(5).foo = 1", "true.foo = 1"] { + let err = eval(source); + assert!(err.contains("TypeError"), "expected TypeError for {source}, got {err}"); + assert!( + !err.contains("IC_SET_PROP on non-object") + && !err.contains("SET_PROP on non-object") + && !err.contains("SET_PROP_DYNAMIC on non-object"), + "unexpected internal opcode message for {source}: {err}" + ); + } +}