diff --git a/crates/oxide_builtins/src/array/element.rs b/crates/oxide_builtins/src/array/element.rs index 36cfb0b1..df7cc177 100644 --- a/crates/oxide_builtins/src/array/element.rs +++ b/crates/oxide_builtins/src/array/element.rs @@ -197,13 +197,18 @@ pub fn array_slice(vm: &mut H, args: &[u8]) -> NativeResult { /// /// # 步骤 /// 1. 折算 `start` / `deleteCount`(负数自尾部、夹到界内)。 -/// 2. 先建 removed 数组:逐被删下标 HasProperty 门控 + Get,命中 -/// CreateDataPropertyOrThrow、缺失保洞;结果对象钉入返回寄存器(其后的 -/// 搬移/插入各带用户调用窗口,Rust 局部指针不属 GC 根集)。 -/// 3. 双向搬移:源存在则 Get + 严格 Set 到目标,源缺失则目标 DeletePropertyOrThrow +/// 2. 先按 ArraySpeciesCreate 建 removed 数组(收集与搬移之前,用户构造器 +/// 读到未改动的 O),结果对象钉入返回寄存器(其后的收集/搬移/插入各带 +/// 用户调用窗口,Rust 局部指针不属 GC 根集)。 +/// 3. 逐被删下标 HasProperty 门控 + Get,命中 CreateDataPropertyOrThrow; +/// 缺失位仅真数组 A 保洞(A 预填 present-undefined 须显式置洞),非数组 +/// A 按规范不建属性。 +/// 4. `Set(A, "length", deleteCount, true)` 在搬移前收尾。 +/// 5. 双向搬移:源存在则 Get + 严格 Set 到目标,源缺失则目标 DeletePropertyOrThrow /// (不可配置抛 TypeError);扩容方向下超出当前元素数的目标先记录,length /// 写入后补置洞。 -/// 4. 插入项逐个严格 Set,length 快写收尾。 +/// 6. 插入项逐个严格 Set,`Set(this, "length", newLen, true)` 收尾(length +/// 不可写抛 TypeError)。 /// /// # 副作用 /// - 元素被删/插/移;Get/Set 可经原型链触发用户代码。 @@ -234,9 +239,13 @@ pub fn array_splice(vm: &mut H, args: &[u8]) -> NativeResult { let insert_count = if args.len() > 3 { args.len() - 3 } else { 0 }; - // removed 先行构建并钉入返回寄存器:搬移/插入窗口的用户 setter 可触发 - // 回收,结果对象指针须保 GC 根,返回时从钉位读回。 - let removed_arr = create_new_array(vm, delete_count); + // removed 先行按 ArraySpeciesCreate 构建(收集/搬移之前)并钉入返回寄存器: + // 搬移/插入窗口的用户 setter 可触发回收,结果对象指针须保 GC 根,返回时 + // 从钉位读回;species 读与构造器抛错即 splice 抛错。 + let removed_arr = match array_species_create(vm, recv, true, delete_count) { + Ok(p) => p, + Err(err) => return NativeResult::Err(err), + }; vm.set_reg(0, JsValue::from_js_object(removed_arr)); for k in 0..delete_count { match gated_get(vm, arr_ptr, start + k, recv) { @@ -246,12 +255,27 @@ pub fn array_splice(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Err(err); } } - // 源缺失位保洞(removed 预填 present-undefined,须显式置洞)。 - Ok(None) => unsafe { (*removed_arr).mark_hole_at(k) }, + // 源缺失位仅真数组 A 保洞(A 预填 present-undefined,须显式置洞); + // 非数组 A 不建属性。 + Ok(None) => { + if unsafe { &*removed_arr }.is_array() { + unsafe { (*removed_arr).mark_hole_at(k) } + } + } Err(err) => return NativeResult::Err(err), } } + // Set(A, "length", actualDeleteCount, true) 先于搬移:真数组 A 构造器已置 + // 长度时为空写,构造器忽略单参时扩长。 + let a_val = JsValue::from_js_object(removed_arr); + let length_si = vm.string_key_si("length"); + if let Err(err) = + vm.ordinary_set(unsafe { &mut *removed_arr }, length_si, js_array_index(delete_count), a_val, true) + { + return NativeResult::Err(from_engine_error(vm, &err)); + } + // 目标超出当前元素数的洞位无法即时置(越界 no-op),length 写入后补。 let mut pending_holes: Vec = Vec::new(); @@ -305,8 +329,12 @@ pub fn array_splice(vm: &mut H, args: &[u8]) -> NativeResult { } } + // Set(this, "length", newLen, true) 同样走 Set:length 不可写在此抛 + // TypeError(与新旧长度是否相同无关)。 let new_len = n + insert_count - delete_count; - unsafe { (*arr_ptr).set_prop_count_fast(new_len) }; + if let Err(err) = vm.ordinary_set(unsafe { &mut *arr_ptr }, length_si, js_array_index(new_len), recv, true) { + return NativeResult::Err(from_engine_error(vm, &err)); + } for h in pending_holes { unsafe { (*arr_ptr).mark_hole_at(h) }; } diff --git a/crates/oxide_builtins/tests/array_hole_semantics_tests.rs b/crates/oxide_builtins/tests/array_hole_semantics_tests.rs index ed3e7a48..59544178 100644 --- a/crates/oxide_builtins/tests/array_hole_semantics_tests.rs +++ b/crates/oxide_builtins/tests/array_hole_semantics_tests.rs @@ -194,3 +194,194 @@ fn test_at_out_of_bounds_undefined() { let (_vm, result) = eval_value("[1, 2].at(9)").unwrap(); assert!(result.is_undefined()); } + +// splice species:Ctor 返回体直接作结果,构造器恰收单参 actualDeleteCount,O 已搬移。 +#[test] +fn test_splice_species_ctor_returned_instance() { + let out = eval_str( + "(() => { let seen = null; const inst = { mark: 1 }; \ + function Ctor(n) { seen = n; inst.len = n; return inst; } \ + const a = [0,1,2,3]; a.constructor = {}; a.constructor[Symbol.species] = Ctor; \ + const r = a.splice(2); \ + return seen + '|' + (r === inst) + '|' + r.len + '|' + a.join(','); })()", + ) + .unwrap(); + assert_eq!(out, "2|true|2|0,1"); +} + +// splice species 普通 Ctor 无返回体:结果非数组、原型为 Ctor.prototype, +// Set(A,"length") 扩长至删除数且值逐位填入。 +#[test] +fn test_splice_species_plain_ctor_extends_length() { + let out = eval_str( + "(() => { function Ctor() {} \ + const a = [0,1,2,3]; a.constructor = {}; a.constructor[Symbol.species] = Ctor; \ + const r = a.splice(0); \ + return Array.isArray(r) + '|' + (Object.getPrototypeOf(r) === Ctor.prototype) + '|' + \ + r.length + '|' + r[0] + ',' + r[1] + ',' + r[2] + ',' + r[3]; })()", + ) + .unwrap(); + assert_eq!(out, "false|true|4|0,1,2,3"); +} + +// splice species 为 null / 缺省:回退真数组(原型为 Array.prototype),值与长度正确。 +#[test] +fn test_splice_species_null_undef_plain_array() { + let out = eval_str( + "(() => { const a = [0,1,2]; a.constructor = {}; a.constructor[Symbol.species] = null; \ + const r1 = a.splice(1); \ + const b = [0,1,2]; b.constructor = {}; \ + const r2 = b.splice(1); \ + return Array.isArray(r1) + '|' + (Object.getPrototypeOf(r1) === Array.prototype) + '|' + \ + r1.length + '|' + r1[0] + '|' + Array.isArray(r2) + '|' + \ + (Object.getPrototypeOf(r2) === Array.prototype) + '|' + r2.length; })()", + ) + .unwrap(); + assert_eq!(out, "true|true|2|1|true|true|2"); +} + +// splice species 构造器收 -0 折算的删除数时收到 +0(非 -0)。 +#[test] +fn test_splice_species_neg_zero() { + let out = eval_str( + "(() => { let isNeg = null; \ + function Ctor(n) { isNeg = Object.is(n, -0); } \ + const a = []; a.constructor = {}; a.constructor[Symbol.species] = Ctor; \ + const r = a.splice(0, -0); \ + return 'neg=' + isNeg + '|len=' + r.length; })()", + ) + .unwrap(); + assert_eq!(out, "neg=false|len=0"); +} + +// splice species 结果 "0" 不可写:CreateDataPropertyOrThrow 覆盖为 present, +// 描述符恢复可写可配置。 +#[test] +fn test_splice_species_writable_false_overwritten() { + let out = eval_str( + "(() => { function Ctor(n) { const o = {}; o.length = n; \ + Object.defineProperty(o, '0', { writable: false, configurable: true, enumerable: true, value: 99 }); \ + return o; } \ + const a = [0,1,2]; a.constructor = {}; a.constructor[Symbol.species] = Ctor; \ + const r = a.splice(1); \ + const d = Object.getOwnPropertyDescriptor(r, '0'); \ + return r[0] + '|' + d.writable + '|' + d.configurable + '|' + r.length; })()", + ) + .unwrap(); + assert_eq!(out, "1|true|true|2"); +} + +// splice species 结果 "0" 不可配置:CreateDataPropertyOrThrow 抛 TypeError。 +#[test] +fn test_splice_species_non_configurable_throws() { + let out = eval_str( + "(() => { function Ctor(n) { const o = {}; o.length = n; \ + Object.defineProperty(o, '0', { writable: true, configurable: false, enumerable: true, value: 99 }); \ + return o; } \ + const a = [0,1,2]; a.constructor = {}; a.constructor[Symbol.species] = Ctor; \ + try { a.splice(1); return 'no-throw'; } catch (e) { return e.constructor.name; } })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// splice species 结果不可扩展:CreateDataPropertyOrThrow 抛 TypeError。 +#[test] +fn test_splice_species_non_extensible_throws() { + let out = eval_str( + "(() => { function Ctor(n) { const o = {}; o.length = n; Object.preventExtensions(o); return o; } \ + const a = [0,1,2]; a.constructor = {}; a.constructor[Symbol.species] = Ctor; \ + try { a.splice(1); return 'no-throw'; } catch (e) { return e.constructor.name; } })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// splice constructor 为非对象非 undefined(null/数字/字符串/布尔四值)均抛 TypeError。 +#[test] +fn test_splice_ctor_non_object_throws() { + let out = eval_str( + "(() => { let ok = 0; \ + for (const cv of [null, 1, 'string', true]) { \ + const a = [0,1,2]; a.constructor = cv; \ + try { a.splice(1); } catch (e) { if (e instanceof TypeError) ok++; } } \ + return String(ok); })()", + ) + .unwrap(); + assert_eq!(out, "4"); +} + +// 防过度修:无 species 时结果仍为真数组、原型为 Array.prototype,自定义 Ctor 不被调用。 +#[test] +fn test_splice_no_species_plain_array() { + let out = eval_str( + "(() => { let calls = 0; function Ctor() { calls++; } \ + const a = [0,1,2]; \ + const r = a.splice(1); \ + return Array.isArray(r) + '|' + (Object.getPrototypeOf(r) === Array.prototype) + '|' + r.length + '|' + calls; })()", + ) + .unwrap(); + assert_eq!(out, "true|true|2|0"); +} + +// 步骤序:构造器在收集与搬移之前执行,读到未改动的 O。 +#[test] +fn test_splice_ctor_reads_o_before_mutation() { + let out = eval_str( + "(() => { let seenLen = null, seen0 = null; \ + function Ctor(n) { seenLen = a.length; seen0 = a[0]; } \ + const a = [0,1,2,3]; a.constructor = {}; a.constructor[Symbol.species] = Ctor; \ + a.splice(0, 2, 9); \ + return seenLen + '|' + seen0 + '|' + a.join(','); })()", + ) + .unwrap(); + assert_eq!(out, "4|0|9,2,3"); +} + +// @@species getter 抛错原样透传(非构造器 constructor 上的 species 读取异常上抛)。 +#[test] +fn test_splice_species_poisoned_propagates() { + let out = eval_str( + "(() => { const a = [0,1,2]; a.constructor = {}; \ + Object.defineProperty(a.constructor, Symbol.species, { get() { throw new RangeError('species-get'); } }); \ + try { a.splice(1); return 'no-throw'; } catch (e) { return e.constructor.name + ':' + e.message; } })()", + ) + .unwrap(); + assert_eq!(out, "RangeError:species-get"); +} + +// length 不可写(语料同形 defineProperty 仅收窄 writable 位):新旧长度相同的 +// 空 splice 与收缩/插入 splice 均抛 TypeError。 +#[test] +fn test_splice_length_nonwritable_throws() { + let out = eval_str( + "(() => { const a = [0,1,2]; Object.defineProperty(a, 'length', { writable: false }); \ + let k1; try { a.splice(1, 2, 4); return 'shrink-no-throw'; } catch (e) { k1 = e.constructor.name; } \ + const b = [0,1,2]; Object.defineProperty(b, 'length', { writable: false }); \ + try { b.splice(0, 0); return 'noop-no-throw'; } catch (e) { return k1 + '|' + e.constructor.name; } })()", + ) + .unwrap(); + assert_eq!(out, "TypeError|TypeError"); +} + +// 冻结数组空删除也走 length 写:抛 TypeError(防空 splice 被特例放行)。 +#[test] +fn test_splice_frozen_empty_delete_throws_length() { + let out = eval_str( + "(() => { try { Object.freeze([0,1,2]).splice(0, 0); return 'no-throw'; } \ + catch (e) { return e.constructor.name; } })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// 冻结数组删除命中元素:元素严格 Set 面抛 TypeError(回归护栏)。 +#[test] +fn test_splice_frozen_element_set_throws() { + let out = eval_str( + "(() => { try { Object.freeze([0,1,2]).splice(1, 1); return 'no-throw'; } \ + catch (e) { return e.constructor.name; } })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +}