From 0c750fa74c2717421a44011d552005c8d8082a65 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Mon, 14 Sep 2026 03:04:03 +0000 Subject: [PATCH] =?UTF-8?q?emit/kernel/vm/builtins:=20=E5=B8=B8=E9=87=8F?= =?UTF-8?q?=E6=B1=A0=E9=94=AE=E7=BC=96=E7=A0=81=20+=20=E9=94=AE=E7=89=A9?= =?UTF-8?q?=E5=8C=96=E7=9C=9F=E5=AE=9E=E4=B8=B2=E5=80=BC=E9=9D=A2=20+=2016?= =?UTF-8?q?=20=E5=9B=9E=E5=BD=92=E9=92=89=E2=80=94=E2=80=94emit=20?= =?UTF-8?q?=E4=BE=A7=E5=AD=97=E9=9D=A2=E9=87=8F/=E9=94=AE/=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E7=BB=9F=E4=B8=80=E6=94=B9=E5=AD=98=E6=97=A0=E6=A0=87?= =?UTF-8?q?=E5=BF=97=E9=94=AE=E7=BC=96=E7=A0=81=EF=BC=9Alone=5Fsurrogates?= =?UTF-8?q?=20=E5=AD=97=E9=9D=A2=E9=87=8F=E4=B8=8E=E6=A8=A1=E6=9D=BF=20coo?= =?UTF-8?q?ked=20=E6=8C=89=20oxc=20marker=20=E8=A7=A3=E7=A0=81=E8=BF=98?= =?UTF-8?q?=E5=8E=9F=E7=9C=9F=E5=8D=95=E5=85=83=E5=BA=8F=E5=88=97=EF=BC=88?= =?UTF-8?q?FFFD+hex4=E2=89=A0fffd=E2=86=92surrogate=20=E5=8D=95=E5=85=83?= =?UTF-8?q?=E3=80=81FFFD+fffd=E2=86=92FFFD=E3=80=81FFFD+=E9=9D=9E=20hex=20?= =?UTF-8?q?=E9=98=B2=E5=BE=A1=E8=87=82=EF=BC=89=EF=BC=8C=E6=97=A0=E6=A0=87?= =?UTF-8?q?=E5=BF=97=E4=B8=B2=20encode=5Fkey(encode=5Futf16())=20=E5=85=A5?= =?UTF-8?q?=E6=B1=A0=EF=BC=88BMP=20=E6=97=A0=20FFFD=20=E9=94=AE=E6=81=92?= =?UTF-8?q?=E7=AD=89=20=3D=20=E7=8E=B0=E5=80=BC=EF=BC=8C=E6=B1=A0=E6=9D=A1?= =?UTF-8?q?=E7=9B=AE=E9=9B=B6=E6=BC=82=E7=A7=BB=EF=BC=89=EF=BC=8Cstring=5F?= =?UTF-8?q?pool=20=E9=9B=86=E4=B8=AD=E6=94=B6=E5=8F=A3=209=20=E5=A4=84?= =?UTF-8?q?=E5=85=A5=E6=B1=A0=E7=82=B9=EF=BC=9Binterner=20string=5Fptr=20?= =?UTF-8?q?=E7=94=B1=E5=8D=95=E5=85=83=E7=89=A9=E5=8C=96=EF=BC=88=E5=90=AB?= =?UTF-8?q?=E5=AD=A4=E7=AB=8B=E2=86=92FlatU16=E3=80=81=E8=89=AF=E5=BD=A2?= =?UTF-8?q?=E2=86=92Flat=EF=BC=89=EF=BC=9B=E9=94=AE=E7=89=A9=E5=8C=96?= =?UTF-8?q?=E9=9D=A2=E6=94=B9=E7=9C=9F=E5=AE=9E=20JS=20=E4=B8=B2=E5=80=BC?= =?UTF-8?q?=EF=BC=88key=5Fsi=5Fto=5Fjs=5Fvalue=20=E7=BB=8F=20decode=5Fkey?= =?UTF-8?q?=EF=BC=8CObject.keys/ownKeys/entries/Reflect.ownKeys/json=20rev?= =?UTF-8?q?iver=20=E9=94=AE=E5=90=8C=E6=BA=90=EF=BC=9B=E6=95=B4=E6=95=B0?= =?UTF-8?q?=E9=94=AE=E5=8F=8D=E8=A7=A3=E6=95=B0=E5=AD=97=E4=B8=B2=EF=BC=89?= =?UTF-8?q?=EF=BC=9B=E6=89=B9=E5=86=85=E4=BF=AE=E5=A4=8D=E4=B8=89=E5=A4=84?= =?UTF-8?q?=E5=AD=90=E9=9B=86=20A/B=20=E5=B7=AE=E5=88=86=E6=92=9E=E5=87=BA?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=EF=BC=9Areplace=20$n=20=E5=B1=95=E5=BC=80?= =?UTF-8?q?=E6=8C=89=20spec=20GetSubstitution=20=E9=87=8D=E5=86=99?= =?UTF-8?q?=EF=BC=88digitCount=202/1=E3=80=81=E8=B6=8A=E7=95=8C=E5=9B=9E?= =?UTF-8?q?=E9=80=80=201=20=E4=BD=8D=E3=80=81=E8=B6=8A=E7=95=8C=E5=90=AB?= =?UTF-8?q?=20$0=20=E5=8E=9F=E6=96=87=E8=BE=93=E5=87=BA=EF=BC=8Cnode=2013?= =?UTF-8?q?=20=E7=94=A8=E4=BE=8B=E5=AF=B9=E7=85=A7=E9=92=89=E4=BD=8F?= =?UTF-8?q?=EF=BC=89=E3=80=81String=20=E6=9E=84=E9=80=A0=E5=99=A8=20Symbol?= =?UTF-8?q?=20=E8=B7=AF=E5=BE=84=E6=8C=89=20new.target=20=E5=88=86?= =?UTF-8?q?=E6=B5=81=EF=BC=88String(sym)=E2=86=92=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E4=B8=B2=E3=80=81new=20String(sym)=E2=86=92TypeError=EF=BC=8C?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=20to=5Fstring=5Ffor=5Fstring=5Fconstructor?= =?UTF-8?q?=EF=BC=89=E3=80=81padStart/padEnd=20=E6=9C=AA=E7=BB=99=20fill?= =?UTF-8?q?=20=E6=97=B6=E6=8C=89=E5=8D=95=E7=A9=BA=E6=A0=BC=E8=A1=A5?= =?UTF-8?q?=EF=BC=88=E5=8E=9F=20lossy=20=E4=B8=B2=E5=8F=96=20'un'=20?= =?UTF-8?q?=E6=9D=82=E8=B4=A8=EF=BC=8Cpre-existing=20=E4=B8=80=E5=B9=B6?= =?UTF-8?q?=E5=BD=92=E9=9B=B6=EF=BC=89=EF=BC=9B=E8=BF=AD=E4=BB=A3=E5=99=A8?= =?UTF-8?q?=20dispose=20=E9=92=89=E6=94=B9=E5=9B=9E=E7=9C=9F=E5=80=BC?= =?UTF-8?q?=E6=96=AD=E8=A8=80=EF=BC=88String(Symbol.dispose)=20=E7=8E=B0?= =?UTF-8?q?=E5=90=88=E6=B3=95=E8=BF=94=E5=9B=9E=E6=8F=8F=E8=BF=B0=E4=B8=B2?= =?UTF-8?q?=EF=BC=89=EF=BC=9Bproperty-escapes=20613/0/0=EF=BC=88B167=20?= =?UTF-8?q?=E5=9F=BA=E7=BA=BF=20582/31/0=EF=BC=8C31=20=E5=BD=92=E9=9B=B6?= =?UTF-8?q?=EF=BC=89=E3=80=81String=20=E5=AD=90=E9=9B=86=20963/250/10=20vs?= =?UTF-8?q?=20BASE=20949/264/10=EF=BC=8814=20=E4=BF=AE=200=20=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=EF=BC=89=E3=80=81for-of=20+1=EF=BC=88string-astral-tr?= =?UTF-8?q?uncated=EF=BC=89=E3=80=81=E4=BA=94=20crate=20113=20=E5=A5=97?= =?UTF-8?q?=E4=BB=B6=202680/0=E3=80=81clippy/fmt=20=E9=9B=B6=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=EF=BC=88JSON=20=E9=9D=A2=E4=B8=8E=E5=85=A8=E9=87=8F?= =?UTF-8?q?=20test262=20=E6=94=B6=E5=B0=BE=E5=9C=A8=20131.2=20=E5=90=8E?= =?UTF-8?q?=E7=BB=AD=20commit=20=E9=97=AD=E5=90=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/oxide_builtins/src/json.rs | 5 +- crates/oxide_builtins/src/object.rs | 69 ++++--- crates/oxide_builtins/src/reflect.rs | 42 ++-- crates/oxide_builtins/src/string.rs | 91 ++++++--- crates/oxide_emit/Cargo.toml | 1 + crates/oxide_emit/src/class/method.rs | 2 +- crates/oxide_emit/src/emit.rs | 4 +- crates/oxide_emit/src/expr/literal.rs | 3 +- crates/oxide_emit/src/expr/member/mod.rs | 11 +- .../src/expr/object/object_literal.rs | 8 +- crates/oxide_emit/src/expr/template/tagged.rs | 7 +- .../src/expr/template/template_literal.rs | 20 +- crates/oxide_emit/src/shared/chain.rs | 9 +- crates/oxide_emit/src/shared/class_key.rs | 12 +- crates/oxide_emit/src/shared/mod.rs | 1 + crates/oxide_emit/src/shared/string_pool.rs | 39 ++++ crates/oxide_kernel/src/string_forge.rs | 2 + .../oxide_vm/tests/builtins_iterator_tests.rs | 7 +- .../oxide_vm/tests/builtins_string_tests.rs | 193 ++++++++++++++++++ 20 files changed, 398 insertions(+), 129 deletions(-) create mode 100644 crates/oxide_emit/src/shared/string_pool.rs diff --git a/Cargo.lock b/Cargo.lock index 3372a3ea..47bbf72f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1205,6 +1205,7 @@ dependencies = [ "oxc_allocator", "oxide_bytecode", "oxide_ir", + "oxide_kernel", "oxide_log", "oxide_parser", "regex", diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index 8b731f28..e0b871d0 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -91,9 +91,8 @@ fn walk_reviver( } } - // 对当前值调用 reviver,用返回值覆盖属性槽。 - let key_str = key_si_to_string(vm, key_si); - let key_val = vm.new_string(&key_str); + // 对当前值调用 reviver,用返回值覆盖属性槽(键物化为 JS 可见串值,含孤立 surrogate 单元)。 + let key_val = crate::object::key_si_to_js_value(vm, key_si); let holder_val = JsValue::from_js_object(holder_ptr); match vm.call_function_sync(reviver, holder_val, &[key_val, val]) { Ok(new_val) => { diff --git a/crates/oxide_builtins/src/object.rs b/crates/oxide_builtins/src/object.rs index 6cc66a91..32cb81aa 100644 --- a/crates/oxide_builtins/src/object.rs +++ b/crates/oxide_builtins/src/object.rs @@ -88,6 +88,9 @@ fn int_or_string_index(vm: &H, si: u32) -> Option { } /// 键 si 物化为字符串文本:整数键反解数字串,其余查 interner。 +/// +/// 注意返回的是**键空间文本**(孤立 surrogate 为 FFFD+hex4 转义形态),仅可用于 +/// 键比较/数字判定等键空间用途;产 JS 可见串值用 `key_si_to_js_value`。 pub fn key_si_to_string(vm: &H, si: u32) -> String { if is_int_key(si) { int_key_value(si).to_string() @@ -96,6 +99,22 @@ pub fn key_si_to_string(vm: &H, si: u32) -> String { } } +/// 键 si 物化为 JS 可见字符串值(Object.keys / ownKeys / entries 族): +/// 整数键反解数字串,字符串键经 `decode_key` 还原单元序列(孤立 surrogate +/// 物化为 FlatU16,良形键与旧行为逐位一致)。 +pub fn key_si_to_js_value(vm: &mut H, si: u32) -> JsValue { + if is_int_key(si) { + return vm.new_string(&int_key_value(si).to_string()); + } + let units = vm + .kernel_core() + .perm_interner() + .lookup(si) + .map(oxide_kernel::string_forge::decode_key) + .unwrap_or_default(); + vm.new_string_units_owned(units) +} + /// 收集对象自身全部 Symbol 键(shape 链),按键序排列(根→叶,即插入序)。 /// 与 [`walk_own_keys`] 互补:只返回 Symbol 键,供 `getOwnPropertySymbols` 使用。 fn walk_own_symbol_keys(vm: &H, obj: &JsObject) -> Vec<(u32, u32)> { @@ -377,21 +396,16 @@ pub fn object_keys(vm: &mut H, args: &[u8]) -> NativeResult { Err(err) => return NativeResult::Err(err), }; - let key_names: Vec; - { - let obj = unsafe { &*obj_ptr }; - let keys = walk_own_keys(vm, obj); - let owned_keys: Vec<(u32, u32)> = keys - .into_iter() - .filter(|(_si, offset)| { - obj.prop_meta_at(*offset) - .map(|m| m.attributes.enumerable()) - .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()) - }) - .collect(); - key_names = owned_keys.iter().map(|(si, _offset)| key_si_to_string(vm, *si)).collect(); - } - let n = key_names.len(); + let obj = unsafe { &*obj_ptr }; + let owned_keys: Vec<(u32, u32)> = walk_own_keys(vm, obj) + .into_iter() + .filter(|(_si, offset)| { + obj.prop_meta_at(*offset) + .map(|m| m.attributes.enumerable()) + .unwrap_or(PropAttributes::DEFAULT_DATA.enumerable()) + }) + .collect(); + let n = owned_keys.len(); let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; let arr = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, @@ -399,10 +413,10 @@ pub fn object_keys(vm: &mut H, args: &[u8]) -> NativeResult { n, vm.epoch().bump(), )); - for (i, k) in key_names.iter().enumerate() { - let str_val = vm.new_string(k); + for (i, (si, _offset)) in owned_keys.iter().enumerate() { + let key_val = key_si_to_js_value(vm, *si); unsafe { - (*arr).set_prop_at(i, str_val); + (*arr).set_prop_at(i, key_val); } } unsafe { @@ -1009,13 +1023,9 @@ pub fn object_is_extensible(vm: &mut H, args: &[u8]) -> NativeResult pub fn object_get_own_property_names(vm: &mut H, args: &[u8]) -> NativeResult { let obj_ptr = native_try!(require_obj_arg(vm, args, "getOwnPropertyNames")); - let key_names: Vec = { - let obj = unsafe { &*obj_ptr }; - let keys = walk_own_keys(vm, obj); - keys.iter().map(|(si, _)| key_si_to_string(vm, *si)).collect() - }; - - let n = key_names.len(); + let obj = unsafe { &*obj_ptr }; + let keys = walk_own_keys(vm, obj); + let n = keys.len(); let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; let arr = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, @@ -1023,10 +1033,10 @@ pub fn object_get_own_property_names(vm: &mut H, args: &[u8]) -> Nati n, vm.epoch().bump(), )); - for (i, k) in key_names.iter().enumerate() { - let str_val = vm.new_string(k); + for (i, (si, _)) in keys.iter().enumerate() { + let key_val = key_si_to_js_value(vm, *si); unsafe { - (*arr).set_prop_at(i, str_val); + (*arr).set_prop_at(i, key_val); } } unsafe { @@ -1333,8 +1343,7 @@ pub fn object_entries(vm: &mut H, args: &[u8]) -> NativeResult { vm.epoch().bump(), )); for (i, (si, offset)) in owned_keys.iter().enumerate() { - let key_str = key_si_to_string(vm, *si); - let key_val = vm.new_string(&key_str); + let key_val = key_si_to_js_value(vm, *si); let val = obj.get_prop_at(*offset); let pair = vm.alloc_object(JsObject::new_array( EMPTY_SHAPE_ID, diff --git a/crates/oxide_builtins/src/reflect.rs b/crates/oxide_builtins/src/reflect.rs index 796667f4..a86e005b 100644 --- a/crates/oxide_builtins/src/reflect.rs +++ b/crates/oxide_builtins/src/reflect.rs @@ -3,7 +3,7 @@ use oxide_types::mem::P; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::value::JsValue; -use crate::object::{delete_own_property, key_si_to_string, walk_own_keys}; +use crate::object::{delete_own_property, key_si_to_js_value, walk_own_keys}; use oxide_runtime_api::{NativeResult, VmHost}; @@ -191,11 +191,25 @@ pub fn reflect_own_keys(vm: &mut H, args: &[u8]) -> NativeResult { return type_error(vm, "Reflect.ownKeys target is not an object"); }; let target = unsafe { &*target_ptr }; - let key_names: Vec = walk_own_keys(vm, target) - .into_iter() - .map(|(si, _)| key_si_to_string(vm, si)) - .collect(); - NativeResult::Ok(make_string_array(vm, &key_names)) + let keys = walk_own_keys(vm, target); + let n = keys.len(); + let array_proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; + let arr = vm.alloc_object(JsObject::new_array( + EMPTY_SHAPE_ID, + JsValue::from_js_object(array_proto), + n, + vm.epoch().bump(), + )); + for (i, (si, _)) in keys.iter().enumerate() { + let key_val = key_si_to_js_value(vm, *si); + unsafe { + (*arr).set_prop_at(i, key_val); + } + } + unsafe { + (*arr).set_prop_count(n); + } + NativeResult::Ok(JsValue::from_js_object(arr)) } /// `Reflect.preventExtensions(target)`:阻止扩展,返回 true。 @@ -273,19 +287,3 @@ fn own_field(vm: &mut H, desc: JsValue, prop_si: u32) -> Option(vm: &mut H, parts: &[String]) -> JsValue { - let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; - let arr = vm.alloc_object(JsObject::new_array( - EMPTY_SHAPE_ID, - JsValue::from_js_object(proto), - parts.len(), - vm.epoch().bump(), - )); - for (idx, part) in parts.iter().enumerate() { - let value = vm.new_string(part); - unsafe { &mut *arr }.set_prop_at(idx, value); - } - unsafe { &mut *arr }.set_prop_count(parts.len()); - JsValue::from_js_object(arr) -} diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index e24f3fce..455dcc81 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -409,22 +409,33 @@ fn expand_dollar_units(text: &[u16], m: ®ress::Match, replacement: &[u16]) -> } else if rest.starts_with(&[0x24, 0x27]) { out.extend_from_slice(&text[range.end..]); i += 2; - } else { - // $n(1-2 位数字)→ 捕获组;未匹配/越界 → 空串。 - let mut j = 1; - while i + j < replacement.len() && (0x30..=0x39).contains(&replacement[i + j]) { - j += 1; - } - if j == 0 { - out.push(0x24); - i += 1; + } else if i + 1 < replacement.len() && (0x30..=0x39).contains(&replacement[i + 1]) { + // $digits:digitCount 取 2(后续尚有数字)否则 1;两位数值越界回退 + // 一位(次位留给后续按字面处理);仍越界(含 $0)→ 整段 ref 字面。 + let digit_count = if i + 2 < replacement.len() && (0x30..=0x39).contains(&replacement[i + 2]) { + 2 } else { - let n: u32 = (0..j).fold(0, |acc, k| acc * 10 + (replacement[i + k] - 0x30) as u32); - if let Some(g) = m.group(n as usize) { + 1 + }; + let d1 = (replacement[i + 1] - 0x30) as u32; + let mut index = if digit_count == 2 { d1 * 10 + (replacement[i + 2] - 0x30) as u32 } else { d1 }; + let mut ref_len = 1 + digit_count; + let capture_len = (m.groups().count() - 1) as u32; + if index > capture_len && digit_count == 2 { + index = d1; + ref_len = 2; + } + if (1..=capture_len).contains(&index) { + if let Some(g) = m.group(index as usize) { out.extend_from_slice(&text[g.start..g.end]); } - i += 1 + j; + } else { + out.extend_from_slice(&replacement[i..i + ref_len]); } + i += ref_len; + } else { + out.push(0x24); + i += 1; } } out @@ -692,22 +703,6 @@ pub fn string_value_of(vm: &mut H, args: &[u8]) -> NativeResult { /// JS `String()` 构造逻辑:把参数转成字符串(单元保真);new 语义返回 /// `[[StringData]]` 包装对象。 pub fn string_constructor(vm: &mut H, args: &[u8]) -> NativeResult { - let str_val = if args.len() > 1 { - // 对象参数须经 ToPrimitive/ToString 完整转换(字符串结果按载荷形态保留)。 - match oxide_runtime_api::to_string_value_full(vm.reg(args[1]), vm) { - Ok(v) => v, - Err(_) => { - // ToString on an object may throw via toString/valueOf; propagate the original exception. - if let Some(exc) = vm.take_uncaught_value() { - return NativeResult::Err(exc); - } - return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert value to a string")); - } - } - } else { - vm.new_string("") - }; - let string_proto = vm.session().builtin_world().string_proto.as_ptr() as *mut JsObject; let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); let is_ctor = if this_val.is_object() { @@ -722,6 +717,30 @@ pub fn string_constructor(vm: &mut H, args: &[u8]) -> NativeResult { false }; + let str_val = if args.len() > 1 { + let v = vm.reg(args[1]); + if is_ctor && v.is_symbol() { + // new String(Symbol):构造器路径走 ToString(规范步骤 3b), + // 对 Symbol 抛 TypeError——与函数调用路径(3a 描述串)相异。 + return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")); + } + // 对象参数须经 ToPrimitive/ToString 完整转换;函数调用路径的 Symbol + // 走 SymbolDescriptiveString(规范步骤 3a,不经 ToString)。 + let v = oxide_runtime_api::to_string_for_string_constructor(v, vm); + match v { + Ok(s) => vm.new_string_owned(s), + Err(_) => { + // ToString on an object may throw via toString/valueOf; propagate the original exception. + if let Some(exc) = vm.take_uncaught_value() { + return NativeResult::Err(exc); + } + return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert value to a string")); + } + } + } else { + vm.new_string("") + }; + if !is_ctor { return NativeResult::Ok(str_val); } @@ -1155,10 +1174,12 @@ pub fn string_pad_start(vm: &mut H, args: &[u8]) -> NativeResult { } else { None }; - let pad: Vec = if args.len() > 2 { - try_string!(as_units(vm, vm.reg(args[2]))).into_owned() - } else { + let pad_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + // 规范口径:padString 为 undefined(含显式传入)回落空格填充。 + let pad: Vec = if pad_val.is_undefined() { vec![0x20] + } else { + try_string!(as_units(vm, pad_val)).into_owned() }; let s: Vec = try_string!(this_units(vm, args)).into_owned(); let s_len = code_point_count(&s); @@ -1194,10 +1215,12 @@ pub fn string_pad_end(vm: &mut H, args: &[u8]) -> NativeResult { } else { None }; - let pad: Vec = if args.len() > 2 { - try_string!(as_units(vm, vm.reg(args[2]))).into_owned() - } else { + let pad_val = if args.len() > 2 { vm.reg(args[2]) } else { JsValue::undefined() }; + // 规范口径:padString 为 undefined(含显式传入)回落空格填充。 + let pad: Vec = if pad_val.is_undefined() { vec![0x20] + } else { + try_string!(as_units(vm, pad_val)).into_owned() }; let s: Vec = try_string!(this_units(vm, args)).into_owned(); let s_len = code_point_count(&s); diff --git a/crates/oxide_emit/Cargo.toml b/crates/oxide_emit/Cargo.toml index 31112c16..c4561fa6 100644 --- a/crates/oxide_emit/Cargo.toml +++ b/crates/oxide_emit/Cargo.toml @@ -8,6 +8,7 @@ description = "OxideJS - AST to IR emit pass" [dependencies] oxide_parser = { path = "../oxide_parser" } +oxide_kernel = { path = "../oxide_kernel" } oxc_allocator.workspace = true oxide_bytecode = { path = "../oxide_bytecode" } oxide_ir = { path = "../oxide_ir" } diff --git a/crates/oxide_emit/src/class/method.rs b/crates/oxide_emit/src/class/method.rs index 49cb8343..1ca19c88 100644 --- a/crates/oxide_emit/src/class/method.rs +++ b/crates/oxide_emit/src/class/method.rs @@ -74,7 +74,7 @@ impl Emitter { } else { (undef_reg, accessor_reg) }; - let key_name = self.class_property_name(&method.key)?; + let key_name = crate::shared::string_pool::pool_key_property(&method.key)?; let key_idx = ctx.add_constant(Constant::String(key_name)); ctx.inst(Inst::define_accessor_attrs( Operand::Reg(home_reg), diff --git a/crates/oxide_emit/src/emit.rs b/crates/oxide_emit/src/emit.rs index a3ec2aad..90d46688 100644 --- a/crates/oxide_emit/src/emit.rs +++ b/crates/oxide_emit/src/emit.rs @@ -1876,8 +1876,6 @@ mod tests { assert!(!deletable, "只读三常量名误入可删集:{name}"); } } - for name in ["$262"] { - assert!(!crate::CompileCtx::is_deletable_global_builtin(name), "不可删名误入可删集:{name}"); - } + assert!(!crate::CompileCtx::is_deletable_global_builtin("$262"), "不可删名误入可删集:$262"); } } diff --git a/crates/oxide_emit/src/expr/literal.rs b/crates/oxide_emit/src/expr/literal.rs index 2df1145b..e8e45507 100644 --- a/crates/oxide_emit/src/expr/literal.rs +++ b/crates/oxide_emit/src/expr/literal.rs @@ -52,7 +52,8 @@ impl Emitter { fn emit_string_literal_expression( &self, s: &oxide_parser::StringLiteral, ctx: &mut CompileCtx, ) -> Result { - let idx = ctx.add_constant(Constant::String(s.value.to_string())); + let idx = ctx + .add_constant(Constant::String(crate::shared::string_pool::pool_key_marker(&s.value, s.lone_surrogates))); let r = ctx.alloc_reg(); ctx.inst(Inst::load_const(Operand::Reg(r), idx)); Ok(r) diff --git a/crates/oxide_emit/src/expr/member/mod.rs b/crates/oxide_emit/src/expr/member/mod.rs index 3ae20426..dd95ea3e 100644 --- a/crates/oxide_emit/src/expr/member/mod.rs +++ b/crates/oxide_emit/src/expr/member/mod.rs @@ -133,10 +133,13 @@ pub(crate) fn is_array_index_str(s: &str) -> bool { /// (运行期非常量 / 键求值有副作用)与规范数组下标串返回 `None`,维持 DYNAMIC 路径。 pub(crate) fn computed_const_key(expr: &Expression) -> Option { let key = match expr { - Expression::StringLiteral(s) => s.value.to_string(), - Expression::TemplateLiteral(tl) if tl.expressions.is_empty() && tl.quasis.len() == 1 => { - tl.quasis[0].value.cooked.as_ref().map(|c| c.to_string()).unwrap_or_default() - } + Expression::StringLiteral(s) => crate::shared::string_pool::pool_key_marker(&s.value, s.lone_surrogates), + Expression::TemplateLiteral(tl) if tl.expressions.is_empty() && tl.quasis.len() == 1 => tl.quasis[0] + .value + .cooked + .as_ref() + .map(|c| crate::shared::string_pool::pool_key_marker(c, tl.quasis[0].lone_surrogates)) + .unwrap_or_default(), _ => return None, }; if is_array_index_str(&key) { diff --git a/crates/oxide_emit/src/expr/object/object_literal.rs b/crates/oxide_emit/src/expr/object/object_literal.rs index f896ed7a..b6d2152f 100644 --- a/crates/oxide_emit/src/expr/object/object_literal.rs +++ b/crates/oxide_emit/src/expr/object/object_literal.rs @@ -24,7 +24,7 @@ impl Emitter { let ObjectPropertyKind::ObjectProperty(p) = prop else { unreachable!("批量前缀必为 ObjectProperty"); }; - let name = self.class_property_name(&p.key).expect("批量前缀键为静态名"); + let name = crate::shared::string_pool::pool_key_property(&p.key).expect("批量前缀键为静态名"); u32::from(ctx.add_constant(Constant::String(name))) }) .collect(); @@ -141,7 +141,8 @@ impl Emitter { )); } None => { - let idx = ctx.add_constant(Constant::String(prop_name.to_string())); + let key = crate::shared::string_pool::pool_key_property(&p.key)?; + let idx = ctx.add_constant(Constant::String(key)); ctx.inst(Inst::define_accessor( Operand::Reg(obj_reg), Operand::Reg(get_reg), @@ -155,7 +156,8 @@ impl Emitter { let key_reg = if computed { self.emit_expression(p.key.to_expression(), ctx)? } else { - let idx = ctx.add_constant(Constant::String(prop_name.to_string())); + let key = crate::shared::string_pool::pool_key_property(&p.key)?; + let idx = ctx.add_constant(Constant::String(key)); let reg = ctx.alloc_reg(); ctx.inst(Inst::load_const(Operand::Reg(reg), idx)); reg diff --git a/crates/oxide_emit/src/expr/template/tagged.rs b/crates/oxide_emit/src/expr/template/tagged.rs index 5da05df9..9bc59d84 100644 --- a/crates/oxide_emit/src/expr/template/tagged.rs +++ b/crates/oxide_emit/src/expr/template/tagged.rs @@ -23,12 +23,15 @@ impl Emitter { for quasi in &tt.quasi.quasis { match quasi.value.cooked.as_ref() { Some(c) => { - let idx = ctx.add_constant(Constant::String(c.to_string())); + let key = crate::shared::string_pool::pool_key_marker(c, quasi.lone_surrogates); + let idx = ctx.add_constant(Constant::String(key)); cooked_words.push(idx as u32); } None => cooked_words.push(0x8000_0000), } - let raw_idx = ctx.add_constant(Constant::String(quasi.value.raw.to_string())); + // raw 是源文本(保留转义原文),无 marker 语义,走普通编码。 + let raw_idx = + ctx.add_constant(Constant::String(crate::shared::string_pool::pool_key_plain(&quasi.value.raw))); raw_idxs.push(raw_idx); } let site_no = ctx.next_template_site; diff --git a/crates/oxide_emit/src/expr/template/template_literal.rs b/crates/oxide_emit/src/expr/template/template_literal.rs index 190c8180..3fb1d421 100644 --- a/crates/oxide_emit/src/expr/template/template_literal.rs +++ b/crates/oxide_emit/src/expr/template/template_literal.rs @@ -20,18 +20,20 @@ impl Emitter { .map(|e| self.emit_expression(e, ctx)) .collect::, _>>()?; - let quasi_const_idxs: Vec = quasis + let mut quasi_keys: Vec = Vec::with_capacity(quasis.len()); + for q in quasis { + quasi_keys.push(match &q.value.cooked { + Some(c) => crate::shared::string_pool::pool_key_marker(c, q.lone_surrogates), + None => String::new(), + }); + } + let quasi_const_idxs: Vec = quasi_keys .iter() - .map(|q| { - let s = q.value.cooked.as_ref().map(|c| c.to_string()).unwrap_or_default(); - ctx.add_constant(Constant::String(s)) - }) + .map(|s| ctx.add_constant(Constant::String(s.clone()))) .collect(); - let total_len_hint: usize = quasis - .iter() - .map(|q| q.value.cooked.as_ref().map(|c| c.len()).unwrap_or(0)) - .sum(); + // 容量提示按单元数口径(键文本经物化解码还原单元序列)。 + let total_len_hint: usize = quasi_keys.iter().map(|k| oxide_kernel::string_forge::decode_key(k).len()).sum(); let mut parts = Vec::with_capacity(quasi_const_idxs.len() * 2); let mut expr_iter = expr_regs.iter(); diff --git a/crates/oxide_emit/src/shared/chain.rs b/crates/oxide_emit/src/shared/chain.rs index 2eb4e786..2a269d8c 100644 --- a/crates/oxide_emit/src/shared/chain.rs +++ b/crates/oxide_emit/src/shared/chain.rs @@ -267,7 +267,10 @@ impl Emitter { // 只读局部符号表,嵌套函数引用外层绑定会静默回退读全局。 PropertyKey::Identifier(_) => self.emit_expression(key.to_expression(), ctx), PropertyKey::StringLiteral(s) => { - let key_idx = ctx.add_constant(Constant::String(s.value.to_string())); + let key_idx = ctx.add_constant(Constant::String(crate::shared::string_pool::pool_key_marker( + &s.value, + s.lone_surrogates, + ))); let key_reg = ctx.alloc_reg(); ctx.inst(Inst::load_const(Operand::Reg(key_reg), key_idx)); Ok(key_reg) @@ -294,7 +297,7 @@ impl Emitter { &self, src_reg: u32, key: &PropertyKey, computed: bool, ctx: &mut CompileCtx, ) -> Result<(u32, Option, Option), String> { if !computed { - let key_name = self.static_property_name(key)?; + let key_name = crate::shared::string_pool::pool_key_property(key)?; let prop_reg = self.emit_object_property_read(src_reg, &key_name, ctx); return Ok((prop_reg, Some(key_name), None)); } @@ -302,7 +305,7 @@ impl Emitter { ctx.inst(Inst::new(OpCode::LOAD_VAR, Operand::Reg(prop_reg), Operand::Reg(src_reg), Operand::None)); // 字符串字面量计算键折叠为 IC 静态路径;标识符/数字键走 DYNAMIC。 if let PropertyKey::StringLiteral(s) = key { - let key_str = s.value.to_string(); + let key_str = crate::shared::string_pool::pool_key_marker(&s.value, s.lone_surrogates); if !is_array_index_str(&key_str) { let key_idx = ctx.add_constant(Constant::String(key_str)); let key_reg = ctx.alloc_reg(); diff --git a/crates/oxide_emit/src/shared/class_key.rs b/crates/oxide_emit/src/shared/class_key.rs index 3645eb12..37728836 100644 --- a/crates/oxide_emit/src/shared/class_key.rs +++ b/crates/oxide_emit/src/shared/class_key.rs @@ -59,7 +59,7 @@ impl Emitter { &self, key: &PropertyKey, computed: bool, ctx: &mut CompileCtx, ) -> Result { if !computed { - let name = self.class_property_name(key)?; + let name = crate::shared::string_pool::pool_key_property(key)?; let idx = ctx.add_constant(Constant::String(name)); let reg = ctx.alloc_reg(); ctx.inst(Inst::load_const(Operand::Reg(reg), idx)); @@ -72,16 +72,6 @@ impl Emitter { self.emit_expression(key.to_expression(), ctx) } - pub(crate) fn static_property_name(&self, key: &PropertyKey) -> Result { - match key { - PropertyKey::StaticIdentifier(ident) => Ok(ident.name.as_str().to_string()), - PropertyKey::Identifier(ident) => Ok(ident.name.as_str().to_string()), - PropertyKey::StringLiteral(s) => Ok(s.value.to_string()), - PropertyKey::NumericLiteral(n) => Ok(n.value.to_string()), - _ => Err("computed destructuring keys not yet supported".into()), - } - } - /// 从类定义期 computed key 数组按 slot 取键(方法/静态字段在类定义期使用)。 pub(crate) fn emit_class_array_key_reg(&self, slot: u8, ctx: &mut CompileCtx) -> Result { let arr_reg = ctx.class_keys_reg.expect("class computed keys array missing"); diff --git a/crates/oxide_emit/src/shared/mod.rs b/crates/oxide_emit/src/shared/mod.rs index 1f2a12f5..816ceb31 100644 --- a/crates/oxide_emit/src/shared/mod.rs +++ b/crates/oxide_emit/src/shared/mod.rs @@ -4,3 +4,4 @@ pub mod binding; pub mod chain; pub mod class_field; pub mod class_key; +pub mod string_pool; diff --git a/crates/oxide_emit/src/shared/string_pool.rs b/crates/oxide_emit/src/shared/string_pool.rs new file mode 100644 index 00000000..0d93e0d9 --- /dev/null +++ b/crates/oxide_emit/src/shared/string_pool.rs @@ -0,0 +1,39 @@ +//! 常量池字符串键编码:池统一存无标志键编码(`oxide_kernel::string_forge::encode_key` +//! 语义),物化侧(perm_string → `decode_key`)是其精确逆变换。 +//! +//! 两类来源文本: +//! - oxc marker 文本(`StringLiteral.value` / `TemplateElement.cooked`): +//! `lone_surrogates` 置位时 marker 与键编码同构(FFFD+hex4 = 孤立 surrogate 单元、 +//! FFFD+fffd = 真实 FFFD),原样即池键;未置位时为普通文本(可含裸 FFFD), +//! 须逃逸编码防裸 FFFD 与 surrogate marker 碰撞; +//! - 普通文本(标识符 / 数字 / 源文本):非 FFFD 恒等,真实 FFFD 逃逸为 FFFD+"fffd"。 + +use oxide_kernel::string_forge::encode_key; +use oxide_parser::PropertyKey; + +/// 普通文本 → 池键:真实 FFFD 逃逸为 FFFD+"fffd",其余逐单元恒等。 +pub(crate) fn pool_key_plain(text: &str) -> String { + let units: Vec = text.encode_utf16().collect(); + encode_key(&units) +} + +/// oxc marker 文本 → 池键,按 `lone_surrogates` 分派(语义见模块头注释)。 +pub(crate) fn pool_key_marker(text: &str, lone_surrogates: bool) -> String { + if lone_surrogates { + text.to_string() + } else { + pool_key_plain(text) + } +} + +/// 属性键 → 池键:字符串字面量键经 marker 解码,标识符 / 数字键走普通编码。 +pub(crate) fn pool_key_property(key: &PropertyKey) -> Result { + match key { + PropertyKey::StaticIdentifier(ident) => Ok(pool_key_plain(ident.name.as_str())), + PropertyKey::Identifier(ident) => Ok(pool_key_plain(ident.name.as_str())), + PropertyKey::StringLiteral(s) => Ok(pool_key_marker(&s.value, s.lone_surrogates)), + PropertyKey::NumericLiteral(n) => Ok(pool_key_plain(&n.value.to_string())), + PropertyKey::PrivateIdentifier(_) => Err("private class elements not yet supported".into()), + _ => Err("unsupported class property key type".into()), + } +} diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index 266f4f58..f6c3ef0c 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -446,6 +446,8 @@ mod tests { assert_eq!(encode_key(&[0xFFFD]), "\u{FFFD}fffd"); // 两种不同单元序列的键文本互异(解码可区分)。 assert_ne!(encode_key(&[0xFFFD]), encode_key(&[0xD800])); + // 文本 [FFFD,'d','8','0','0'] 与单元 [D800] 的键必须可区分(防物化串键)。 + assert_ne!(encode_key(&[0xFFFD, 0x64, 0x38, 0x30, 0x30]), encode_key(&[0xD800])); } #[test] diff --git a/crates/oxide_vm/tests/builtins_iterator_tests.rs b/crates/oxide_vm/tests/builtins_iterator_tests.rs index 24f3dfc4..88702527 100644 --- a/crates/oxide_vm/tests/builtins_iterator_tests.rs +++ b/crates/oxide_vm/tests/builtins_iterator_tests.rs @@ -528,9 +528,10 @@ fn symbol_dispose_and_async_dispose_registered() { let result = eval(&mut vm, src).unwrap(); assert_eq!(result.as_bool(), expected, "for {}", src); } - // ToString(Symbol) 按规格抛 TypeError(String 构造器无描述性名称通道)。 - let err = eval(&mut vm, "try { String(Symbol.dispose); 'no-throw' } catch (e) { e.name }").unwrap(); - assert_eq!(to_str(&vm, err), "TypeError"); + // String(Symbol) 走 SymbolDescriptiveString(规范 String 构造器步骤), + // 不经 ToString——well-known 符号对象按描述名产出。 + let s = eval(&mut vm, "String(Symbol.dispose)").unwrap(); + assert_eq!(to_str(&vm, s), "Symbol(Symbol.dispose)"); } #[test] diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index 7daec193..fdd9fd71 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -150,6 +150,9 @@ fn string_pad_end() { let mut vm = Vm::new(); let s = eval(&mut vm, "'hi'.padEnd(4)").unwrap(); assert_eq!(to_str(&vm, s), "hi "); + // 显式 undefined 填充与缺省同义:回落空格(规范 PadString 步骤)。 + let s = eval(&mut vm, "'hi'.padEnd(4, undefined)").unwrap(); + assert_eq!(to_str(&vm, s), "hi "); } #[test] @@ -264,6 +267,17 @@ fn string_missing_arg_searches_undefined() { assert_eq!(r.as_int(), 0); } +#[test] +fn string_ctor_symbol_paths() { + // 函数调用:Symbol → SymbolDescriptiveString(描述串,不抛)。 + let mut vm = Vm::new(); + let s = eval(&mut vm, "String(Symbol('66'))").unwrap(); + assert_eq!(to_str(&vm, s), "Symbol(66)"); + // new 语义:构造器路径走 ToString,Symbol 抛 TypeError。 + let name = eval(&mut vm, "try { new String(Symbol('66')); 'no-throw' } catch (e) { e.name }").unwrap(); + assert_eq!(to_str(&vm, name), "TypeError"); +} + #[test] fn string_symbol_argument_throws_type_error() { // Symbol 参数经 as_string 抛 TypeError(不静默降级为文本查找)。 @@ -425,6 +439,37 @@ fn string_replace_function_replacer_string_pattern() { assert_eq!(to_str(&vm, s), "a1c"); } +#[test] +fn string_replace_dollar_group_expansion() { + // `$n` 展开按规范 GetSubstitution:两位数值越界回退一位、仍越界(含 $0) + // 整段 ref 字面、未参与捕获组为空。预言值对照 V8(node)实测。 + let mut vm = Vm::new(); + let cases = [ + // 两位越界回退一位(次位留字面)。 + ("\"uid=31\".replace(/(uid=)(\\d+)/, \"$11\" + 15)", "uid=115"), + ("\"x\".replace(/(x)/, \"$12\")", "x2"), + ("\"xy\".replace(/(x)(y)/, \"$12\")", "x2"), + // 越界(含 $0)→ ref 字面;无捕获组全字面。 + ("\"xy\".replace(/(x)(y)/, \"$3\")", "$3"), + ("\"abc\".replace(/b/, \"$0$1\")", "a$0$1c"), + ("\"abc\".replace(/(b)/, \"$22\")", "a$22c"), + ("\"x\".replace(/x/, \"$10\")", "$10"), + // $& / $` 与组引用混排。 + ("\"aaaaa,aaaaa\".replace(/(a+),/, \"$1$&$`$1\")", "aaaaaaaaaa,aaaaaaaaaa"), + // $$ 折叠与文本内 $ 无关;$ 序列多位数字逐段消费。 + ("\"a$b$c\".replace(/b/, \"$$$$\")", "a$$$$c"), + ("\"x12y\".replace(/1/, \"$1$12$123\")", "x$1$12$1232y"), + ("\"x12y\".replace(/(1)/, \"$1$12$123\")", "x1121232y"), + ("\"x12y\".replace(/(1)(2)/, \"$1$12$123\")", "x112123y"), + // 组参与但未匹配 → 空。 + ("\"x12y\".replace(/(1)(2)(3)?/, \"$3\")", "xy"), + ]; + for (src, expected) in cases { + let s = eval(&mut vm, src).unwrap(); + assert_eq!(to_str(&vm, s), expected, "for {}", src); + } +} + #[test] fn string_replace_function_replacer_string_arg() { // 回调第 4 参为原字符串:原始 receiver 直接传值(=== 原串), @@ -1154,3 +1199,151 @@ fn string_utf16_replace_position_callback() { let s = eval(&mut vm, "'\\u{1F600}ab'.replaceAll('a', function(m, o){ return o })").unwrap(); assert_eq!(to_str(&vm, s), "\u{1F600}2b"); } + +// UTF-16 载荷层回归钉:孤立 surrogate 一等单元(fromCodePoint/fromCharCode 直推单元、 +// 字面量/模板 oxc marker 解码、切片/迭代/搜索/键/正则面单元语义)。 + +#[test] +fn utf16_from_code_point_lone_surrogate() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "String.fromCodePoint(0xD800).length===1 && String.fromCodePoint(0xD800).charCodeAt(0)===0xD800", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_from_char_code_pair_units() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "String.fromCharCode(0xD800,0xDC00).length===2 && String.fromCharCode(0xD800,0xDC00).charCodeAt(1)===0xDC00", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_from_code_point_astral_pair() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "String.fromCodePoint(0x1F600).length===2 && String.fromCodePoint(0x1F600).charCodeAt(0)===0xD83D && String.fromCodePoint(0x1F600).charCodeAt(1)===0xDE00", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_literal_lone_surrogate_emit_decode() { + let mut vm = Vm::new(); + let r = eval(&mut vm, "'\\ud800'.length===1 && '\\ud800'.charCodeAt(0)===0xD800").unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_template_cooked_lone_surrogate() { + let mut vm = Vm::new(); + let r = eval(&mut vm, "`\\ud800`.length===1").unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_char_at_single_unit_astral() { + // charAt 按单元取:astral 字符首单元为高代理(非整码点)。 + let mut vm = Vm::new(); + let r = eval(&mut vm, "'\u{1D11E}'.charAt(0).charCodeAt(0)===0xD834").unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_slice_substr_single_unit_astral() { + // slice/substr 单元精确:'\u{1D11E}' = [0xD834, 0xDD1E]。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "'\u{1D11E}'.slice(-1).charCodeAt(0)===0xDD1E && '\u{1D11E}'.substr(1).charCodeAt(0)===0xDD1E && '\u{1D11E}'.slice(0,1).charCodeAt(0)===0xD834", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_iterator_lone_surrogate_units() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "(function(){ let n=0, ok=true; for (const c of 'a\\ud800') { n++; if (n===2) ok = c.length===1 && c.charCodeAt(0)===0xD800; } return n===2 && ok; })()", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_spread_and_to_object_lone_surrogate() { + // 迭代/spread 按单元产出;rest 解构的 ToObject 字符串下标读得 1 单元串 + // (非空串)。字符串包装对象的索引/length 读(new String(...)[0])另有 + // 独立既有缺口,不在此钉。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "(function(){ const arr=[...'a\\ud800']; const o={...'a\\ud800'}; const { ...rest } = 'a\\ud800'; return arr.length===2 && arr[1].length===1 && arr[1].charCodeAt(0)===0xD800 && o[1].length===1 && o[1].charCodeAt(0)===0xD800 && rest[1].length===1 && rest[1].charCodeAt(0)===0xD800; })()", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_regex_surrogate_property_class() { + // 属性转义须 u 标志(无 u 为规范 SyntaxError,引擎宽容面不在此钉)。 + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "/\\p{General_Category=Surrogate}/u.test(String.fromCodePoint(0xD800))===true && /\\P{ASCII}/u.test(String.fromCodePoint(0xD800))===true && /\\p{General_Category=Surrogate}/u.test('a')===false", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_is_well_formed_to_well_formed() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "'a\\ud800b'.isWellFormed()===false && 'a\\ud800b'.toWellFormed().length===3 && 'a\\ud800b'.toWellFormed().charCodeAt(1)===0xFFFD", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_index_of_lone_surrogate() { + let mut vm = Vm::new(); + let r = eval(&mut vm, "'a\\ud800\\ud800'.indexOf('\\ud800')===1").unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_property_key_round_trip() { + let mut vm = Vm::new(); + // 孤立 surrogate 计算键:写入/读取 round-trip,Object.keys 出 1 单元键。 + let r = eval( + &mut vm, + "(function(){ const k='\\ud800'; const o={}; o[k]=1; const keys=Object.keys(o); return o[k]===1 && keys.length===1 && keys[0].length===1 && keys[0].charCodeAt(0)===0xD800; })()", + ) + .unwrap(); + assert!(r.as_bool()); +} + +#[test] +fn utf16_normalize_astral_identity() { + let mut vm = Vm::new(); + let r = eval( + &mut vm, + "'\u{1F441}'.normalize('NFD')==='\u{1F441}' && '\u{1F441}'.normalize('NFC').length===2", + ) + .unwrap(); + assert!(r.as_bool()); +}