diff --git a/crates/oxide_builtins/src/typed_array.rs b/crates/oxide_builtins/src/typed_array.rs index c73628e..8c70901 100644 --- a/crates/oxide_builtins/src/typed_array.rs +++ b/crates/oxide_builtins/src/typed_array.rs @@ -227,6 +227,67 @@ pub fn typed_array_element_get(vm: &mut H, obj: &JsObject, index: u32 Ok(read_element(view.kind, buffer, absolute_byte_offset(view, index as usize))) } +/// 若对象是 TypedArray 且属性键是整数索引,返回 `(索引, 视图长度)`;否则 `None`。 +/// 供 VM 在 receiver ≠ TA 时裁决整数索引的写前语义(越界直接返回,界内落到 receiver)。 +pub fn typed_array_integer_index(vm: &mut H, obj: &JsObject, prop_name_si: u32) -> Option<(usize, usize)> { + let ptr = typed_array_data_ptr(obj)?; + if ptr.is_null() { + return None; + } + let view = unsafe { *ptr }; + let key = vm.kernel_core().perm_interner().lookup(prop_name_si)?; + if key.is_empty() || (key.len() > 1 && key.starts_with('0')) { + return None; + } + let index = key.parse::().ok()?; + Some((index as usize, view.length)) +} + +/// 写 TypedArray 指定整数索引的元素(供 VM 普通属性 set 的 typed 分支使用)。 +/// 越界忽略(不创建属性、不报错);内部状态非法返回 Err(String)。 +pub fn typed_array_element_set( + vm: &mut H, obj: &JsObject, index: u32, value: JsValue, +) -> Result<(), String> { + let this_val = JsValue::from_js_object(obj as *const JsObject as *mut JsObject); + let view = get_typed_array_data(vm, this_val).map_err(|e| format!("{e}"))?; + write_typed_array_element(vm, view, index, value) +} + +/// 定义 TypedArray 整数索引元素(defineProperty 语义)。 +/// +/// # 边界与前提 +/// - 索引越界:拒绝定义(Err),与整数索引 exotic 对象的 [[DefineOwnProperty]] 一致 +/// +/// # 副作用 +/// - 把值 ToNumber 后写入底层 buffer +// ponytail: 不校验 writable 描述符——调用方把"省略 writable"折叠为 false, +// 无法与显式 false 区分;TA 元素天然可写,直接写入。 +pub fn typed_array_element_define( + vm: &mut H, obj: &JsObject, index: u32, value: JsValue, +) -> Result<(), String> { + let this_val = JsValue::from_js_object(obj as *const JsObject as *mut JsObject); + let view = get_typed_array_data(vm, this_val).map_err(|e| format!("{e}"))?; + if index as usize >= view.length { + return Err("cannot define property: TypedArray index out of range".to_string()); + } + write_typed_array_element(vm, view, index, value) +} + +fn write_typed_array_element( + vm: &mut H, view: TypedArrayData, index: u32, value: JsValue, +) -> Result<(), String> { + // 先 ToNumber(valueOf 副作用先于越界判定触发),越界再静默忽略。 + let n = numeric_value(vm, value); + if index as usize >= view.length { + return Ok(()); + } + let buffer_ptr = array_buffer_data_ptr(vm, view.buffer).map_err(|e| format!("{e}"))?; + // SAFETY: buffer_ptr 经 array_buffer_data_ptr 校验为合法 ArrayBuffer。 + let buffer = unsafe { &mut *buffer_ptr }; + write_element(view.kind, buffer, absolute_byte_offset(view, index as usize), n); + Ok(()) +} + fn read_element(kind: TypedArrayKind, bytes: &[u8], offset: usize) -> JsValue { match kind { TypedArrayKind::Int8 => JsValue::int(bytes[offset] as i8 as i32), diff --git a/crates/oxide_vm/src/bindings/bind_typed_array.rs b/crates/oxide_vm/src/bindings/bind_typed_array.rs index f09f909..bd88ec9 100644 --- a/crates/oxide_vm/src/bindings/bind_typed_array.rs +++ b/crates/oxide_vm/src/bindings/bind_typed_array.rs @@ -3,13 +3,40 @@ use std::sync::Arc; use crate::bind_constructor; use crate::bindings::{apply_binding_table, configure_native_constructor}; use oxide_kernel::kernel::{KernelCore, KernelSession}; -use oxide_types::object::JsObject; +use oxide_types::object::{JsObject, PropAttributes}; +use oxide_types::value::JsValue; + +/// 给构造器与对应原型设置 `BYTES_PER_ELEMENT`(只读、不可枚举、不可配置)。 +/// +/// # 步骤 +/// 1. 构造器走 hash_props 存储(与 `bind_constructor` 的 hash 路径一致)。 +/// 2. 原型走 shape 属性存储(与 `bind_method`/`bind_accessor_getter` 一致)。 +fn set_bypes_per_element(core: &Arc, ctor_ptr: *mut JsObject, proto_ptr: *mut JsObject, bpe: u32) { + let bpe_si = core.perm_interner().intern("BYTES_PER_ELEMENT").0; + let bpe_val = JsValue::int(bpe as i32); + let attrs = PropAttributes::new(false, false, false); + + let ctor = unsafe { &mut *ctor_ptr }; + ctor.ensure_hash_props().push(bpe_val); + let ctor_pos = ctor.ensure_hash_props().len() - 1; + let ctor_shape = core.shape_forge().make_shape(ctor.shape_id(), bpe_si); + ctor.set_shape_id(ctor_shape); + ctor.set_data_meta(ctor_pos as u32, attrs); + + let proto = unsafe { &mut *proto_ptr }; + let proto_shape = core.shape_forge().make_shape(proto.shape_id(), bpe_si); + proto.set_shape_id(proto_shape); + let proto_pos = proto.push_prop(bpe_val); + proto.set_data_meta(proto_pos as u32, attrs); + proto.bump_generation(); +} macro_rules! bind_typed_array_constructor { - ($core:expr, $global:expr, $name:literal, $ctor_ptr:expr, $ctor_fn:path) => {{ + ($core:expr, $global:expr, $name:literal, $ctor_ptr:expr, $proto_ptr:expr, $bpe:expr, $ctor_fn:path) => {{ let ctor = unsafe { &mut *$ctor_ptr }; configure_native_constructor(ctor, ($ctor_fn as fn(&mut $crate::vm::Vm, &[u8]) -> oxide_runtime_api::NativeResult) as *const (), 1); bind_constructor!($core, $global, $name, $ctor_ptr, $ctor_fn, 1, hash: true); + set_bypes_per_element($core, $ctor_ptr, $proto_ptr, $bpe); }}; } @@ -44,6 +71,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "Int8Array", session.builtin_world().int8array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().int8array_proto.as_ptr() as *mut JsObject, + 1, oxide_builtins::typed_array::int8array_constructor ); bind_typed_array_constructor!( @@ -51,6 +80,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "Uint8Array", session.builtin_world().uint8array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().uint8array_proto.as_ptr() as *mut JsObject, + 1, oxide_builtins::typed_array::uint8array_constructor ); bind_typed_array_constructor!( @@ -58,6 +89,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "Uint8ClampedArray", session.builtin_world().uint8clampedarray_constructor.as_ptr() as *mut JsObject, + session.builtin_world().uint8clampedarray_proto.as_ptr() as *mut JsObject, + 1, oxide_builtins::typed_array::uint8clampedarray_constructor ); bind_typed_array_constructor!( @@ -65,6 +98,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "Int16Array", session.builtin_world().int16array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().int16array_proto.as_ptr() as *mut JsObject, + 2, oxide_builtins::typed_array::int16array_constructor ); bind_typed_array_constructor!( @@ -72,6 +107,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "Uint16Array", session.builtin_world().uint16array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().uint16array_proto.as_ptr() as *mut JsObject, + 2, oxide_builtins::typed_array::uint16array_constructor ); bind_typed_array_constructor!( @@ -79,6 +116,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "Int32Array", session.builtin_world().int32array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().int32array_proto.as_ptr() as *mut JsObject, + 4, oxide_builtins::typed_array::int32array_constructor ); bind_typed_array_constructor!( @@ -86,6 +125,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "Uint32Array", session.builtin_world().uint32array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().uint32array_proto.as_ptr() as *mut JsObject, + 4, oxide_builtins::typed_array::uint32array_constructor ); bind_typed_array_constructor!( @@ -93,6 +134,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "Float32Array", session.builtin_world().float32array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().float32array_proto.as_ptr() as *mut JsObject, + 4, oxide_builtins::typed_array::float32array_constructor ); bind_typed_array_constructor!( @@ -100,6 +143,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "Float64Array", session.builtin_world().float64array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().float64array_proto.as_ptr() as *mut JsObject, + 8, oxide_builtins::typed_array::float64array_constructor ); bind_typed_array_constructor!( @@ -107,6 +152,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "BigInt64Array", session.builtin_world().bigint64array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().bigint64array_proto.as_ptr() as *mut JsObject, + 8, oxide_builtins::typed_array::bigint64array_constructor ); bind_typed_array_constructor!( @@ -114,6 +161,8 @@ pub fn bind_typed_array(core: &Arc, session: &KernelSession, global: global, "BigUint64Array", session.builtin_world().biguint64array_constructor.as_ptr() as *mut JsObject, + session.builtin_world().biguint64array_proto.as_ptr() as *mut JsObject, + 8, oxide_builtins::typed_array::biguint64array_constructor ); } diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 23ba644..6886376 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -164,6 +164,16 @@ impl Vm { prop_name_si, use_frame_push ); + // TypedArray 整数索引:receiver 为 TA 本体时写底层 buffer(越界静默忽略); + // receiver 非 TA 时按规范把写入落到 receiver 对象,不碰 TA buffer。 + if obj.is_typed_array_obj() { + if let Some(index) = self.array_index_from_property_key(prop_name_si) { + if receiver.as_js_object_ptr() != obj as *const JsObject as *mut JsObject { + return self.set_to_receiver(obj, prop_name_si, val, receiver, index as usize, use_frame_push); + } + return oxide_builtins::typed_array::typed_array_element_set(self, obj, index, val); + } + } if let Some(pos) = self.get_own_property_slot(obj, prop_name_si) { if let Some(meta) = obj.prop_meta_at(pos) { if meta.is_accessor { @@ -197,6 +207,29 @@ impl Vm { Ok(()) } + /// TypedArray 整数索引在 `receiver` ≠ TA 时的 [[Set]] 语义:越界或非对象 + /// receiver 直接返回 true(不写、不 ToNumber);界内对象 receiver 走普通 set + /// 把属性落到 receiver 自身。 + fn set_to_receiver( + &mut self, ta_obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, index: usize, + use_frame_push: bool, + ) -> Result<(), String> { + let Some((_, length)) = oxide_builtins::typed_array::typed_array_integer_index(self, ta_obj, prop_name_si) + else { + return Ok(()); + }; + if index >= length { + return Ok(()); + } + let receiver_ptr = receiver.as_js_object_ptr(); + if receiver_ptr.is_null() { + return Ok(()); + } + let promoted = self.promote_if_needed_for_write_ptr(receiver_ptr, val); + let receiver_obj = unsafe { &mut *receiver_ptr }; + self.ordinary_set_inner(receiver_obj, prop_name_si, promoted, receiver, use_frame_push) + } + fn call_or_push_setter( &mut self, setter: JsValue, receiver: JsValue, val: JsValue, use_frame_push: bool, ) -> Result<(), String> { @@ -310,6 +343,13 @@ impl Vm { pub(crate) fn set_or_create_prop_value(&mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue) { vm_trace!("set_or_create_prop_value: shape_id={} prop_name_si={}", obj.shape_id(), prop_name_si); + // TypedArray 整数索引写 buffer(越界忽略),不进入 shape/prop 槽。 + if obj.is_typed_array_obj() { + if let Some(index) = self.array_index_from_property_key(prop_name_si) { + let _ = oxide_builtins::typed_array::typed_array_element_set(self, obj, index, val); + return; + } + } // 数组下标键写入元素区(维护 array_prop_count),不进入 shape 链。 if obj.is_array() { if let Some(index) = self.array_index_from_property_key(prop_name_si) { @@ -333,6 +373,12 @@ impl Vm { ) -> Result<(), String> { vm_trace!("define_data_property: shape={} prop_si={}", obj.shape_id(), prop_name_si); let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); + // TypedArray 整数索引:走元素定义(界内写 buffer,越界/非法描述符拒绝)。 + if obj.is_typed_array_obj() { + if let Some(index) = self.array_index_from_property_key(prop_name_si) { + return oxide_builtins::typed_array::typed_array_element_define(self, obj, index, val); + } + } if obj.is_array() { if let Some(index) = self.array_index_from_property_key(prop_name_si) { // 数组索引属性存元素区并维护 array_prop_count(元素数随索引增长)。 diff --git a/crates/oxide_vm/tests/builtins_typed_array_tests.rs b/crates/oxide_vm/tests/builtins_typed_array_tests.rs index 4c6ab57..617bd44 100644 --- a/crates/oxide_vm/tests/builtins_typed_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_typed_array_tests.rs @@ -134,3 +134,67 @@ fn typed_array_to_string_is_identifiable() { let result = eval(&mut vm, "var ta = new Uint8Array(1); ta.toString()").unwrap(); assert_eq!(to_str(&vm, result), "[object TypedArray]"); } + +#[test] +fn typed_array_element_write_roundtrips() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var ta = new Int32Array(2); ta[0] = 42; ta[1] = ta[0] * 2; ta[1] === 84", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_element_write_clamps_int8() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var ta = new Int8Array(1); ta[0] = 300; ta[0]").unwrap(); + assert_eq!(result.as_int(), 44); +} + +#[test] +fn typed_array_out_of_bounds_write_is_ignored() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var ta = new Int8Array(1); ta[5] = 1; ta.length === 1 && ta[5] === undefined", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_bypes_per_element_on_ctor_and_proto() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "Int8Array.BYTES_PER_ELEMENT === 1 && Float64Array.BYTES_PER_ELEMENT === 8 && \ + Int8Array.prototype.BYTES_PER_ELEMENT === 1 && Uint32Array.prototype.BYTES_PER_ELEMENT === 4", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_define_property_writes_element() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var ta = new Uint8Array(3); Object.defineProperty(ta, 1, { value: 9 }); ta[1] === 9", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn typed_array_reflect_set_writes_to_receiver() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var ta = new Uint8Array(3); var o = {}; \ + Reflect.set(ta, 1, 5, o) && o[1] === 5 && ta[1] === 0", + ) + .unwrap(); + assert!(result.as_bool()); +}