diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 0f90b2d0..6a431f32 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -199,6 +199,29 @@ pub struct BuiltinWorld { pub array_buffer_proto: P, pub data_view_constructor: P, pub data_view_proto: P, + pub typed_array_proto: P, + pub int8array_constructor: P, + pub int8array_proto: P, + pub uint8array_constructor: P, + pub uint8array_proto: P, + pub uint8clampedarray_constructor: P, + pub uint8clampedarray_proto: P, + pub int16array_constructor: P, + pub int16array_proto: P, + pub uint16array_constructor: P, + pub uint16array_proto: P, + pub int32array_constructor: P, + pub int32array_proto: P, + pub uint32array_constructor: P, + pub uint32array_proto: P, + pub float32array_constructor: P, + pub float32array_proto: P, + pub float64array_constructor: P, + pub float64array_proto: P, + pub bigint64array_constructor: P, + pub bigint64array_proto: P, + pub biguint64array_constructor: P, + pub biguint64array_proto: P, pub sym_match: P, pub sym_replace: P, pub sym_search: P, @@ -293,6 +316,30 @@ impl BuiltinWorld { make_pair(string_forge, shape_forge, "ArrayBuffer", si_prototype, si_constructor, si_name); let (data_view_proto, data_view_constructor) = make_pair(string_forge, shape_forge, "DataView", si_prototype, si_constructor, si_name); + let obj_proto_val = JsValue::from_js_object(object_proto.as_ptr() as *mut JsObject); + let typed_array_proto = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, obj_proto_val)); + let (int8array_proto, int8array_constructor) = + make_pair(string_forge, shape_forge, "Int8Array", si_prototype, si_constructor, si_name); + let (uint8array_proto, uint8array_constructor) = + make_pair(string_forge, shape_forge, "Uint8Array", si_prototype, si_constructor, si_name); + let (uint8clampedarray_proto, uint8clampedarray_constructor) = + make_pair(string_forge, shape_forge, "Uint8ClampedArray", si_prototype, si_constructor, si_name); + let (int16array_proto, int16array_constructor) = + make_pair(string_forge, shape_forge, "Int16Array", si_prototype, si_constructor, si_name); + let (uint16array_proto, uint16array_constructor) = + make_pair(string_forge, shape_forge, "Uint16Array", si_prototype, si_constructor, si_name); + let (int32array_proto, int32array_constructor) = + make_pair(string_forge, shape_forge, "Int32Array", si_prototype, si_constructor, si_name); + let (uint32array_proto, uint32array_constructor) = + make_pair(string_forge, shape_forge, "Uint32Array", si_prototype, si_constructor, si_name); + let (float32array_proto, float32array_constructor) = + make_pair(string_forge, shape_forge, "Float32Array", si_prototype, si_constructor, si_name); + let (float64array_proto, float64array_constructor) = + make_pair(string_forge, shape_forge, "Float64Array", si_prototype, si_constructor, si_name); + let (bigint64array_proto, bigint64array_constructor) = + make_pair(string_forge, shape_forge, "BigInt64Array", si_prototype, si_constructor, si_name); + let (biguint64array_proto, biguint64array_constructor) = + make_pair(string_forge, shape_forge, "BigUint64Array", si_prototype, si_constructor, si_name); let sym_match = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_replace = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); @@ -332,10 +379,20 @@ impl BuiltinWorld { wire_ctor_proto(&map_constructor, &map_proto); wire_ctor_proto(&array_buffer_constructor, &array_buffer_proto); wire_ctor_proto(&data_view_constructor, &data_view_proto); + wire_ctor_proto(&int8array_constructor, &int8array_proto); + wire_ctor_proto(&uint8array_constructor, &uint8array_proto); + wire_ctor_proto(&uint8clampedarray_constructor, &uint8clampedarray_proto); + wire_ctor_proto(&int16array_constructor, &int16array_proto); + wire_ctor_proto(&uint16array_constructor, &uint16array_proto); + wire_ctor_proto(&int32array_constructor, &int32array_proto); + wire_ctor_proto(&uint32array_constructor, &uint32array_proto); + wire_ctor_proto(&float32array_constructor, &float32array_proto); + wire_ctor_proto(&float64array_constructor, &float64array_proto); + wire_ctor_proto(&bigint64array_constructor, &bigint64array_proto); + wire_ctor_proto(&biguint64array_constructor, &biguint64array_proto); // Wire prototype chains: all constructor prototypes inherit from Object.prototype. - let obj_proto_val = JsValue::from_js_object(object_proto.as_ptr() as *mut JsObject); - let non_object_protos: [&P; 13] = [ + let non_object_protos: [&P; 14] = [ &array_proto, &function_proto, &string_proto, @@ -349,11 +406,30 @@ impl BuiltinWorld { ®exp_proto, &array_buffer_proto, &data_view_proto, + &typed_array_proto, ]; for proto in &non_object_protos { let p = proto.as_ptr() as *mut JsObject; unsafe { &mut *p }.set_proto(obj_proto_val).ok(); } + let typed_array_proto_val = JsValue::from_js_object(typed_array_proto.as_ptr() as *mut JsObject); + let typed_array_protos: [&P; 11] = [ + &int8array_proto, + &uint8array_proto, + &uint8clampedarray_proto, + &int16array_proto, + &uint16array_proto, + &int32array_proto, + &uint32array_proto, + &float32array_proto, + &float64array_proto, + &bigint64array_proto, + &biguint64array_proto, + ]; + for proto in &typed_array_protos { + let p = proto.as_ptr() as *mut JsObject; + unsafe { &mut *p }.set_proto(typed_array_proto_val).ok(); + } wire_ctor_proto(®exp_constructor, ®exp_proto); Self { @@ -393,6 +469,29 @@ impl BuiltinWorld { array_buffer_proto, data_view_constructor, data_view_proto, + typed_array_proto, + int8array_constructor, + int8array_proto, + uint8array_constructor, + uint8array_proto, + uint8clampedarray_constructor, + uint8clampedarray_proto, + int16array_constructor, + int16array_proto, + uint16array_constructor, + uint16array_proto, + int32array_constructor, + int32array_proto, + uint32array_constructor, + uint32array_proto, + float32array_constructor, + float32array_proto, + float64array_constructor, + float64array_proto, + bigint64array_constructor, + bigint64array_proto, + biguint64array_constructor, + biguint64array_proto, sym_match, sym_replace, sym_search, diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 0c001bb6..5d5fda37 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -41,6 +41,32 @@ unsafe impl Sync for NativeFnPtr {} pub type ShapeId = u32; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypedArrayKind { + Int8, + Uint8, + Uint8Clamped, + Int16, + Uint16, + Int32, + Uint32, + Float32, + Float64, + BigInt64, + BigUint64, +} + +impl TypedArrayKind { + pub const fn bytes_per_element(self) -> usize { + match self { + Self::Int8 | Self::Uint8 | Self::Uint8Clamped => 1, + Self::Int16 | Self::Uint16 => 2, + Self::Int32 | Self::Uint32 | Self::Float32 => 4, + Self::Float64 | Self::BigInt64 | Self::BigUint64 => 8, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct PropAttributes(pub u8); diff --git a/crates/oxide_vm/src/bindings/bind_typed_array.rs b/crates/oxide_vm/src/bindings/bind_typed_array.rs new file mode 100644 index 00000000..f5c09cd2 --- /dev/null +++ b/crates/oxide_vm/src/bindings/bind_typed_array.rs @@ -0,0 +1,110 @@ +use std::sync::Arc; + +use crate::bind_constructor_hash; +use crate::bindings::{apply_binding_table, configure_native_constructor}; +use oxide_kernel::kernel::{KernelCore, KernelSession}; +use oxide_types::object::JsObject; + +macro_rules! bind_typed_array_constructor { + ($core:expr, $global:expr, $name:literal, $ctor_ptr:expr, $ctor_fn:path) => {{ + let ctor = unsafe { &mut *$ctor_ptr }; + configure_native_constructor(ctor, $ctor_fn as *const (), 1); + bind_constructor_hash!($core, $global, $name, $ctor_ptr, $ctor_fn, 1); + }}; +} + +pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: &mut JsObject) { + let shared_proto_ptr = session.builtin_world().typed_array_proto.as_ptr() as *mut JsObject; + let shared_proto = unsafe { &mut *shared_proto_ptr }; + apply_binding_table( + session.builtin_world(), + shared_proto, + core, + &[ + ("at", crate::builtins::typed_array::typed_array_at as *const (), 1), + ("fill", crate::builtins::typed_array::typed_array_fill as *const (), 3), + ("set", crate::builtins::typed_array::typed_array_set as *const (), 2), + ("slice", crate::builtins::typed_array::typed_array_slice as *const (), 2), + ("subarray", crate::builtins::typed_array::typed_array_subarray as *const (), 2), + ("toString", crate::builtins::typed_array::typed_array_to_string as *const (), 0), + ], + ); + + bind_typed_array_constructor!( + core, + global, + "Int8Array", + session.builtin_world().int8array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::int8array_constructor + ); + bind_typed_array_constructor!( + core, + global, + "Uint8Array", + session.builtin_world().uint8array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::uint8array_constructor + ); + bind_typed_array_constructor!( + core, + global, + "Uint8ClampedArray", + session.builtin_world().uint8clampedarray_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::uint8clampedarray_constructor + ); + bind_typed_array_constructor!( + core, + global, + "Int16Array", + session.builtin_world().int16array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::int16array_constructor + ); + bind_typed_array_constructor!( + core, + global, + "Uint16Array", + session.builtin_world().uint16array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::uint16array_constructor + ); + bind_typed_array_constructor!( + core, + global, + "Int32Array", + session.builtin_world().int32array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::int32array_constructor + ); + bind_typed_array_constructor!( + core, + global, + "Uint32Array", + session.builtin_world().uint32array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::uint32array_constructor + ); + bind_typed_array_constructor!( + core, + global, + "Float32Array", + session.builtin_world().float32array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::float32array_constructor + ); + bind_typed_array_constructor!( + core, + global, + "Float64Array", + session.builtin_world().float64array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::float64array_constructor + ); + bind_typed_array_constructor!( + core, + global, + "BigInt64Array", + session.builtin_world().bigint64array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::bigint64array_constructor + ); + bind_typed_array_constructor!( + core, + global, + "BigUint64Array", + session.builtin_world().biguint64array_constructor.as_ptr() as *mut JsObject, + crate::builtins::typed_array::biguint64array_constructor + ); +} diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index ea659888..9df7d0af 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -18,6 +18,7 @@ pub mod bind_set; pub mod bind_string; pub mod bind_stubs; pub mod bind_symbol; +pub mod bind_typed_array; use std::sync::Arc; @@ -91,6 +92,7 @@ pub fn init_kernel_builtins(core: &Arc, session: &mut KernelSession) bind_array::bind_array(core, session, global); bind_array_buffer::bind_array_buffer(core, session, global); bind_data_view::bind_data_view(core, session, global); + bind_typed_array::bind_typed_array(core, session, global); bind_error::bind_error(core, session, global); bind_string::bind_string(core, session, global); bind_number::bind_number(core, session, global); diff --git a/crates/oxide_vm/src/builtins/array_buffer.rs b/crates/oxide_vm/src/builtins/array_buffer.rs index b93c5426..3ca8aebf 100644 --- a/crates/oxide_vm/src/builtins/array_buffer.rs +++ b/crates/oxide_vm/src/builtins/array_buffer.rs @@ -50,7 +50,7 @@ fn set_named_prop(vm: &mut Vm, obj: &mut JsObject, name: &str, value: JsValue, a let _ = vm.define_data_property(obj, si, value, attributes); } -fn new_array_buffer(vm: &mut Vm, data: Vec) -> *mut JsObject { +pub(crate) fn new_array_buffer(vm: &mut Vm, data: Vec) -> *mut JsObject { let proto = vm.session().builtin_world().array_buffer_proto.as_ptr() as *mut JsObject; let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto)); obj.type_tag = JsObject::OBJ_TYPE_ARRAY_BUFFER; diff --git a/crates/oxide_vm/src/builtins/mod.rs b/crates/oxide_vm/src/builtins/mod.rs index 5f15a8f4..30987140 100644 --- a/crates/oxide_vm/src/builtins/mod.rs +++ b/crates/oxide_vm/src/builtins/mod.rs @@ -18,3 +18,4 @@ pub mod set; pub mod string; pub mod stubs; pub mod symbol; +pub mod typed_array; diff --git a/crates/oxide_vm/src/builtins/typed_array.rs b/crates/oxide_vm/src/builtins/typed_array.rs new file mode 100644 index 00000000..b62c8907 --- /dev/null +++ b/crates/oxide_vm/src/builtins/typed_array.rs @@ -0,0 +1,413 @@ +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes, TypedArrayKind}; +use oxide_types::value::JsValue; + +use crate::builtins::array_buffer::{array_buffer_data_ptr, new_array_buffer}; +use crate::coercion; +use crate::native::NativeResult; +use crate::vm::Vm; + +#[derive(Clone, Copy)] +pub(crate) struct TypedArrayData { + pub kind: TypedArrayKind, + pub buffer: JsValue, + pub byte_offset: usize, + pub length: usize, +} + +macro_rules! native_try { + ($expr:expr) => { + match $expr { + Ok(value) => value, + Err(err) => return NativeResult::Err(err), + } + }; +} + +fn type_error(vm: &mut Vm, msg: &str) -> JsValue { + crate::builtins::error::create_type_error(vm, msg) +} + +fn range_error(vm: &mut Vm, msg: &str) -> JsValue { + crate::builtins::error::create_range_error(vm, msg) +} + +fn to_index(vm: &mut Vm, value: JsValue, msg: &str) -> Result { + let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref()); + if n.is_nan() { + return Ok(0); + } + if !n.is_finite() || n < 0.0 { + return Err(range_error(vm, msg)); + } + Ok(n.trunc() as usize) +} + +fn normalize_index(vm: &mut Vm, value: JsValue, len: usize) -> usize { + let n = coercion::to_number(value, vm.kernel_core().string_forge().as_ref()); + if n.is_nan() { + return 0; + } + let int = n.trunc() as isize; + if int < 0 { + len.saturating_sub((-int) as usize) + } else { + (int as usize).min(len) + } +} + +fn set_named_prop(vm: &mut Vm, obj: &mut JsObject, name: &str, value: JsValue, attributes: PropAttributes) { + let si = vm.kernel_core().string_forge().intern(name).0; + let _ = vm.define_data_property(obj, si, value, attributes); +} + +fn typed_array_proto_ptr(vm: &mut Vm, kind: TypedArrayKind) -> *mut JsObject { + let world = vm.session().builtin_world(); + match kind { + TypedArrayKind::Int8 => world.int8array_proto.as_ptr() as *mut JsObject, + TypedArrayKind::Uint8 => world.uint8array_proto.as_ptr() as *mut JsObject, + TypedArrayKind::Uint8Clamped => world.uint8clampedarray_proto.as_ptr() as *mut JsObject, + TypedArrayKind::Int16 => world.int16array_proto.as_ptr() as *mut JsObject, + TypedArrayKind::Uint16 => world.uint16array_proto.as_ptr() as *mut JsObject, + TypedArrayKind::Int32 => world.int32array_proto.as_ptr() as *mut JsObject, + TypedArrayKind::Uint32 => world.uint32array_proto.as_ptr() as *mut JsObject, + TypedArrayKind::Float32 => world.float32array_proto.as_ptr() as *mut JsObject, + TypedArrayKind::Float64 => world.float64array_proto.as_ptr() as *mut JsObject, + TypedArrayKind::BigInt64 => world.bigint64array_proto.as_ptr() as *mut JsObject, + TypedArrayKind::BigUint64 => world.biguint64array_proto.as_ptr() as *mut JsObject, + } +} + +fn create_typed_array( + vm: &mut Vm, kind: TypedArrayKind, buffer: JsValue, byte_offset: usize, length: usize, +) -> *mut JsObject { + let proto = typed_array_proto_ptr(vm, kind); + let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(proto)); + obj.type_tag = JsObject::OBJ_TYPE_TYPED_ARRAY; + let data = Box::into_raw(Box::new(TypedArrayData { + kind, + buffer, + byte_offset, + length, + })); + // SAFETY: TypedArray instances are not callable; native_fn stores opaque Box, + // matching ArrayBuffer/DataView typed-object storage in this VM. + obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(data as *const ()) })); + + let byte_length = length.saturating_mul(kind.bytes_per_element()); + set_named_prop(vm, &mut obj, "buffer", buffer, PropAttributes::new(false, false, false)); + set_named_prop( + vm, + &mut obj, + "byteOffset", + JsValue::int(byte_offset as i32), + PropAttributes::new(false, false, false), + ); + set_named_prop( + vm, + &mut obj, + "byteLength", + JsValue::int(byte_length as i32), + PropAttributes::new(false, false, false), + ); + set_named_prop( + vm, + &mut obj, + "length", + JsValue::int(length as i32), + PropAttributes::new(false, false, false), + ); + vm.epoch().alloc(obj) +} + +pub(crate) fn get_typed_array_data(vm: &mut Vm, this_val: JsValue) -> Result { + if !this_val.is_object() { + return Err(type_error(vm, "TypedArray method called on incompatible receiver")); + } + let obj_ptr = this_val.as_js_object_ptr(); + if obj_ptr.is_null() { + return Err(type_error(vm, "TypedArray internal state invalid")); + } + let obj = unsafe { &*obj_ptr }; + if !obj.is_typed_array_obj() { + return Err(type_error(vm, "TypedArray method called on incompatible receiver")); + } + let Some(data_ptr) = obj.native_fn() else { + return Err(type_error(vm, "TypedArray internal state invalid")); + }; + let data_ptr = data_ptr.as_ptr() as *const TypedArrayData; + if data_ptr.is_null() { + return Err(type_error(vm, "TypedArray internal state invalid")); + } + Ok(unsafe { *data_ptr }) +} + +fn absolute_byte_offset(view: TypedArrayData, index: usize) -> usize { + view.byte_offset + index * view.kind.bytes_per_element() +} + +fn read_element(kind: TypedArrayKind, bytes: &[u8], offset: usize) -> JsValue { + match kind { + TypedArrayKind::Int8 => JsValue::int(bytes[offset] as i8 as i32), + TypedArrayKind::Uint8 | TypedArrayKind::Uint8Clamped => JsValue::int(bytes[offset] as i32), + TypedArrayKind::Int16 => JsValue::int(i16::from_ne_bytes(bytes[offset..offset + 2].try_into().unwrap()) as i32), + TypedArrayKind::Uint16 => { + JsValue::int(u16::from_ne_bytes(bytes[offset..offset + 2].try_into().unwrap()) as i32) + } + TypedArrayKind::Int32 => JsValue::int(i32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap())), + TypedArrayKind::Uint32 => { + JsValue::float(u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()) as f64) + } + TypedArrayKind::Float32 => { + JsValue::float(f32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()) as f64) + } + TypedArrayKind::Float64 => JsValue::float(f64::from_ne_bytes(bytes[offset..offset + 8].try_into().unwrap())), + TypedArrayKind::BigInt64 => { + JsValue::float(i64::from_ne_bytes(bytes[offset..offset + 8].try_into().unwrap()) as f64) + } + TypedArrayKind::BigUint64 => { + JsValue::float(u64::from_ne_bytes(bytes[offset..offset + 8].try_into().unwrap()) as f64) + } + } +} + +fn numeric_value(vm: &mut Vm, value: JsValue) -> f64 { + coercion::to_number(value, vm.kernel_core().string_forge().as_ref()) +} + +fn write_element(kind: TypedArrayKind, bytes: &mut [u8], offset: usize, value: f64) { + match kind { + TypedArrayKind::Int8 => bytes[offset] = value as i32 as u8 as i8 as u8, + TypedArrayKind::Uint8 => bytes[offset] = value as i32 as u8, + TypedArrayKind::Uint8Clamped => bytes[offset] = value.clamp(0.0, 255.0).round() as u8, + TypedArrayKind::Int16 => bytes[offset..offset + 2].copy_from_slice(&(value as i32 as u16 as i16).to_ne_bytes()), + TypedArrayKind::Uint16 => bytes[offset..offset + 2].copy_from_slice(&(value as i32 as u16).to_ne_bytes()), + TypedArrayKind::Int32 => bytes[offset..offset + 4].copy_from_slice(&(value as i32).to_ne_bytes()), + TypedArrayKind::Uint32 => bytes[offset..offset + 4].copy_from_slice(&(value as u32).to_ne_bytes()), + TypedArrayKind::Float32 => bytes[offset..offset + 4].copy_from_slice(&(value as f32).to_ne_bytes()), + TypedArrayKind::Float64 => bytes[offset..offset + 8].copy_from_slice(&value.to_ne_bytes()), + TypedArrayKind::BigInt64 => bytes[offset..offset + 8].copy_from_slice(&(value as i64).to_ne_bytes()), + TypedArrayKind::BigUint64 => bytes[offset..offset + 8].copy_from_slice(&(value as u64).to_ne_bytes()), + } +} + +fn collect_array_like(vm: &mut Vm, value: JsValue) -> Result, JsValue> { + if !value.is_object() { + return Err(type_error(vm, "TypedArray source must be array-like")); + } + let ptr = value.as_js_object_ptr(); + if ptr.is_null() { + return Err(type_error(vm, "TypedArray source must be array-like")); + } + let obj = unsafe { &*ptr }; + if obj.is_array() { + let len = obj.prop_count() as usize; + return Ok((0..len).map(|i| obj.get_prop_at(i)).collect()); + } + if obj.is_typed_array_obj() { + let view = get_typed_array_data(vm, value)?; + let buffer_ptr = array_buffer_data_ptr(vm, view.buffer)?; + let buffer = unsafe { &*buffer_ptr }; + return Ok((0..view.length) + .map(|i| read_element(view.kind, buffer, absolute_byte_offset(view, i))) + .collect()); + } + Err(type_error(vm, "TypedArray source must be Array, ArrayBuffer, or TypedArray")) +} + +fn typed_array_new(vm: &mut Vm, args: &[u8], kind: TypedArrayKind) -> NativeResult { + let first = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::int(0) }; + let bpe = kind.bytes_per_element(); + + let (buffer, byte_offset, length) = if first.is_int() || first.is_double() || first.is_undefined() { + let len = native_try!(to_index(vm, first, "invalid TypedArray length")); + let Some(byte_len) = len.checked_mul(bpe) else { + return NativeResult::Err(range_error(vm, "invalid TypedArray length")); + }; + let buffer = JsValue::from_js_object(new_array_buffer(vm, vec![0; byte_len])); + (buffer, 0, len) + } else if first.is_object() { + let first_ptr = first.as_js_object_ptr(); + let first_obj = unsafe { &*first_ptr }; + if first_obj.is_array_buffer_obj() { + let buffer_ptr = native_try!(array_buffer_data_ptr(vm, first)); + let buffer_len = unsafe { (*buffer_ptr).len() }; + let byte_offset = if args.len() > 2 { + native_try!(to_index(vm, vm.reg(args[2]), "TypedArray byteOffset out of bounds")) + } else { + 0 + }; + if byte_offset > buffer_len || byte_offset % bpe != 0 { + return NativeResult::Err(range_error(vm, "TypedArray byteOffset out of bounds")); + } + let remaining = buffer_len - byte_offset; + let length = if args.len() > 3 { + native_try!(to_index(vm, vm.reg(args[3]), "TypedArray length out of bounds")) + } else { + remaining / bpe + }; + let Some(byte_length) = length.checked_mul(bpe) else { + return NativeResult::Err(range_error(vm, "TypedArray length out of bounds")); + }; + if byte_length > remaining { + return NativeResult::Err(range_error(vm, "TypedArray length out of bounds")); + } + (first, byte_offset, length) + } else { + let values = native_try!(collect_array_like(vm, first)); + let byte_len = values.len().saturating_mul(bpe); + let buffer = JsValue::from_js_object(new_array_buffer(vm, vec![0; byte_len])); + let buffer_ptr = native_try!(array_buffer_data_ptr(vm, buffer)); + let buffer_ref = unsafe { &mut *buffer_ptr }; + for (idx, value) in values.into_iter().enumerate() { + let n = numeric_value(vm, value); + write_element(kind, buffer_ref, idx * bpe, n); + } + (buffer, 0, byte_len / bpe) + } + } else { + return NativeResult::Err(type_error(vm, "invalid TypedArray constructor argument")); + }; + + NativeResult::Ok(JsValue::from_js_object(create_typed_array(vm, kind, buffer, byte_offset, length))) +} + +macro_rules! typed_array_ctor { + ($name:ident, $kind:ident) => { + pub fn $name(vm: &mut Vm, args: &[u8]) -> NativeResult { + typed_array_new(vm, args, TypedArrayKind::$kind) + } + }; +} + +typed_array_ctor!(int8array_constructor, Int8); +typed_array_ctor!(uint8array_constructor, Uint8); +typed_array_ctor!(uint8clampedarray_constructor, Uint8Clamped); +typed_array_ctor!(int16array_constructor, Int16); +typed_array_ctor!(uint16array_constructor, Uint16); +typed_array_ctor!(int32array_constructor, Int32); +typed_array_ctor!(uint32array_constructor, Uint32); +typed_array_ctor!(float32array_constructor, Float32); +typed_array_ctor!(float64array_constructor, Float64); +typed_array_ctor!(bigint64array_constructor, BigInt64); +typed_array_ctor!(biguint64array_constructor, BigUint64); + +pub fn typed_array_at(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let view = native_try!(get_typed_array_data(vm, this_val)); + let raw_index = if args.len() > 1 { + numeric_value(vm, vm.reg(args[1])).trunc() as isize + } else { + 0 + }; + let idx = if raw_index < 0 { view.length as isize + raw_index } else { raw_index }; + if idx < 0 || idx as usize >= view.length { + return NativeResult::Ok(JsValue::undefined()); + } + let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer)); + let buffer = unsafe { &*buffer_ptr }; + NativeResult::Ok(read_element(view.kind, buffer, absolute_byte_offset(view, idx as usize))) +} + +pub fn typed_array_fill(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let view = native_try!(get_typed_array_data(vm, this_val)); + let value = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() }; + let start = if args.len() > 2 { + normalize_index(vm, vm.reg(args[2]), view.length) + } else { + 0 + }; + let end = if args.len() > 3 { + normalize_index(vm, vm.reg(args[3]), view.length) + } else { + view.length + }; + let n = numeric_value(vm, value); + let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer)); + let buffer = unsafe { &mut *buffer_ptr }; + for idx in start..end.max(start) { + write_element(view.kind, buffer, absolute_byte_offset(view, idx), n); + } + NativeResult::Ok(this_val) +} + +pub fn typed_array_slice(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let view = native_try!(get_typed_array_data(vm, this_val)); + let start = if args.len() > 1 { + normalize_index(vm, vm.reg(args[1]), view.length) + } else { + 0 + }; + let end = if args.len() > 2 { + normalize_index(vm, vm.reg(args[2]), view.length) + } else { + view.length + }; + let count = end.max(start).saturating_sub(start); + let bpe = view.kind.bytes_per_element(); + let src_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer)); + let src = unsafe { &*src_ptr }; + let mut out = vec![0u8; count * bpe]; + let src_start = absolute_byte_offset(view, start); + let src_end = src_start + count * bpe; + out.copy_from_slice(&src[src_start..src_end]); + let buffer = JsValue::from_js_object(new_array_buffer(vm, out)); + NativeResult::Ok(JsValue::from_js_object(create_typed_array(vm, view.kind, buffer, 0, count))) +} + +pub fn typed_array_subarray(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let view = native_try!(get_typed_array_data(vm, this_val)); + let start = if args.len() > 1 { + normalize_index(vm, vm.reg(args[1]), view.length) + } else { + 0 + }; + let end = if args.len() > 2 { + normalize_index(vm, vm.reg(args[2]), view.length) + } else { + view.length + }; + let count = end.max(start).saturating_sub(start); + let byte_offset = view.byte_offset + start * view.kind.bytes_per_element(); + NativeResult::Ok(JsValue::from_js_object(create_typed_array( + vm, + view.kind, + view.buffer, + byte_offset, + count, + ))) +} + +pub fn typed_array_set(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + let view = native_try!(get_typed_array_data(vm, this_val)); + if args.len() < 2 { + return NativeResult::Err(type_error(vm, "TypedArray.set source required")); + } + let source = vm.reg(args[1]); + let offset = if args.len() > 2 { + native_try!(to_index(vm, vm.reg(args[2]), "TypedArray.set offset out of bounds")) + } else { + 0 + }; + let values = native_try!(collect_array_like(vm, source)); + if offset > view.length || values.len() > view.length - offset { + return NativeResult::Err(range_error(vm, "TypedArray.set offset out of bounds")); + } + let numbers: Vec = values.into_iter().map(|v| numeric_value(vm, v)).collect(); + let buffer_ptr = native_try!(array_buffer_data_ptr(vm, view.buffer)); + let buffer = unsafe { &mut *buffer_ptr }; + for (i, n) in numbers.into_iter().enumerate() { + write_element(view.kind, buffer, absolute_byte_offset(view, offset + i), n); + } + NativeResult::Ok(JsValue::undefined()) +} + +pub fn typed_array_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + native_try!(get_typed_array_data(vm, this_val)); + NativeResult::Ok(vm.intern("[object TypedArray]")) +} diff --git a/crates/oxide_vm/tests/builtins_typed_array_tests.rs b/crates/oxide_vm/tests/builtins_typed_array_tests.rs new file mode 100644 index 00000000..36a81d7f --- /dev/null +++ b/crates/oxide_vm/tests/builtins_typed_array_tests.rs @@ -0,0 +1,139 @@ +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) +} + +fn to_str(vm: &Vm, val: JsValue) -> String { + vm.kernel_core() + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default() +} + +#[test] +fn typed_array_globals_exist() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "typeof Int8Array === 'function' && typeof Uint8Array === 'function' && \ + typeof Uint8ClampedArray === 'function' && typeof Int16Array === 'function' && \ + typeof Uint16Array === 'function' && typeof Int32Array === 'function' && \ + typeof Uint32Array === 'function' && typeof Float32Array === 'function' && \ + typeof Float64Array === 'function' && typeof BigInt64Array === 'function' && \ + typeof BigUint64Array === 'function'", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_length_constructor_sets_metadata() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var ta = new Int16Array(4); ta.length === 4 && ta.byteLength === 8 && ta.byteOffset === 0", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_fill_and_at_read_elements() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var ta = new Int8Array(4); ta.fill(7); ta.at(0) === 7 && ta.at(3) === 7").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_uint8_clamped_clamps_values() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var ta = new Uint8ClampedArray(2); ta.fill(300); ta.at(0) === 255").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_slice_copies_values() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var ta = new Int8Array([1, 2, 3, 4]); var out = ta.slice(1, 3); \ + out.length === 2 && out.at(0) === 2 && out.at(1) === 3", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_subarray_shares_buffer() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var ta = new Int8Array([1, 2, 3, 4]); var sub = ta.subarray(1, 3); \ + sub.fill(9); ta.at(1) === 9 && ta.at(2) === 9", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_set_copies_from_array() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var ta = new Int32Array(4); var src = [5, 6]; var offset = 1; \ + ta.set(src, offset); ta.at(0) === 0 && ta.at(1) === 5 && ta.at(2) === 6", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_constructs_from_array_buffer() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var buf = new ArrayBuffer(8); var view = new Int32Array(buf); \ + view.length === 2 && view.byteLength === 8 && view.buffer === buf && ArrayBuffer.isView(view)", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_views_share_data_with_data_view() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var buf = new ArrayBuffer(8); var dv = new DataView(buf); var ta = new Uint8Array(buf); \ + ta.fill(12); dv.getUint8(0) === 12 && dv.getUint8(7) === 12", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_float64_roundtrips() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var ta = new Float64Array(1); ta.fill(3.5); ta.at(0)").unwrap(); + assert_eq!(result.as_double(), 3.5); +} + +#[test] +fn typed_array_bigint_fallback_returns_float() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var ta = new BigInt64Array(1); ta.fill(42); ta.at(0)").unwrap(); + assert_eq!(result.as_double(), 42.0); +} + +#[test] +fn typed_array_to_string_is_identifiable() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var ta = new Uint8Array(1); ta.toString()").unwrap(); + assert_eq!(to_str(&vm, result), "[object TypedArray]"); +}