From f8dacb192a2ba393e3f9a371736df43b37a14f17 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 15 Aug 2026 08:16:49 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20member=20=E5=A4=8D=E5=90=88=E5=86=99?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E6=8E=A5=E5=85=A5=20IC=20=E5=86=99=E5=91=BD?= =?UTF-8?q?=E4=B8=AD=EF=BC=88depth=200=EF=BC=89=E6=B6=88=E9=99=A4=E6=AF=8F?= =?UTF-8?q?=E6=AC=A1=20DashMap=20=E6=9F=A5=E8=AF=A2=E4=B8=8E=E5=9B=9E?= =?UTF-8?q?=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit set_member_prop 增加 ic_set_hit_own(仅接受 depth==0 槽,跳过读侧学习的 原型条目防原型污染)命中直写槽,ext_pc 由 read_member_prop 显式带出消除 隐式 pc 契约;写新属性 miss 分支(IC_SET 分发 + member 写)统一改 CreateDataProperty 快路径(保留原型链 setter/readonly 检查),砍掉 ordinary_set 全链路的重复查询与 shape 转换开销。 --- crates/oxide_vm/src/dispatch/member.rs | 36 ++-- crates/oxide_vm/src/dispatch/property.rs | 24 +-- crates/oxide_vm/src/ic_helper.rs | 48 +++++ crates/oxide_vm/src/vm_props.rs | 92 +++++++-- crates/oxide_vm/tests/ic_polymorphic_tests.rs | 8 +- .../oxide_vm/tests/member_write_ic_tests.rs | 188 ++++++++++++++++++ tests/stress/member_compound.js | 17 ++ 7 files changed, 361 insertions(+), 52 deletions(-) create mode 100644 crates/oxide_vm/tests/member_write_ic_tests.rs create mode 100644 tests/stress/member_compound.js diff --git a/crates/oxide_vm/src/dispatch/member.rs b/crates/oxide_vm/src/dispatch/member.rs index 2a7e7eb..a85b5dd 100644 --- a/crates/oxide_vm/src/dispatch/member.rs +++ b/crates/oxide_vm/src/dispatch/member.rs @@ -126,10 +126,10 @@ impl Vm { }; let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; + let (prop_val, ext_pc) = self.read_member_prop(obj, prop_name_si, receiver)?; let n = self.coerce_number_bounded(prop_val)?; let new_val = JsValue::float(n + 1.0); - self.set_member_prop(obj, prop_name_si, new_val, receiver)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver, ext_pc)?; self.regs[a] = new_val; Ok(()) } @@ -141,10 +141,10 @@ impl Vm { }; let receiver = self.regs[rd]; let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; + let (prop_val, ext_pc) = self.read_member_prop(obj, prop_name_si, receiver)?; let n = self.coerce_number_bounded(prop_val)?; let new_val = JsValue::float(n - 1.0); - self.set_member_prop(obj, prop_name_si, new_val, receiver)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver, ext_pc)?; self.regs[a] = new_val; Ok(()) } @@ -187,7 +187,7 @@ impl Vm { return Ok(()); }; let receiver = self.regs[rd]; - let prop_raw = { + let (prop_raw, ext_pc) = { let obj = unsafe { &mut *obj_ptr }; self.read_member_prop(obj, prop_name_si, receiver)? }; @@ -203,7 +203,7 @@ impl Vm { JsValue::float(ln + rn) }; let obj = unsafe { &mut *obj_ptr }; - self.set_member_prop(obj, prop_name_si, new_val, receiver)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver, ext_pc)?; self.regs[a] = new_val; Ok(()) } @@ -219,16 +219,16 @@ impl Vm { return Ok(()); }; let receiver = self.regs[rd]; - let ln = { + let (ln, ext_pc) = { let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; - self.coerce_number_bounded(prop_val)? + let (prop_val, ext_pc) = self.read_member_prop(obj, prop_name_si, receiver)?; + (self.coerce_number_bounded(prop_val)?, ext_pc) }; let rn = self.coerce_number_bounded(self.regs[a])?; let new_val = JsValue::float(op(ln, rn)); self.regs[a] = new_val; let obj = unsafe { &mut *obj_ptr }; - self.set_member_prop(obj, prop_name_si, new_val, receiver)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver, ext_pc)?; Ok(()) } @@ -243,16 +243,16 @@ impl Vm { return Ok(()); }; let receiver = self.regs[rd]; - let ln = { + let (ln, ext_pc) = { let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; - self.coerce_int32_bounded(prop_val)? + let (prop_val, ext_pc) = self.read_member_prop(obj, prop_name_si, receiver)?; + (self.coerce_int32_bounded(prop_val)?, ext_pc) }; let rn = self.coerce_int32_bounded(self.regs[a])?; let new_val = JsValue::int(op(ln, rn)); self.regs[a] = new_val; let obj = unsafe { &mut *obj_ptr }; - self.set_member_prop(obj, prop_name_si, new_val, receiver)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver, ext_pc)?; Ok(()) } @@ -265,10 +265,10 @@ impl Vm { return Ok(()); }; let receiver = self.regs[rd]; - let ln = { + let (ln, ext_pc) = { let obj = unsafe { &mut *obj_ptr }; - let prop_val = self.read_member_prop(obj, prop_name_si, receiver)?; - self.coerce_int32_bounded(prop_val)? + let (prop_val, ext_pc) = self.read_member_prop(obj, prop_name_si, receiver)?; + (self.coerce_int32_bounded(prop_val)?, ext_pc) }; let shift = self.coerce_uint32_bounded(self.regs[a])? & 0x1F; let new_val = if !is_right { @@ -285,7 +285,7 @@ impl Vm { }; self.regs[a] = new_val; let obj = unsafe { &mut *obj_ptr }; - self.set_member_prop(obj, prop_name_si, new_val, receiver)?; + self.set_member_prop(obj, prop_name_si, new_val, receiver, ext_pc)?; Ok(()) } } diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index af49af1..0d81f7e 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -1,6 +1,5 @@ use crate::{ic_debug, ic_trace, vm_trace}; use oxide_bytecode::opcode::{OpCode, IC_EXT_WORDS}; -use oxide_kernel::prop_forge::PropTemplate; use oxide_runtime_api as coercion; use oxide_types::object::JsObject; use oxide_types::private_key::{int_key_value, is_int_key, make_private_name_id}; @@ -443,23 +442,14 @@ impl Vm { ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos); } else { self.profiling.record_ic_miss(); - let old_shape = obj.shape_id(); - self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; - if old_shape != obj.shape_id() { - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - ic_helper::write_ic_back(self.bytecode_mut(), ic_pc, obj.shape_id(), pos, 0); - ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos); - self.kernel_core.prop_forge().upsert( - obj.shape_id(), - PropTemplate { - shape_id: obj.shape_id(), - prop_name: prop_name_si, - position: pos, - generation: obj.generation(), - }, - ); - } + prop_cache_miss(); + // 写新属性(shape 转换):数组 length / 整数索引键仍走完整 + // ordinary_set_dispatch 语义,其余走 CreateDataProperty 快路径。 + if self.named_prop_create_needs_ordinary_set(obj, prop_name_si) { + self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; + return Ok(()); } + self.create_named_prop_fast(obj, prop_name_si, value, receiver, ic_pc, true)?; } Ok(()) diff --git a/crates/oxide_vm/src/ic_helper.rs b/crates/oxide_vm/src/ic_helper.rs index 484f7b6..0826c8c 100644 --- a/crates/oxide_vm/src/ic_helper.rs +++ b/crates/oxide_vm/src/ic_helper.rs @@ -239,3 +239,51 @@ pub(crate) fn ic_set_hit_poly(obj: &mut JsObject, bytecode: &[Instr], ext_pc: us } false } + +/// member 复合写(读改写共用同一扩展字组)的写侧命中判定:顺序遍历 4 槽,只接受 +/// depth==0 的数据槽并直写。 +/// +/// 读侧会把继承属性解析结果以 depth>0 条目写回同一扩展字组;写侧若命中这些条目 +/// 会把新值直写原型对象(原型污染,规范要求 shadow 到接收者 own 属性)——因此 +/// 原型条目一律跳过,落到慢路径走完整 [[Set]] 语义。 +/// +/// `pc` 为越过该 IC 指令全部扩展字后的位置(与 [`write_ic_back`] 同一约定), +/// 命中遍历从 `pc - IC_EXT_WORDS` 起的扩展字开始。 +#[inline(always)] +pub(crate) fn ic_set_hit_own(obj: &mut JsObject, bytecode: &[Instr], pc: usize, value: JsValue) -> bool { + let base = pc - IC_EXT_WORDS; + let shape_id = bytecode[base] & 0x00FF_FFFF; + if shape_id == 0 { + return false; + } + let proto_depth = (bytecode[base] >> 24) as u8; + let slot_index = bytecode[base + 1]; + if proto_depth == 0 && obj.shape_id() == shape_id && slot_index < obj.prop_vec_len() as u32 { + obj.set_prop_shape(slot_index, value); + return true; + } + ic_set_hit_own_poly(obj, bytecode, base, value) +} + +/// 写侧多态槽(1..3)遍历,depth==0 only;`inline(never)` 防主循环代码膨胀。 +/// `ext_pc` 为扩展字起始位置(首个扩展字处)。 +#[inline(never)] +fn ic_set_hit_own_poly(obj: &mut JsObject, bytecode: &[Instr], ext_pc: usize, value: JsValue) -> bool { + for i in 1..IC_SLOTS { + let base = ext_pc + i * 2; + let shape_id = bytecode[base] & 0x00FF_FFFF; + if shape_id == 0 { + break; + } + let proto_depth = (bytecode[base] >> 24) as u8; + if proto_depth != 0 { + continue; + } + let slot_index = bytecode[base + 1]; + if obj.shape_id() == shape_id && slot_index < obj.prop_vec_len() as u32 { + obj.set_prop_shape(slot_index, value); + return true; + } + } + false +} diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index 1b61f9d..cbfc892 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -1,5 +1,6 @@ use crate::vm::{FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH}; use crate::{ic_trace, vm_trace}; +use oxide_kernel::prop_forge::PropTemplate; use oxide_runtime_api as coercion; use oxide_types::object::{JsObject, PropAttributes, PropMetaEntry}; use oxide_types::value::JsValue; @@ -333,15 +334,19 @@ impl Vm { Ok(()) } + /// 读取 member 复合操作数(obj.x / obj["x"]),返回 (值, IC 扩展字起始位置)。 + /// + /// `ext_pc` 供同一指令的写侧 [`Self::set_member_prop`] 使用:读侧负责推进 pc + /// 越过扩展字,写侧显式接收该位置、不再接触 pc,消除隐式 pc 契约。 pub(crate) fn read_member_prop( &mut self, obj: &JsObject, prop_name_si: u32, receiver: JsValue, - ) -> Result { + ) -> Result<(JsValue, usize), String> { vm_trace!("read_member_prop: shape_id={} prop_name_si={}", obj.shape_id(), prop_name_si); let (cached_shape_id, cached_slot, cached_depth) = crate::ic_helper::read_ic_slot0(&self.bytecode, &mut self.pc); let ic_pc = self.pc; if obj.has_prop_meta() { - return self.ordinary_get(obj, prop_name_si, receiver); + return self.ordinary_get(obj, prop_name_si, receiver).map(|v| (v, ic_pc)); } let val = if let Some(v) = crate::ic_helper::ic_get_hit(obj, cached_shape_id, cached_slot, cached_depth) { @@ -362,7 +367,7 @@ impl Vm { } else { self.proto_chain_ic_get(obj, prop_name_si, receiver)? }; - Ok(val) + Ok((val, ic_pc)) } /// 执行 ordinary_get 并把 IC 按原型链深度写回(own 属性 depth=0,原型链按层计数)。 @@ -398,25 +403,86 @@ impl Vm { Ok(resolved) } + /// member 复合写(obj.x += 1 / obj.x++ 等)的写侧:优先 IC 写命中(depth==0 槽) + /// 直写返回,miss 走查表/写新属性快路径。 + /// + /// `ext_pc` 由调用方从 [`Self::read_member_prop`] 返回值带出(读侧已把 pc 推进 + /// 越过本 IC 指令扩展字后的位置);写侧不再接触 pc,避免二次推进导致指令定位错位。 pub(crate) fn set_member_prop( - &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, ext_pc: usize, ) -> Result<(), String> { ic_trace!("set_member_prop: shape_id={} prop_name_si={}", obj.shape_id(), prop_name_si); - let ic_pc = self.pc; let val = self.promote_if_needed_for_write_ptr(obj as *mut JsObject, val); - if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { - if obj.has_prop_meta() { - self.ordinary_set(obj, prop_name_si, val, receiver)?; - return Ok(()); - } - obj.set_prop_shape(pos, val); - crate::ic_helper::write_ic_back(self.bytecode_mut(), ic_pc, obj.shape_id(), pos, 0); - } else { + if obj.has_prop_meta() { self.ordinary_set(obj, prop_name_si, val, receiver)?; + return Ok(()); + } + if crate::ic_helper::ic_set_hit_own(obj, &self.bytecode, ext_pc, val) { + self.profiling.record_ic_hit(); + return Ok(()); + } + self.profiling.record_ic_miss(); + if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + obj.set_prop_shape(pos, val); + crate::ic_helper::write_ic_back(self.bytecode_mut(), ext_pc, obj.shape_id(), pos, 0); + } else { + self.create_named_prop_fast(obj, prop_name_si, val, receiver, ext_pc, false)?; } Ok(()) } + /// 写新属性的 CreateDataProperty 快路径(shape 转换,IC 命中在数学上无法覆盖): + /// 原型链同名 accessor/只读检查后,一次 make_shape + 追加槽 + IC 写回 + 模板 upsert。 + /// + /// # 边界与前提 + /// - 调用方已确认属性不存在于 shape 链(lookup_position miss)。 + /// - 数组 length / 数组与 TA 整数索引键不进入本路径(调用方先分流到 ordinary_set)。 + /// - `use_frame_push` 与 ordinary_set* 入口一致(member 写 false / IC_SET 分发 true)。 + pub(crate) fn create_named_prop_fast( + &mut self, obj: &mut JsObject, prop_name_si: u32, val: JsValue, receiver: JsValue, ext_pc: usize, + use_frame_push: bool, + ) -> Result<(), String> { + // 原型链同名 accessor 须触发 setter、只读 data 须报错(与 ordinary_set 一致), + // 命中时不得创建 own 属性(规范 [[Set]] shadow 语义)。 + if let Some(meta) = self.inherited_property_meta(obj, prop_name_si) { + if meta.is_accessor { + if meta.set.is_undefined() { + return self.raise_type_error("property has no setter"); + } + return self.call_or_push_setter(meta.set, receiver, val, use_frame_push); + } + if !meta.attributes.writable() { + return self.raise_type_error("cannot assign to read-only property"); + } + } + // 新 shape 槽位 = 追加前命名属性数(与 push_prop 的追加位置一致)。 + let slot = obj.prop_vec_len() as u32; + let new_shape_id = self.kernel_core.shape_forge().make_shape(obj.shape_id(), prop_name_si); + obj.set_shape_id(new_shape_id); + obj.push_prop(val); + obj.bump_generation(); + crate::ic_helper::write_ic_back(self.bytecode_mut(), ext_pc, new_shape_id, slot, 0); + self.kernel_core.prop_forge().upsert( + new_shape_id, + PropTemplate { + shape_id: new_shape_id, + prop_name: prop_name_si, + position: slot, + generation: obj.generation(), + }, + ); + Ok(()) + } + + /// 写新属性须分流到完整 ordinary_set 语义的场景:数组 length(ArraySetLength)与 + /// 数组/TA 整数索引键(元素区 / buffer 写)——这些键不在 shape 链上,CreateDataProperty + /// 快路径会错误地为其建命名属性。 + pub(crate) fn named_prop_create_needs_ordinary_set(&self, obj: &JsObject, prop_name_si: u32) -> bool { + (obj.is_array() && prop_name_si == self.length_si) + || (obj.is_typed_array_obj() || obj.is_array()) + && self.array_index_from_property_key(prop_name_si).is_some() + } + 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 槽。 diff --git a/crates/oxide_vm/tests/ic_polymorphic_tests.rs b/crates/oxide_vm/tests/ic_polymorphic_tests.rs index a63b4cd..63894e3 100644 --- a/crates/oxide_vm/tests/ic_polymorphic_tests.rs +++ b/crates/oxide_vm/tests/ic_polymorphic_tests.rs @@ -154,10 +154,10 @@ fn rerun_clears_polymorphic_slots() { ); } -/// member 读侧(obj.x++)多 shape 轮换:read_member_prop 多态命中, -/// 写侧(set_member_prop)暂不接命中——本任务只验证格式同步不错位。 +/// member 复合写(obj.x++)多 shape 轮换:读侧与写侧都经 4 槽缓存命中, +/// 每 shape 仅读侧学习 1 次 miss(写侧复用读侧条目直写)。 #[test] -fn member_compound_polymorphic_read_side_hits() { +fn member_compound_polymorphic_read_write_hits() { let (r, _hits, misses) = run_once( r#"var a = { x: 1 }, b = { x: 2, y: 3 }; for (var i = 0; i < 10000; i++) { @@ -167,7 +167,7 @@ fn member_compound_polymorphic_read_side_hits() { a.x + b.x"#, ); assert_eq!(r, "10003", "两对象 x 各自增 5000 次(从 1/2 起)"); - assert!(misses < 5000, "读侧 4 槽缓存使 miss 远低于迭代数(实际 {misses},写侧新属性路径不计入)"); + assert_eq!(misses, 2, "两 shape 各 1 次读侧学习 miss,写侧全命中"); } /// 非对象 receiver(字符串)经 IC primitive 分支:手动跳越全部扩展字后语义正确。 diff --git a/crates/oxide_vm/tests/member_write_ic_tests.rs b/crates/oxide_vm/tests/member_write_ic_tests.rs new file mode 100644 index 0000000..9f63571 --- /dev/null +++ b/crates/oxide_vm/tests/member_write_ic_tests.rs @@ -0,0 +1,188 @@ +//! member 复合写(obj.x += 1 / obj.x++ 等)接入 IC 写命中的行为测试。 +//! +//! 覆盖三组语义:①写侧命中(depth==0 槽直写,读写对称);②写新属性走 +//! CreateDataProperty 快路径(shape 转换,命中无法覆盖);③继承属性 shadow / +//! 原型 setter / 只读继承属性(写侧 depth==0 限制与原型链检查兜底,防原型污染)。 +//! +//! 计数断言基于单次 run 内 miss/hit 对比(run() 每次冷启动,IC 学习不跨 run)。 + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::vm::Vm; + +/// 编译并单次运行,返回 (结果, hits, misses)。 +fn run_once(source: &str) -> (String, u64, u64) { + let allocator = Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + let mut vm = Vm::new(); + let result = vm.run(&module).expect("run"); + // 字符串结果经 lookup_str 提取内容(JsValue Display 只打印 {string})。 + let r = vm.lookup_str(result).unwrap_or_else(|| format!("{result}")); + (r, vm.ic_hit_count(), vm.ic_miss_count()) +} + +/// 单态 member 复合写:写侧命中直写(读侧命中不计数,pre-existing 口径—— +/// 计数仅来自写侧与独立 IC_GET/IC_SET 分发站点)。 +#[test] +fn member_compound_monomorphic_write_hits() { + let (r, hits, misses) = run_once( + r#"var t = { x: 0 }; + for (var i = 0; i < 10000; i++) { t.x += 1; } + t.x"#, + ); + assert_eq!(r, "10000"); + assert_eq!(misses, 1, "仅末尾 `t.x` IC_GET 站点学习 miss,循环写侧零 miss"); + assert_eq!(hits, 10000, "循环 10000 次写侧全命中(实际 {hits})"); +} + +/// 4 shape 轮换 member 复合写:写侧复用读侧学习的 4 槽条目,循环内零写 miss。 +#[test] +fn member_compound_poly4_read_write_hits() { + let (r, hits, misses) = run_once( + r#"var objs = [{x:0}, {x:0,y:1}, {x:0,z:1}, {x:0,y:1,z:2}]; + for (var i = 0; i < 10000; i++) { + var t = objs[i % 4]; + t.x += 1; + } + objs[0].x + objs[1].x + objs[2].x + objs[3].x"#, + ); + assert_eq!(r, "10000", "四对象 x 各增 2500 次"); + assert_eq!(misses, 4, "末尾 4 个 `objs[i].x` IC_GET 站点各学习 1 次"); + assert_eq!(hits, 10000, "循环写侧 10000 全命中(实际 {hits})"); +} + +/// 继承属性 shadow(核心正确性):写侧只接受 depth==0 槽,原型不被污染。 +#[test] +fn inherited_prop_shadows_receiver_not_proto() { + let (r, _hits, _misses) = run_once( + r#"var p = { x: 1 }; + var o = Object.create(p); + o.x += 1; + o.x + ":" + p.x"#, + ); + assert_eq!(r, "2:1", "o 获得 own x=2,p.x 保持 1(未直写原型)"); +} + +/// IC_SET 独立赋值路径的继承 shadow:`Object.create(p).x = 5` 同样不写原型。 +#[test] +fn ic_set_inherited_shadow_receiver_only() { + let (r, _hits, _misses) = run_once( + r#"var p = { x: 1 }; + var o = Object.create(p); + o.x = 5; + p.x"#, + ); + assert_eq!(r, "1", "p.x 保持 1,o 得到 own x=5"); +} + +/// 原型 accessor setter:IC_SET 与 member 复合写都触发 setter,接收者不建新槽。 +#[test] +fn proto_setter_called_both_write_paths() { + let (r, _hits, _misses) = run_once( + r#"var calls = 0; + var p = { set x(v) { calls++; } }; + var o = Object.create(p); + o.x = 5; + o.x += 2; + calls + ":" + Object.keys(o).length"#, + ); + assert_eq!(r, "2:0", "setter 被调 2 次(IC_SET 与 member 各 1),o 无 own 属性"); +} + +/// 原型只读 data:两条写路径都按 ordinary_set 抛 TypeError。 +#[test] +fn readonly_inherited_data_raises() { + let (r, _hits, _misses) = run_once( + r#"var p = {}; + Object.defineProperty(p, 'x', { value: 1, writable: false }); + var o = Object.create(p); + var msg = ''; + try { o.x = 5; } catch (e) { msg = e.message; } + try { o.x += 1; } catch (e) { msg += '|' + e.message; } + msg"#, + ); + assert_eq!( + r, "cannot assign to read-only property|cannot assign to read-only property", + "两条路径都拒绝写只读继承属性" + ); +} + +/// 写新属性(shape 转换):快路径建 shape + 写回,循环内同 shape 写全命中。 +#[test] +fn write_new_prop_then_hit() { + let (r, hits, misses) = run_once( + r#"var o = {}; + o.y = 0; + for (var i = 0; i < 10000; i++) { o.y += 1; } + o.y"#, + ); + assert_eq!(r, "10000"); + assert_eq!(misses, 2, "`o.y = 0` 站点 1 次 + 末尾 `o.y` IC_GET 站点 1 次"); + assert_eq!(hits, 10000, "compound 写侧 10000 全命中(实际 {hits})"); +} + +/// ic_cache.js 同款模式(批量建对象 + 分批写 y/z 新属性):快路径建 shape 正确。 +#[test] +fn ic_cache_style_new_prop_writes() { + let (r, _hits, _misses) = run_once( + r#"var objs = []; + for (var i = 0; i < 100; i++) { + objs.push({ x: i }); + if (i % 3 === 0) objs[i].y = i; + if (i % 5 === 0) objs[i].z = i; + } + var yok = 0, zok = 0; + for (var i = 0; i < 100; i++) { + if (i % 3 === 0 && objs[i].y === i) yok++; + if (i % 5 === 0 && objs[i].z === i) zok++; + } + yok + ":" + zok"#, + ); + assert_eq!(r, "34:20", "y/z 全部写入成功(含 i%15==0 的 y+z 同对象场景)"); +} + +/// has_prop_meta 对象 member 写走 ordinary_set(不缓存,accessor 语义保留)。 +#[test] +fn meta_object_member_write_uses_ordinary_set() { + let (r, _hits, _misses) = run_once( + r#"var store = 1; + var o = { get a() { return store; }, set a(v) { store = v; } }; + o.a += 5; + store"#, + ); + assert_eq!(r, "6", "accessor 对象走 setter,store 更新为 6"); +} + +/// 命中率口径:member 写 hit/miss 计入后 ic_hit_rate 反映写侧全量命中。 +#[test] +fn member_write_counts_in_ic_hit_rate() { + let allocator = Allocator::default(); + let program = oxide_parser::parse( + &allocator, + r#"var t = { x: 0 }; + for (var i = 0; i < 1000; i++) { t.x += 1; } + t.x"#, + ) + .expect("parse"); + let module = Compiler::new().compile(&program).expect("compile"); + let mut vm = Vm::new(); + let result = vm.run(&module).expect("run"); + let r = vm.lookup_str(result).unwrap_or_else(|| format!("{result}")); + assert_eq!(r, "1000"); + // 循环写侧 1000 命中 + 末尾 IC_GET 1 miss → 命中率 ~0.999(写侧计入后趋近真实)。 + assert!(vm.ic_hit_rate() > 0.99, "member 写侧命中计入命中率(实际 {:.4})", vm.ic_hit_rate()); +} + +/// `__proto__` 赋值 + 继承成员复合写:原型链建立正常,shadow 语义不变(回归锚定)。 +#[test] +fn proto_assignment_then_compound_shadow() { + let (r, _hits, _misses) = run_once( + r#"var p = { x: 1 }; + var o = {}; + o.__proto__ = p; + o.x += 1; + o.x + ":" + p.x"#, + ); + assert_eq!(r, "2:1", "__proto__ 赋值建链后复合写 shadow 到 o,p 不被污染"); +} diff --git a/tests/stress/member_compound.js b/tests/stress/member_compound.js new file mode 100644 index 0000000..8f49d4f --- /dev/null +++ b/tests/stress/member_compound.js @@ -0,0 +1,17 @@ +var ITERATIONS = 100000; + +// 单态段:同一对象复合写,IC 读写全命中(写侧直写槽、零查表零回写)。 +var t = { x: 0 }; +for (var i = 0; i < ITERATIONS; i++) { + t.x += 1; +} + +// 多态段:4 种 shape 轮换复合写,4 槽缓存覆盖,仅学习 miss。 +var objs = [{ x: 0 }, { x: 0, y: 1 }, { x: 0, z: 1 }, { x: 0, y: 1, z: 2 }]; +var s = 0; +for (var i = 0; i < ITERATIONS; i++) { + var o = objs[i % 4]; + o.x += 1; + s += o.x; +} +t.x + s