diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 5b10c6a..52cd1e4 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -66,6 +66,15 @@ pub struct StringMethods { pub search: *const (), } +pub struct NumberMethods { + pub is_nan: *const (), + pub is_finite: *const (), + pub parse_int: *const (), + pub parse_float: *const (), + pub to_string: *const (), + pub to_fixed: *const (), +} + pub struct BuiltinWorld { pub object_proto: P, pub array_proto: P, @@ -557,6 +566,61 @@ impl BuiltinWorld { ); } + pub fn bind_number_methods( + &self, + methods: &NumberMethods, + string_forge: &StringForge, + shape_forge: &ShapeForge, + ) { + let ctor_ptr = P::as_ptr(&self.number_constructor) as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + let proto_ptr = P::as_ptr(&self.number_proto) as *mut JsObject; + let proto = unsafe { &mut *proto_ptr }; + + let _ = Self::bind_method(ctor, shape_forge, string_forge, "isNaN", methods.is_nan, 1); + let _ = Self::bind_method( + ctor, + shape_forge, + string_forge, + "isFinite", + methods.is_finite, + 1, + ); + let _ = Self::bind_method( + ctor, + shape_forge, + string_forge, + "parseInt", + methods.parse_int, + 1, + ); + let _ = Self::bind_method( + ctor, + shape_forge, + string_forge, + "parseFloat", + methods.parse_float, + 1, + ); + + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "toString", + methods.to_string, + 0, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "toFixed", + methods.to_fixed, + 0, + ); + } + pub fn bind_method( proto: &mut JsObject, shape_forge: &ShapeForge, diff --git a/crates/oxide_vm/src/builtins/mod.rs b/crates/oxide_vm/src/builtins/mod.rs index 276aea7..9abc4fd 100644 --- a/crates/oxide_vm/src/builtins/mod.rs +++ b/crates/oxide_vm/src/builtins/mod.rs @@ -1,5 +1,6 @@ pub mod array; pub mod error; pub mod global; +pub mod number; pub mod object; pub mod string; diff --git a/crates/oxide_vm/src/builtins/number.rs b/crates/oxide_vm/src/builtins/number.rs new file mode 100644 index 0000000..6f2032c --- /dev/null +++ b/crates/oxide_vm/src/builtins/number.rs @@ -0,0 +1,169 @@ +use oxide_types::value::JsValue; + +use crate::coercion; +use crate::native::NativeResult; +use crate::vm::Vm; + +pub fn number_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { + let n = if args.len() > 1 { + coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) + } else { + 0.0 + }; + if n.fract() == 0.0 && n.is_finite() && n >= i32::MIN as f64 && n <= i32::MAX as f64 { + Ok(JsValue::int(n as i32)) + } else { + Ok(JsValue::float(n)) + } +} + +pub fn number_is_nan(vm: &mut Vm, args: &[u8]) -> NativeResult { + if args.len() < 2 { + return Ok(JsValue::bool(false)); + } + let val = vm.reg(args[1]); + if val.is_double() { + Ok(JsValue::bool(val.as_double().is_nan())) + } else { + Ok(JsValue::bool(false)) + } +} + +pub fn number_is_finite(vm: &mut Vm, args: &[u8]) -> NativeResult { + if args.len() < 2 { + return Ok(JsValue::bool(false)); + } + let val = vm.reg(args[1]); + if val.is_int() { + return Ok(JsValue::bool(true)); + } + if val.is_double() { + return Ok(JsValue::bool(val.as_double().is_finite())); + } + let n = coercion::to_number(val, vm.kernel().string_forge().as_ref()); + Ok(JsValue::bool(n.is_finite())) +} + +pub fn number_parse_int(vm: &mut Vm, args: &[u8]) -> NativeResult { + if args.len() < 2 { + return Ok(JsValue::float(f64::NAN)); + } + let sf = vm.kernel().string_forge().as_ref(); + let s = coercion::to_string(sf, vm.reg(args[1])); + let s = s.trim(); + + if s.is_empty() { + return Ok(JsValue::float(f64::NAN)); + } + + let radix = if args.len() > 2 { + let r = coercion::to_number(vm.reg(args[2]), sf) as i32; + if r == 0 { + 10 + } else { + r.clamp(2, 36) + } + } else { + 10 + }; + + let (rest, hex) = if s.starts_with("0x") || s.starts_with("0X") { + if radix == 16 || radix == 0 || (args.len() <= 2) { + (s[2..].to_string(), true) + } else { + (s.to_string(), false) + } + } else { + (s.to_string(), false) + }; + + let actual_radix = if hex { 16u32 } else { radix as u32 }; + + if let Ok(n) = i32::from_str_radix(&rest, actual_radix) { + return Ok(JsValue::int(n)); + } + + Ok(JsValue::float(f64::NAN)) +} + +pub fn number_parse_float(vm: &mut Vm, args: &[u8]) -> NativeResult { + if args.len() < 2 { + return Ok(JsValue::float(f64::NAN)); + } + let sf = vm.kernel().string_forge().as_ref(); + let s = coercion::to_string(sf, vm.reg(args[1])); + let s = s.trim(); + + if s.is_empty() { + return Ok(JsValue::float(f64::NAN)); + } + + match fast_float::parse::(&s) { + Ok(v) => Ok(JsValue::float(v)), + Err(_) => Ok(JsValue::float(f64::NAN)), + } +} + +pub fn number_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + let n = coercion::to_number(vm.reg(args[0]), vm.kernel().string_forge().as_ref()); + let radix = if args.len() > 1 { + let r = coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as u32; + r.clamp(2, 36) + } else { + 10u32 + }; + + if radix == 10 { + if n.is_nan() { + return Ok(vm.intern("NaN")); + } + if n.is_infinite() { + if n.is_sign_positive() { + return Ok(vm.intern("Infinity")); + } + return Ok(vm.intern("-Infinity")); + } + if n.fract() == 0.0 && n >= i32::MIN as f64 && n <= i32::MAX as f64 { + return Ok(vm.intern(&(n as i64).to_string())); + } + let mut buf = ryu::Buffer::new(); + Ok(vm.intern(buf.format(n))) + } else { + if n.is_nan() { + return Ok(vm.intern("NaN")); + } + let nn = n as i64; + let mut result = String::new(); + let mut value = nn.abs(); + if value == 0 { + result.push('0'); + } else { + let chars = "0123456789abcdefghijklmnopqrstuvwxyz"; + let mut digits = Vec::new(); + while value > 0 { + digits.push(chars.as_bytes()[(value % radix as i64) as usize] as char); + value /= radix as i64; + } + for ch in digits.iter().rev() { + result.push(*ch); + } + } + if nn < 0 { + result.insert(0, '-'); + } + Ok(vm.intern(&result)) + } +} + +pub fn number_to_fixed(vm: &mut Vm, args: &[u8]) -> NativeResult { + let n = coercion::to_number(vm.reg(args[0]), vm.kernel().string_forge().as_ref()); + let digits = if args.len() > 1 { + coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as usize + } else { + 0usize + } + .min(20); + + let formatted = format!("{:.digits$}", n); + Ok(vm.intern(&formatted)) +} diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 48be491..1cd36f6 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -8,7 +8,9 @@ use oxide_compiler::opcode::{self, OpCode}; use crate::coercion; use crate::native::NativeFn; -use oxide_kernel::builtin::{ArrayMethods, ErrorMethods, ObjectMethods, StringMethods}; +use oxide_kernel::builtin::{ + ArrayMethods, ErrorMethods, NumberMethods, ObjectMethods, StringMethods, +}; use oxide_kernel::kernel::{KernelConfig, OxideKernel}; use oxide_kernel::prop_forge::PropTemplate; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; @@ -65,8 +67,7 @@ pub fn init_kernel_builtins(kernel: &Arc) { let cur_count = global.prop_count(); global.set_shape_id(obj_shape); global.set_prop_count(cur_count + 1); - let bump = bumpalo::Bump::new(); - global.set_prop_expand(cur_count, obj_val, &bump); + global.set_prop_expand_heap(cur_count, obj_val); let _array_methods = ArrayMethods { push: crate::builtins::array::array_push as *const (), @@ -102,8 +103,7 @@ pub fn init_kernel_builtins(kernel: &Arc) { let cur_count = global.prop_count(); global.set_shape_id(arr_shape); global.set_prop_count(cur_count + 1); - let bump2 = bumpalo::Bump::new(); - global.set_prop_expand(cur_count, arr_val, &bump2); + global.set_prop_expand_heap(cur_count, arr_val); global.bump_generation(); let error_methods = ErrorMethods { @@ -128,8 +128,7 @@ pub fn init_kernel_builtins(kernel: &Arc) { let cur_count = global.prop_count(); global.set_shape_id(err_shape); global.set_prop_count(cur_count + 1); - let bump3 = bumpalo::Bump::new(); - global.set_prop_expand(cur_count, err_val, &bump3); + global.set_prop_expand_heap(cur_count, err_val); global.bump_generation(); let string_methods = StringMethods { @@ -167,9 +166,61 @@ pub fn init_kernel_builtins(kernel: &Arc) { let cur_count = global.prop_count(); global.set_shape_id(str_shape); global.set_prop_count(cur_count + 1); - let bump4 = bumpalo::Bump::new(); - global.set_prop_expand(cur_count, str_val, &bump4); + global.set_prop_expand_heap(cur_count, str_val); global.bump_generation(); + + let number_methods = NumberMethods { + is_nan: crate::builtins::number::number_is_nan as *const (), + is_finite: crate::builtins::number::number_is_finite as *const (), + parse_int: crate::builtins::number::number_parse_int as *const (), + parse_float: crate::builtins::number::number_parse_float as *const (), + to_string: crate::builtins::number::number_to_string as *const (), + to_fixed: crate::builtins::number::number_to_fixed as *const (), + }; + kernel.builtin_world().bind_number_methods( + &number_methods, + kernel.string_forge().as_ref(), + kernel.shape_forge().as_ref(), + ); + + let si_num = kernel.string_forge().intern("Number").0; + let num_shape = kernel.shape_forge().make_shape(global.shape_id(), si_num); + let num_val = JsValue::from_js_object( + kernel.builtin_world().number_constructor.as_ptr() as *mut JsObject + ); + let cur_count = global.prop_count(); + global.set_shape_id(num_shape); + global.set_prop_count(cur_count + 1); + global.set_prop_expand_heap(cur_count, num_val); + global.bump_generation(); + + { + let num_ctor_ptr = kernel.builtin_world().number_constructor.as_ptr() as *mut JsObject; + let num_ctor = unsafe { &mut *num_ctor_ptr }; + num_ctor.set_native_fn(Some( + crate::builtins::number::number_constructor as *const (), + )); + num_ctor.set_native_arg_count(1); + } + + let pi_fn = crate::builtins::number::number_parse_int as *const (); + let pf_fn = crate::builtins::number::number_parse_float as *const (); + let _ = oxide_kernel::builtin::BuiltinWorld::bind_method( + global, + kernel.shape_forge().as_ref(), + kernel.string_forge().as_ref(), + "parseInt", + pi_fn, + 1, + ); + let _ = oxide_kernel::builtin::BuiltinWorld::bind_method( + global, + kernel.shape_forge().as_ref(), + kernel.string_forge().as_ref(), + "parseFloat", + pf_fn, + 1, + ); } impl Vm { @@ -605,6 +656,17 @@ impl Vm { continue; } } + if val.is_int() || val.is_double() { + let proto_ptr = + self.kernel.builtin_world().number_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &*proto_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + if let Some(resolved) = self.resolve_property(proto, prop_name_si) { + self.regs[a] = resolved; + self.pc += 1; + continue; + } + } return Err("IC_GET_PROP on non-object".into()); } let obj = unsafe { &*obj_ptr }; diff --git a/crates/oxide_vm/tests/builtins_number_tests.rs b/crates/oxide_vm/tests/builtins_number_tests.rs new file mode 100644 index 0000000..9479825 --- /dev/null +++ b/crates/oxide_vm/tests/builtins_number_tests.rs @@ -0,0 +1,100 @@ +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = + oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new() + .compile(&program) + .map_err(|e| format!("Compile error: {}", e))?; + vm.run(&module) +} + +#[test] +fn number_is_nan_with_nan() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Number.isNaN(NaN)").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn number_is_nan_with_number() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Number.isNaN(42)").unwrap(); + assert!(!result.as_bool()); +} + +#[test] +fn number_is_finite_with_number() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Number.isFinite(42)").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn number_is_finite_with_infinity() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Number.isFinite(1/0)").unwrap(); + assert!(!result.as_bool()); +} + +#[test] +fn parse_int_decimal() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "parseInt('42')").unwrap(); + assert_eq!(result.as_int(), 42); +} + +#[test] +fn parse_int_hex() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "parseInt('0xFF')").unwrap(); + assert_eq!(result.as_int(), 255); +} + +#[test] +fn parse_float_decimal() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "parseFloat('3.14')").unwrap(); + assert!((result.as_double() - 3.14).abs() < 0.001); +} + +#[test] +fn parse_float_invalid() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "parseFloat('abc')").unwrap(); + assert!(result.as_double().is_nan()); +} + +#[test] +fn number_constructor() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Number('42')").unwrap(); + assert_eq!(result.as_int(), 42); +} + +#[test] +fn to_string_of_number() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var n = 42; n.toString()").unwrap(); + let s = vm + .kernel() + .string_forge() + .lookup(result.as_string_index()) + .unwrap_or_default(); + assert_eq!(s, "42"); +} + +#[test] +fn to_fixed_of_number() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var n = 3.14159; n.toFixed(2)").unwrap(); + let s = vm + .kernel() + .string_forge() + .lookup(result.as_string_index()) + .unwrap_or_default(); + assert_eq!(s, "3.14"); +}