fix: 读路径按规范跳洞对齐并补 HasProperty 存在判定

This commit is contained in:
2026-09-19 11:52:53 +00:00
parent 6610384d90
commit f5330da5f0
10 changed files with 277 additions and 42 deletions
+37
View File
@@ -212,6 +212,43 @@ impl Vm {
None
}
/// 存在性判定(规范 HasProperty):自身与原型链逐级检查,任一层 P 为自有
/// 属性即存在。数组 length 虚拟属性恒存在;数组元素区 hole 视同缺失;
/// TypedArray 整数索引按视图长度判在界;其余按 shape 槽判定。
///
/// 与 `resolve_property` 的差异:原型链上每层都检查数组元素区与 TypedArray
/// 元素(`resolve_property` 只在顶层检查元素区,链上仅走 shape 槽),
/// 继承自父数组/TypedArray 的索引属性在此判存在。
pub(crate) fn has_property(&self, obj: &JsObject, prop_name_si: u32) -> bool {
let length_si = self.length_si;
let mut current = Some(obj);
let mut depth = 0usize;
while let Some(obj) = current {
if obj.is_array() && prop_name_si == length_si {
return true;
}
if obj.is_typed_array_obj() {
if let Some((index, len)) =
oxide_builtins::typed_array::typed_array_integer_index(self, obj, prop_name_si)
{
if index < len {
return true;
}
}
}
if self.get_own_property_slot(obj, prop_name_si).is_some() {
return true;
}
if depth >= MAX_PROTO_CHAIN_DEPTH {
break;
}
depth += 1;
let proto = obj.proto();
current = proto.is_object().then(|| unsafe { &*proto.as_js_object_ptr() });
}
false
}
/// 查找自身属性槽下标:数组 length 虚拟属性返回 `None`,元素区返回下标,
/// shape 槽按数组(元素区之后偏移)与普通对象(槽位即下标)各自定位;
/// 未命中的槽返回 `None`。
+3
View File
@@ -87,6 +87,9 @@ impl oxide_runtime_api::VmHost for Vm {
fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option<JsValue> {
self.resolve_property(obj, prop_name_si)
}
fn has_property(&self, obj: &JsObject, prop_name_si: u32) -> bool {
self.has_property(obj, prop_name_si)
}
fn get_own_property_slot(&self, obj: &JsObject, prop_name_si: u32) -> Option<u32> {
self.get_own_property_slot(obj, prop_name_si)
}
@@ -263,3 +263,60 @@ fn new_array_holes_pop_shift() {
assert_eq!(eval_str("var a=new Array(1); var p=a.pop(); (p===undefined)+':'+a.length"), "true:0");
assert_eq!(eval_str("var a=new Array(1); var s=a.shift(); (s===undefined)+':'+a.length"), "true:0");
}
#[test]
fn read_paths_skip_holes_per_spec() {
// iterate 族:洞位不触发回调;map 结果在源洞位留洞、长度不变。
assert_eq!(eval_str("var n=0; new Array(2).forEach(function(){n++;}); ''+n"), "0");
assert_eq!(
eval_str("var n=0; var r=[1,,3].map(function(x){n++;return x;}); n+':'+(1 in r)+':'+r.length"),
"2:false:3"
);
assert_eq!(
eval_str("var r=new Array(3).map(function(x){return x;}); (0 in r)+':'+r.length"),
"false:3"
);
assert_eq!(eval_str("new Array(2).filter(function(){return true;}).length"), "0");
// reduce/reduceRight:无初值取首个/末尾 present 位作累加器,洞位跳过;全洞抛 TypeError。
assert_eq!(
eval_str("[1,,].reduce(function(a,b){return b;})+':'+(function(){try{new Array(1).reduce(function(){});return 'no-throw';}catch(e){return e.name;}})()"),
"1:TypeError"
);
assert_eq!(
eval_str("[,1,2].reduceRight(function(a,b){return a+b;})+':'+(function(){try{new Array(1).reduceRight(function(){});return 'no-throw';}catch(e){return e.name;}})()"),
"3:TypeError"
);
// flatMap:洞位不触发回调;嵌套数组展开丢弃洞位(紧凑结果)。
assert_eq!(
eval_str("new Array(1).flatMap(function(x){return [x];}).length+':'+[1].flatMap(function(){return [1,,3];}).join(',')+':'+[1].flatMap(function(){return [1,,3];}).length"),
"0:1,3:2"
);
// indexOf/lastIndexOf:洞位不参与比较。
assert_eq!(eval_str("[1,,3].indexOf(undefined)+':'+[1,,3].lastIndexOf(undefined)"), "-1:-1");
// slice:终长 = end - start;源洞位在结果中留洞。
assert_eq!(
eval_str("new Array(3).slice().length+':'+[1,,3].slice(0,2).length+':'+(1 in [1,,3].slice(0,2))"),
"3:2:false"
);
// concat:this 与数组实参的洞位均在结果中保洞。
assert_eq!(
eval_str("[1].concat([,2]).length+':'+(1 in [1].concat([,2]))+':'+[1].concat(new Array(1)).length"),
"3:false:2"
);
// flat:顶层与嵌套洞位均丢弃(紧凑结果)。
assert_eq!(eval_str("[,1].flat().length+':'+[1,[2,,3],4].flat().join(',')"), "1:1,2,3,4");
// sort:洞位不参与比较;present 值紧凑写回,尾部转洞。
assert_eq!(
eval_str("var a=[,1,3,2].sort(); (0 in a)+':'+(3 in a)+':'+a.join(',')"),
"true:false:1,2,3,"
);
assert_eq!(
eval_str("var b=[5,,1,3].sort(function(x,y){return x-y;}); b.join(',')+':'+(3 in b)"),
"1,3,5,:false"
);
// 防过度门控:find 族无门控(洞位照常调回调)、includes 无门控(洞 Get 得 undefined)。
assert_eq!(
eval_str("[,1].findIndex(function(){return true;})+':'+[1,,3].includes(undefined)+':'+[,1].findIndex(function(x){return x===1;})"),
"0:true:1"
);
}
@@ -392,19 +392,19 @@ fn array_fill_single() {
}
#[test]
fn array_iteration_hole_reads_undefined() {
// 稀疏数组(字面量空洞 / delete)元素读取落到 undefined:引擎对 hole 不跳过
// 回调而是读 undefined(pre-existing 语义,直读 fast path 与慢路径一致)。
fn array_iteration_holes_skipped_per_spec() {
// 稀疏数组(字面量空洞 / delete):洞位不触发回调,map 结果在洞位留洞,
// join 把洞位渲染为空串。
let (vm, result) = eval("[10,,30].map(x=>x*2).join(',')").unwrap();
assert_eq!(to_str(&vm, result), "20,NaN,60");
assert_eq!(to_str(&vm, result), "20,,60");
// delete 产生的 hole 经 map 读 undefined,不残留洞前值。
// delete 产生的洞位经 map 同样跳过,不残留洞前值。
let (vm, result) = eval("var a=[1,2,3]; delete a[1]; a.map(x=>x+1).join(',')").unwrap();
assert_eq!(to_str(&vm, result), "2,NaN,4");
assert_eq!(to_str(&vm, result), "2,,4");
// reduce 对 hole 累加 undefined。
// reduce 跳过洞位:初值直接接到首个 present 元素。
let (vm, result) = eval("var a=[1,,3]; a.reduce((s,x)=>s+','+x,'')").unwrap();
assert_eq!(to_str(&vm, result), ",1,undefined,3");
assert_eq!(to_str(&vm, result), ",1,3");
}
#[test]