From c5e331885776f11945c95065b43fa36dd3397979 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 20 Sep 2026 14:38:41 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20concat=20=E6=8C=89=E7=89=A9=E7=A7=8D?= =?UTF-8?q?=E6=9E=84=E9=80=A0=E4=B8=8E=20isConcatSpreadable=20=E8=A7=84?= =?UTF-8?q?=E8=8C=83=E6=95=B4=E6=BA=90=E5=B1=95=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/array/element.rs | 204 ++++++++++++++---- crates/oxide_builtins/src/array/from.rs | 19 +- .../tests/array_hole_semantics_tests.rs | 193 +++++++++++++++++ crates/oxide_types/src/private_key.rs | 8 +- 4 files changed, 368 insertions(+), 56 deletions(-) diff --git a/crates/oxide_builtins/src/array/element.rs b/crates/oxide_builtins/src/array/element.rs index 980bd5a5..e8f73a5a 100644 --- a/crates/oxide_builtins/src/array/element.rs +++ b/crates/oxide_builtins/src/array/element.rs @@ -8,12 +8,16 @@ use oxide_runtime_api::{NativeResult, VmHost}; use crate::builtins_debug; use crate::builtins_error; +use oxide_types::private_key::{make_well_known_symbol_key, WELL_KNOWN_SYMBOL_IS_CONCAT_SPREADABLE}; + use super::common::{ array_ptr, array_type_error, arraylike_get, arraylike_get_or_err, arraylike_index_present, check_array_create_len, - clamp_relative, create_new_array, get_this_array_ref, get_this_arraylike, js_array_index, - to_integer_or_infinity_bounded, + clamp_relative, get_this_array_ref, get_this_arraylike, js_array_index, string_arraylike_units, + to_integer_or_infinity_bounded, unit_string_value, +}; +use super::from::{ + array_from_set_length, array_from_set_prop, array_species_create, create_data_property_or_throw, from_engine_error, }; -use super::from::{array_species_create, create_data_property_or_throw, from_engine_error}; /// 按规范读单个元素:HasProperty 门控(自身与原型链任一层命中才算存在, /// 数组元素区 hole 视同缺失),命中时返回 Get 值(用户 getter 的异常原样 @@ -395,63 +399,171 @@ pub fn array_splice(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(vm.reg(0)) } -/// `Array.prototype.concat(...items)`:连接 this 与参数(数组参数展开)返回新数组。 +/// `Array.prototype.concat(...items)`:连接 this 与参数返回新数组。 +/// +/// # 步骤 +/// 1. `ToObject` 装箱基元 this(null/undefined 抛 TypeError),装箱体钉入返回 +/// 寄存器跨用户窗口保 GC 根(结果 A 钉入后让位,装箱体此后按接收者值 +/// 传递保活)。 +/// 2. `A = ArraySpeciesCreate(O, 0)`(真数组按 constructor/@@species 构造, +/// 读取先于一切 isConcatSpreadable 读取)并钉入返回寄存器;元素写入一律 +/// CreateDataPropertyOrThrow 语义(frozen 等受限目标抛 TypeError)。 +/// 3. 逐源 `E ∈ [O, ...args]`:`IsConcatSpreadable(E)`——非对象 false;对象 +/// 读 `@@isConcatSpreadable`(getter 异常透传),值非 undefined 取 +/// ToBoolean,否则取 IsArray(E)。 +/// 4. spreadable 源:`len = ToLength(Get(E, "length"))`(getter/数值转换异常 +/// 透传);`n + len > 2^53-1` 抛 TypeError;逐索引 HasProperty 门控 + Get, +/// 命中写 `A[n+k]`,缺失位记录洞(真数组目标越界写会扩成 present-undefined, +/// length 写入后补置洞)。 +/// 5. 非 spreadable 源:整值写 `A[n]`;`n += 1`。 +/// 6. 收尾 `Set(A, "length", n, true)`(length 不可写抛 TypeError)。 +/// +/// # 边界与前提 +/// - 字符串 exotic 源(装箱串)索引未物化:length 与单元直读 [[StringData]], +/// 与 Array.from 的 array-like 路径同面。 +/// - 引擎密集数组上限 2^32-1(真数组源 len 天然不越界),len 越上限的展开 +/// 仅可能来自人造大 length 对象,写入按引擎密集封顶处理。 +/// +/// # 副作用 +/// - species/constructor、isConcatSpreadable、length 与逐元素 Get 各可经原型链 +/// 触发用户代码;结果 A 钉返回寄存器,返回时自钉位读回。 pub fn array_concat(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("Array.prototype.concat called with {} args", args.len()); - let this_val = vm.reg(args[0]); - let (arr_ptr, n, _is_array) = match get_this_arraylike(vm, this_val) { + // ToObject:基元 this 装箱(null/undefined 抛 TypeError)。 + let this_val = match oxide_runtime_api::to_object(vm.reg(args[0]), vm) { Ok(v) => v, - Err(e) => return NativeResult::Err(e), + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), }; - // this 与数组实参的洞位按缺失复制:结果对应位留洞(记录洞下标,收尾置洞)。 - let mut all: Vec = Vec::new(); + // 装箱体钉入返回寄存器:它只存于 Rust 局部,this 寄存器持原始基元(不在 + // 根集),跨 constructor/species 读取用户窗口须保 GC 根;结果 A 钉入后 + // 让位,其后装箱体按接收者值传递保活。 + if !args.contains(&0) { + vm.set_reg(0, this_val); + } + // SAFETY: this_val 为对象值,指针指向存活对象。 + let is_array = unsafe { &*this_val.as_js_object_ptr() }.is_array(); + + // 全部源值(this 在前、实参随后)先读入局部:结果钉返回寄存器可能覆写 + // 实参寄存器槽,须先于钉位完成读取。 + let mut sources: Vec = Vec::with_capacity(args.len()); + sources.push(this_val); + for ® in args.iter().skip(1) { + sources.push(vm.reg(reg)); + } + + // ArraySpeciesCreate(O, 0):真数组按 constructor/@@species 构造目标, + // array-like 直接 ArrayCreate(不读 constructor)。 + let a_ptr = match array_species_create(vm, this_val, is_array, 0) { + Ok(p) => p, + Err(err) => return NativeResult::Err(err), + }; + // SAFETY: a_ptr 来自 ArraySpeciesCreate,指向存活对象。 + let a_is_array = unsafe { &*a_ptr }.is_array(); + // 结果钉入返回寄存器:其后的元素读取 / 写入窗口保 GC 根,返回时自钉位读回。 + vm.set_reg(0, JsValue::from_js_object(a_ptr)); + + let spread_key = make_well_known_symbol_key(WELL_KNOWN_SYMBOL_IS_CONCAT_SPREADABLE); + let length_si = vm.string_key_si("length"); + let mut n_acc: u64 = 0; let mut holes: Vec = Vec::new(); - if let Err(err) = check_array_create_len(vm, n) { - return NativeResult::Err(err); - } - for i in 0..n { - if arraylike_index_present(vm, arr_ptr, i) { - all.push(arraylike_get_or_err!(vm, arr_ptr, i)); + + // 源按序展开。 + for e_val in sources.iter().copied() { + // IsConcatSpreadable:非对象 false;对象读 @@isConcatSpreadable(getter + // 异常透传),值非 undefined 取 ToBoolean,否则取 IsArray(E)。 + let e_ptr = e_val.as_js_object_ptr(); + let spreadable = if !e_val.is_object() || e_ptr.is_null() { + false } else { - holes.push(all.len()); - all.push(JsValue::undefined()); + // SAFETY: e_ptr 来自对象值,指向存活对象。 + let v = match vm.ordinary_get(unsafe { &*e_ptr }, spread_key, e_val) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + if v.is_undefined() { + // SAFETY: 同上。 + unsafe { &*e_ptr }.is_array() + } else { + oxide_runtime_api::to_boolean(v) + } + }; + if !spreadable { + // 非 spreadable 源:整值追加。 + if let Err(err) = array_from_set_prop(vm, a_ptr, a_is_array, n_acc as usize, e_val) { + return NativeResult::Err(err); + } + n_acc += 1; + continue; } - } - for &arg_reg in args.iter().skip(1) { - let val = vm.reg(arg_reg); - if val.is_object() { - let o_ptr = val.as_js_object_ptr(); - if !o_ptr.is_null() { - let o = unsafe { &*o_ptr }; - let on = o.prop_count() as usize; - // 真数组时展开元素(空数组展开为零元素,贡献无;洞位同样留洞)。 - if o.is_array() { - let dense = o.array_elements_meta_vec().is_none(); - for i in 0..on { - if dense || !o.prop_meta_at(i).is_some_and(|m| m.is_hole()) { - all.push(o.get_prop_at(i)); - } else { - holes.push(all.len()); - all.push(JsValue::undefined()); - } - } - continue; + // 字符串 exotic 源(装箱串)单元直读(与 Array.from 同面):length 即 + // 单元数,其余源 len = ToLength(Get(E, "length")),getter 与数值转换 + // 异常透传。 + let string_units = string_arraylike_units(vm, e_val); + let len = match &string_units { + Some(units) => units.len(), + None => { + let len_val = match vm.ordinary_get(unsafe { &*e_ptr }, length_si, e_val) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + if len_val.is_symbol() { + return NativeResult::Err(array_type_error(vm, "Cannot convert a Symbol value to a number")); + } + let len_num = match vm.coerce_number_bounded(len_val) { + Ok(v) => v, + Err(msg) => return NativeResult::Err(from_engine_error(vm, &msg)), + }; + if len_num.is_nan() || len_num <= 0.0 { + 0usize + } else { + len_num.min(9_007_199_254_740_991.0) as usize } } + }; + // n + len > 2^53-1 抛 TypeError(f64 比较免整数上溢)。 + if n_acc as f64 + len as f64 > 9_007_199_254_740_991.0 { + return NativeResult::Err(array_type_error(vm, "Invalid array length")); } - all.push(val); + for k in 0..len { + let elem = match &string_units { + Some(units) => { + if k < units.len() { + Some(unit_string_value(vm, units[k])) + } else { + None + } + } + None => match gated_get(vm, e_ptr, k, e_val) { + Ok(v) => v, + Err(err) => return NativeResult::Err(err), + }, + }; + match elem { + Some(val) => { + if let Err(err) = array_from_set_prop(vm, a_ptr, a_is_array, (n_acc as usize) + k, val) { + return NativeResult::Err(err); + } + } + // 缺失位:真数组目标越界写扩成 present-undefined,length 写入后 + // 补置洞;非数组目标不建属性。 + None => holes.push((n_acc as usize) + k), + } + } + n_acc += len as u64; } - let new_arr = create_new_array(vm, all.len()); - unsafe { - for (i, val) in all.iter().enumerate() { - (*new_arr).set_prop_at(i, *val); - } + + // 收尾 Set(A, "length", n, true):length 不可写抛 TypeError。 + if let Err(err) = array_from_set_length(vm, a_ptr, n_acc as usize) { + return NativeResult::Err(err); + } + // 缺失位在 length 写入后补洞(真数组元素区由 length 写扩展,落洞方在界内)。 + if a_is_array { for h in holes { - (*new_arr).mark_hole_at(h); + // SAFETY: a_ptr 为存活结果对象,h < 终长。 + unsafe { (*a_ptr).mark_hole_at(h) }; } - (*new_arr).set_prop_count(all.len()); } - NativeResult::Ok(JsValue::from_js_object(new_arr)) + NativeResult::Ok(vm.reg(0)) } /// `Array.prototype.join(separator)`:用分隔符连接元素字符串(null/undefined 视为空串)。 diff --git a/crates/oxide_builtins/src/array/from.rs b/crates/oxide_builtins/src/array/from.rs index 3f098d20..22784d35 100644 --- a/crates/oxide_builtins/src/array/from.rs +++ b/crates/oxide_builtins/src/array/from.rs @@ -362,14 +362,15 @@ pub(crate) fn create_data_property_or_throw( } } -/// 把元素写入 Array.from 的结果对象(CreateDataPropertyOrThrow 语义):真数组 -/// 走密集元素区,普通对象属性名按十进制索引字符串。 +/// 把元素写入 Array 族结果对象(CreateDataPropertyOrThrow 语义,Array.from/of +/// 与 Array.prototype.concat 共用):真数组走密集元素区,普通对象属性名按十进制 +/// 索引字符串。 /// /// # 边界与前提 /// - 真数组快路径(可扩展 + length 可写 + 槽位无元数据)与规范 define 同形, /// 裸写零漂移;完整性受限目标(frozen/sealed/preventExtensions)与携带元 /// 数据的槽位(洞/只读/访问器)一律落慢路径,按规范抛 TypeError。 -fn array_from_set_prop( +pub(crate) fn array_from_set_prop( vm: &mut H, a: *mut JsObject, is_array: bool, i: usize, val: JsValue, ) -> Result<(), JsValue> { if is_array { @@ -390,15 +391,17 @@ fn array_from_set_prop( create_data_property_or_throw(vm, unsafe { &mut *a }, key_si, val) } -/// 收尾设置 Array.from 结果对象的 length:统一走普通 Set——真数组 length 写 -/// 按 ArraySetLength 可写性判定(frozen / length 收窄不可写目标抛 TypeError), -/// 普通对象可触发继承的 length setter,其异常透传。 -fn array_from_set_length(vm: &mut H, a: *mut JsObject, len: usize) -> Result<(), JsValue> { +/// 收尾设置 Array 族结果对象的 length(Array.from/of 与 Array.prototype.concat +/// 共用):统一走普通 Set——真数组 length 写按 ArraySetLength 可写性判定 +/// (frozen / length 收窄不可写目标抛 TypeError),普通对象可触发继承的 length +/// setter,其异常透传。length 值经 `js_array_index` 编码(超 i32 范围用 float +/// 精确表示,避免收窄为负数)。 +pub(crate) fn array_from_set_length(vm: &mut H, a: *mut JsObject, len: usize) -> Result<(), JsValue> { let a_obj = unsafe { &mut *a }; let length_key = vm.new_string("length"); let length_si = vm.property_key_si(length_key); let a_val = JsValue::from_js_object(a); - match vm.ordinary_set(a_obj, length_si, JsValue::int(len as i32), a_val, true) { + match vm.ordinary_set(a_obj, length_si, js_array_index(len), a_val, true) { Ok(()) => Ok(()), Err(err) => Err(from_engine_error(vm, &err)), } diff --git a/crates/oxide_builtins/tests/array_hole_semantics_tests.rs b/crates/oxide_builtins/tests/array_hole_semantics_tests.rs index 37a380f1..dd244e6b 100644 --- a/crates/oxide_builtins/tests/array_hole_semantics_tests.rs +++ b/crates/oxide_builtins/tests/array_hole_semantics_tests.rs @@ -565,6 +565,199 @@ fn test_concat_mixed_args_order_and_holes() { assert_eq!(out, "5|1|1,2,3,0|4|1|false"); } +// ── concat species 面收口钉(node v20.19.2 实测值钉死) ─────────────────── + +// 引擎钉:concat 基元 this 经 ToObject 装箱——数字/布尔接收者展开为单元素 +// 真数组,包装体身份保留。 +#[test] +fn test_concat_primitive_this_boxed() { + let out = eval_str( + "(() => { const r1 = Array.prototype.concat.call(101); \ + const r2 = Array.prototype.concat.call(true); \ + return (r1[0] instanceof Number) + '|' + r1.length + '|' + \ + (r2[0] instanceof Boolean) + '|' + r2.length; })()", + ) + .unwrap(); + assert_eq!(out, "true|1|true|1"); +} + +// 引擎钉:spreadable 对象源按 HasProperty 门控展开——洞位保洞、present 位 +// 按 Get 取值,length 决定展开宽度。 +#[test] +fn test_concat_spreadable_object_holes() { + let out = eval_str( + "(() => { const o = { length: 6 }; o[Symbol.isConcatSpreadable] = true; \ + o[0] = 'A'; o[2] = 'C'; \ + const r = [].concat(o); \ + return r.length + '|' + (0 in r) + '|' + (1 in r) + '|' + r[0] + '|' + r[2]; })()", + ) + .unwrap(); + assert_eq!(out, "6|true|false|A|C"); +} + +// 引擎钉:spreadable 源 length 为负数 / 经 toString 折算 NaN 均 ToLength 归 0, +// 不展开任何元素。 +#[test] +fn test_concat_spreadable_length_to_zero() { + let out = eval_str( + "(() => { const o = { length: -5, 0: 'A' }; o[Symbol.isConcatSpreadable] = true; \ + const r1 = [].concat(o); \ + const o2 = { 0: 'A' }; o2[Symbol.isConcatSpreadable] = true; \ + o2.length = { toString() { return 'SIX'; }, valueOf: null }; \ + return r1.length + '|' + [].concat(o2).length; })()", + ) + .unwrap(); + assert_eq!(out, "0|0"); +} + +// 引擎钉:spreadable 源 length getter 抛错 / 索引访问器抛错均原样透传。 +#[test] +fn test_concat_poisoned_length_and_index_propagate() { + let out = eval_str( + "(() => { const o = {}; o[Symbol.isConcatSpreadable] = true; \ + Object.defineProperty(o, 'length', { get() { throw new Error('lc'); } }); \ + let t1 = ''; try { [].concat(o); } catch (e) { t1 = e.message; } \ + const a = [1, 2, 3]; \ + Object.defineProperty(a, 1, { configurable: true, get() { throw new Error('ie'); } }); \ + let t2 = ''; try { [].concat(a); } catch (e) { t2 = e.message; } \ + return t1 + '|' + t2; })()", + ) + .unwrap(); + assert_eq!(out, "lc|ie"); +} + +// 引擎钉:isConcatSpreadable getter 抛错透传(读序上先于一切展开)。 +#[test] +fn test_concat_is_concat_spreadable_getter_throws() { + let out = eval_str( + "(() => { const o = {}; \ + Object.defineProperty(o, Symbol.isConcatSpreadable, { get() { throw new Error('sg'); } }); \ + let t = ''; try { [].concat(o); } catch (e) { t = e.message; } \ + return t; })()", + ) + .unwrap(); + assert_eq!(out, "sg"); +} + +// 引擎钉:读序 constructor → isConcatSpreadable(this) → isConcatSpreadable(实参), +// getter 返回值 undefined 时实参整值追加。 +#[test] +fn test_concat_read_order_constructor_then_spreadable() { + let out = eval_str( + "(() => { const calls = []; \ + const arr = []; \ + Object.defineProperty(arr, 'constructor', { configurable: true, get() { calls.push('c'); return Array; } }); \ + Object.defineProperty(arr, Symbol.isConcatSpreadable, { configurable: true, get() { calls.push('s'); } }); \ + const arg = {}; \ + Object.defineProperty(arg, Symbol.isConcatSpreadable, { configurable: true, get() { calls.push('as'); } }); \ + const r = arr.concat(arg); \ + return calls.join(',') + '|' + (r[0] === arg) + '|' + r.length; })()", + ) + .unwrap(); + assert_eq!(out, "c,s,as|true|1"); +} + +// 引擎钉:isConcatSpreadable 值非 undefined 取 ToBoolean——truthy 展开(零元素 +// length)、falsey 整值追加。 +#[test] +fn test_concat_spreadable_value_boolean_coercion() { + let out = eval_str( + "(() => { const o = {}; o[Symbol.isConcatSpreadable] = 86; \ + const r1 = [].concat(o); \ + o[Symbol.isConcatSpreadable] = false; \ + const r2 = [].concat(o); \ + return r1.length + '|' + (r2[0] === o) + '|' + r2.length; })()", + ) + .unwrap(); + assert_eq!(out, "0|true|1"); +} + +// 引擎钉:函数 / 正则 / 装箱串默认整值,显式 spreadable 时按索引展开(含 +// 原型链命中),实参侧真数组参数自有索引访问器按 Get 触发。 +#[test] +fn test_concat_spreadable_function_and_proto_chain() { + let out = eval_str( + "(() => { const fn = function (a, b, c) {}; \ + const r0 = [].concat(fn); \ + fn[Symbol.isConcatSpreadable] = true; fn[0] = 1; fn[1] = 2; fn[2] = 3; \ + const r1 = [].concat(fn); \ + const a = [1, 2]; \ + Object.defineProperty(a, 1, { configurable: true, get() { return 99; } }); \ + const r2 = [0].concat(a); \ + return (r0[0] === fn) + '|' + r1.join(',') + '|' + r1.length + '|' + r2[2] + '|' + r2.length; })()", + ) + .unwrap(); + assert_eq!(out, "true|1,2,3|3|99|3"); +} + +// 引擎钉:arguments 源 spreadable + 删位——删位保洞、余位按值展开。 +#[test] +fn test_concat_arguments_deleted_slot_hole() { + let out = eval_str( + "(() => { const args = (function (a, b, c) { return arguments; })(1, 2, 3); \ + delete args[1]; args[Symbol.isConcatSpreadable] = true; \ + const r = [].concat(args); \ + return r.length + '|' + (1 in r) + '|' + r[0] + '|' + r[2]; })()", + ) + .unwrap(); + assert_eq!(out, "3|false|1|3"); +} + +// 引擎钉:装箱串源 spreadable 时按 UTF-16 单元展开(单元未物化,直读串数据)。 +#[test] +fn test_concat_string_wrapper_spreadable_units() { + let out = eval_str( + "(() => { const s = new String('ab'); s[Symbol.isConcatSpreadable] = true; \ + const r = [].concat(s); \ + return r.join(',') + '|' + r.length + '|' + (0 in r); })()", + ) + .unwrap(); + assert_eq!(out, "a,b|2|true"); +} + +// 引擎钉:非数组对象 this 整值追加且实参保留(结果真数组、proto 为 +// Array.prototype,不读 constructor)。 +#[test] +fn test_concat_non_array_this_whole_value_args_kept() { + let out = eval_str( + "(() => { let ctorReads = 0; \ + const o = { length: 2, 0: 'x' }; \ + Object.defineProperty(o, 'constructor', { configurable: true, get() { ctorReads++; } }); \ + const r = Array.prototype.concat.call(o, 4, 5, 6); \ + return r.length + '|' + (r[0] === o) + '|' + r[1] + '|' + r[3] + '|' + \ + Array.isArray(r) + '|' + ctorReads; })()", + ) + .unwrap(); + assert_eq!(out, "4|true|4|6|true|0"); +} + +// 引擎钉:species 目标自有不可写 length——收尾 Set 抛 TypeError。 +#[test] +fn test_concat_species_non_writable_length_throws() { + let out = eval_str( + "(() => { const A = function (_l) { Object.defineProperty(this, 'length', { value: 0, writable: false }); }; \ + const arr = []; arr.constructor = {}; arr.constructor[Symbol.species] = A; \ + let t = ''; try { arr.concat(1); } catch (e) { t = e.name; } \ + return t; })()", + ) + .unwrap(); + assert_eq!(out, "TypeError"); +} + +// 引擎钉:Array.prototype 只读 "0" 下 CDO 写自有 writable:true 位。 +#[test] +fn test_concat_prototype_readonly_index_own_writable() { + let out = eval_str( + "(() => { Object.defineProperty(Array.prototype, '0', { value: 100, writable: false, configurable: true }); \ + try { const r = [101].concat(); \ + const d = Object.getOwnPropertyDescriptor(r, '0'); \ + return d.value + '|' + d.writable + '|' + r.hasOwnProperty('0'); } \ + finally { delete Array.prototype['0']; } })()", + ) + .unwrap(); + assert_eq!(out, "101|true|true"); +} + // ── flat / flatMap 收口钉(node v20.19.2 实测值钉死) ────────────────────── // 引擎钉:reverse (S,N) 臂非可配置 lower——Delete 先于 Set,抛时零改动。 diff --git a/crates/oxide_types/src/private_key.rs b/crates/oxide_types/src/private_key.rs index f66314cb..40ec7b86 100644 --- a/crates/oxide_types/src/private_key.rs +++ b/crates/oxide_types/src/private_key.rs @@ -53,8 +53,9 @@ pub const SYMBOL_KEY_BASE: u32 = 0xA000_0000; /// 保留给 well-known symbol 的键槽位数(位于 Symbol 键区间最低端)。 /// /// 键序与 TC39 well-known symbol 表一致:0-10 为既有项,11/12 分别为 -/// `Symbol.asyncDispose` 与 `Symbol.dispose`(显式资源管理提案)。 -pub const WELL_KNOWN_SYMBOL_COUNT: u32 = 13; +/// `Symbol.asyncDispose` 与 `Symbol.dispose`(显式资源管理提案),13 为 +/// `Symbol.isConcatSpreadable`。 +pub const WELL_KNOWN_SYMBOL_COUNT: u32 = 14; /// well-known symbol 的名称表,索引即 well-known symbol id。 /// @@ -75,6 +76,7 @@ pub const WELL_KNOWN_SYMBOL_NAMES: [&str; WELL_KNOWN_SYMBOL_COUNT as usize] = [ "Symbol.species", "Symbol.asyncDispose", "Symbol.dispose", + "Symbol.isConcatSpreadable", ]; /// well-known symbol 具名 id:`0`(键值 = `SYMBOL_KEY_BASE + id`)。 @@ -103,6 +105,8 @@ pub const WELL_KNOWN_SYMBOL_SPECIES: u32 = 10; pub const WELL_KNOWN_SYMBOL_ASYNC_DISPOSE: u32 = 11; /// well-known symbol 具名 id:`Symbol.dispose`。 pub const WELL_KNOWN_SYMBOL_DISPOSE: u32 = 12; +/// well-known symbol 具名 id:`Symbol.isConcatSpreadable`。 +pub const WELL_KNOWN_SYMBOL_IS_CONCAT_SPREADABLE: u32 = 13; /// 用户 symbol 下标的保留位掩码(低 28 位)。 const SYMBOL_INDEX_MASK: u32 = 0x0FFF_FFFF;