From 6d961723da6805eb31d567290cf2453cb6070f40 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 28 Sep 2026 20:57:14 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=95=B0=E7=BB=84=E8=B6=8A=E7=95=8C?= =?UTF-8?q?=E5=85=83=E7=B4=A0=E5=86=99=E5=85=A5=E6=96=B0=E5=BB=BA=E6=88=90?= =?UTF-8?q?=E6=AE=B5=E6=8C=89=E8=A7=84=E8=8C=83=E7=BD=AE=E4=B8=BA=E7=A9=BA?= =?UTF-8?q?=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_types/src/object/props.rs | 16 +- .../tests/array_growth_holes_tests.rs | 195 ++++++++++++++++++ 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 crates/oxide_vm/tests/array_growth_holes_tests.rs diff --git a/crates/oxide_types/src/object/props.rs b/crates/oxide_types/src/object/props.rs index 1a4908f0..32e7bbdc 100644 --- a/crates/oxide_types/src/object/props.rs +++ b/crates/oxide_types/src/object/props.rs @@ -53,6 +53,8 @@ impl JsObject { } /// 设置数组元素数 / 命名属性向量长度。截断或补 undefined 扩展。 + /// 扩展只补值区(新槽为在位 undefined)与元数据区(新槽为 `None`),不写洞标记; + /// 越界增长的空洞语义由增长方(`set_prop_at` / length 赋值路径)按规范写入。 /// 数组对象只调整独立元素区(`array_elements` + `array_prop_count`),命名属性区 /// (`hash_props`)零搬移——push 为摊销 O(1) 的 `Vec::push`。 pub fn set_prop_count(&mut self, count: impl PropIndex) { @@ -431,10 +433,13 @@ impl JsObject { } /// 设置数组元素 position 处的值(数组对象);普通对象按绝对下标写入并自动扩容。 - /// 数组元素写入会更新 `array_prop_count`(元素数随最高索引增长)。 + /// 数组元素写入会更新 `array_prop_count`(元素数随最高索引增长);写入位置超出 + /// 当前元素数时越界增长,新建成段逐槽置为空洞标记,写入槽本身随后覆盖写并 + /// 清除其 hole 标记。 /// /// # 副作用 /// - 递增本对象 generation:值覆盖属可观察变更,脏检测依赖世代对比。 + /// - 越界增长时分配元素元数据向量(首次分配 O(pos) 建槽),新建成段逐槽写 hole。 pub fn set_prop_at(&mut self, position: impl PropIndex, val: JsValue) { let pos = position.to_u32() as usize; if pos > MAX_DENSE_PROPS { @@ -443,7 +448,16 @@ impl JsObject { if self.is_array() { // 元素写入越过元素区:只扩独立元素区,命名属性区零搬移。 if pos >= self.array_prop_count as usize { + let old = self.array_prop_count as usize; self.set_prop_count(pos + 1); + // 越界增长:规范新建成段 (old, pos) 为空洞而非在位 undefined; + // pos 槽随后覆盖写,hole 标记由 clear_hole_marker 清除。 + if old < pos { + let meta = self.ensure_array_elements_meta(); + for idx in old..pos { + meta[idx] = Some(PropMetaEntry::hole()); + } + } } let vec = self.ensure_array_elements(); vec[pos] = val; diff --git a/crates/oxide_vm/tests/array_growth_holes_tests.rs b/crates/oxide_vm/tests/array_growth_holes_tests.rs new file mode 100644 index 00000000..924046ed --- /dev/null +++ b/crates/oxide_vm/tests/array_growth_holes_tests.rs @@ -0,0 +1,195 @@ +//! 数组越界增长置洞引擎钉:define 数据/访问器、SET_ELEM 快慢路径、push / unshift +//! 扩长统一走 `set_prop_at` 增长臂,越界增长新建成段按规范置为空洞而非在位 +//! undefined,行为对齐 `in`、`length`、`Object.keys`、GOPD、原型链读取与既有洞保留。 + +use std::sync::Arc; + +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(&Arc::new(module))?; + Ok((vm, result)) +} + +fn eval_str(source: &str) -> Result { + let (vm, v) = eval(source)?; + // 字符串结果须在同一 VM 上读:perm 串指向 VM 私有内核,VM drop 后指针悬垂。 + if v.is_string() { + Ok(vm.lookup_str(v).unwrap_or_default()) + } else { + Ok(format!("{v}")) + } +} + +// 钉 1:define 越界数据属性后中间槽为洞(`in` 为 false、length 含新边界)。 +#[test] +fn define_out_of_bounds_middle_is_hole() { + assert_eq!( + eval_str( + "const a=[0,1]; Object.defineProperty(a,'5',{value:5,configurable:true,writable:true,enumerable:true}); \ + ''+(3 in a)+':'+(4 in a)+':'+(5 in a)+':'+a.length", + ) + .unwrap(), + "false:false:true:6", + ); +} + +// 钉 2:同形下 Object.keys 不含洞下标。 +#[test] +fn define_out_of_bounds_keys_skip_holes() { + assert_eq!( + eval_str( + "const a=[0,1]; Object.defineProperty(a,'5',{value:5,configurable:true,writable:true,enumerable:true}); \ + JSON.stringify(Object.keys(a))", + ) + .unwrap(), + "[\"0\",\"1\",\"5\"]", + ); +} + +// 钉 3:洞位读回落原型链(原型有 3 下标时读回原型上的值)。 +#[test] +fn define_out_of_bounds_read_falls_to_proto() { + assert_eq!( + eval_str( + "const a=[0,1]; Object.defineProperty(a,'5',{value:5,configurable:true,writable:true,enumerable:true}); \ + Array.prototype[3]='p'; ''+a[3]", + ) + .unwrap(), + "p", + ); +} + +// 钉 4:洞位 GOPD 返回 undefined(无描述符)。 +#[test] +fn define_out_of_bounds_gopd_hole_is_undefined() { + assert_eq!( + eval_str( + "const a=[0,1]; Object.defineProperty(a,'5',{value:5,configurable:true,writable:true,enumerable:true}); \ + ''+(Object.getOwnPropertyDescriptor(a,'3')===undefined)", + ) + .unwrap(), + "true", + ); +} + +// 钉 5:SET_ELEM 越界写后中间槽为洞(`in` 为 false、length 含新边界)。 +#[test] +fn set_elem_out_of_bounds_middle_is_hole() { + assert_eq!( + eval_str("const a=[0,1]; a[5]=9; ''+(3 in a)+':'+(4 in a)+':'+(5 in a)+':'+a.length").unwrap(), + "false:false:true:6", + ); +} + +// 钉 6:define 越界访问器属性后中间槽为洞、目标槽 getter 在场。 +#[test] +fn define_out_of_bounds_accessor_middle_is_hole() { + assert_eq!( + eval_str( + "const a=[0,1]; Object.defineProperty(a,'5',{get(){return 5},configurable:true}); \ + ''+(3 in a)+':'+(Object.getOwnPropertyDescriptor(a,'5').get!==undefined)", + ) + .unwrap(), + "false:true", + ); +} + +// 钉 7:洞位重写后恢复在位(`in` 为 true、读回写入值)。 +#[test] +fn rewrite_hole_restores_present() { + assert_eq!(eval_str("const a=[0,1]; a[5]=9; a[3]=1; ''+(3 in a)+':'+a[3]").unwrap(), "true:1",); +} + +// 钉 8:越界增长不抹已有洞(delete 后的洞与新增段洞并存)。 +#[test] +fn existing_hole_preserved_on_growth() { + assert_eq!( + eval_str("const a=[0,1]; delete a[0]; a[5]=9; ''+(0 in a)+':'+(2 in a)+':'+(5 in a)").unwrap(), + "false:false:true", + ); +} + +// 钉 9:边界恰 length 位写入不产生中间洞(push 形)。 +#[test] +fn write_at_length_boundary_is_present() { + assert_eq!( + eval_str("const a=[0,1]; a[2]=9; ''+(2 in a)+':'+a.length+':'+JSON.stringify(Object.keys(a))").unwrap(), + "true:3:[\"0\",\"1\",\"2\"]", + ); +} + +// 钉 10:define 边界恰 length 位同形(无中间洞)。 +#[test] +fn define_at_length_boundary_is_present() { + assert_eq!( + eval_str( + "const a=[0,1]; Object.defineProperty(a,'2',{value:2,configurable:true,writable:true,enumerable:true}); \ + ''+(2 in a)+':'+a.length+':'+JSON.stringify(Object.keys(a))", + ) + .unwrap(), + "true:3:[\"0\",\"1\",\"2\"]", + ); +} + +// 钉 11:push 扩长全在位(零新增元数据语义)。 +#[test] +fn push_growth_all_present() { + assert_eq!( + eval_str("const a=[1,2]; a.push(3); ''+(0 in a)+':'+(1 in a)+':'+(2 in a)+':'+a.length").unwrap(), + "true:true:true:3", + ); +} + +// 钉 12:unshift 扩长全在位。 +#[test] +fn unshift_growth_all_present() { + assert_eq!( + eval_str("const a=[1,2]; a.unshift(0); ''+(0 in a)+':'+(1 in a)+':'+(2 in a)+':'+a.length").unwrap(), + "true:true:true:3", + ); +} + +// 钉 13:frozen 数组越界 define 抛 TypeError(增长前先校验冻结)。 +#[test] +fn define_out_of_bounds_on_frozen_throws() { + assert_eq!( + eval_str( + "try { const a=[0,1]; Object.freeze(a); \ + Object.defineProperty(a,'5',{value:5,configurable:true,writable:true,enumerable:true}); 'no-throw'; } \ + catch(e) { e.constructor.name }", + ) + .unwrap(), + "TypeError", + ); +} + +// 钉 14:length 不可写(frozen)时越界 SET_ELEM 为 no-op(sloppy)。 +#[test] +fn set_elem_out_of_bounds_noop_when_length_non_writable() { + assert_eq!( + eval_str("const a=[0,1]; Object.freeze(a); a[5]=9; ''+a.length+':'+(5 in a)").unwrap(), + "2:false", + ); +} + +// 钉 15:防过度修——普通对象 undefined 值属性仍在位(`in` 为 true)。 +#[test] +fn plain_object_undefined_value_is_present() { + assert_eq!(eval_str("const o={}; o.k=undefined; ''+('k' in o)").unwrap(), "true"); +} + +// 钉 16:防过度修——数组命名属性 undefined 值仍在位(hasOwn 与 keys 均含)。 +#[test] +fn array_named_undefined_value_is_present() { + assert_eq!( + eval_str("const a=[0,1]; a.x=undefined; ''+Object.hasOwn(a,'x')+':'+JSON.stringify(Object.keys(a))").unwrap(), + "true:[\"0\",\"1\",\"x\"]", + ); +}