diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index de2c4c63..458b1303 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -131,7 +131,7 @@ impl Vm { ); // 函数名推断:emit 端在变量声明/对象属性赋值点设置 function_name。 let func_obj = unsafe { &mut *result.as_js_object_ptr() }; - let length_si = self.kernel_core.perm_interner().intern("length").0; + let length_si = self.length_si; let name_si = self.kernel_core.perm_interner().intern("name").0; // length/name 描述符均为不可写、不可枚举、可配置(SetFunctionLength / // SetFunctionName 语义);length 先于 name 定义保证属性序 [prototype, length, name]。 diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index e19b5c25..6cda263b 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -216,7 +216,7 @@ impl Vm { } // length:实参个数,可写、不可枚举、可配置。 - let length_si = self.kernel_core.perm_interner().intern("length").0; + let length_si = self.length_si; if let Err(msg) = self.define_data_property( obj, length_si, diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index 648490c4..5ebea6c3 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -3,7 +3,7 @@ use oxide_bytecode::opcode::OpCode; use oxide_kernel::prop_forge::PropTemplate; use oxide_runtime_api as coercion; use oxide_types::object::JsObject; -use oxide_types::private_key::make_private_name_id; +use oxide_types::private_key::{int_key_value, is_int_key, make_private_name_id}; use oxide_types::value::JsValue; use crate::ic_helper::{self, ic_get_hit, ic_set_hit}; @@ -46,7 +46,7 @@ impl Vm { fn primitive_property_get(&mut self, val: JsValue, prop_name_si: u32) -> Result, String> { if val.is_string() { - let length_si = self.kernel_core.perm_interner().intern("length").0; + let length_si = self.length_si; if prop_name_si == length_si { // SAFETY: val 是字符串值。utf16_len 构造时缓存,O(1) 读取。 let len = unsafe { (*val.as_string_ptr()).utf16_len() }; @@ -522,15 +522,33 @@ impl Vm { fn dispatch_get_prop_dynamic(&mut self, rd: usize, a: usize, b: usize) -> Result<(), String> { vm_trace!("GET_PROP_DYNAMIC rd={} a={} b={}", rd, a, b); 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)? { + let receiver = self.regs[rd]; + // 数组元素 fast path:整数键 + 真数组 + 无元素 meta(无 hole/accessor/描述符) + // + 界内 → 直读元素区,免 primitive_property_get / checked_object_ptr / + // ordinary_get 调用链。判定条件与 ordinary_get_inner 数组分支严格等价。 + // 非对象 receiver(如 `"abc"[0]`)须先 is_object 快检再 checked_object_ptr, + // 否则该检查会误抛 TypeError(原路径对原始值走 primitive_property_get 不抛)。 + if is_int_key(prop_name_si) && receiver.is_object() { + if let Some(obj_ptr) = self.checked_object_ptr(receiver, "GET_PROP_DYNAMIC on non-object")? { + let obj = unsafe { &*obj_ptr }; + if obj.is_array() && obj.array_elements_meta_vec().is_none() { + let idx = int_key_value(prop_name_si); + if idx < obj.array_prop_count { + self.regs[b] = obj.get_prop_at(idx); + return Ok(()); + } + } + } + } + if let Some(value) = self.primitive_property_get(receiver, 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 { + let Some(obj_ptr) = self.checked_object_ptr(receiver, "GET_PROP_DYNAMIC on non-object")? else { return Ok(()); }; let obj = unsafe { &*obj_ptr }; - let val = self.ordinary_get_with_target(obj, prop_name_si, self.regs[rd], b as u8)?; + let val = self.ordinary_get_with_target(obj, prop_name_si, receiver, b as u8)?; if self.accessor_frame_target_reg.take().is_none() { self.regs[b] = val; } @@ -543,7 +561,28 @@ impl Vm { return Ok(()); }; let prop_name_si = self.property_key_si(self.regs[a])?; - if self.kernel_core.perm_interner().lookup(prop_name_si) == Some("__proto__") { + // 数组元素 fast path:整数键 + 真数组 + 无 meta + 界内 → 直写元素区, + // 免 __proto__ 查询 / ordinary_set_dispatch / get_own_property_slot 调用链。 + // 与 ordinary_set_inner 数组分支等价(界内写不触发 length 扩展、无 shadow + // 同步)。越界写必须走原路径(CreateDataProperty + length 扩展),绝不可 + // 直写——set_prop_storage 对越界下标会落到 hash_props 属性区。 + if is_int_key(prop_name_si) { + let obj = unsafe { &*obj_ptr }; + if obj.is_array() && obj.array_elements_meta_vec().is_none() { + let idx = int_key_value(prop_name_si); + if idx < obj.array_prop_count { + // promote 必须保留:obj 是 session 根、写入值可能是 epoch 对象, + // 不 promote 下轮 GC 会悬垂。 + let value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]); + let obj = unsafe { &mut *obj_ptr }; + obj.set_prop_storage(idx as usize, value); + return Ok(()); + } + } + } + // 整数键恒不可能等于 "__proto__"(字符串 intern id 远小于整数键区间), + // 短路免去每次 set 的 RwLock 查询。 + if !is_int_key(prop_name_si) && self.kernel_core.perm_interner().lookup(prop_name_si) == Some("__proto__") { let proto_value = self.promote_if_needed_for_write_ptr(obj_ptr, self.regs[b]); if self.is_object_prototype(obj_ptr) && !proto_value.is_null() { self.raise_type_error("Object.prototype.__proto__ is immutable")?; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index c978761c..80a1992f 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -308,6 +308,10 @@ pub struct Vm { pub(crate) frames: SmallVec<[CallFrame; 16]>, pub(crate) kernel_core: Arc, pub(crate) session: KernelSession, + /// `"length"` 属性键的 intern id 缓存:进程内稳定(PermInterner append-only、 + /// KernelCore 不重建),属性 get/set 热路径免每次 intern(hash64 + DashMap + + /// RwLock 读锁)。 + pub(crate) length_si: u32, pub epoch: Epoch, pub object_prototype: P, /// `%GeneratorPrototype%`:生成器实例的原型(next/return/throw 方法挂此)。 @@ -1003,7 +1007,7 @@ impl Vm { pub(crate) fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option { vm_trace!("resolve_property: shape_id={} prop_name_si={}", obj.shape_id(), prop_name_si); - let length_si = self.kernel_core.perm_interner().intern("length").0; + let length_si = self.length_si; if obj.is_array() && prop_name_si == length_si { return Some(obj.logical_len_value()); } @@ -1042,7 +1046,7 @@ impl Vm { } pub(crate) fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option { - let length_si = self.kernel_core.perm_interner().intern("length").0; + let length_si = self.length_si; if obj.is_array() && prop_name_si == length_si { return None; } diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 93f7103a..b115cbed 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -21,7 +21,7 @@ impl Vm { &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, target_reg: Option, ) -> Result { vm_trace!("ordinary_get_inner: shape={} prop_si={}", obj.shape_id(), prop_name_si); - let length_si = self.kernel_core.perm_interner().intern("length").0; + let length_si = self.length_si; let mut current = Some(obj); let mut depth = 0usize; while let Some(obj) = current { @@ -186,7 +186,7 @@ impl Vm { // 数组 length 赋值:ArraySetLength 语义(ToUint32 + 调整元素区)。 // 旧行为会把 length 存成影子命名属性,导致 `arr.length = N` 后 // prop_count/迭代/内置方法看到的长度不一致。 - let length_si = self.kernel_core.perm_interner().intern("length").0; + let length_si = self.length_si; if obj.is_array() && prop_name_si == length_si { let pc_before = self.pc; let number_len = self.coerce_number_bounded(val)?; diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 895f7327..6cd60e36 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -21,6 +21,9 @@ impl Vm { let core = KernelCore::new(KernelConfig::minimal()); let mut session = KernelSession::new(&core); bindings::init_kernel_builtins(&core, &mut session); + // 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到 + // 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。 + let length_si = core.perm_interner().intern("length").0; let obj_proto = P::clone(&session.builtin_world().object_proto); let mut vm = Self { regs: [JsValue::undefined(); 256], @@ -31,6 +34,7 @@ impl Vm { frames: smallvec::SmallVec::new(), kernel_core: core, session, + length_si, epoch: Epoch::new(), object_prototype: obj_proto, generator_proto: P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), @@ -115,6 +119,9 @@ impl Vm { pub fn with_kernel_core(core: Arc) -> Self { let mut session = KernelSession::new(&core); bindings::init_kernel_builtins(&core, &mut session); + // 在 builtin 绑定之后取 id:绑定过程已 intern "length",此处命中缓存得到 + // 非 0 的稳定 id(保持枚举层"id 0 哨兵"的既有约定,见 walk_own_keys)。 + let length_si = core.perm_interner().intern("length").0; let obj_proto = P::clone(&session.builtin_world().object_proto); let mut vm = Self { regs: [JsValue::undefined(); 256], @@ -125,6 +132,7 @@ impl Vm { frames: smallvec::SmallVec::new(), kernel_core: core, session, + length_si, epoch: Epoch::new(), object_prototype: obj_proto, generator_proto: P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())), @@ -637,10 +645,7 @@ mod tests { // wrapper 是跨重置存活对象,其原型链必须仍能解析出 call/apply/bind: // 任何一条路径失效都说明 wrapper 原型指向了已释放的旧 Function 原型。 assert_eq!(run_source(&mut vm, "Array.prototype.push.call([1], 2)"), JsValue::int(2)); - assert_eq!( - run_source(&mut vm, "Array.prototype.push.apply([], [1, 2, 3])"), - JsValue::int(3) - ); + assert_eq!(run_source(&mut vm, "Array.prototype.push.apply([], [1, 2, 3])"), JsValue::int(3)); let replaced = run_source(&mut vm, "String.prototype.replace.call('a', 'a', 'b')"); assert_eq!(vm.lookup_str(replaced).as_deref(), Some("b")); let has = run_source(&mut vm, "Object.prototype.hasOwnProperty.call({x: 1}, 'x')"); diff --git a/crates/oxide_vm/tests/property_array_fastpath_tests.rs b/crates/oxide_vm/tests/property_array_fastpath_tests.rs new file mode 100644 index 00000000..fa6133ad --- /dev/null +++ b/crates/oxide_vm/tests/property_array_fastpath_tests.rs @@ -0,0 +1,158 @@ +//! 03.2 数组元素访问 dispatch 内联 fast path 语义测试:验证 fast path 命中 +//! (密集数组 int 键界内直读写)与各回退场景(越界、hole、accessor、非数组、 +//! 非对象 receiver)与 ordinary 属性路径行为一致。 + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result<(Vm, JsValue), String> { + 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))?; + let mut vm = Vm::new(); + let result = vm.run(&module)?; + Ok((vm, result)) +} + +fn eval_str(source: &str) -> Result { + let (_vm, v) = eval(source)?; + if v.is_string() { + Ok(oxide_runtime_api::to_string(v)) + } else { + Ok(format!("{v}")) + } +} + +// 密集数组整数键界内读:命中 fast path 直读元素区。 +#[test] +fn dense_index_read_hits_fast_path() { + assert_eq!(eval_str("var a=[1,2,3]; a[1]").unwrap(), "2"); + assert_eq!(eval_str("var a=[10,20,30]; a[0]+a[2]").unwrap(), "40"); +} + +// 密集数组整数键界内写:命中 fast path 直写元素区,读回一致。 +#[test] +fn dense_index_write_hits_fast_path() { + assert_eq!(eval_str("var a=[1,2,3]; a[1]=9; a[1]").unwrap(), "9"); + assert_eq!(eval_str("var a=[1,2,3]; a[0]=5; a[0]").unwrap(), "5"); +} + +// 字符串规范数字键与整数键等价(`a["1"]` == `a[1]`,同一 fast path 键)。 +#[test] +fn string_canonical_index_key_equivalent() { + assert_eq!(eval_str("var a=[1,2,3]; a[\"1\"]").unwrap(), "2"); + assert_eq!(eval_str("var a=[1,2,3]; a[\"1\"]=7; a[1]").unwrap(), "7"); +} + +// 整值 double 键(`a[1.0]`)与整数键等价。 +#[test] +fn integral_double_key_equivalent() { + assert_eq!(eval_str("var a=[1,2,3]; a[1.0]").unwrap(), "2"); +} + +// 越界读不命中 fast path,落原型链(undefined / 原型上元素)。 +#[test] +fn out_of_bounds_read_falls_to_proto_chain() { + assert_eq!(eval_str("var a=[1,2,3]; a[5]").unwrap(), "undefined"); + assert_eq!(eval_str("var a=[1,2,3]; a.__proto__={5:\"x\"}; a[5]").unwrap(), "x"); + assert_eq!(eval_str("Array.prototype[9]=\"proto9\"; var a=[1,2,3]; a[9]").unwrap(), "proto9"); +} + +// 越界写不命中 fast path,走 CreateDataProperty + length 扩展。 +#[test] +fn out_of_bounds_write_extends_length() { + assert_eq!(eval_str("var a=[1,2,3]; a[5]=50; a.length").unwrap(), "6"); + assert_eq!(eval_str("var a=[1,2,3]; a[5]=50; a[5]").unwrap(), "50"); +} + +// delete 后的 hole:元素 meta 非空,不命中 fast path,读落原型链。 +#[test] +fn deleted_hole_read_falls_to_proto() { + assert_eq!(eval_str("var a=[1,2,3]; delete a[1]; a[1]").unwrap(), "undefined"); +} + +// defineProperty accessor:元素 meta 非空,不命中 fast path,getter/setter 仍触发。 +#[test] +fn accessor_getter_and_setter_still_trigger() { + assert_eq!( + eval_str("var a=[1,2,3]; Object.defineProperty(a,1,{get:function(){return 99;}}); a[1]").unwrap(), + "99" + ); + assert_eq!( + eval_str( + "var a=[1,2,3]; Object.defineProperty(a,1,{set:function(v){this._s=v;},get:function(){return this._s;}}); a[1]=7; a[1]" + ) + .unwrap(), + "7" + ); +} + +// 非写属性(writable:false)写入:fast path 不命中,走完整协议抛 TypeError。 +#[test] +fn non_writable_index_write_throws() { + let r = eval_str( + "var a=[1,2,3]; Object.defineProperty(a,0,{writable:false,value:1}); try { a[0]=9; 'no' } catch(e) { e instanceof TypeError ? 'throws' : 'wrong' }" + ) + .unwrap(); + assert_eq!(r, "throws", "expected TypeError, got {r:?}"); +} + +// 非规范数字串(前导零)不映射整数键,走字符串键慢路径(不命中 fast path)。 +#[test] +fn non_canonical_index_string_uses_slow_path() { + assert_eq!(eval_str("var a=[1,2,3]; a[\"05\"]").unwrap(), "undefined"); + assert_eq!(eval_str("var a=[1,2,3]; a[\"05\"]=9; a.length").unwrap(), "3"); +} + +// 负索引/小数键不进入整数键区间,走字符串键慢路径。 +#[test] +fn negative_and_fractional_keys_use_slow_path() { + assert_eq!(eval_str("var a=[1,2,3]; a[-1]").unwrap(), "undefined"); + assert_eq!(eval_str("var a=[1,2,3]; a[1.5]").unwrap(), "undefined"); +} + +// 普通对象(非数组)整数键不受 fast path 影响,走普通属性路径。 +#[test] +fn plain_object_int_key_unaffected() { + assert_eq!(eval_str("var o={0:\"x\"}; o[0]").unwrap(), "x"); + assert_eq!(eval_str("var o={}; o[0]=\"y\"; o[0]").unwrap(), "y"); +} + +// 非对象 receiver(`"abc"[0]`):is_object 快检先行,不 raise、返回 undefined。 +#[test] +fn primitive_string_index_does_not_raise() { + assert_eq!(eval_str("\"abc\"[0]").unwrap(), "undefined"); + assert_eq!(eval_str("var a=\"abc\"; a[0]").unwrap(), "undefined"); +} + +// push 构造的数组 fast path 读写与 length 一致。 +#[test] +fn push_built_array_fast_path_consistent() { + assert_eq!(eval_str("var a=[]; a.push(1); a.push(2); a.length").unwrap(), "2"); + assert_eq!(eval_str("var a=[]; a.push(1); a.push(2); a[0]+a[1]").unwrap(), "3"); +} + +// TypedArray 整数索引不命中数组 fast path(is_array 为假),走 TA 分支。 +#[test] +fn typed_array_int_index_unaffected() { + assert_eq!(eval_str("var t=new Int32Array([5,6,7]); t[1]").unwrap(), "6"); +} + +// fast path 写后 length 读一致(shadow length 同步不受影响)。 +#[test] +fn fast_path_write_keeps_length_in_sync() { + assert_eq!(eval_str("var a=[1,2,3]; a[0]=7; a.length").unwrap(), "3"); +} + +// 空数组 / 稀疏空洞(字面量 hole,无 meta)读 undefined,与慢路径一致。 +#[test] +fn sparse_literal_hole_reads_undefined() { + assert_eq!(eval_str("var a=[10,,30]; a[1]").unwrap(), "undefined"); +} + +// 越界写后读取原型链元素(fast path 不越界直写)。 +#[test] +fn oob_write_then_proto_read() { + assert_eq!(eval_str("Array.prototype[9]=\"proto9\"; var a=[1,2,3]; a[9]=90; a[9]").unwrap(), "90"); +}