From 3acac721b08d4785926c90404976d91f527536b2 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sun, 13 Sep 2026 22:42:10 +0000 Subject: [PATCH] =?UTF-8?q?types/kernel/vm/builtins:=20JsString=20UTF-16?= =?UTF-8?q?=20=E8=BD=BD=E8=8D=B7=E5=B1=82=EF=BC=88FlatU16=20=E5=8F=98?= =?UTF-8?q?=E4=BD=93=20+=20=E5=8D=95=E5=85=83=E5=85=A5=E5=8F=A3=EF=BC=89?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=8E=9F=20UTF-8=20=E8=BD=BD=E8=8D=B7?= =?UTF-8?q?=E5=8F=AA=E5=AD=98=20well-formed=20=E4=B8=B2=E3=80=81=E5=AD=A4?= =?UTF-8?q?=E7=AB=8B=20surrogate=20lossy=20=E8=BD=AC=20U+FFFD=20=E5=9B=9E?= =?UTF-8?q?=E5=86=99=EF=BC=8CFromCodePoint/=E5=B1=9E=E6=80=A7=E8=BD=AC?= =?UTF-8?q?=E4=B9=89=E4=B8=8E=E5=AD=97=E7=AC=A6=E4=B8=B2=E9=9D=A2=E7=A0=81?= =?UTF-8?q?=E7=82=B9=E8=AF=AD=E4=B9=89=E7=9A=86=E7=A0=B4=EF=BC=9B=E7=8E=B0?= =?UTF-8?q?=E8=BD=BD=E8=8D=B7=E7=9B=B4=E5=AD=98=20[u64;3]=EF=BC=88Flat=20w?= =?UTF-8?q?ell-formed=20/=20FlatU16=20=E5=8D=95=E5=85=83=E5=BA=8F=E5=88=97?= =?UTF-8?q?=20/=20Cons=20=E6=8B=BC=E6=8E=A5=EF=BC=89=EF=BC=8Cutf16=5Flen?= =?UTF-8?q?=20=E7=8B=AC=E7=AB=8B=2032=20=E4=BD=8D=E5=AD=97=EF=BC=8C32B=20?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E9=94=9A=E4=BF=9D=E6=8C=81=EF=BC=9Bfrom=5Fun?= =?UTF-8?q?its=20=E6=99=BA=E8=83=BD=E8=B7=AF=E7=94=B1=EF=BC=88=E5=90=AB?= =?UTF-8?q?=E5=AD=A4=E7=AB=8B=E2=86=92FlatU16=EF=BC=89=EF=BC=8Cas=5Fstr=20?= =?UTF-8?q?=E4=BB=85=E9=99=90=20Flat=EF=BC=8Cunits/units=5Fborrowed=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E5=80=9F=E7=94=A8=EF=BC=8Cas=5Flossy=5Fstr?= =?UTF-8?q?=20=E5=AE=BD=E5=AE=B9=E8=AF=BB=EF=BC=9Bstring=5Fforge=20?= =?UTF-8?q?=E7=BC=96=E8=A7=A3=E7=A0=81=20surrogate=20=E6=84=9F=E7=9F=A5?= =?UTF-8?q?=EF=BC=88=E5=AD=A4=E7=AB=8B=20surrogate=20=E9=80=83=E9=80=B8=20?= =?UTF-8?q?hex4=20=E7=BC=96=E7=A0=81=E3=80=81=E8=A7=A3=E7=A0=81=E5=B1=95?= =?UTF-8?q?=E5=BC=80=20astral=20=E4=BB=A3=E7=90=86=E5=AF=B9=E3=80=81string?= =?UTF-8?q?=5Fptr=20=E7=94=B1=E5=8D=95=E5=85=83=E7=89=A9=E5=8C=96=EF=BC=89?= =?UTF-8?q?=EF=BC=9BVmHost=20=E5=8D=95=E5=85=83=E7=89=A9=E5=8C=96/?= =?UTF-8?q?=E5=88=86=E9=85=8D/=E6=9F=A5=E8=AF=A2=E5=85=A5=E5=8F=A3=20+=20r?= =?UTF-8?q?untime=5Fapi=20=E5=8D=95=E5=85=83=20API=EF=BC=9B=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=E5=86=85=E7=BD=AE=E5=85=A8=E6=B3=95=E5=8D=95?= =?UTF-8?q?=E5=85=83=E5=8C=96=EF=BC=88charAt/slice/substr/split/replace/se?= =?UTF-8?q?arch/match/matchAll=20=E5=8D=95=E5=85=83=E5=AE=9A=E4=BD=8D?= =?UTF-8?q?=EF=BC=9B=E7=A9=BA=E6=A8=A1=E5=BC=8F=E8=BE=B9=E7=95=8C=EF=BC=9A?= =?UTF-8?q?search=E2=86=920=E3=80=81match=E2=86=92[""]=E3=80=81replace=20?= =?UTF-8?q?=E6=8F=92=200=20=E4=BD=8D=E3=80=81matchAll=20=E6=B8=B8=E6=A0=87?= =?UTF-8?q?=E8=80=97=E5=B0=BD=E5=AE=88=E5=8D=AB=EF=BC=9B=E6=8E=A5=E6=94=B6?= =?UTF-8?q?=E8=80=85=20ToString=20=E9=94=99=E8=AF=AF=E5=8E=9F=E6=A0=B7?= =?UTF-8?q?=E4=BC=A0=E6=92=AD=EF=BC=89=EF=BC=9B=E6=AD=A3=E5=88=99=E5=86=85?= =?UTF-8?q?=E7=BD=AE=E5=8D=95=E5=85=83=E5=8C=96=EF=BC=88exec/test/search/s?= =?UTF-8?q?ymbol=5F*=20=E5=8D=95=E5=85=83=E6=96=87=E6=9C=AC=EF=BC=8C?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E4=B8=B2=20\u{...}=20=E4=B8=8E=E5=AD=A4?= =?UTF-8?q?=E7=AB=8B=20surrogate=20=E6=96=87=E6=9C=AC=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=8ClastIndex=20=E4=B8=8E=20matchAll=20=E6=B8=B8=E6=A0=87?= =?UTF-8?q?=E5=8D=95=E5=85=83=E5=8C=96=EF=BC=89=EF=BC=9B=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E8=BF=AD=E4=BB=A3=E5=99=A8=E7=A0=81=E7=82=B9=E5=8C=96?= =?UTF-8?q?=EF=BC=88=E9=AB=98=E4=BB=A3=E7=90=86+=E5=90=8E=E7=BB=AD?= =?UTF-8?q?=E4=BD=8E=E4=BB=A3=E7=90=86=E6=88=90=E5=AF=B9=E4=BA=A7=E5=87=BA?= =?UTF-8?q?=EF=BC=8Castral=20=E5=80=BC=E5=9B=9E=E7=9C=9F=E5=AE=9E=E7=A0=81?= =?UTF-8?q?=E7=82=B9=EF=BC=89=EF=BC=9BString(Symbol)=20=E6=8A=9B=20TypeErr?= =?UTF-8?q?or=EF=BC=9B=E5=9B=9E=E5=BD=92=E9=92=89=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=BA=94=E5=A4=84=EF=BC=88charAt/split/slice/substr=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E7=B2=BE=E7=A1=AE=E8=AF=AD=E4=B9=89=20+=20di?= =?UTF-8?q?spose=20=E9=92=89=E6=94=B9=20TypeError=20=E6=96=AD=E8=A8=80?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E5=9B=9B=20crate=20=E5=85=A8=E7=BB=BF?= =?UTF-8?q?=EF=BC=88oxide=5Fvm=2098=20=E5=A5=97=E4=BB=B6=202403=20?= =?UTF-8?q?=E6=B5=8B=EF=BC=89+=20clippy/fmt=20=E9=9B=B6=E6=96=B0=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_builtins/src/eval.rs | 2 +- crates/oxide_builtins/src/iterator.rs | 75 +- crates/oxide_builtins/src/module.rs | 4 +- crates/oxide_builtins/src/regexp.rs | 176 +- crates/oxide_builtins/src/set.rs | 5 +- crates/oxide_builtins/src/string.rs | 1947 ++++++++++------- crates/oxide_kernel/src/string_forge.rs | 164 +- crates/oxide_runtime_api/src/lib.rs | 122 +- crates/oxide_types/src/object.rs | 499 +++-- crates/oxide_vm/src/dispatch/arithmetic.rs | 50 +- crates/oxide_vm/src/dispatch/misc.rs | 5 +- crates/oxide_vm/src/session_gc.rs | 18 +- crates/oxide_vm/src/vm.rs | 85 +- crates/oxide_vm/src/vm_dispatch_misc.rs | 88 +- crates/oxide_vm/src/vm_support.rs | 44 +- .../oxide_vm/tests/builtins_iterator_tests.rs | 6 +- .../oxide_vm/tests/builtins_string_tests.rs | 73 +- 17 files changed, 2177 insertions(+), 1186 deletions(-) diff --git a/crates/oxide_builtins/src/eval.rs b/crates/oxide_builtins/src/eval.rs index 62e3c46a..c9b18405 100644 --- a/crates/oxide_builtins/src/eval.rs +++ b/crates/oxide_builtins/src/eval.rs @@ -27,7 +27,7 @@ pub fn eval(vm: &mut H, args: &[u8]) -> NativeResult { if !arg.is_string() { return NativeResult::Ok(arg); } - let code = vm.string_ref(arg).to_string(); + let code = vm.lookup_str(arg).unwrap_or_default(); match vm.create_dynamic_script(&code) { Ok(func) => { let global = JsValue::from_js_object(vm.session().global_object().as_ptr() as *mut JsObject); diff --git a/crates/oxide_builtins/src/iterator.rs b/crates/oxide_builtins/src/iterator.rs index 9bf0f78b..08bebdfa 100644 --- a/crates/oxide_builtins/src/iterator.rs +++ b/crates/oxide_builtins/src/iterator.rs @@ -1487,6 +1487,30 @@ fn get_iterator(vm: &mut H, value: JsValue) -> Result (usize, Option) { + let Some(&u) = units.get(at) else { + return (0, None); + }; + if (0xD800..=0xDBFF).contains(&u) { + if let Some(lo) = units.get(at + 1) { + if (0xDC00..=0xDFFF).contains(lo) { + return (at + 2, Some(StrIterElem::Pair(u, *lo))); + } + } + } + (at + 1, Some(StrIterElem::One(u))) +} + fn next_array_like( vm: &mut H, wrapper: &mut JsObject, inner: JsValue, index_si: u32, ) -> Result, JsValue> { @@ -1513,18 +1537,47 @@ fn next_array_like( } if inner.is_string() { - // index 槽在字符串分支存字节游标(每步推进一个 char 的 UTF-8 宽度)而非 - // 元素序号:该槽只被 next 内部读写、无外部消费者,包装器创建后类型固定 - // 不跨类型复用,故可安全借用语义(字符串=字节偏移)。 - let byteoff = current_index(vm, wrapper, index_si); - // 源串裸指针借用压缩到单个表达式:ch 是 Copy 的 char,不携带借用, + // index 槽在字符串分支存游标(按载荷形态钉死):单元形态(FlatU16/Cons) + // 存单元偏移;Flat 形态存 字节偏移×2(与单元口径对齐,每字符步长按其 + // UTF-16 单元数)。游标只被 next 内部读写、无外部消费者,包装器创建后 + // 类型固定不跨类型复用。 + // + // 产出按 Unicode 标量(规格口径):合法代理对整体产出一个 2 单元元素, + // 孤立 surrogate 各产出 1 单元元素。 + let cursor = current_index(vm, wrapper, index_si); + // 源串裸指针借用压缩到单个表达式:产出元素为 Copy 值,不携带借用, // 之后对 VM 状态的可变访问不再与源串借用共存。 - let ch = unsafe { &*inner.as_string_ptr() }.as_str()[byteoff..].chars().next(); - if let Some(ch) = ch { - vm.set_or_create_prop_value(wrapper, index_si, JsValue::int((byteoff + ch.len_utf8()) as i32)); - let value = match vm.single_char(ch) { - Some(v) => v, - None => vm.new_string(&ch.to_string()), + let (next_cursor, elem): (usize, Option) = unsafe { + let sp = &*inner.as_string_ptr(); + if sp.is_flat() { + let byteoff = cursor / 2; + match sp.as_str().get(byteoff..).unwrap_or("").chars().next() { + // 超平面字符:出整个代理对(单元素),游标跳越字符。 + Some(c) if c.len_utf8() == 4 => { + let v = c as u32; + let hi = (0xD800 + (((v - 0x10000) >> 10) & 0x3FF)) as u16; + let lo = (0xDC00 + ((v - 0x10000) & 0x3FF)) as u16; + ((byteoff + 4) * 2, Some(StrIterElem::Pair(hi, lo))) + } + Some(c) => ((byteoff + c.len_utf8()) * 2, Some(StrIterElem::One(c as u16))), + None => (0, None), + } + } else if let Some(units) = sp.units_borrowed() { + str_iter_step(units, cursor) + } else { + let units = sp.units(); + str_iter_step(&units, cursor) + } + }; + if let Some(elem) = elem { + vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(next_cursor as i32)); + let value = match elem { + // ASCII 单元走单字符缓存零分配,其余(含孤立 surrogate)落串创建。 + StrIterElem::One(u) => match vm.single_unit(u) { + Some(v) => v, + None => vm.new_string_units(&[u]), + }, + StrIterElem::Pair(hi, lo) => vm.new_string_units(&[hi, lo]), }; return Ok(Some(make_iter_result(vm, value, false))); } diff --git a/crates/oxide_builtins/src/module.rs b/crates/oxide_builtins/src/module.rs index 045d1d8d..ec6b9802 100644 --- a/crates/oxide_builtins/src/module.rs +++ b/crates/oxide_builtins/src/module.rs @@ -196,8 +196,8 @@ pub fn module_data(vm: &mut H, args: &[u8]) -> NativeResult { } } "text" => { - // 经借用路径拷贝出 owned String 后再入堆,避免借用跨过 new_string 分配点。 - let text = vm.string_ref(content_val).to_string(); + // lossy 文本拷贝出 owned String 再入堆(模块数据文本桥接,131.2 边界)。 + let text = vm.lookup_str(content_val).unwrap_or_default(); vm.new_string_owned(text) } _ => return type_error(vm, "__moduleData: unsupported data kind"), diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index 31d53ce6..e10413ba 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -4,6 +4,8 @@ use oxide_types::value::JsValue; use oxide_runtime_api::{NativeResult, VmHost}; +use crate::string::{make_units_array, regex_replace_fn, regex_replace_manual_units, OwnedText}; + fn get_regexp_ptr(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, JsValue> { let this_val = vm.reg(args[0]); if !this_val.is_object() { @@ -199,19 +201,21 @@ pub fn regexp_test(vm: &mut H, args: &[u8]) -> NativeResult { // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - let haystack = match regexp_string_arg(vm, args) { + let haystack = match regexp_text_arg(vm, args) { Ok(s) => s, Err(err) => return NativeResult::Err(err), }; let last_index = vm.coerce_number_bounded(get_prop(re, 0)).unwrap_or(f64::NAN) as usize; let is_global = is_global(re); - if is_global { - let result = regex.find_from(&haystack, last_index).next().is_some(); - NativeResult::Ok(JsValue::bool(result)) + // lastIndex 为码元口径;Str 臂的内部字节换算在 find_from_units 内完成。 + let text = haystack.as_match_text(); + let found = if is_global { + text.find_from_units(regex, last_index).is_some() } else { - NativeResult::Ok(JsValue::bool(regex.find(&haystack).is_some())) - } + text.find_from_units(regex, 0).is_some() + }; + NativeResult::Ok(JsValue::bool(found)) } /// `RegExp.prototype.exec(string)`:执行匹配并返回数组(含捕获组、index、input)。 @@ -234,7 +238,7 @@ pub fn regexp_exec(vm: &mut H, args: &[u8]) -> NativeResult { // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - let haystack = match regexp_string_arg(vm, args) { + let haystack = match regexp_text_arg(vm, args) { Ok(s) => s, Err(err) => return NativeResult::Err(err), }; @@ -246,13 +250,16 @@ pub fn regexp_exec(vm: &mut H, args: &[u8]) -> NativeResult { (li, g) }; + // 匹配范围取臂原生命径(Str 臂字节、Units 臂码元);index/lastIndex 落盘 + // 前统一换算到码元口径(规格口径)。 + let text = haystack.as_match_text(); let match_result = if is_global { - if last_index > haystack.len() { + if last_index > text.len_units() { return NativeResult::Ok(JsValue::null()); } - regex.find_from(&haystack, last_index).next() + text.find_from_units(regex, last_index) } else { - regex.find(&haystack) + text.find_from_units(regex, 0) }; if let Some(m) = match_result { @@ -263,11 +270,13 @@ pub fn regexp_exec(vm: &mut H, args: &[u8]) -> NativeResult { let arr = vm.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto), n, vm.epoch().bump())); unsafe { - (*arr).set_prop_at(0, vm.new_string(&haystack[range.start..range.end])); + (*arr).set_prop_at(0, vm.new_string_units_owned(text.slice(range.start, range.end).into_owned())); // 捕获组:未参与匹配的组为 undefined。 for i in 1..=group_count { match m.group(i) { - Some(g) => (*arr).set_prop_at(i, vm.new_string(&haystack[g.start..g.end])), + Some(g) => { + (*arr).set_prop_at(i, vm.new_string_units_owned(text.slice(g.start, g.end).into_owned())) + } None => (*arr).set_prop_at(i, JsValue::undefined()), } } @@ -275,14 +284,14 @@ pub fn regexp_exec(vm: &mut H, args: &[u8]) -> NativeResult { } let index_si = vm.kernel_core().perm_interner().intern("index").0; - vm.set_or_create_prop_value(unsafe { &mut *arr }, index_si, JsValue::int(range.start as i32)); + vm.set_or_create_prop_value(unsafe { &mut *arr }, index_si, JsValue::int(text.unit_pos(range.start) as i32)); + let input_val = haystack.to_value(vm); let input_si = vm.kernel_core().perm_interner().intern("input").0; - let haystack_val = vm.new_string(&haystack); - vm.set_or_create_prop_value(unsafe { &mut *arr }, input_si, haystack_val); + vm.set_or_create_prop_value(unsafe { &mut *arr }, input_si, input_val); let groups_si = vm.kernel_core().perm_interner().intern("groups").0; vm.set_or_create_prop_value(unsafe { &mut *arr }, groups_si, JsValue::undefined()); if is_global { - set_prop_at(re_ptr, 0, JsValue::int(range.end as i32)); + set_prop_at(re_ptr, 0, JsValue::int(text.unit_pos(range.end) as i32)); } NativeResult::Ok(JsValue::from_js_object(arr)) @@ -327,8 +336,33 @@ fn is_global(re: &JsObject) -> bool { } } +/// 取匹配文本参数,按 ToString 语义完整转换(对象经 toString/valueOf); +/// 结果按载荷形态 owned:良形文本走 Str 臂,含孤立 surrogate 等走 Units 臂。 +/// 对象 ToString 抛出的原生异常原样传播。 +fn regexp_text_arg(vm: &mut H, args: &[u8]) -> Result { + let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] }); + match oxide_runtime_api::to_string_value_full(val, vm) { + Ok(v) => { + // SAFETY: v 为字符串值,借用即时消费。 + let sp = unsafe { &*v.as_string_ptr() }; + if sp.is_flat() { + Ok(OwnedText::Str(sp.as_str().to_string())) + } else { + Ok(OwnedText::Units(sp.units().into_owned())) + } + } + Err(_) => { + // ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播。 + if let Some(exc) = vm.take_uncaught_value() { + return Err(exc); + } + Err(crate::error::create_type_error(vm, "Cannot convert value to a string")) + } + } +} + /// 取 this 的已编译 regress 正则与匹配文本;this 非 RegExp 或缺失编译结果时返回 Err。 -fn regexp_regex_and_text(vm: &mut H, args: &[u8]) -> Result<(&'static regress::Regex, String), JsValue> { +fn regexp_regex_and_text(vm: &mut H, args: &[u8]) -> Result<(&'static regress::Regex, OwnedText), JsValue> { let re_ptr = get_regexp_ptr(vm, args)?; let fn_ptr = { let re = unsafe { &*re_ptr }; @@ -339,21 +373,20 @@ fn regexp_regex_and_text(vm: &mut H, args: &[u8]) -> Result<(&'static }; // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - let haystack = regexp_string_arg(vm, args)?; + let haystack = regexp_text_arg(vm, args)?; Ok((regex, haystack)) } -/// 取匹配文本参数,按 ToString 语义完整转换(对象经 toString/valueOf);异常原样传播。 -fn regexp_string_arg(vm: &mut H, args: &[u8]) -> Result { - let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] }); - match oxide_runtime_api::to_string_full(val, vm) { - Ok(s) => Ok(s), +/// 替换参数转单元序列:字符串值按载荷形态原样借出,其余经完整 ToString; +/// Symbol 按规范抛 TypeError;对象 ToString 抛出的原生异常原样传播。 +fn replacement_units(vm: &mut H, val: JsValue) -> Result, JsValue> { + match oxide_runtime_api::to_string_value_full(val, vm) { + Ok(v) => Ok(vm.string_units(v).into_owned()), Err(_) => { - // ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播。 if let Some(exc) = vm.take_uncaught_value() { return Err(exc); } - Err(crate::error::create_type_error(vm, "Cannot convert value to a string")) + Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")) } } } @@ -379,22 +412,23 @@ pub fn regexp_symbol_match(vm: &mut H, args: &[u8]) -> NativeResult { Err(err) => return NativeResult::Err(err), }; set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(0)); - let mut matches: Vec = Vec::new(); - let mut last_end = 0; - for m in regex.find_iter(&haystack) { + let text = haystack.as_match_text(); + let mut matches: Vec> = Vec::new(); + let mut last_end = 0usize; + text.for_each_match(regex, |m| { let range = m.range(); - matches.push(haystack[range.start..range.end].to_string()); - last_end = range.end; - } + matches.push(text.slice(range.start, range.end).into_owned()); + last_end = text.unit_pos(range.end); + }); set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(last_end as i32)); if matches.is_empty() { return NativeResult::Ok(JsValue::null()); } - NativeResult::Ok(crate::string::make_string_array(vm, matches)) + NativeResult::Ok(make_units_array(vm, matches)) } /// `RegExp.prototype[Symbol.replace](string, replacement)`:按匹配替换。 -/// 字符串 replacement 展开 `$` 引用(`$$`/`$&`/``$` ``/`$'`/`$n`),函数 replacement +/// 字符串 replacement 展开 `$` 引用(`$$`/`$&`/`` $` ``/`$'`/`$n`),函数 replacement /// 逐匹配调用;global 全替换,否则替换首个。 /// /// # 步骤 @@ -414,6 +448,10 @@ pub fn regexp_symbol_replace(vm: &mut H, args: &[u8]) -> NativeResult set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(0)); } + // 替换文本统一走单元口径($ 展开/回调参数/拼接全在单元序列上进行)。 + let text = haystack.as_match_text(); + let units = text.units(); + let result = if args.len() > 2 { let replacer_val = vm.reg(args[2]); if replacer_val.is_object() { @@ -422,49 +460,56 @@ pub fn regexp_symbol_replace(vm: &mut H, args: &[u8]) -> NativeResult // 回调第 4 参(原字符串)预构一次:原始字符串参数直接复用零拷贝, // 对象参数复用已转换的 haystack 建单个会话串(原每匹配整串复制)。 let text_val = vm.reg(if args.len() > 1 { args[1] } else { args[0] }); - let text_arg = if text_val.is_string() { text_val } else { vm.new_string(&haystack) }; - crate::string::regex_replace_fn(vm, regex, &haystack, replacer_val, is_global, text_arg) + let text_arg = if text_val.is_string() { text_val } else { haystack.to_value(vm) }; + regex_replace_fn(vm, regex, &units, replacer_val, is_global, text_arg) } else { - let replacement = oxide_runtime_api::to_string(replacer_val); - NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual( + let replacement = match replacement_units(vm, replacer_val) { + Ok(u) => u, + Err(err) => return NativeResult::Err(err), + }; + NativeResult::Ok(vm.new_string_units_owned(regex_replace_manual_units( regex, - &haystack, + &units, &replacement, is_global, ))) } } else { - let replacement = oxide_runtime_api::to_string(replacer_val); - NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual( + let replacement = match replacement_units(vm, replacer_val) { + Ok(u) => u, + Err(err) => return NativeResult::Err(err), + }; + NativeResult::Ok(vm.new_string_units_owned(regex_replace_manual_units( regex, - &haystack, + &units, &replacement, is_global, ))) } } else { - NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual(regex, &haystack, "", is_global))) + NativeResult::Ok(vm.new_string_units_owned(regex_replace_manual_units(regex, &units, &[], is_global))) }; if is_global { - let mut last_end = 0; - for m in regex.find_iter(&haystack) { - last_end = m.range().end; - } + let mut last_end = 0usize; + text.for_each_match(regex, |m| { + last_end = text.unit_pos(m.range().end); + }); set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(last_end as i32)); } result } -/// `RegExp.prototype[Symbol.search](string)`:返回首个匹配位置(字符索引), +/// `RegExp.prototype[Symbol.search](string)`:返回首个匹配位置(码元索引), /// 无匹配返回 -1。global 标志不影响 search。 pub fn regexp_symbol_search(vm: &mut H, args: &[u8]) -> NativeResult { let (regex, haystack) = match regexp_regex_and_text(vm, args) { Ok(pair) => pair, Err(err) => return NativeResult::Err(err), }; - if let Some(m) = regex.find(&haystack) { - return NativeResult::Ok(JsValue::int(m.range().start as i32)); + let text = haystack.as_match_text(); + if let Some(m) = text.find_from_units(regex, 0) { + return NativeResult::Ok(JsValue::int(text.unit_pos(m.range().start) as i32)); } NativeResult::Ok(JsValue::int(-1)) } @@ -487,32 +532,35 @@ pub fn regexp_symbol_split(vm: &mut H, args: &[u8]) -> NativeResult { u32::MAX as usize }; - let mut parts: Vec = Vec::new(); - let mut last_end = 0; - for m in regex.find_iter(&haystack) { + // 片段切片取臂原生命径(Str 臂字节、Units 臂码元);limit 触顶后提前停推 + // 但保持游标语义一致(尾段仅在未触顶时追加)。 + let text = haystack.as_match_text(); + let mut parts: Vec> = Vec::new(); + let mut last_end = 0usize; + text.for_each_match(regex, |m| { if parts.len() >= limit { - break; + return; } let range = m.range(); - parts.push(haystack[last_end..range.start].to_string()); + parts.push(text.slice(last_end, range.start).into_owned()); if parts.len() >= limit { - break; + return; } for i in 1..=m.captures.len() { if parts.len() >= limit { break; } match m.group(i) { - Some(g) => parts.push(haystack[g.start..g.end].to_string()), - None => parts.push(String::new()), + Some(g) => parts.push(text.slice(g.start, g.end).into_owned()), + None => parts.push(Vec::new()), } } last_end = range.end; + }); + if parts.len() < limit { + parts.push(text.slice(last_end, text.raw_len()).into_owned()); } - if last_end <= haystack.len() && parts.len() < limit { - parts.push(haystack[last_end..].to_string()); - } - NativeResult::Ok(crate::string::make_string_array(vm, parts)) + NativeResult::Ok(make_units_array(vm, parts)) } /// `RegExp.prototype[Symbol.matchAll](string)`:返回按 global 语义逐个产出 @@ -526,7 +574,7 @@ pub fn regexp_symbol_match_all(vm: &mut H, args: &[u8]) -> NativeResu let re = unsafe { &*re_ptr }; let is_global = is_global(re); let last_index = vm.coerce_number_bounded(get_prop(re, PROP_LAST_INDEX)).unwrap_or(f64::NAN) as usize; - let haystack = match regexp_string_arg(vm, args) { + let haystack = match regexp_text_arg(vm, args) { Ok(s) => s, Err(err) => return NativeResult::Err(err), }; @@ -573,7 +621,9 @@ pub fn regexp_symbol_match_all(vm: &mut H, args: &[u8]) -> NativeResu let input_si = vm.kernel_core().perm_interner().intern(crate::string::MALL_INPUT).0; let index_si = vm.kernel_core().perm_interner().intern(crate::string::MALL_INDEX).0; let re_si = vm.kernel_core().perm_interner().intern(crate::string::MALL_RE).0; - let input_val = vm.new_string(&haystack); + // input 属性存完整转换后的字符串值(单元保真);index 游标为码元口径, + // next 按 input 载荷形态各自消费。 + let input_val = haystack.to_value(vm); vm.set_or_create_prop_value(wrapper_obj, input_si, input_val); vm.set_or_create_prop_value(wrapper_obj, index_si, JsValue::int(last_index as i32)); vm.set_or_create_prop_value(wrapper_obj, re_si, iter_re); diff --git a/crates/oxide_builtins/src/set.rs b/crates/oxide_builtins/src/set.rs index 12cc703a..a6318ae8 100644 --- a/crates/oxide_builtins/src/set.rs +++ b/crates/oxide_builtins/src/set.rs @@ -44,12 +44,13 @@ impl Hash for SetKey { let bits = if d == 0.0 { 0.0f64.to_bits() } else { JsValue::float(d).to_bits() }; bits.hash(state); } - // String:内容字节(rope 经 `as_str` 惰性扁平化,幂等)。 + // String:单元序列(rope 经扁平化缓存,幂等;单元口径与 + // same_value_zero 的内容比较同域,同值同哈希)。 JsType::String => { 1u8.hash(state); // SAFETY: 串指针在 native 调用期间存活(session 串不在调用中途释放), // 借出在本哈希调用内即时消费。 - unsafe { (*v.as_string_ptr()).as_str().as_bytes().hash(state) }; + unsafe { (*v.as_string_ptr()).units().as_ref().hash(state) }; } // BigInt:`to_bytes_le` 规范形(零号统一 NoZero,同值同字节)。 JsType::BigInt => { diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index fb4c3888..e24f3fce 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -4,7 +4,6 @@ use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::object::JsObject; use oxide_types::value::JsValue; -use memchr::memchr; use oxide_runtime_api::{NativeResult, VmHost}; use crate::builtins_debug; @@ -19,13 +18,280 @@ macro_rules! try_string { }; } -/// 取 this 的字符串内容供只读扫描:原始字符串零拷贝借用,对象经完整 ToString。 +// ── 文本通道 ───────────────────────────────────────────────────────────── +// +// 字符串值的载荷分形态:Flat(良形 UTF-8)、FlatU16(单元载荷,可含孤立 +// surrogate)、Cons(rope)。正则家族按文本通道分臂——Str 臂走良形文本 +// (字节口径,与旧代码路径一致),Units 臂走单元序列(regress utf16 接口, +// 匹配范围即码元);其余方法统一按单元序列处理(规格口径)。 + +/// 正则家族的匹配文本通道。 +pub(crate) enum MatchText<'a> { + Str(&'a str), + Units(Cow<'a, [u16]>), +} + +impl<'a> MatchText<'a> { + /// 从字符串值按载荷形态借用:Flat → Str 零拷贝,其余 → Units + /// (FlatU16 直接借用、Cons 经扁平化缓存)。 + fn from_value(val: JsValue) -> Self { + debug_assert!(val.is_string()); + // SAFETY: val 为字符串值,借用期存活由调用方借用纪律保证。 + let sp = unsafe { &*val.as_string_ptr() }; + if sp.is_flat() { + MatchText::Str(sp.as_str()) + } else { + MatchText::Units(sp.units()) + } + } + + /// 码元数。 + pub(crate) fn len_units(&self) -> usize { + match self { + MatchText::Str(s) => { + if s.is_ascii() { + s.len() + } else { + s.encode_utf16().count() + } + } + MatchText::Units(u) => u.len(), + } + } + + /// 臂原生长度(Str 臂字节、Units 臂码元)——`slice` 的端点口径。 + pub(crate) fn raw_len(&self) -> usize { + match self { + MatchText::Str(s) => s.len(), + MatchText::Units(u) => u.len(), + } + } + + /// 借出单元序列(Str 臂一次性编码为 owned;Units 臂借用/浅拷)。 + pub(crate) fn units(&self) -> Cow<'a, [u16]> { + match self { + MatchText::Str(s) => { + if s.is_ascii() { + Cow::Owned(s.as_bytes().iter().map(|&b| b as u16).collect()) + } else { + Cow::Owned(s.encode_utf16().collect()) + } + } + MatchText::Units(u) => u.clone(), + } + } + + /// 从码元位置开始的首个匹配(Str 臂内部换算为字节位置)。 + pub(crate) fn find_from_units(&self, regex: ®ress::Regex, start_units: usize) -> Option { + match self { + MatchText::Str(s) => regex.find_from(s, unit_to_byte(s, start_units)).next(), + MatchText::Units(u) => regex.find_from_utf16(u, start_units).next(), + } + } + + /// 按序消费全部非重叠匹配(自码元 0 起)。 + pub(crate) fn for_each_match(&self, regex: ®ress::Regex, mut f: impl FnMut(®ress::Match)) { + match self { + MatchText::Str(s) => { + for m in regex.find_iter(s) { + f(&m); + } + } + MatchText::Units(u) => { + for m in regex.find_from_utf16(u, 0) { + f(&m); + } + } + } + } + + /// 匹配片段为单元序列(start/end 取臂自身匹配范围口径:Str 臂字节、 + /// Units 臂码元——臂由文本通道钉死,调用方直接传 `m.range()`)。 + pub(crate) fn slice(&self, start: usize, end: usize) -> Cow<'a, [u16]> { + match self { + MatchText::Str(s) => { + let sub = &s[start..end]; + Cow::Owned(if sub.is_ascii() { + sub.as_bytes().iter().map(|&b| b as u16).collect() + } else { + sub.encode_utf16().collect() + }) + } + MatchText::Units(u) => Cow::Owned(u[start..end].to_vec()), + } + } + + /// 匹配范围端点换算到码元口径(Str 臂范围是字节口径,Units 臂即码元)。 + pub(crate) fn unit_pos(&self, pos: usize) -> usize { + match self { + MatchText::Str(s) => byte_to_unit(s, pos), + MatchText::Units(_) => pos, + } + } +} + +/// 良形文本的前置转换缓存:owned 匹配文本(regexp 模块的 haystack 载体)。 +pub(crate) enum OwnedText { + Str(String), + Units(Vec), +} + +impl OwnedText { + pub(crate) fn as_match_text(&self) -> MatchText<'_> { + match self { + OwnedText::Str(s) => MatchText::Str(s), + OwnedText::Units(u) => MatchText::Units(Cow::Borrowed(u)), + } + } + + /// 物化为字符串值(Str 臂文本 / Units 臂单元,载荷形态各自最小)。 + pub(crate) fn to_value(&self, vm: &mut H) -> JsValue { + match self { + OwnedText::Str(s) => vm.new_string(s), + OwnedText::Units(u) => vm.new_string_units_owned(u.clone()), + } + } +} + +// ── 单元基础工具 ───────────────────────────────────────────────────────── + +/// 单元序列首个匹配位置(自 from 起,返回 `hay` 内的绝对下标);needle 空 +/// 由调用方先行处理。 +fn find_units(hay: &[u16], needle: &[u16], from: usize) -> Option { + if from >= hay.len() { + return None; + } + let hay = &hay[from..]; + if needle.len() == 1 { + return hay.iter().position(|&u| u == needle[0]).map(|i| from + i); + } + hay.windows(needle.len()).position(|w| w == needle).map(|i| from + i) +} + +/// 单元序列末个匹配位置(限 end 前缀内起始)。 +fn rfind_units(hay: &[u16], needle: &[u16], end: usize) -> Option { + let hay = &hay[..end]; + if needle.len() == 1 { + return hay.iter().rposition(|&u| u == needle[0]); + } + hay.windows(needle.len()).rposition(|w| w == needle) +} + +/// 自 start 起的最长良形段终点(代理对整体归段,孤立 surrogate 截断段)。 +fn well_formed_segment_end(units: &[u16], start: usize) -> usize { + let mut i = start; + while i < units.len() { + let u = units[i]; + if (0xD800..=0xDBFF).contains(&u) { + if i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) { + i += 2; + } else { + break; + } + } else if (0xDC00..=0xDFFF).contains(&u) { + break; + } else { + i += 1; + } + } + i +} + +/// 码点数(代理对计 1,孤立 surrogate 计 1)——pad 族的目标长度口径。 +fn code_point_count(units: &[u16]) -> usize { + let mut n = 0; + let mut i = 0; + while i < units.len() { + if (0xD800..=0xDBFF).contains(&units[i]) && i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) { + i += 2; + } else { + i += 1; + } + n += 1; + } + n +} + +/// 取序列头 count 个码点对应的单元前缀。 +fn take_code_points(units: &[u16], count: usize) -> &[u16] { + let mut n = 0; + let mut i = 0; + while i < units.len() && n < count { + if (0xD800..=0xDBFF).contains(&units[i]) && i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) { + i += 2; + } else { + i += 1; + } + n += 1; + } + &units[..i] +} + +/// ES 白空格判定的单元口径:Rust 空白表与 White_Space+LineTerminator 等价; +/// 孤立 surrogate 非白空格。 +fn is_trim_unit(u: u16) -> bool { + if u < 0x80 { + return matches!(u, 0x09..=0x0D | 0x20); + } + if let Some(c) = char::from_u32(u as u32) { + return c.is_whitespace(); + } + false +} + +/// 良形文本的字节偏移 → 码元数(Str 臂匹配范围/index 的码元换算入口)。 +fn byte_to_unit(s: &str, byte: usize) -> usize { + if s.is_ascii() { + byte + } else { + s[..byte].encode_utf16().count() + } +} + +/// 良形文本的码元位置 → 字节偏移(位置须落在字符边界:匹配游标恒对齐)。 +fn unit_to_byte(s: &str, unit: usize) -> usize { + if s.is_ascii() { + return unit.min(s.len()); + } + let mut acc = 0usize; + for (byte, ch) in s.char_indices() { + if unit == acc { + return byte; + } + acc += ch.len_utf16(); + } + s.len() +} + +/// 大小写/规范化按"良形段"分段映射:整段经 UTF-8 往返调用映射函数,孤立 +/// surrogate 单元原样透传(映射对码点定义,对孤立 surrogate 无定义)。 +fn map_well_formed_segments(units: &[u16], mut f: impl FnMut(&str) -> String) -> Vec { + let mut out = Vec::with_capacity(units.len()); + let mut i = 0; + while i < units.len() { + let seg_start = i; + i = well_formed_segment_end(units, i); + if i == seg_start { + out.push(units[i]); + i += 1; + continue; + } + let s = String::from_utf16(&units[seg_start..i]).expect("良形段 from_utf16 必成功"); + out.extend(f(&s).encode_utf16()); + } + out +} + +// ── this / 参数转换 ───────────────────────────────────────────────────── + +/// 取 this 的单元序列供只读扫描:原始字符串按载荷形态借用(Flat 惰性编码、 +/// FlatU16 直接借用、Cons 经扁平化缓存),对象经完整 ToString。 /// null/undefined/symbol 抛 TypeError;对象 ToString 抛出的原生异常原样传播。 /// /// # 注意事项 -/// 返回借用绑定本次 `&mut` 借用,存活期内禁止任何 `&mut` 调用;需与参数字符串 -/// 并存时,调用方先把参数转换为 owned(见 `as_string` 借用纪律)。 -fn this_string<'a, H: VmHost>(vm: &'a mut H, args: &[u8]) -> Result, JsValue> { +/// 返回借用绑定本次 `&mut` 借用,存活期内禁止任何 `&mut` 调用;需与参数并存 +/// 时调用方先 `into_owned` 落地。 +fn this_units<'a, H: VmHost>(vm: &'a mut H, args: &[u8]) -> Result, JsValue> { let this_val = vm.reg(args[0]); if this_val.is_null() || this_val.is_undefined() { return Err(crate::error::create_type_error(vm, "String.prototype method called on null or undefined")); @@ -34,11 +300,10 @@ fn this_string<'a, H: VmHost>(vm: &'a mut H, args: &[u8]) -> Result return Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")); } if this_val.is_string() { - return Ok(Cow::Borrowed(vm.string_ref(this_val))); + return Ok(vm.string_units(this_val)); } - // 对象 this 须经 ToString 完整转换(boxed Number/String 等取内部原始值)。 - match oxide_runtime_api::to_string_full(this_val, vm) { - Ok(s) => Ok(Cow::Owned(s)), + match oxide_runtime_api::to_string_value_full(this_val, vm) { + Ok(v) => Ok(vm.string_units(v)), Err(_) => { // ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播, // 否则会被展平为普通 Error 丢失原始异常对象。 @@ -50,28 +315,327 @@ fn this_string<'a, H: VmHost>(vm: &'a mut H, args: &[u8]) -> Result } } -/// `String.fromCharCode(...codes)`:把各参数按低 16 位转成字符拼接为字符串。 -pub fn string_from_char_code(vm: &mut H, args: &[u8]) -> NativeResult { - builtins_debug!("String.prototype.fromCharCode called with {} args", args.len()); - let mut out = String::new(); - for &arg_reg in args.iter().skip(1) { - let code = oxide_runtime_api::to_uint32(vm.reg(arg_reg)) & 0xFFFF; - if let Some(ch) = char::from_u32(code) { - out.push(ch); - } else { - out.push('\u{FFFD}'); +/// 取 this 的匹配文本通道:原始字符串按载荷形态零拷贝借用(Flat → Str、其余 +/// → Units),对象经完整 ToString(结果为字符串值,同形态借用)。 +/// null/undefined/symbol 抛 TypeError;对象 ToString 抛出的原生异常原样传播。 +fn this_text<'a, H: VmHost>(vm: &'a mut H, args: &[u8]) -> Result, JsValue> { + let this_val = vm.reg(args[0]); + if this_val.is_null() || this_val.is_undefined() { + return Err(crate::error::create_type_error(vm, "String.prototype method called on null or undefined")); + } + if this_val.is_symbol() { + return Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")); + } + if this_val.is_string() { + return Ok(MatchText::from_value(this_val)); + } + match oxide_runtime_api::to_string_value_full(this_val, vm) { + Ok(v) => Ok(MatchText::from_value(v)), + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return Err(exc); + } + Err(crate::error::create_type_error(vm, "Cannot convert this value to a string")) } } - NativeResult::Ok(vm.new_string_owned(out)) +} + +/// 取参数字符串的单元序列:原始字符串按载荷形态借用,其余经完整 ToString。 +/// Symbol 与其余转换失败按规范抛 TypeError;对象 ToString 抛出的原生异常原样传播。 +fn as_units<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Result, JsValue> { + if val.is_string() { + return Ok(vm.string_units(val)); + } + match oxide_runtime_api::to_string_value_full(val, vm) { + Ok(v) => Ok(vm.string_units(v)), + Err(_) => { + if let Some(exc) = vm.take_uncaught_value() { + return Err(exc); + } + Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")) + } + } +} + +// ── 数组产出 ───────────────────────────────────────────────────────────── + +/// 以已构造的字符串值构建字符串数组(元素零拷贝落地),供逐单元产出路径 +/// (空分隔 split)复用,跳过 `Vec` 中间层。 +pub(crate) fn make_string_array_values(vm: &mut H, parts: Vec) -> JsValue { + let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; + let n = parts.len(); + let arr = + vm.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto), n, vm.epoch().bump())); + unsafe { + for (i, sv) in parts.into_iter().enumerate() { + (*arr).set_prop_at(i, sv); + } + (*arr).set_prop_count(n); + } + JsValue::from_js_object(arr) +} + +/// 以单元序列集合构建字符串数组(元素按智能路由物化为字符串值),供 +/// split/match 的片段产出路径复用。 +pub(crate) fn make_units_array(vm: &mut H, parts: Vec>) -> JsValue { + let values: Vec = parts.into_iter().map(|u| vm.new_string_units_owned(u)).collect(); + make_string_array_values(vm, values) +} + +// ── 正则替换(单元口径) ──────────────────────────────────────────────── + +/// 展开单个匹配的 replacement `$` 引用(`$$`、`$&`、`` $` ``、`$'`、`$n`), +/// 输入/输出均为单元序列。 +fn expand_dollar_units(text: &[u16], m: ®ress::Match, replacement: &[u16]) -> Vec { + let mut out = Vec::new(); + let mut i = 0; + while i < replacement.len() { + if replacement[i] != 0x24 { + out.push(replacement[i]); + i += 1; + continue; + } + let range = m.range(); + let rest = &replacement[i..]; + if rest.starts_with(&[0x24, 0x24]) { + out.push(0x24); + i += 2; + } else if rest.starts_with(&[0x24, 0x26]) { + out.extend_from_slice(&text[range.start..range.end]); + i += 2; + } else if rest.starts_with(&[0x24, 0x60]) { + out.extend_from_slice(&text[..range.start]); + i += 2; + } 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 { + 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) { + out.extend_from_slice(&text[g.start..g.end]); + } + i += 1 + j; + } + } + } + out +} + +/// 正则替换的手动实现(单元口径):按 JS 语义展开 replacement 中的 `$` 引用, +/// global 全替换否则首个。 +pub(crate) fn regex_replace_manual_units( + regex: ®ress::Regex, text: &[u16], replacement: &[u16], global: bool, +) -> Vec { + let mut out = Vec::new(); + let mut last_end = 0; + let matches: Vec = if global { + regex.find_from_utf16(text, 0).collect() + } else { + regex.find_from_utf16(text, 0).take(1).collect() + }; + for m in &matches { + let range = m.range(); + out.extend_from_slice(&text[last_end..range.start]); + out.extend_from_slice(&expand_dollar_units(text, m, replacement)); + last_end = range.end; + } + out.extend_from_slice(&text[last_end..]); + out +} + +/// 调用函数 replacer 并把返回值转为单元序列;调用抛出的异常原样恢复。 +fn call_replacer(vm: &mut H, replacer: JsValue, cb_args: &[JsValue]) -> Result, JsValue> { + match vm.call_function_sync(replacer, JsValue::undefined(), cb_args) { + Ok(result) => match oxide_runtime_api::to_units_full(result, vm) { + Ok(units) => Ok(units), + Err(_) => { + // 转换失败(Symbol 等)或回调异常均须原样传播。 + if let Some(exc) = vm.take_uncaught_value() { + return Err(exc); + } + Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")) + } + }, + Err(err) => Err(vm + .take_uncaught_value() + .unwrap_or_else(|| crate::error::create_type_error(vm, &format!("replace replacer: {}", err)))), + } +} + +/// 函数 replacer 的回调参数:匹配串、各捕获组(未匹配为 undefined)、position +/// (码元口径)、原字符串。原字符串参数由调用方预构传入(`text_arg`), +/// 避免每匹配复制整个源串——字符串不可变,同一 `JsValue` 可安全复用。 +fn replacer_cb_args(vm: &mut H, text: &[u16], m: ®ress::Match, text_arg: JsValue) -> Vec { + let range = m.range(); + let mut cb_args: Vec = Vec::with_capacity(m.captures.len() + 2); + cb_args.push(vm.new_string_units(&text[range.start..range.end])); + for i in 1..=m.captures.len() { + match m.group(i) { + Some(g) => cb_args.push(vm.new_string_units(&text[g.start..g.end])), + None => cb_args.push(JsValue::undefined()), + } + } + cb_args.push(JsValue::int(range.start as i32)); + cb_args.push(text_arg); + cb_args +} + +/// 正则模式 + 函数 replacer:global 全替换否则替换首个,逐匹配调用回调, +/// 返回值转单元序列作为替换文本(不展开 `$` 引用)。`text_arg` 为回调第 4 参 +/// (原字符串),调用方预构一次,回调期按值复用。 +pub(crate) fn regex_replace_fn( + vm: &mut H, regex: ®ress::Regex, text: &[u16], replacer: JsValue, global: bool, text_arg: JsValue, +) -> NativeResult { + let matches: Vec = if global { + regex.find_from_utf16(text, 0).collect() + } else { + regex.find_from_utf16(text, 0).take(1).collect() + }; + let mut out = Vec::new(); + let mut last_end = 0; + for m in &matches { + let range = m.range(); + out.extend_from_slice(&text[last_end..range.start]); + let cb_args = replacer_cb_args(vm, text, m, text_arg); + let repl = try_string!(call_replacer(vm, replacer, &cb_args)); + out.extend_from_slice(&repl); + last_end = range.end; + } + out.extend_from_slice(&text[last_end..]); + NativeResult::Ok(vm.new_string_units_owned(out)) +} + +/// 单元序列上的整体替换(空模式在每个单元边界插入替换文本,与 String::replace +/// 口径一致)。 +fn replace_units_all(hay: &[u16], from: &[u16], to: &[u16]) -> Vec { + if from.is_empty() { + let mut out = Vec::with_capacity(hay.len() + (hay.len() + 1) * to.len()); + out.extend_from_slice(to); + for &u in hay { + out.push(u); + out.extend_from_slice(to); + } + return out; + } + let mut out = Vec::with_capacity(hay.len()); + let mut start = 0; + while let Some(p) = find_units(hay, from, start) { + out.extend_from_slice(&hay[start..p]); + out.extend_from_slice(to); + start = p + from.len(); + } + out.extend_from_slice(&hay[start..]); + out +} + +/// 单元序列上的首个替换;未命中返回原序列拷贝。空模式在位置 0 命中 +/// (插入替换文本于串首,与 String::replace 口径一致)。 +fn replace_units_first(hay: &[u16], from: &[u16], to: &[u16]) -> Vec { + if from.is_empty() { + let mut out = Vec::with_capacity(hay.len() + to.len()); + out.extend_from_slice(to); + out.extend_from_slice(hay); + return out; + } + if let Some(rel) = find_units(hay, from, 0) { + let mut out = Vec::with_capacity(hay.len()); + out.extend_from_slice(&hay[..rel]); + out.extend_from_slice(to); + out.extend_from_slice(&hay[rel + from.len()..]); + return out; + } + hay.to_vec() +} + +/// 字符串模式 + 函数 replacer:all 全替换否则替换首个。回调参数 +/// `(match, position, string)`(无捕获组)。空模式在每个单元边界匹配一次。 +/// `text_arg` 为回调第 4 参(原字符串),调用方预构一次。 +fn string_replace_fn( + vm: &mut H, text: &[u16], pattern: &[u16], replacer: JsValue, all: bool, text_arg: JsValue, +) -> NativeResult { + let search_length = pattern.len(); + let mut positions: Vec = Vec::new(); + if search_length == 0 { + positions.push(0); + if all { + for i in 1..=text.len() { + positions.push(i); + } + } + } else { + let mut start = 0; + while let Some(p) = find_units(text, pattern, start) { + positions.push(p); + if !all { + break; + } + start = p + search_length; + } + } + let mut out = Vec::new(); + let mut last_end = 0; + for p in positions { + out.extend_from_slice(&text[last_end..p]); + let cb_args = [vm.new_string_units(&text[p..p + search_length]), JsValue::int(p as i32), text_arg]; + let repl = try_string!(call_replacer(vm, replacer, &cb_args)); + out.extend_from_slice(&repl); + last_end = p + search_length; + } + out.extend_from_slice(&text[last_end..]); + NativeResult::Ok(vm.new_string_units_owned(out)) +} + +fn is_regexp_obj(val: JsValue, vm: &H) -> bool { + if !val.is_object() { + return false; + } + let ptr = val.as_js_object_ptr(); + if ptr.is_null() { + return false; + } + let obj = unsafe { &*ptr }; + let proto = obj.proto(); + if !proto.is_object() { + return false; + } + let proto_ptr = proto.as_js_object_ptr(); + if proto_ptr.is_null() { + return false; + } + let rp = vm.session().builtin_world().regexp_proto.as_ptr() as *mut JsObject; + std::ptr::eq(proto_ptr, rp) +} + +// ── 静态方法 / 构造 ───────────────────────────────────────────────────── + +/// `String.fromCharCode(...codes)`:把各参数按 ToUint32 低 16 位转为单元拼接 +/// 为字符串(surrogate 区间合法产出孤立 surrogate 单元)。 +pub fn string_from_char_code(vm: &mut H, args: &[u8]) -> NativeResult { + builtins_debug!("String.prototype.fromCharCode called with {} args", args.len()); + let mut units: Vec = Vec::new(); + for &arg_reg in args.iter().skip(1) { + let code = oxide_runtime_api::to_uint32(vm.reg(arg_reg)) & 0xFFFF; + units.push(code as u16); + } + NativeResult::Ok(vm.new_string_units_owned(units)) } /// `String.fromCodePoint(...codes)`:把各参数按 ToNumber 语义转成 code point /// (0..0x10FFFF 的整数)拼接为字符串;非整数、NaN 或越界抛 RangeError, -/// Symbol 抛 TypeError。surrogate 区间按现行规范接受(孤立 surrogate 无法在 -/// UTF-8 表示,与字面量编码一致输出 U+FFFD + 四位小写 hex 文本)。 +/// Symbol 抛 TypeError。surrogate 区间按现行规范接受(产出孤立 surrogate +/// 单元,单元载荷可承载)。 pub fn string_from_code_point(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.fromCodePoint called with {} args", args.len()); - let mut out = String::new(); + let mut units: Vec = Vec::new(); // 经 native_arg_count/native_arg_at 读取:大实参集(如 harness 的 // `String.fromCodePoint.apply(null, codePoints)` 每块 10000 码位)在寄存器 // 窗口外走 spill 溢出区,小实参集与既有寄存器路径一致。 @@ -91,14 +655,15 @@ pub fn string_from_code_point(vm: &mut H, args: &[u8]) -> NativeResul return NativeResult::Err(crate::error::create_range_error(vm, "Invalid code point")); } let code = n as u32; - if (0xD800..=0xDFFF).contains(&code) { - out.push('\u{FFFD}'); - out.push_str(&format!("{code:04x}")); + if code < 0x10000 { + units.push(code as u16); } else { - out.push(char::from_u32(code).unwrap()); + let v = code - 0x10000; + units.push(0xD800 + (v >> 10) as u16); + units.push(0xDC00 + (v & 0x3FF) as u16); } } - NativeResult::Ok(vm.new_string_owned(out)) + NativeResult::Ok(vm.new_string_units_owned(units)) } /// `String.prototype.valueOf`:返回包装对象的原始字符串;其它 this 抛 TypeError。 @@ -124,12 +689,13 @@ pub fn string_value_of(vm: &mut H, args: &[u8]) -> NativeResult { )) } -/// JS `String()` 构造逻辑:把参数转成字符串;new 语义返回 `[[StringData]]` 包装对象。 +/// JS `String()` 构造逻辑:把参数转成字符串(单元保真);new 语义返回 +/// `[[StringData]]` 包装对象。 pub fn string_constructor(vm: &mut H, args: &[u8]) -> NativeResult { - let s = if args.len() > 1 { - // 对象参数须经 ToPrimitive/ToString 完整转换(数组 → join,对象 → toString)。 - match oxide_runtime_api::to_string_for_string_constructor(vm.reg(args[1]), vm) { - Ok(s) => s, + 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() { @@ -139,9 +705,8 @@ pub fn string_constructor(vm: &mut H, args: &[u8]) -> NativeResult { } } } else { - String::new() + vm.new_string("") }; - let str_val = vm.new_string_owned(s); 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] }); @@ -188,367 +753,30 @@ pub fn string_to_string(vm: &mut H, args: &[u8]) -> NativeResult { )) } -pub(crate) fn make_string_array(vm: &mut H, parts: Vec) -> JsValue { - let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; - let n = parts.len(); - let arr = - vm.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto), n, vm.epoch().bump())); - unsafe { - for (i, s) in parts.into_iter().enumerate() { - let sv = vm.new_string_owned(s); - (*arr).set_prop_at(i, sv); - } - (*arr).set_prop_count(n); - } - JsValue::from_js_object(arr) -} +// ── 查找 / 索引 ───────────────────────────────────────────────────────── -/// 以已构造的字符串值构建字符串数组(元素零拷贝落地),供逐字符产出路径 -/// (空分隔 split)复用,跳过 `Vec` 中间层。 -pub(crate) fn make_string_array_values(vm: &mut H, parts: Vec) -> JsValue { - let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; - let n = parts.len(); - let arr = - vm.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto), n, vm.epoch().bump())); - unsafe { - for (i, sv) in parts.into_iter().enumerate() { - (*arr).set_prop_at(i, sv); - } - (*arr).set_prop_count(n); - } - JsValue::from_js_object(arr) -} - -/// 取参数字符串内容:原始字符串零拷贝借用,其余经完整 ToString(对象 ToPrimitive)。 -/// Symbol 与其余转换失败按规范抛 TypeError;对象 ToString 抛出的原生异常原样传播。 -/// -/// # 边界与前提 -/// - 失败返回异常值,调用方经 `try_string!` 原样抛出。 -/// -/// # 注意事项 -/// 返回借用绑定本次 `&mut` 借用;需与 `this_string` 借用并存时先 `into_owned` 落地。 -fn as_string<'a, H: VmHost>(vm: &'a mut H, val: JsValue) -> Result, JsValue> { - if val.is_string() { - return Ok(Cow::Borrowed(vm.string_ref(val))); - } - match oxide_runtime_api::to_string_full(val, vm) { - Ok(s) => Ok(Cow::Owned(s)), - Err(_) => { - // ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播, - // 否则会被展平为普通 Error 丢失原始异常对象。 - if let Some(exc) = vm.take_uncaught_value() { - return Err(exc); - } - Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")) - } - } -} - -/// 正则替换的手动实现:按 JS 语义展开 replacement 中的 `$` 引用 -/// (`$$`、`$&`、`` $` ``、`$'`、`$n`),global 全替换否则首个。 -pub(crate) fn regex_replace_manual(regex: ®ress::Regex, text: &str, replacement: &str, global: bool) -> String { - let mut out = String::new(); - let mut last_end = 0; - let matches: Vec = if global { - regex.find_iter(text).collect() - } else { - regex.find(text).into_iter().collect() - }; - for m in matches { - let range = m.range(); - out.push_str(&text[last_end..range.start]); - out.push_str(&expand_dollar(text, &m, replacement)); - last_end = range.end; - } - out.push_str(&text[last_end..]); - out -} - -/// 展开单个匹配的 replacement `$` 引用。 -fn expand_dollar(text: &str, m: ®ress::Match, replacement: &str) -> String { - let mut out = String::new(); - let bytes = replacement.as_bytes(); - let mut i = 0; - while i < bytes.len() { - if bytes[i] != b'$' { - out.push(replacement[i..].chars().next().unwrap_or('\u{FFFD}')); - i += 1; - continue; - } - let range = m.range(); - let rest = &replacement[i..]; - if rest.starts_with("$$") { - out.push('$'); - i += 2; - } else if rest.starts_with("$&") { - out.push_str(&text[range.start..range.end]); - i += 2; - } else if rest.starts_with("$`") { - out.push_str(&text[..range.start]); - i += 2; - } else if rest.starts_with("$'") { - out.push_str(&text[range.end..]); - i += 2; - } else { - // $n(1-2 位数字)→ 捕获组;未匹配/越界 → 空串。 - let mut j = 1; - while j < bytes.len().saturating_sub(i) && bytes[i + j].is_ascii_digit() { - j += 1; - } - let digits = &replacement[i + 1..i + j]; - if digits.is_empty() { - out.push('$'); - i += 1; - } else { - let n: usize = digits.parse().unwrap_or(0); - if let Some(g) = m.group(n) { - out.push_str(&text[g.start..g.end]); - } - i += 1 + digits.len(); - } - } - } - out -} - -/// 调用函数 replacer 并把返回值 ToString 为替换文本;调用抛出的异常原样恢复。 -fn call_replacer(vm: &mut H, replacer: JsValue, cb_args: &[JsValue]) -> Result { - match vm.call_function_sync(replacer, JsValue::undefined(), cb_args) { - Ok(result) => Ok(oxide_runtime_api::to_string(result)), - Err(err) => Err(vm - .take_uncaught_value() - .unwrap_or_else(|| crate::error::create_type_error(vm, &format!("replace replacer: {}", err)))), - } -} - -/// 函数 replacer 的回调参数:匹配串、各捕获组(未匹配为 undefined)、position、原字符串。 -/// position 按 UTF-16 单元计数(非字节偏移)。原字符串参数由调用方预构传入(`text_arg`), -/// 避免每匹配复制整个源串——字符串不可变,同一 `JsValue` 可安全复用。 -fn replacer_cb_args(vm: &mut H, text: &str, m: ®ress::Match, text_arg: JsValue) -> Vec { - let range = m.range(); - let mut cb_args: Vec = Vec::with_capacity(m.captures.len() + 2); - cb_args.push(vm.new_string(&text[range.start..range.end])); - for i in 1..=m.captures.len() { - match m.group(i) { - Some(g) => cb_args.push(vm.new_string(&text[g.start..g.end])), - None => cb_args.push(JsValue::undefined()), - } - } - cb_args.push(JsValue::int(byte_to_unit(text, range.start) as i32)); - cb_args.push(text_arg); - cb_args -} - -/// 正则模式 + 函数 replacer:global 全替换否则替换首个,逐匹配调用回调, -/// 返回值 ToString 作为替换文本(不展开 `$` 引用)。`text_arg` 为回调第 4 参 -/// (原字符串),调用方预构一次,回调期按值复用。 -pub(crate) fn regex_replace_fn( - vm: &mut H, regex: ®ress::Regex, text: &str, replacer: JsValue, global: bool, text_arg: JsValue, -) -> NativeResult { - let matches: Vec = if global { - regex.find_iter(text).collect() - } else { - regex.find(text).into_iter().collect() - }; - let mut out = String::new(); - let mut last_end = 0; - for m in matches { - let range = m.range(); - out.push_str(&text[last_end..range.start]); - let cb_args = replacer_cb_args(vm, text, &m, text_arg); - let repl = try_string!(call_replacer(vm, replacer, &cb_args)); - out.push_str(&repl); - last_end = range.end; - } - out.push_str(&text[last_end..]); - NativeResult::Ok(vm.new_string_owned(out)) -} - -/// 字符串模式 + 函数 replacer:all 全替换否则替换首个。回调参数 -/// `(match, position, string)`(无捕获组)。空模式在每个字符边界匹配一次。 -/// `text_arg` 为回调第 4 参(原字符串),调用方预构一次。 -fn string_replace_fn( - vm: &mut H, text: &str, pattern: &str, replacer: JsValue, all: bool, text_arg: JsValue, -) -> NativeResult { - let search_length = pattern.len(); - let mut positions: Vec = Vec::new(); - if search_length == 0 { - positions.push(0); - if all { - let mut byte = 0; - for ch in text.chars() { - byte += ch.len_utf8(); - positions.push(byte); - } - } - } else { - let mut start = 0; - while let Some(rel) = text[start..].find(pattern) { - let p = start + rel; - positions.push(p); - if !all { - break; - } - start = p + search_length; - } - } - let mut out = String::new(); - let mut last_end = 0; - for p in positions { - out.push_str(&text[last_end..p]); - let cb_args = [ - vm.new_string(&text[p..p + search_length]), - JsValue::int(byte_to_unit(text, p) as i32), - text_arg, - ]; - let repl = try_string!(call_replacer(vm, replacer, &cb_args)); - out.push_str(&repl); - last_end = p + search_length; - } - out.push_str(&text[last_end..]); - NativeResult::Ok(vm.new_string_owned(out)) -} - -/// Unicode 标量数(供 pad 族按字符计数;索引/切片族须用 [`utf16_len`])。 -fn char_len(s: &str) -> usize { - s.chars().count() -} - -/// 取串头 count 个 Unicode 标量(供 pad 族填充计数)。 -fn take_chars(s: &str, count: usize) -> String { - s.chars().take(count).collect() -} - -/// UTF-16 code unit 数:astral 字符计 2 单元;ASCII 串与字节数一致直接短路。 -fn utf16_len(s: &str) -> usize { - if s.is_ascii() { - s.len() - } else { - s.encode_utf16().count() - } -} - -/// 字节偏移前缀的 UTF-16 单元数(索引型返回值统一换算入口)。 -/// 调用方须保证 `byte` 落在字符边界(well-formed 匹配恒对齐边界)。 -fn byte_to_unit(s: &str, byte: usize) -> usize { - if s.is_ascii() { - byte - } else { - s[..byte].encode_utf16().count() - } -} - -/// UTF-16 单元位置所在字符的起始字节偏移(字符访问/切片起点定位:位置在 -/// 字符边界时即该边界,落在代理对中间时取所在 astral 字符起始)。 -fn unit_to_char_start(s: &str, unit: usize) -> usize { - if s.is_ascii() { - return unit.min(s.len()); - } - let mut acc = 0usize; - for (byte, ch) in s.char_indices() { - if unit < acc + ch.len_utf16() { - return byte; - } - acc += ch.len_utf16(); - } - s.len() -} - -/// UTF-16 单元位置起最近的字符边界字节偏移(查找窗口起点:位置在边界时 -/// 即该边界,落在代理对中间时取所在字符末尾——well-formed 匹配不可能从 -/// 代理对内部起始)。 -fn unit_to_next_boundary(s: &str, unit: usize) -> usize { - if s.is_ascii() { - return unit.min(s.len()); - } - let mut acc = 0usize; - for (byte, ch) in s.char_indices() { - let next = acc + ch.len_utf16(); - if unit <= next { - return if unit <= acc { byte } else { byte + ch.len_utf8() }; - } - acc = next; - } - s.len() -} - -/// UTF-16 单元位置是否为字符边界(返回该边界的字节偏移);落在代理对 -/// 中间时返回 None(该位置无法对齐任何 well-formed 子串边界)。 -fn unit_to_boundary(s: &str, unit: usize) -> Option { - if s.is_ascii() { - return Some(unit.min(s.len())); - } - let mut acc = 0usize; - for (byte, ch) in s.char_indices() { - if unit == acc { - return Some(byte); - } - acc += ch.len_utf16(); - } - (unit == acc).then_some(s.len()) -} - -/// 按 UTF-16 单元区间取子串:起点取所在字符起始、终点取最近字符边界, -/// 保证结果恒为 well-formed(代理对整体保留)。 -fn unit_slice(s: &str, start: usize, end: usize) -> &str { - if start >= end { - return ""; - } - &s[unit_to_char_start(s, start)..unit_to_next_boundary(s, end)] -} - -fn is_regexp_obj(val: JsValue, vm: &H) -> bool { - if !val.is_object() { - return false; - } - let ptr = val.as_js_object_ptr(); - if ptr.is_null() { - return false; - } - let obj = unsafe { &*ptr }; - let proto = obj.proto(); - if !proto.is_object() { - return false; - } - let proto_ptr = proto.as_js_object_ptr(); - if proto_ptr.is_null() { - return false; - } - let rp = vm.session().builtin_world().regexp_proto.as_ptr() as *mut JsObject; - std::ptr::eq(proto_ptr, rp) -} - -/// `String.prototype.indexOf(searchString, position)`:按 UTF-16 单元查找首次出现位置。 +/// `String.prototype.indexOf(searchString, position)`:按码元查找首次出现位置。 pub fn string_index_of(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.indexOf called with {} args", args.len()); // 参数转换先行:search 缺省为 undefined(经 ToString 得 "undefined" 参与查找), // position 缺省 0;均可能触发对象 ToString/ToNumber(&mut 路径)。 + // 单元序列落地为 owned:借用落地后与后续 &mut 调用无交叠。 let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; - let search = try_string!(as_string(vm, search_val)).into_owned(); + let search: Vec = try_string!(as_units(vm, search_val)).into_owned(); let pos_raw = if args.len() > 2 { vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize } else { 0 }; - // 借用 this 原始字符串零拷贝,扫描期内不再有 &mut 调用。 - let s = try_string!(this_string(vm, args)); - let n = utf16_len(&s); + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let n = s.len(); let pos = pos_raw.min(n); if search.is_empty() { return NativeResult::Ok(JsValue::int(pos as i32)); } - - let start_byte = unit_to_next_boundary(&s, pos); - let haystack = &s[start_byte..]; - if search.len() == 1 { - if let Some(idx) = memchr(search.as_bytes()[0], haystack.as_bytes()) { - let matched_byte = start_byte + idx; - return NativeResult::Ok(JsValue::int(byte_to_unit(&s, matched_byte) as i32)); - } - } else if let Some(idx) = haystack.find(&search) { - let matched_byte = start_byte + idx; - return NativeResult::Ok(JsValue::int(byte_to_unit(&s, matched_byte) as i32)); + if let Some(idx) = find_units(&s, &search, pos) { + return NativeResult::Ok(JsValue::int(idx as i32)); } NativeResult::Ok(JsValue::int(-1)) } @@ -558,29 +786,22 @@ pub fn string_includes(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.includes called with {} args", args.len()); // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; - let search = try_string!(as_string(vm, search_val)).into_owned(); + let search: Vec = try_string!(as_units(vm, search_val)).into_owned(); let pos_raw = if args.len() > 2 { vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize } else { 0 }; - let s = try_string!(this_string(vm, args)); - let n = utf16_len(&s); - let pos = pos_raw.min(n); + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let pos = pos_raw.min(s.len()); if search.is_empty() { return NativeResult::Ok(JsValue::bool(true)); } - - let haystack = &s[unit_to_next_boundary(&s, pos)..]; - if search.len() == 1 { - NativeResult::Ok(JsValue::bool(memchr(search.as_bytes()[0], haystack.as_bytes()).is_some())) - } else { - NativeResult::Ok(JsValue::bool(haystack.contains(&search))) - } + NativeResult::Ok(JsValue::bool(find_units(&s, &search, pos).is_some())) } -/// `String.prototype.charAt(index)`:返回指定 UTF-16 单元位置的单字符;越界返回空串。 +/// `String.prototype.charAt(index)`:返回指定码元位置的 1 单元字符串;越界返回空串。 pub fn string_char_at(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.charAt called with {} args", args.len()); // index 可能触发对象 ToNumber(&mut 路径),先行转换。 @@ -589,63 +810,122 @@ pub fn string_char_at(vm: &mut H, args: &[u8]) -> NativeResult { } else { 0 }; - let s = try_string!(this_string(vm, args)); - if args.len() < 2 { - if s.is_empty() { - return NativeResult::Ok(vm.new_string("")); - } - let first = s.chars().next().unwrap(); - // ASCII 走单字符缓存零分配,其余回落普通创建(借用已随 char 提取结束)。 - return match vm.single_char(first) { - Some(v) => NativeResult::Ok(v), - None => NativeResult::Ok(vm.new_string(&first.to_string())), - }; - } - if idx < 0 || idx as usize >= utf16_len(&s) { + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + if idx < 0 || idx as usize >= s.len() { return NativeResult::Ok(vm.new_string("")); } - // 按 UTF-16 单元定位;落在代理对中间时返回所在 astral 字符(孤立代理 - // 无法在 UTF-8 表示,见架构限制)。 - let byte = unit_to_char_start(&s, idx as usize); - let ch = s[byte..].chars().next().unwrap(); - match vm.single_char(ch) { + let u = s[idx as usize]; + // ASCII 单元走单字符缓存零分配,其余(含孤立 surrogate)回落 1 单元串创建。 + match vm.single_unit(u) { Some(v) => NativeResult::Ok(v), - None => NativeResult::Ok(vm.new_string(&ch.to_string())), + None => NativeResult::Ok(vm.new_string_units(&[u])), } } -/// `String.prototype.charCodeAt(index)`:返回指定 UTF-16 单元位置的 code unit;越界返回 NaN。 +/// `String.prototype.charCodeAt(index)`:返回指定码元位置的 code unit;越界返回 NaN。 pub fn string_char_code_at(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.charCodeAt called with {} args", args.len()); // index 可能触发对象 ToNumber(&mut 路径),先行转换。 let idx = if args.len() >= 2 { vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as i32 } else { - -1 + 0 }; - let s = try_string!(this_string(vm, args)); - if args.len() < 2 { - if s.is_empty() { - return NativeResult::Ok(JsValue::float(f64::NAN)); - } - // 缺省 index=0:返回首单元(astral 首字符为高代理)。 - return NativeResult::Ok(JsValue::int(s.encode_utf16().next().unwrap() as i32)); - } - if idx < 0 || idx as usize >= utf16_len(&s) { + let s = try_string!(this_units(vm, args)); + if idx < 0 || idx as usize >= s.len() { return NativeResult::Ok(JsValue::float(f64::NAN)); } - // 按 UTF-16 单元展开取码元:astral 字符的高/低代理分别精确返回。 - let unit = s.encode_utf16().nth(idx as usize).unwrap(); - NativeResult::Ok(JsValue::int(unit as i32)) + NativeResult::Ok(JsValue::int(s[idx as usize] as i32)) } -/// `String.prototype.concat(...strings)`:拼接 this 与各参数返回新字符串。 +/// `String.prototype.codePointAt(pos)`:按码元位置取 code point(surrogate 对 +/// 合并);越界或孤立代理返回 undefined。 +pub fn string_code_point_at(vm: &mut H, args: &[u8]) -> NativeResult { + builtins_debug!("String.prototype.codePointAt called with {} args", args.len()); + // pos 转换先行(&mut 路径),后取 this 单元序列定位。 + let pos = if args.len() > 1 { + let pos_val = vm.reg(args[1]); + if pos_val.is_symbol() { + return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a number")); + } + match vm.coerce_number_bounded(pos_val) { + Ok(n) => n, + Err(_) => { + // pos 对象 valueOf/toString 抛出的异常须原样传播。 + if let Some(exc) = vm.take_uncaught_value() { + return NativeResult::Err(exc); + } + return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert argument to a number")); + } + } + } else { + 0.0 + }; + let pos = if pos.is_nan() { 0.0 } else { pos.trunc() }; + if pos < 0.0 || pos > u32::MAX as f64 { + return NativeResult::Ok(JsValue::undefined()); + } + let pos = pos as usize; + let s = try_string!(this_units(vm, args)); + if pos >= s.len() { + return NativeResult::Ok(JsValue::undefined()); + } + let first = s[pos]; + if (0xD800..=0xDBFF).contains(&first) && pos + 1 < s.len() { + let second = s[pos + 1]; + if (0xDC00..=0xDFFF).contains(&second) { + let cp = 0x10000 + (((first - 0xD800) as u32) << 10) + (second - 0xDC00) as u32; + return NativeResult::Ok(JsValue::int(cp as i32)); + } + } + NativeResult::Ok(JsValue::int(first as i32)) +} + +/// `String.prototype.lastIndexOf(searchString, position)`:从后往前查找首次 +/// 出现位置(起始位置 ≤ position,position 缺省串长)。 +pub fn string_last_index_of(vm: &mut H, args: &[u8]) -> NativeResult { + builtins_debug!("String.prototype.lastIndexOf called with {} args", args.len()); + // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 + let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; + let search: Vec = try_string!(as_units(vm, search_val)).into_owned(); + let pos_raw = if args.len() > 2 { + let p = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])); + if p.is_nan() { + None + } else { + Some(p as usize) + } + } else { + None + }; + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let n = s.len(); + let pos = match pos_raw { + Some(p) => p.min(n), + None => n, + }; + + if search.is_empty() { + return NativeResult::Ok(JsValue::int(pos as i32)); + } + // 窗口 = 前 pos+1 个码元:起始位置 ≤ pos 的末次出现。 + let window = (pos + 1).min(n); + if let Some(idx) = rfind_units(&s, &search, window) { + return NativeResult::Ok(JsValue::int(idx as i32)); + } + NativeResult::Ok(JsValue::int(-1)) +} + +// ── 切片 / 变换 ───────────────────────────────────────────────────────── + +/// `String.prototype.concat(...strings)`:拼接 this 与各参数返回新字符串 +/// (单元口径,参数经完整 ToString)。 pub fn string_concat(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.concat called with {} args", args.len()); - let mut result = try_string!(this_string(vm, args)).into_owned(); + let mut units = try_string!(this_units(vm, args)).into_owned(); for &arg_reg in args.iter().skip(1) { - match oxide_runtime_api::to_string_full(vm.reg(arg_reg), vm) { - Ok(s) => result.push_str(&s), + match oxide_runtime_api::to_string_value_full(vm.reg(arg_reg), vm) { + Ok(v) => units.extend(vm.string_units(v).as_ref()), Err(_) => { // ToString on an object may throw via toString/valueOf; propagate the original exception. if let Some(exc) = vm.take_uncaught_value() { @@ -655,10 +935,10 @@ pub fn string_concat(vm: &mut H, args: &[u8]) -> NativeResult { } } } - NativeResult::Ok(vm.new_string_owned(result)) + NativeResult::Ok(vm.new_string_units_owned(units)) } -/// `String.prototype.slice(start, end)`:按 UTF-16 单元区间(支持负索引)取子串。 +/// `String.prototype.slice(start, end)`:按码元区间(支持负索引)取子串。 pub fn string_slice(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.slice called with {} args", args.len()); // 位置参数先行(&mut 转换),后借 this 取子串。 @@ -672,8 +952,8 @@ pub fn string_slice(vm: &mut H, args: &[u8]) -> NativeResult { } else { None }; - let s = try_string!(this_string(vm, args)); - let n = utf16_len(&s) as i32; + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let n = s.len() as i32; let start = match start_raw { Some(v) => { if v < 0 { @@ -694,9 +974,14 @@ pub fn string_slice(vm: &mut H, args: &[u8]) -> NativeResult { } None => n, }; - let result = if start < end { unit_slice(&s, start as usize, end as usize) } else { "" }; - let owned = result.to_string(); - NativeResult::Ok(vm.new_string_owned(owned)) + // 码元区间精确切片(孤立 surrogate 按单元保留,不吸附字符边界)。 + let out: Vec = if start < end { + let (a, b) = (start as usize, end as usize); + s[a.min(s.len())..b.min(s.len())].to_vec() + } else { + Vec::new() + }; + NativeResult::Ok(vm.new_string_units_owned(out)) } /// `String.prototype.substring(start, end)`:取子串,start/end 自动对调且取非负。 @@ -705,8 +990,8 @@ pub fn string_substring(vm: &mut H, args: &[u8]) -> NativeResult { // 寄存器取值先行(纯函数),后借 this 取子串。 let start_arg = if args.len() > 1 { Some(vm.reg(args[1])) } else { None }; let end_arg = if args.len() > 2 { Some(vm.reg(args[2])) } else { None }; - let s = try_string!(this_string(vm, args)); - let n = utf16_len(&s) as i32; + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let n = s.len() as i32; let mut start = match start_arg { Some(v) => { let v = oxide_runtime_api::to_integer_or_infinity(v); @@ -732,20 +1017,19 @@ pub fn string_substring(vm: &mut H, args: &[u8]) -> NativeResult { if start > end { std::mem::swap(&mut start, &mut end); } - let result = unit_slice(&s, start as usize, end as usize); - let owned = result.to_string(); - NativeResult::Ok(vm.new_string_owned(owned)) + let out: Vec = s[start as usize..end as usize].to_vec(); + NativeResult::Ok(vm.new_string_units_owned(out)) } -/// `String.prototype.substr(start, length)`:从 start 起取 length 个 UTF-16 单元 +/// `String.prototype.substr(start, length)`:从 start 起取 length 个码元 /// (Annex B,支持负 start)。 pub fn string_substr(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.substr called with {} args", args.len()); // 寄存器取值先行(纯函数),后借 this 截取。 let start_arg = if args.len() > 1 { Some(vm.reg(args[1])) } else { None }; let length_arg = if args.len() > 2 { Some(vm.reg(args[2])) } else { None }; - let s = try_string!(this_string(vm, args)); - let len = utf16_len(&s) as isize; + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let len = s.len() as isize; let start = match start_arg { Some(v) => { let n = oxide_runtime_api::to_integer_or_infinity(v) as isize; @@ -761,15 +1045,13 @@ pub fn string_substr(vm: &mut H, args: &[u8]) -> NativeResult { Some(v) => (oxide_runtime_api::to_integer_or_infinity(v) as isize).max(0) as usize, None => len as usize - start, }; - // 起点按 UTF-16 单元定位取窗口(起点落在代理对中间时取所在 astral 字符, - // 孤立代理无法在 UTF-8 表示,见架构限制)。 let count = length.min(len as usize - start); - let result = unit_slice(&s, start, start + count); - let owned = result.to_string(); - NativeResult::Ok(vm.new_string_owned(owned)) + let out: Vec = s[start..start + count].to_vec(); + NativeResult::Ok(vm.new_string_units_owned(out)) } -/// `String.prototype.at(index)`:按 UTF-16 单元索引取字符(支持负索引);越界返回 undefined。 +/// `String.prototype.at(index)`:按码元索引取 1 单元字符串(支持负索引); +/// 越界返回 undefined。 pub fn string_at(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.at called with {} args", args.len()); let idx = if args.len() > 1 { @@ -777,80 +1059,73 @@ pub fn string_at(vm: &mut H, args: &[u8]) -> NativeResult { } else { 0 }; - let s = try_string!(this_string(vm, args)); - let len = utf16_len(&s) as i32; + let s = try_string!(this_units(vm, args)); + let len = s.len() as i32; let idx = if idx < 0 { len + idx } else { idx }; if idx < 0 || idx >= len { return NativeResult::Ok(JsValue::undefined()); } - // 按 UTF-16 单元定位;落在代理对中间时返回所在 astral 字符(架构限制)。 - let byte = unit_to_char_start(&s, idx as usize); - let ch = s[byte..].chars().next().unwrap().to_string(); - NativeResult::Ok(vm.new_string(&ch)) + let u = s[idx as usize]; + match vm.single_unit(u) { + Some(v) => NativeResult::Ok(v), + None => NativeResult::Ok(vm.new_string_units(&[u])), + } } -/// `String.prototype.lastIndexOf(searchString, position)`:从后往前查找首次出现位置。 -pub fn string_last_index_of(vm: &mut H, args: &[u8]) -> NativeResult { - builtins_debug!("String.prototype.lastIndexOf called with {} args", args.len()); - // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 - let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; - let search = try_string!(as_string(vm, search_val)).into_owned(); - let pos_raw = if args.len() > 2 { - let p = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])); - if p.is_nan() { - None - } else { - Some(p as usize) - } - } else { - None - }; - let s = try_string!(this_string(vm, args)); - let n = utf16_len(&s); - let pos = match pos_raw { - Some(p) => p.min(n), - None => n, - }; - - if search.is_empty() { - return NativeResult::Ok(JsValue::int(pos as i32)); - } - - let end_byte = unit_to_next_boundary(&s, (pos + 1).min(n)); - let haystack = &s[..end_byte]; - - if search.len() == 1 { - if let Some(idx) = memchr::memrchr(search.as_bytes()[0], haystack.as_bytes()) { - return NativeResult::Ok(JsValue::int(byte_to_unit(&s, idx) as i32)); - } - } else if let Some(idx) = haystack.rfind(&search) { - return NativeResult::Ok(JsValue::int(byte_to_unit(&s, idx) as i32)); - } - NativeResult::Ok(JsValue::int(-1)) -} - -/// `String.prototype.toUpperCase`:全大写转换。 +/// `String.prototype.toUpperCase`:全大写转换(良形段映射,孤立 surrogate +/// 单元原样保留)。 pub fn string_to_upper_case(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.toUpperCase called with {} args", args.len()); - let s = try_string!(this_string(vm, args)); - let upper = s.to_uppercase(); - NativeResult::Ok(vm.new_string_owned(upper)) + let s = try_string!(this_units(vm, args)); + let out = map_well_formed_segments(&s, |seg| seg.to_uppercase()); + NativeResult::Ok(vm.new_string_units_owned(out)) } -/// `String.prototype.toLowerCase`:全小写转换。 +/// `String.prototype.toLowerCase`:全小写转换(良形段映射,孤立 surrogate +/// 单元原样保留)。 pub fn string_to_lower_case(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.toLowerCase called with {} args", args.len()); - let s = try_string!(this_string(vm, args)); - let lower = s.to_lowercase(); - NativeResult::Ok(vm.new_string_owned(lower)) + let s = try_string!(this_units(vm, args)); + let out = map_well_formed_segments(&s, |seg| seg.to_lowercase()); + NativeResult::Ok(vm.new_string_units_owned(out)) } -/// `String.prototype.trim`:去除两端空白。 +/// `String.prototype.trim`:去除两端白空格(单元口径)。 pub fn string_trim(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.trim called with {} args", args.len()); - let s = try_string!(this_string(vm, args)); - let trimmed = s.trim().to_string(); - NativeResult::Ok(vm.new_string_owned(trimmed)) + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let n = s.len(); + let mut start = 0; + while start < n && is_trim_unit(s[start]) { + start += 1; + } + let mut end = n; + while end > start && is_trim_unit(s[end - 1]) { + end -= 1; + } + NativeResult::Ok(vm.new_string_units_owned(s[start..end].to_vec())) +} + +/// `String.prototype.trimStart`:去除头部白空格。 +pub fn string_trim_start(vm: &mut H, args: &[u8]) -> NativeResult { + builtins_debug!("String.prototype.trimStart called with {} args", args.len()); + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let mut start = 0; + while start < s.len() && is_trim_unit(s[start]) { + start += 1; + } + NativeResult::Ok(vm.new_string_units_owned(s[start..].to_vec())) +} + +/// `String.prototype.trimEnd`:去除尾部白空格。 +pub fn string_trim_end(vm: &mut H, args: &[u8]) -> NativeResult { + builtins_debug!("String.prototype.trimEnd called with {} args", args.len()); + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let mut end = s.len(); + while end > 0 && is_trim_unit(s[end - 1]) { + end -= 1; + } + NativeResult::Ok(vm.new_string_units_owned(s[..end].to_vec())) } /// `String.prototype.repeat(count)`:重复字符串 count 次(当前上限 10000 防滥用)。 @@ -862,12 +1137,16 @@ pub fn string_repeat(vm: &mut H, args: &[u8]) -> NativeResult { } else { 1 }; - let s = try_string!(this_string(vm, args)); - let repeated = s.repeat(n); - NativeResult::Ok(vm.new_string_owned(repeated)) + let s = try_string!(this_units(vm, args)); + let mut out = Vec::with_capacity(s.len() * n); + for _ in 0..n { + out.extend_from_slice(&s); + } + NativeResult::Ok(vm.new_string_units_owned(out)) } -/// `String.prototype.padStart(targetLength, padString)`:在头部补足 padString 到目标长度。 +/// `String.prototype.padStart(targetLength, padString)`:在头部补足 padString +/// 到目标码点数。 pub fn string_pad_start(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.padStart called with {} args", args.len()); // 参数转换先行(&mut 路径):targetLength 与 padString 均可能触发对象转换。 @@ -876,31 +1155,37 @@ pub fn string_pad_start(vm: &mut H, args: &[u8]) -> NativeResult { } else { None }; - let pad = if args.len() > 2 { - try_string!(as_string(vm, vm.reg(args[2]))).into_owned() + let pad: Vec = if args.len() > 2 { + try_string!(as_units(vm, vm.reg(args[2]))).into_owned() } else { - " ".to_string() + vec![0x20] }; - let s = try_string!(this_string(vm, args)); - let s_len = char_len(&s); + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let s_len = code_point_count(&s); let target = target_arg.unwrap_or(s_len); if target > 10000 { builtins_error!("String.prototype.padStart: invalid receiver"); return NativeResult::Err(crate::error::create_range_error(vm, "Invalid string length")); } if s_len >= target || pad.is_empty() { - let owned = s.into_owned(); - return NativeResult::Ok(vm.new_string_owned(owned)); + return NativeResult::Ok(vm.new_string_units_owned(s.to_vec())); } let needed = target - s_len; - let pad_len = char_len(&pad).max(1); + let pad_len = code_point_count(&pad).max(1); let reps = needed.div_ceil(pad_len); - let mut out = take_chars(&pad.repeat(reps), needed); - out.push_str(&s); - NativeResult::Ok(vm.new_string_owned(out)) + let mut pad_rep = Vec::with_capacity(pad.len() * reps); + for _ in 0..reps { + pad_rep.extend_from_slice(&pad); + } + let prefix = take_code_points(&pad_rep, needed); + let mut out = Vec::with_capacity(prefix.len() + s.len()); + out.extend_from_slice(prefix); + out.extend_from_slice(&s); + NativeResult::Ok(vm.new_string_units_owned(out)) } -/// `String.prototype.padEnd(targetLength, padString)`:在尾部补足 padString 到目标长度。 +/// `String.prototype.padEnd(targetLength, padString)`:在尾部补足 padString +/// 到目标码点数。 pub fn string_pad_end(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.padEnd called with {} args", args.len()); // 参数转换先行(&mut 路径):targetLength 与 padString 均可能触发对象转换。 @@ -909,76 +1194,76 @@ pub fn string_pad_end(vm: &mut H, args: &[u8]) -> NativeResult { } else { None }; - let pad = if args.len() > 2 { - try_string!(as_string(vm, vm.reg(args[2]))).into_owned() + let pad: Vec = if args.len() > 2 { + try_string!(as_units(vm, vm.reg(args[2]))).into_owned() } else { - " ".to_string() + vec![0x20] }; - let s = try_string!(this_string(vm, args)); - let s_len = char_len(&s); + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let s_len = code_point_count(&s); let target = target_arg.unwrap_or(s_len); if target > 10000 { builtins_error!("String.prototype.padEnd: invalid receiver"); return NativeResult::Err(crate::error::create_range_error(vm, "Invalid string length")); } if s_len >= target || pad.is_empty() { - let owned = s.into_owned(); - return NativeResult::Ok(vm.new_string_owned(owned)); + return NativeResult::Ok(vm.new_string_units_owned(s.to_vec())); } let needed = target - s_len; - let pad_len = char_len(&pad).max(1); + let pad_len = code_point_count(&pad).max(1); let reps = needed.div_ceil(pad_len); - let mut out = s.into_owned(); - out.push_str(&take_chars(&pad.repeat(reps), needed)); - NativeResult::Ok(vm.new_string_owned(out)) + let mut pad_rep = Vec::with_capacity(pad.len() * reps); + for _ in 0..reps { + pad_rep.extend_from_slice(&pad); + } + let suffix = take_code_points(&pad_rep, needed); + let mut out = Vec::with_capacity(s.len() + suffix.len()); + out.extend_from_slice(&s); + out.extend_from_slice(suffix); + NativeResult::Ok(vm.new_string_units_owned(out)) } -/// `String.prototype.startsWith(searchString, position)`:是否以指定子串开头。 +/// `String.prototype.startsWith(searchString, position)`:是否以指定子串开头 +/// (position 起的前缀码元比较,不做字符边界吸附——规格口径)。 pub fn string_starts_with(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.startsWith called with {} args", args.len()); // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; - let search = try_string!(as_string(vm, search_val)).into_owned(); + let search: Vec = try_string!(as_units(vm, search_val)).into_owned(); let pos_raw = if args.len() > 2 { vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize } else { 0 }; - let s = try_string!(this_string(vm, args)); - let n = utf16_len(&s); - let pos = pos_raw.min(n); - // 位置须对齐字符边界才有 well-formed 前缀可比;落在代理对中间时仅空串可匹配。 - let result = match unit_to_boundary(&s, pos) { - Some(byte) => s[byte..].starts_with(&search), - None => search.is_empty(), - }; + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let pos = pos_raw.min(s.len()); + let result = if search.is_empty() { true } else { s[pos..].starts_with(&search) }; NativeResult::Ok(JsValue::bool(result)) } -/// `String.prototype.endsWith(searchString, endPosition)`:是否以指定子串结尾。 +/// `String.prototype.endsWith(searchString, endPosition)`:是否以指定子串结尾 +/// (截断至 endPosition 的后缀码元比较,不做字符边界吸附——规格口径)。 pub fn string_ends_with(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.endsWith called with {} args", args.len()); // 参数转换先行(&mut 路径):search 缺省为 undefined(经 ToString 得 "undefined")。 let search_val = if args.len() >= 2 { vm.reg(args[1]) } else { JsValue::undefined() }; - let search = try_string!(as_string(vm, search_val)).into_owned(); + let search: Vec = try_string!(as_units(vm, search_val)).into_owned(); let end_pos_raw = if args.len() > 2 { vm.coerce_number_bounded(vm.reg(args[2])).unwrap_or(f64::NAN) as usize } else { usize::MAX }; - let s = try_string!(this_string(vm, args)); - let n = utf16_len(&s); - let end_pos = end_pos_raw.min(n); - // 截断位置须对齐字符边界才有 well-formed 后缀可比;落在代理对中间时仅空串可匹配。 - let result = match unit_to_boundary(&s, end_pos) { - Some(byte) => s[..byte].ends_with(&search), - None => search.is_empty(), - }; + let s: Vec = try_string!(this_units(vm, args)).into_owned(); + let end_pos = end_pos_raw.min(s.len()); + let result = if search.is_empty() { true } else { s[..end_pos].ends_with(&search) }; NativeResult::Ok(JsValue::bool(result)) } +// ── 拆分 / 正则匹配 ───────────────────────────────────────────────────── + /// `String.prototype.split(separator, limit)`:按分隔符拆分为字符串数组; -/// 分隔符可为 RegExp(含捕获组)或字符串。 +/// 分隔符可为 RegExp(含捕获组)或字符串。空分隔按单元逐个产出(规格口径, +/// 孤立 surrogate 为 1 单元元素)。 pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.split called with {} args", args.len()); // 参数转换先行:separator 为对象时 ToString 可能触发用户代码(&mut 路径), @@ -998,11 +1283,6 @@ pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { let re = unsafe { &*re_ptr }; re.native_fn().is_some() }; - let sep = if is_undefined_sep || has_native_re { - String::new() - } else { - try_string!(as_string(vm, sep_val.unwrap())).into_owned() - }; // ToUint32(limit),缺省为 2^32-1。 let limit = if args.len() > 2 { let l = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2])); @@ -1014,25 +1294,29 @@ pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { } else { u32::MAX as usize }; - let s = try_string!(this_string(vm, args)); + let sep_units: Vec = if is_undefined_sep || has_native_re { + Vec::new() + } else { + try_string!(as_units(vm, sep_val.unwrap())).into_owned() + }; + let s: Vec = try_string!(this_units(vm, args)).into_owned(); // 规范:separator 为 undefined 时返回 [this]。 if is_undefined_sep { - let owned = s.into_owned(); - return NativeResult::Ok(make_string_array(vm, vec![owned])); + return NativeResult::Ok(make_units_array(vm, vec![s.to_vec()])); } if is_re { let re_ptr = sep_val.unwrap().as_js_object_ptr(); let re = unsafe { &*re_ptr }; if let Some(fn_ptr) = re.native_fn() { let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - let mut parts: Vec = Vec::new(); + let mut parts: Vec> = Vec::new(); let mut last_end = 0; - for m in regex.find_iter(&s) { + for m in regex.find_from_utf16(&s, 0) { if parts.len() >= limit { break; } let range = m.range(); - parts.push(s[last_end..range.start].to_string()); + parts.push(s[last_end..range.start].to_vec()); if parts.len() >= limit { break; } @@ -1041,44 +1325,57 @@ pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { break; } match m.group(i) { - Some(g) => parts.push(s[g.start..g.end].to_string()), - None => parts.push(String::new()), + Some(g) => parts.push(s[g.start..g.end].to_vec()), + None => parts.push(Vec::new()), } } last_end = range.end; } if last_end <= s.len() && parts.len() < limit { - parts.push(s[last_end..].to_string()); + parts.push(s[last_end..].to_vec()); } - return NativeResult::Ok(make_string_array(vm, parts)); + return NativeResult::Ok(make_units_array(vm, parts)); } // 无原生正则的类正则对象回退到字符串路径。 } - if sep.is_empty() { - // 两段式:先借 this 收集字符(Copy),借用结束后再逐字符产出—— - // ASCII 走单字符缓存零分配,其余回落普通创建。 - let chars: Vec = s.chars().take(limit).collect(); - let mut values = Vec::with_capacity(chars.len()); - for c in chars { - values.push(match vm.single_char(c) { + if sep_units.is_empty() { + // 每单元一个元素(ASCII 走单字符缓存零分配,其余 1 单元串创建)。 + let mut values = Vec::with_capacity(s.len().min(limit)); + for &u in s.iter().take(limit) { + values.push(match vm.single_unit(u) { Some(v) => v, - None => vm.new_string(&c.to_string()), + None => vm.new_string_units(&[u]), }); } return NativeResult::Ok(make_string_array_values(vm, values)); } - let parts: Vec = s.split(&sep).map(|p| p.to_string()).take(limit).collect(); - NativeResult::Ok(make_string_array(vm, parts)) + // 非空字符串分隔:手动查找循环 + limit。 + let mut parts: Vec> = Vec::new(); + let mut start = 0; + loop { + if parts.len() >= limit { + break; + } + match find_units(&s, &sep_units, start) { + Some(p) => { + parts.push(s[start..p].to_vec()); + start = p + sep_units.len(); + } + None => { + parts.push(s[start..].to_vec()); + break; + } + } + } + NativeResult::Ok(make_units_array(vm, parts)) } -/// `replace`/`replaceAll` 共用的分叉实现:按 replacer 类型与参数形态选择路径, -/// 全原始字符串场景零拷贝(共享借用三提取),函数 replacer 场景 receiver 走 -/// owned(回调跨 `&mut` 的硬约束)。 +/// `replace`/`replaceAll` 共用的分叉实现:按 replacer 类型与参数形态选择路径。 /// /// # 步骤 /// 1. receiver 前置校验(RequireObjectCoercible,纯 is_* 读取)。 /// 2. 纯对象头读取判定分叉:正则身份/编译正则/global 标志/函数 replacer。 -/// 3. 按分支执行替换。 +/// 3. 按分支执行替换(单元口径;良形内容的结果与旧文本路径逐位一致)。 /// /// # 边界与前提 /// - 缺省 searchValue/replaceValue 均按 ToString(undefined)="undefined" 处理 @@ -1087,12 +1384,6 @@ pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { /// - replaceAll 遇非 global 正则抛 TypeError;类正则对象(proto 恒等 /// RegExp.prototype 但无编译正则)replace/replaceAll 统一按 ToString 文本走 /// 字符串路径。 -/// -/// # 注意事项 -/// - 分支 B 的 `&H` 共享借用与 `&mut` 互斥由编译器强制,三 `&str` 同源可共存 -/// (E0499 仅发生在 `&mut` 派生 Cow 并存)。 -/// - 函数 replacer 分支 receiver 必须 owned:`call_function_sync` 是 `&mut`, -/// 文本若借自 vm 会 E0499。 fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeResult { let this_val = vm.reg(args[0]); @@ -1139,7 +1430,7 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR }; // replaceAll 要求正则带 global:非 global 正则直接抛 TypeError(规范 flags 检查)。 - // 类正则对象(无编译正则)无 flags 概念,统一走 as_string 文本路径,不在此检查。 + // 类正则对象(无编译正则)无 flags 概念,统一走 as_units 文本路径,不在此检查。 if all && has_native_re && !is_global { return NativeResult::Err(crate::error::create_type_error( vm, @@ -1147,13 +1438,25 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR )); } - // 分支 A:函数 replacer。回调经 call_function_sync(&mut),文本若借自 vm - // 会 E0499,receiver 必须 owned 本地 String(跨回调的硬约束)。 + // 分支 A:函数 replacer。回调经 call_function_sync(&mut),receiver 单元 + // 序列先落地为 owned(跨回调的硬约束)。 if let Some(replacer_val) = replacer_val { - let s = try_string!(this_string(vm, args)).into_owned(); - // 回调第 4 参(原字符串)预构一次:原始字符串 receiver 直接复用 - // this_val 零拷贝,对象 receiver 提升为单次会话串(原每匹配整串复制)。 - let text_arg = if this_val.is_string() { this_val } else { vm.new_string(&s) }; + let s = try_string!(this_units(vm, args)).into_owned(); + // 回调第 4 参(原字符串)预构一次:原始字符串 receiver 直接复用 this_val + // 零拷贝,对象 receiver 提升为单次转换值(原每匹配整串复制)。 + let text_arg = match this_val.is_string() { + true => this_val, + false => match oxide_runtime_api::to_string_value_full(this_val, vm) { + Ok(v) => v, + Err(_) => { + // ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播。 + 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")); + } + }, + }; if has_native_re { let re_ptr = pattern_val.as_js_object_ptr(); let re = unsafe { &*re_ptr }; @@ -1162,25 +1465,29 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; return regex_replace_fn(vm, regex, &s, replacer_val, is_global, text_arg); } - let pattern = try_string!(as_string(vm, pattern_val)).into_owned(); + let pattern = try_string!(as_units(vm, pattern_val)).into_owned(); return string_replace_fn(vm, &s, &pattern, replacer_val, all, text_arg); } let replacement_is_string = args.len() <= 2 || replacement_val.is_string(); - // 分支 B:全原始字符串快路径——共享借用三提取,receiver/pattern/replacement - // 零拷贝(`&H` 共享借用派生多个 `&str` 可共存,E0499 只发生在 `&mut` 派生)。 + // 分支 B:全原始字符串快路径——三值按载荷形态共享借用(`&H` 共享借用 + // 派生多个单元借用可共存,E0499 只发生在 `&mut` 派生)。 if this_val.is_string() && pattern_val.is_string() && replacement_is_string { let h = &*vm; - let s = h.string_ref(this_val); - let p = h.string_ref(pattern_val); - let r: &str = if replacement_val.is_string() { - h.string_ref(replacement_val) + let s = h.string_units(this_val); + let p = h.string_units(pattern_val); + let r: Cow<'_, [u16]> = if replacement_val.is_string() { + h.string_units(replacement_val) } else { - "undefined" + Cow::Owned("undefined".encode_utf16().collect()) }; - let result = if all { s.replace(p, r) } else { s.replacen(p, r, 1) }; - return NativeResult::Ok(vm.new_string_owned(result)); + let result = if all { + replace_units_all(&s, &p, &r) + } else { + replace_units_first(&s, &p, &r) + }; + return NativeResult::Ok(vm.new_string_units_owned(result)); } // 分支 B2:正则 pattern 快路径——regex 为对象内裸指针(不占 vm 借用), @@ -1192,50 +1499,30 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; let h = &*vm; - let s = h.string_ref(this_val); - let r: &str = if replacement_val.is_string() { - h.string_ref(replacement_val) + let s = h.string_units(this_val); + let r: Cow<'_, [u16]> = if replacement_val.is_string() { + h.string_units(replacement_val) } else { - "undefined" + Cow::Owned("undefined".encode_utf16().collect()) }; - let result = regex_replace_manual(regex, s, r, is_global); - return NativeResult::Ok(vm.new_string_owned(result)); + let result = regex_replace_manual_units(regex, &s, &r, is_global); + return NativeResult::Ok(vm.new_string_units_owned(result)); } - // 分支 C:一般路径(对象参与转换)。 - // 原始字符串 receiver:参数转换前置(&mut 路径)再借 receiver 零拷贝扫描; - // 对象 receiver:先 ToString 得 owned(into_owned 即结束借用),转换顺序与 - // 改造前一致。 - let (s, pattern, replacement) = if this_val.is_string() { - let pattern = if has_native_re { - String::new() - } else if args.len() < 2 { - "undefined".to_string() - } else { - try_string!(as_string(vm, pattern_val)).into_owned() - }; - let replacement = if args.len() > 2 { - try_string!(as_string(vm, replacement_val)).into_owned() - } else { - "undefined".to_string() - }; - let s = try_string!(this_string(vm, args)); - (s, pattern, replacement) + // 分支 C:一般路径(对象参与转换)。原始字符串 receiver 与对象 receiver + // 统一:参数转换前置(&mut 路径)后按单元序列扫描。 + let s = try_string!(this_units(vm, args)).into_owned(); + let pattern = if has_native_re { + Vec::new() + } else if args.len() < 2 { + "undefined".encode_utf16().collect() } else { - let s = try_string!(this_string(vm, args)).into_owned(); - let pattern = if has_native_re { - String::new() - } else if args.len() < 2 { - "undefined".to_string() - } else { - try_string!(as_string(vm, pattern_val)).into_owned() - }; - let replacement = if args.len() > 2 { - try_string!(as_string(vm, replacement_val)).into_owned() - } else { - "undefined".to_string() - }; - (Cow::Owned(s), pattern, replacement) + try_string!(as_units(vm, pattern_val)).into_owned() + }; + let replacement = if args.len() > 2 { + try_string!(as_units(vm, replacement_val)).into_owned() + } else { + "undefined".encode_utf16().collect() }; // 统一扫描:命中编译正则走手动 $ 展开,否则字符串子串替换。 @@ -1245,15 +1532,15 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR let fn_ptr = re.native_fn().unwrap(); // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - let result = regex_replace_manual(regex, &s, &replacement, is_global); - return NativeResult::Ok(vm.new_string_owned(result)); + let result = regex_replace_manual_units(regex, &s, &replacement, is_global); + return NativeResult::Ok(vm.new_string_units_owned(result)); } let result = if all { - s.replace(&pattern, &replacement) + replace_units_all(&s, &pattern, &replacement) } else { - s.replacen(&pattern, &replacement, 1) + replace_units_first(&s, &pattern, &replacement) }; - NativeResult::Ok(vm.new_string_owned(result)) + NativeResult::Ok(vm.new_string_units_owned(result)) } /// `String.prototype.replace(pattern, replacement)`:替换首个匹配; @@ -1270,11 +1557,11 @@ pub fn string_match_fn(vm: &mut H, args: &[u8]) -> NativeResult { // 正则判定与参数字符串转换先行(&mut 路径),后借 this 匹配扫描。 let pattern_val = if args.len() >= 2 { Some(vm.reg(args[1])) } else { None }; let is_re = pattern_val.map(|v| is_regexp_obj(v, vm)).unwrap_or(false); - let pattern = match pattern_val { - Some(v) if !is_re => try_string!(as_string(vm, v)).into_owned(), - _ => String::new(), + let pattern: Vec = match pattern_val { + Some(v) if !is_re => try_string!(as_units(vm, v)).into_owned(), + _ => Vec::new(), }; - let s = try_string!(this_string(vm, args)); + let text = try_string!(this_text(vm, args)); if args.len() < 2 { return NativeResult::Ok(JsValue::null()); } @@ -1290,31 +1577,43 @@ pub fn string_match_fn(vm: &mut H, args: &[u8]) -> NativeResult { let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false); if is_global { - let matches: Vec = regex.find_iter(&s).map(|m| s[m.range()].to_string()).collect(); - return NativeResult::Ok(make_string_array(vm, matches)); + let mut matches: Vec> = Vec::new(); + text.for_each_match(regex, |m| { + let range = m.range(); + matches.push(text.slice(range.start, range.end).into_owned()); + }); + if matches.is_empty() { + return NativeResult::Ok(JsValue::null()); + } + return NativeResult::Ok(make_units_array(vm, matches)); } - if let Some(m) = regex.find(&s) { + if let Some(m) = text.find_from_units(regex, 0) { let range = m.range(); - let mut parts: Vec = Vec::with_capacity(m.captures.len() + 1); - parts.push(s[range.start..range.end].to_string()); + let mut parts: Vec> = Vec::with_capacity(m.captures.len() + 1); + parts.push(text.slice(range.start, range.end).into_owned()); for i in 1..=m.captures.len() { match m.group(i) { - Some(g) => parts.push(s[g.start..g.end].to_string()), - None => parts.push(String::new()), + Some(g) => parts.push(text.slice(g.start, g.end).into_owned()), + None => parts.push(Vec::new()), } } - return NativeResult::Ok(make_string_array(vm, parts)); + return NativeResult::Ok(make_units_array(vm, parts)); } return NativeResult::Ok(JsValue::null()); } - if let Some(pos) = s.find(&pattern) { - let matched = s[pos..pos + pattern.len()].to_string(); - return NativeResult::Ok(make_string_array(vm, vec![matched])); + // 空模式命中串头:产出单元素空串数组(规格口径)。 + if pattern.is_empty() { + return NativeResult::Ok(make_units_array(vm, vec![Vec::new()])); + } + let s = text.units(); + if let Some(pos) = find_units(&s, &pattern, 0) { + let matched = s[pos..pos + pattern.len()].to_vec(); + return NativeResult::Ok(make_units_array(vm, vec![matched])); } NativeResult::Ok(JsValue::null()) } -/// `String.prototype.search(pattern)`:返回首个匹配位置(UTF-16 单元),无匹配返回 -1。 +/// `String.prototype.search(pattern)`:返回首个匹配位置(码元),无匹配返回 -1。 pub fn string_search(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.search called with {} args", args.len()); // 参数读取守卫先行:无参调用缺省 searchString 为 undefined(args 仅含 this 槽)。 @@ -1322,12 +1621,12 @@ pub fn string_search(vm: &mut H, args: &[u8]) -> NativeResult { // 正则判定与参数转换先行(&mut 路径),后借 this 扫描。 let is_re = args.len() >= 2 && is_regexp_obj(pattern_val, vm); - let pattern = if args.len() >= 2 && !is_re { - try_string!(as_string(vm, pattern_val)).into_owned() + let pattern: Vec = if args.len() >= 2 && !is_re { + try_string!(as_units(vm, pattern_val)).into_owned() } else { - String::new() + Vec::new() }; - let s = try_string!(this_string(vm, args)); + let text = try_string!(this_text(vm, args)); if args.len() < 2 { return NativeResult::Ok(JsValue::int(-1)); } @@ -1340,94 +1639,40 @@ pub fn string_search(vm: &mut H, args: &[u8]) -> NativeResult { }; // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - if let Some(m) = regex.find(&s) { - return NativeResult::Ok(JsValue::int(byte_to_unit(&s, m.range().start) as i32)); + if let Some(m) = text.find_from_units(regex, 0) { + return NativeResult::Ok(JsValue::int(text.unit_pos(m.range().start) as i32)); } return NativeResult::Ok(JsValue::int(-1)); } - if let Some(pos) = s.find(&pattern) { - return NativeResult::Ok(JsValue::int(byte_to_unit(&s, pos) as i32)); + // 空模式命中串头(规格口径)。 + if pattern.is_empty() { + return NativeResult::Ok(JsValue::int(0)); + } + let s = text.units(); + if let Some(pos) = find_units(&s, &pattern, 0) { + return NativeResult::Ok(JsValue::int(pos as i32)); } NativeResult::Ok(JsValue::int(-1)) } -/// `String.prototype.trimStart`:去除头部空白。 -pub fn string_trim_start(vm: &mut H, args: &[u8]) -> NativeResult { - builtins_debug!("String.prototype.trimStart called with {} args", args.len()); - let s = try_string!(this_string(vm, args)); - let trimmed = s.trim_start().to_string(); - NativeResult::Ok(vm.new_string_owned(trimmed)) -} - -/// `String.prototype.trimEnd`:去除尾部空白。 -pub fn string_trim_end(vm: &mut H, args: &[u8]) -> NativeResult { - builtins_debug!("String.prototype.trimEnd called with {} args", args.len()); - let s = try_string!(this_string(vm, args)); - let trimmed = s.trim_end().to_string(); - NativeResult::Ok(vm.new_string_owned(trimmed)) -} - -/// `String.prototype.codePointAt(pos)`:按 UTF-16 code unit 位置取 code point -/// (surrogate pair 合并);越界或孤立代理返回 undefined。 -pub fn string_code_point_at(vm: &mut H, args: &[u8]) -> NativeResult { - builtins_debug!("String.prototype.codePointAt called with {} args", args.len()); - // pos 转换先行(&mut 路径),后借 this 按 code unit 定位。 - let pos = if args.len() > 1 { - let pos_val = vm.reg(args[1]); - if pos_val.is_symbol() { - return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a number")); - } - match vm.coerce_number_bounded(pos_val) { - Ok(n) => n, - Err(_) => { - // pos 对象 valueOf/toString 抛出的异常须原样传播。 - if let Some(exc) = vm.take_uncaught_value() { - return NativeResult::Err(exc); - } - return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert argument to a number")); - } - } - } else { - 0.0 - }; - let pos = if pos.is_nan() { 0.0 } else { pos.trunc() }; - if pos < 0.0 || pos > u32::MAX as f64 { - return NativeResult::Ok(JsValue::undefined()); - } - let pos = pos as usize; - let s = try_string!(this_string(vm, args)); - // JS 规范索引是 UTF-16 code unit 位置,须按代理对展开定位(astral 字符占两单元)。 - let units: Vec = s.encode_utf16().collect(); - if pos >= units.len() { - return NativeResult::Ok(JsValue::undefined()); - } - let first = units[pos]; - if (0xD800..=0xDBFF).contains(&first) && pos + 1 < units.len() { - let second = units[pos + 1]; - if (0xDC00..=0xDFFF).contains(&second) { - let cp = 0x10000 + (((first - 0xD800) as u32) << 10) + (second - 0xDC00) as u32; - return NativeResult::Ok(JsValue::int(cp as i32)); - } - } - NativeResult::Ok(JsValue::int(first as i32)) -} - -/// `String.prototype.isWellFormed()`:字符串无孤立 surrogate(每个 UTF-16 -/// 码元要么是合法字符,要么与相邻码元成代理对)时返回 true。 +/// `String.prototype.isWellFormed()`:字符串无孤立 surrogate(每个码元要么是 +/// 合法字符,要么与相邻码元成代理对)时返回 true。 pub fn string_is_well_formed(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.isWellFormed called with {} args", args.len()); - let s = try_string!(this_string(vm, args)); - let mut iter = s.encode_utf16().peekable(); - while let Some(&first) = iter.peek() { - if (0xDC00..=0xDFFF).contains(&first) { + let s = try_string!(this_units(vm, args)); + let mut i = 0; + while i < s.len() { + let u = s[i]; + if (0xDC00..=0xDFFF).contains(&u) { return NativeResult::Ok(JsValue::bool(false)); } - if (0xD800..=0xDBFF).contains(&first) { - if !matches!(iter.nth(1), Some(second) if (0xDC00..=0xDFFF).contains(&second)) { + if (0xD800..=0xDBFF).contains(&u) { + if !(i + 1 < s.len() && (0xDC00..=0xDFFF).contains(&s[i + 1])) { return NativeResult::Ok(JsValue::bool(false)); } + i += 2; } else { - iter.next(); + i += 1; } } NativeResult::Ok(JsValue::bool(true)) @@ -1436,37 +1681,49 @@ pub fn string_is_well_formed(vm: &mut H, args: &[u8]) -> NativeResult /// `String.prototype.toWellFormed()`:把孤立 surrogate 替换为 U+FFFD 返回新字符串。 pub fn string_to_well_formed(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.toWellFormed called with {} args", args.len()); - let s = try_string!(this_string(vm, args)); - // 引擎字符串为合法 UTF-8,Rust char 不可能落在 surrogate 区间;遍历保留 - // 通用来正确编码未来可能出现的替换路径。 - let mut out = String::with_capacity(s.len()); - for c in s.chars() { - if (0xD800..=0xDFFF).contains(&(c as u32)) { - out.push('\u{FFFD}'); + let s = try_string!(this_units(vm, args)); + let mut out = Vec::with_capacity(s.len()); + let mut i = 0; + while i < s.len() { + let u = s[i]; + if (0xD800..=0xDBFF).contains(&u) { + if i + 1 < s.len() && (0xDC00..=0xDFFF).contains(&s[i + 1]) { + out.push(u); + out.push(s[i + 1]); + i += 2; + continue; + } + out.push(0xFFFD); + } else if (0xDC00..=0xDFFF).contains(&u) { + out.push(0xFFFD); } else { - out.push(c); + out.push(u); } + i += 1; } - NativeResult::Ok(vm.new_string_owned(out)) + NativeResult::Ok(vm.new_string_units_owned(out)) } -/// `String.prototype.normalize(form)`:按 NFC/NFD/NFKC/NFKD 规范化为 Unicode 规范形式。 +/// `String.prototype.normalize(form)`:按 NFC/NFD/NFKC/NFKD 规范化为 Unicode +/// 规范形式(良形段映射,孤立 surrogate 单元原样保留)。 pub fn string_normalize(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.normalize called with {} args", args.len()); use unicode_normalization::UnicodeNormalization; let form = if args.len() > 1 { - try_string!(as_string(vm, vm.reg(args[1]))).into_owned() + try_string!(as_units(vm, vm.reg(args[1]))).into_owned() } else { - "NFC".to_string() + "NFC".encode_utf16().collect() }; - let s = try_string!(this_string(vm, args)); - let result: String = match form.as_str() { - "NFD" => s.nfd().collect(), - "NFKC" => s.nfkc().collect(), - "NFKD" => s.nfkd().collect(), - _ => s.nfc().collect(), - }; - NativeResult::Ok(vm.new_string_owned(result)) + // 规范化形式名按良形文本比对(NFC 等名无孤立 surrogate)。 + let form_name = String::from_utf16_lossy(&form); + let s = try_string!(this_units(vm, args)); + let out = map_well_formed_segments(&s, |seg| match form_name.as_str() { + "NFD" => seg.nfd().collect(), + "NFKC" => seg.nfkc().collect(), + "NFKD" => seg.nfkd().collect(), + _ => seg.nfc().collect(), + }); + NativeResult::Ok(vm.new_string_units_owned(out)) } pub(crate) const MALL_INPUT: &str = "__mal_input__"; @@ -1474,10 +1731,29 @@ pub(crate) const MALL_INDEX: &str = "__mal_index__"; pub(crate) const MALL_RE: &str = "__mal_re__"; /// `String.prototype.matchAll(pattern)`:返回带 `next` 的迭代器,逐步产出全部匹配 -/// (要求 RegExp 带 global 标志;普通字符串会被转义成等效正则)。 +/// (要求 RegExp 带 global 标志;普通字符串会被转义成等效正则)。包装器 input +/// 属性存原始字符串值(单元保真),index 属性为码元游标。 pub fn string_match_all(vm: &mut H, args: &[u8]) -> NativeResult { builtins_debug!("String.prototype.matchAll called with {} args", args.len()); - let s = try_string!(this_string(vm, args)).into_owned(); + let this_val = vm.reg(args[0]); + if this_val.is_null() || this_val.is_undefined() { + return NativeResult::Err(crate::error::create_type_error( + vm, + "String.prototype method called on null or undefined", + )); + } + if this_val.is_symbol() { + return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")); + } + let input_val = match oxide_runtime_api::to_string_value_full(this_val, vm) { + Ok(v) => v, + Err(_) => { + 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")); + } + }; if args.len() < 2 { builtins_error!("String.prototype.matchAll: invalid receiver"); return NativeResult::Err(JsValue::undefined()); @@ -1496,8 +1772,11 @@ pub fn string_match_all(vm: &mut H, args: &[u8]) -> NativeResult { } pattern_val } else { - let pattern_str = try_string!(as_string(vm, pattern_val)); - let escaped = regress::escape(pattern_str.as_ref()); + let pattern_units = try_string!(as_units(vm, pattern_val)).into_owned(); + // 正则模式按文本编译:单元序列桥接为 lossy 文本(孤立 surrogate 映射 + // FFFD,与 RegExp 构造器的文本口径编译一致)。 + let pattern_str = String::from_utf16_lossy(&pattern_units); + let escaped = regress::escape(&pattern_str); let rx_str = if escaped.is_empty() { String::from("(?:)") } else { format!("({})", escaped) }; let compiled = match regress::Regex::new(&rx_str) { Ok(rx) => rx, @@ -1518,10 +1797,10 @@ pub fn string_match_all(vm: &mut H, args: &[u8]) -> NativeResult { }; // 构建包装对象。 - builder_wrapper(vm, &s, re_obj) + builder_wrapper(vm, input_val, re_obj) } -fn builder_wrapper(vm: &mut H, input: &str, re_obj: JsValue) -> NativeResult { +fn builder_wrapper(vm: &mut H, input_val: JsValue, re_obj: JsValue) -> NativeResult { // matchAll 迭代器挂 %RegExpStringIteratorPrototype%(链到 %IteratorPrototype%), // next 由原型提供(不挂实例 own)。 let regexp_iter_proto = vm.session().builtin_world().regexp_string_iterator_proto.as_ptr() as *mut JsObject; @@ -1532,7 +1811,6 @@ fn builder_wrapper(vm: &mut H, input: &str, re_obj: JsValue) -> Nativ let index_si = vm.kernel_core().perm_interner().intern(MALL_INDEX).0; let re_si = vm.kernel_core().perm_interner().intern(MALL_RE).0; - let input_val = vm.new_string(input); vm.set_or_create_prop_value(wrapper_obj, input_si, input_val); vm.set_or_create_prop_value(wrapper_obj, index_si, JsValue::int(0)); vm.set_or_create_prop_value(wrapper_obj, re_si, re_obj); @@ -1540,12 +1818,12 @@ fn builder_wrapper(vm: &mut H, input: &str, re_obj: JsValue) -> Nativ NativeResult::Ok(JsValue::from_js_object(wrapper)) } -/// `String.prototype[Symbol.iterator]()`:返回按 Unicode code point 迭代字符的迭代器。 +/// `String.prototype[Symbol.iterator]()`:返回按码元迭代字符的迭代器。 /// /// # 步骤 /// 1. null/undefined 抛 TypeError(RequireObjectCoercible) /// 2. this 经 ToString 完整转换(对象取 toString 结果,Symbol 抛 TypeError) -/// 3. 包成统一迭代器包装(next 逐 code point 产出,耗尽后 done) +/// 3. 包成统一迭代器包装(next 逐码元产出,耗尽后 done) /// /// # 边界与前提 /// - 对象 toString 抛出的原始异常原样传播 @@ -1558,8 +1836,8 @@ pub fn string_symbol_iterator(vm: &mut H, args: &[u8]) -> NativeResul "String.prototype[Symbol.iterator] called on null or undefined", )); } - let s = match oxide_runtime_api::to_string_full(this_val, vm) { - Ok(s) => s, + let s_val = match oxide_runtime_api::to_string_value_full(this_val, vm) { + Ok(v) => v, Err(_) => { // ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播。 if let Some(exc) = vm.take_uncaught_value() { @@ -1568,7 +1846,6 @@ pub fn string_symbol_iterator(vm: &mut H, args: &[u8]) -> NativeResul return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert value to a string")); } }; - let s_val = vm.new_string_owned(s); // String 迭代器挂 %StringIteratorPrototype%(链到 %IteratorPrototype%): // 包装机制复用通用路径,仅替换原型为中继层。 let string_iter_proto = vm.session().builtin_world().string_iterator_proto.as_ptr() as *mut JsObject; @@ -1579,6 +1856,7 @@ pub fn string_symbol_iterator(vm: &mut H, args: &[u8]) -> NativeResul } /// `matchAll` 迭代器的 `next`:返回 `{value: 匹配数组, done}`,耗尽后 done 为 true。 +/// 游标为码元位置(两臂统一口径);空匹配推进 1 个码元。 pub fn string_match_all_next(vm: &mut H, args: &[u8]) -> NativeResult { let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); if !this_val.is_object() { @@ -1590,15 +1868,14 @@ pub fn string_match_all_next(vm: &mut H, args: &[u8]) -> NativeResult let re_si = vm.kernel_core().perm_interner().intern(MALL_RE).0; let input_val = match vm.ordinary_get(wrapper, input_si, this_val) { - Ok(v) => v, - Err(_) => return make_match_done_result(vm, JsValue::undefined()), + Ok(v) if v.is_string() => v, + _ => return make_match_done_result(vm, JsValue::undefined()), }; - let input_str = oxide_runtime_api::to_string(input_val); let idx_val = match vm.ordinary_get(wrapper, index_si, this_val) { Ok(v) => v, Err(_) => return make_match_done_result(vm, JsValue::undefined()), }; - let mut idx = if idx_val.is_int() { idx_val.as_int().max(0) as usize } else { 0 }; + let idx = if idx_val.is_int() { idx_val.as_int().max(0) as usize } else { 0 }; let re_val = match vm.ordinary_get(wrapper, re_si, this_val) { Ok(v) if v.is_object() => v, _ => return make_match_done_result(vm, JsValue::undefined()), @@ -1610,34 +1887,63 @@ pub fn string_match_all_next(vm: &mut H, args: &[u8]) -> NativeResult }; let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - if let Some(m) = regex.find_from(&input_str, idx).next() { - let range = m.range(); - let mut parts: Vec = Vec::new(); - parts.push(input_str[range.start..range.end].to_string()); - for i in 1..=m.captures.len() { - match m.group(i) { - Some(g) => parts.push(input_str[g.start..g.end].to_string()), - None => parts.push(String::new()), - } - } - // 空匹配(range.start == range.end)必须推进至少一个 UTF-8 字符,否则同一 - // 位置反复空匹配死循环;空匹配落在串尾时把游标推到串尾之后,保证下一轮 - // 直接耗尽(与 regress find_iter 的 next_right_pos 推进语义一致)。 - idx = if range.end > range.start { - range.end - } else { - range.end - + input_str - .get(range.end..) - .and_then(|s| s.chars().next()) - .map_or(1, char::len_utf8) - }; - vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(idx as i32)); - let arr_val = make_string_array(vm, parts); - make_match_done_result(vm, arr_val) - } else { - make_match_done_result(vm, JsValue::undefined()) + // 按 input 载荷形态分臂:Flat 走字节通道(游标经码元↔字节换算), + // 其余走单元通道(游标即码元)。 + // SAFETY: input_val 已校验为字符串值,裸指针借用压缩到单个表达式。 + let sp = unsafe { &*input_val.as_string_ptr() }; + // 耗尽守卫:空匹配正则(如 /(?:)/g、/a*/g)在串尾会反复命中同一末端空 + // 匹配,游标推进越过码元总数后须直接 done,否则同一末端空匹配死循环。 + let total_units = sp.utf16_len() as usize; + if idx > total_units { + return make_match_done_result(vm, JsValue::undefined()); } + let (next_idx, parts): (usize, Vec>) = if sp.is_flat() { + let s = sp.as_str(); + match regex.find_from(s, unit_to_byte(s, idx)).next() { + Some(m) => { + let range = m.range(); + let mut parts = Vec::with_capacity(m.captures.len() + 1); + parts.push(s[range.start..range.end].encode_utf16().collect()); + for i in 1..=m.captures.len() { + match m.group(i) { + Some(g) => parts.push(s[g.start..g.end].encode_utf16().collect()), + None => parts.push(Vec::new()), + } + } + // 空匹配(range 无推进)须推进至少一个码元,否则同一位置反复 + // 空匹配死循环;非空匹配游标落匹配末尾(码元口径)。 + let end_units = byte_to_unit(s, range.end); + let next_idx = if range.end > range.start { end_units } else { end_units + 1 }; + (next_idx, parts) + } + None => (0, Vec::new()), + } + } else { + let u = sp.units(); + match regex.find_from_utf16(&u, idx).next() { + Some(m) => { + let range = m.range(); + let mut parts = Vec::with_capacity(m.captures.len() + 1); + parts.push(u[range.start..range.end].to_vec()); + for i in 1..=m.captures.len() { + match m.group(i) { + Some(g) => parts.push(u[g.start..g.end].to_vec()), + None => parts.push(Vec::new()), + } + } + let next_idx = if range.end > range.start { range.end } else { range.end + 1 }; + (next_idx, parts) + } + None => (0, Vec::new()), + } + }; + + if parts.is_empty() { + return make_match_done_result(vm, JsValue::undefined()); + } + vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(next_idx as i32)); + let value = make_units_array(vm, parts); + make_match_done_result(vm, value) } fn make_match_done_result(vm: &mut H, value: JsValue) -> NativeResult { @@ -1660,8 +1966,7 @@ pub fn string_replace_all(vm: &mut H, args: &[u8]) -> NativeResult { } /// `String.raw(template, ...substitutions)`:从模板对象的 `raw` 数组元素与 -/// substitutions 交错拼接,返回原始字符串。raw 元素按 ToString 转换, -/// substitutions 按原始 JS 语义经 ToString 拼接。 +/// substitutions 交错拼接,返回原始字符串(单元口径,lone surrogate 保真)。 /// /// # 步骤 /// 1. 取 template 的 `raw` 属性(必须为对象,否则抛 TypeError) @@ -1729,17 +2034,17 @@ pub fn string_raw(vm: &mut H, args: &[u8]) -> NativeResult { }; // Step 4: 逐下标拼接 raw[i] + substitutions[i]。 - let mut result = String::new(); + let mut result: Vec = Vec::new(); let raw_obj_ref = unsafe { &*raw_ptr }; for i in 0..raw_len { // 读 raw[i]:用 property_key_si 把整数 i 映射为整数键 si, // 再经 ordinary_get 沿原型链查找(触发 getter)。 let index_key = vm.property_key_si(JsValue::int(i as i32)); let raw_elem = vm.ordinary_get(raw_obj_ref, index_key, raw_val); - let raw_str = match raw_elem { + let raw_units = match raw_elem { // ToString 完整路径:对象经 ToPrimitive(异常原样传播),Symbol 抛 TypeError。 - Ok(v) => match oxide_runtime_api::to_string_full(v, vm) { - Ok(s) => s, + Ok(v) => match oxide_runtime_api::to_string_value_full(v, vm) { + Ok(sv) => vm.string_units(sv).into_owned(), Err(msg) => { let exc = vm .take_uncaught_value() @@ -1751,10 +2056,10 @@ pub fn string_raw(vm: &mut H, args: &[u8]) -> NativeResult { if let Some(exc) = vm.take_uncaught_value() { return NativeResult::Err(exc); } - "undefined".to_string() + "undefined".encode_utf16().collect() } }; - result.push_str(&raw_str); + result.extend(raw_units); // 段间(i + 1 < length)才追加 substitutions[i];substitutions 缺失时 // 追加空串(spec 24.b.i),末段后不再追加任何内容。 @@ -1763,8 +2068,8 @@ pub fn string_raw(vm: &mut H, args: &[u8]) -> NativeResult { if sub_idx < args.len() { let sub_val = vm.reg(args[sub_idx]); // ToString 完整路径:对象 ToPrimitive 异常原样传播,Symbol 抛 TypeError。 - let sub_str = match oxide_runtime_api::to_string_full(sub_val, vm) { - Ok(s) => s, + let sub_units = match oxide_runtime_api::to_string_value_full(sub_val, vm) { + Ok(sv) => vm.string_units(sv).into_owned(), Err(msg) => { let exc = vm .take_uncaught_value() @@ -1772,9 +2077,9 @@ pub fn string_raw(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Err(exc); } }; - result.push_str(&sub_str); + result.extend(sub_units); } } } - NativeResult::Ok(vm.new_string_owned(result)) + NativeResult::Ok(vm.new_string_units_owned(result)) } diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index 2a4f6f02..266f4f58 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -15,6 +15,111 @@ fn hash64(s: &str) -> u64 { h.finish() } +/// 把字符串单元序列编码为 interner 键文本。配对感知: +/// - 良配 surrogate 对(高+低)编码为对应单字符(超平面字符逐字保留, +/// 键文本保持恒等); +/// - 孤立 surrogate 编码为 U+FFFD 后跟 4 位小写十六进制; +/// - 单元 U+FFFD 编码为 U+FFFD 后跟字面文本 `fffd`(与任何 surrogate +/// 的转义文本可区分)。 +/// +/// 编码无损:`decode_key` 是其精确逆变换(对无 FFFD 的良形文本为恒等)。 +pub fn encode_key(units: &[u16]) -> String { + let mut out = String::with_capacity(units.len()); + let mut i = 0; + while i < units.len() { + let u = units[i]; + if (0xD800..=0xDBFF).contains(&u) && i + 1 < units.len() { + let lo = units[i + 1]; + if (0xDC00..=0xDFFF).contains(&lo) { + let cp = 0x10000u32 + (((u as u32) - 0xD800) << 10) + (lo as u32 - 0xDC00); + out.push(char::from_u32(cp).expect("良配 surrogate 对必映射为合法码点")); + i += 2; + continue; + } + } + if (0xD800..=0xDFFF).contains(&u) { + out.push('\u{FFFD}'); + out.push_str(&format!("{:04x}", u)); + } else if u == 0xFFFD { + out.push('\u{FFFD}'); + out.push_str("fffd"); + } else { + // 非 surrogate 单元恒可单字符表示(BMP 或超平面字符由对分支处理)。 + out.push(char::from_u32(u as u32).expect("非 surrogate 单元必为合法码点")); + } + i += 1; + } + out +} + +/// `encode_key` 的逆变换:把键文本还原为单元序列。 +/// +/// 转义约定:U+FFFD 后跟 `fffd` 还原为单元 U+FFFD;U+FFFD 后跟 surrogate +/// 段内的 4 位小写十六进制还原为对应孤立 surrogate 单元。FFFD 后跟其他 +/// 文本是防御性兜底(`encode_key` 不产生该形态):输出 FFFD 单元并把后续 +/// 已消费字符原样补回。 +/// +/// 已知边界:用户来源的键文本若含裸 FFFD 且其后恰为 `fffd` 或 surrogate +/// 段 4 位十六进制,物化时该 4 字符会被当作转义吞掉。物化面仅限 builtin +/// 永久键(恒为良形恒等文本),故该歧义不实际发生。 +pub fn decode_key(text: &str) -> Vec { + let mut out: Vec = Vec::with_capacity(text.len()); + let mut chars = text.chars().peekable(); + while let Some(c) = chars.next() { + if c != '\u{FFFD}' { + if (c as u32) > 0xFFFF { + // 超平面字符还原为其良配 surrogate 对。 + let v = (c as u32) - 0x10000; + out.push(0xD800 + (v >> 10) as u16); + out.push(0xDC00 + (v & 0x3FF) as u16); + } else { + out.push(c as u16); + } + continue; + } + // FFFD:消费其后最多 4 字符判定转义形态。 + let mut tail: [char; 4] = [' '; 4]; + let mut n = 0; + while n < 4 { + if let Some(&ch) = chars.peek() { + tail[n] = ch; + chars.next(); + n += 1; + } else { + break; + } + } + if n == 4 && tail == ['f', 'f', 'f', 'd'] { + out.push(0xFFFD); + continue; + } + if n == 4 { + let mut v: u16 = 0; + let mut ok = true; + for &ch in tail.iter() { + // 编码端恒产小写十六进制(0-9 或 a-f),大写/非 hex 一律视为裸文本。 + match ch.to_digit(16) { + Some(d) if matches!(ch, '0'..='9' | 'a'..='f') => v = v * 16 + d as u16, + _ => { + ok = false; + break; + } + } + } + if ok && (0xD800..=0xDFFF).contains(&v) { + out.push(v); + continue; + } + } + // 防御性兜底:裸 FFFD,后续字符原样补回。 + out.push(0xFFFD); + for ch in tail.iter().take(n) { + out.push(*ch as u16); + } + } + out +} + /// 一条 intern 过的键。`data` 是泄漏的 `&'static str`——永久键从不释放 /// (按设计 append-only),所以泄漏即存储模型,而非 bug。键 id 与 64 位 /// 哈希经 `DashMap` 的哈希键→候选 id 表寻址,条目自身不存哈希。 @@ -129,7 +234,10 @@ impl PermInterner { perm.resize_with(id as usize + 1, || None); } if perm[id as usize].is_none() { - perm[id as usize] = Some(Box::new(JsString::new(text.to_string()))); + // 键文本经逆编码还原为单元序列再物化:良形键为逐字恒等, + // 含转义的键(见 encode_key 约定)还原出孤立 surrogate 单元。 + let units = decode_key(text); + perm[id as usize] = Some(Box::new(JsString::from_units(units))); } perm[id as usize].as_ref().unwrap().as_ref() as *const JsString } @@ -326,6 +434,58 @@ mod tests { } } + #[test] + fn encode_key_identity_and_escapes() { + // 良形且无 FFFD(含超平面良配对)编码为恒等文本。 + assert_eq!(encode_key(&"abc🚀".encode_utf16().collect::>()), "abc🚀"); + // 孤立 surrogate:FFFD + 4 位小写十六进制。 + assert_eq!(encode_key(&[0xD800]), "\u{FFFD}d800"); + assert_eq!(encode_key(&[0xDFFF]), "\u{FFFD}dfff"); + assert_eq!(encode_key(&[0xDBFF, 0x61]), "\u{FFFD}dbffa"); + // FFFD 单元:FFFD + 字面 "fffd"。 + assert_eq!(encode_key(&[0xFFFD]), "\u{FFFD}fffd"); + // 两种不同单元序列的键文本互异(解码可区分)。 + assert_ne!(encode_key(&[0xFFFD]), encode_key(&[0xD800])); + } + + #[test] + fn encode_decode_roundtrip() { + let cases: Vec> = vec![ + Vec::new(), + "a".encode_utf16().collect(), + "abc🚀😀".encode_utf16().collect(), + vec![0xD800, 0x42, 0xDC00], + vec![0xFFFD, 0xFFFD, 0x41, 0xFFFD, 0x66, 0x66, 0x66, 0x64], + vec![0xFFFF, 0x0041, 0xDBFF, 0xDC00, 0xD800], + ]; + for units in &cases { + let key = encode_key(units); + assert_eq!(decode_key(&key), units.as_slice(), "roundtrip 失败: key={key:?}"); + } + } + + #[test] + fn decode_key_defensive_raw_fffd() { + // 裸 FFFD(文本末尾或后跟非转义文本)还原为 FFFD 单元,后续字符不被吞。 + assert_eq!(decode_key("a\u{FFFD}"), &[0x61, 0xFFFD]); + assert_eq!(decode_key("a\u{FFFD}zz"), &[0x61, 0xFFFD, 0x7A, 0x7A]); + } + + #[test] + fn string_ptr_materializes_units() { + let interner = PermInterner::new(); + // 良形键:物化内容与旧行为逐位一致,且二次调用返回同一稳定指针。 + let (id, _) = interner.intern("perm"); + let ptr = interner.string_ptr(id); + assert_eq!(unsafe { (*ptr).as_str() }, "perm"); + assert_eq!(interner.string_ptr(id), ptr); + // 含转义的键:物化出孤立 surrogate 单元形态。 + let (id2, _) = interner.intern(&encode_key(&[0xD800])); + let ptr2 = interner.string_ptr(id2); + assert!(unsafe { (*ptr2).has_lone_surrogate() }); + assert_eq!(unsafe { (*ptr2).units() }, &[0xD800][..]); + } + #[test] fn perm_table_concurrent_first_use_unique_ptr() { // 并发首用竞态回归:96 线程同一时刻命中冷表槽,强制多线程同入慢路径。 @@ -365,6 +525,6 @@ mod tests { assert_eq!(unsafe { (*(c0 as *const JsString)).as_str() }, "x"); let t0 = t0 as *const JsString; assert_eq!(unsafe { (*t0).as_str() }, "object"); - assert_eq!(unsafe { (*t0).len() }, 6); + assert_eq!(unsafe { (*t0).utf16_len() }, 6); } } diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index 9c02eeac..3b747c33 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -120,13 +120,23 @@ pub trait VmHost { None } } - /// 借出字符串值的文本内容,生命周期绑定到 `&self` 借用。 - /// - /// perm 字符串由内核持有、永不释放;session 字符串只在 `&mut self` 路径 - /// (`new_string`/`new_string_owned`/`maybe_collect_session_gc`)释放,`&self` - /// 借用与 `&mut` 互斥由编译器强制,借用期内该字符串不会回收。实现内部 - /// `unsafe` 解引用,调用方须保证 `val` 为字符串值。 - fn string_ref(&self, val: JsValue) -> &str; + /// 借出字符串值的 UTF-16 单元序列(Flat 惰性编码 / FlatU16 直接借用 / + /// Cons 扁平化缓存),生命周期绑定到 `&self` 借用。调用方须保证 + /// `val` 为字符串值。 + fn string_units(&self, val: JsValue) -> std::borrow::Cow<'_, [u16]>; + /// 取单单元的永久字符串值:ASCII 单元命中共享 perm 串(零分配、可指针 + /// 短路),非 ASCII 返回 `None` 由调用方回落单元创建。 + fn single_unit(&self, u: u16) -> Option { + if u < 0x80 { + Some(JsValue::string(oxide_kernel::string_forge::single_char_ptr(u as u8))) + } else { + None + } + } + /// 以单元序列创建会话字符串(智能路由:含孤立 surrogate 落 FlatU16)。 + fn new_string_units(&mut self, units: &[u16]) -> JsValue; + /// 同 `new_string_units`,以 owned 单元序列接收,避免一次克隆。 + fn new_string_units_owned(&mut self, units: Vec) -> JsValue; /// 分配 BigInt 值(num_bigint::BigInt box 登记到 VM,返回携带指针的 `JsValue`)。 fn new_bigint(&mut self, v: num_bigint::BigInt) -> JsValue; /// 读取 BigInt 值;调用方须保证 `val.is_bigint()`。 @@ -217,23 +227,25 @@ pub fn format_error_message(name: &str, msg: &str) -> String { } } -/// 借出字符串值的文本内容,生命周期为 `'static`。仅限同一函数内即时消费。 +/// 取字符串值的 lossy 文本(孤立 surrogate 单元映射为 U+FFFD),生命周期为 +/// `'static`。仅限同一函数内即时消费。 /// /// # Safety -/// 调用方必须保证:从取用返回值到最后一次使用之间,不发生任何可能释放该字符串 +/// 调用方必须保证:从取用返回值到最后一次消费之间,不发生任何可能释放该字符串 /// 的操作(分配、GC、`full_reset`)——session 字符串在执行期 GC 或 reset 时会被 -/// 释放,跨分配点持有引用即悬垂。需要跨分配点消费的场景改用 [`VmHost::string_ref`] -/// 或公开的 [`to_string`] owned 路径。 +/// 释放,跨分配点持有即悬垂。需要单元语义(不可 lossy)的场景走 +/// [`VmHost::string_units`] / [`to_units_full`]。 #[inline] -pub(crate) unsafe fn string_data(val: JsValue) -> &'static str { - (*val.as_string_ptr()).as_str() +pub(crate) unsafe fn string_data(val: JsValue) -> std::borrow::Cow<'static, str> { + // Borrowed 臂借用期名义绑定解引用临时;与旧 `&'static str` 版本同款契约 + // (同函数内即时消费),生命周期统一升为 'static。 + std::mem::transmute((*val.as_string_ptr()).as_lossy_str()) } /// 按内容比较两个字符串 `JsValue` 是否相等。 /// -/// 先做指针级短路(同一 interned 字符串必等),否则比较 `JsString` 的整块文本 -/// (rope 未扁平化时经 `as_str` 惰性扁平化;内容比较对"rope vs 同内容不同指针" -/// 也正确)。 +/// 先做指针级短路(同一 interned 字符串必等);双 Flat 比文本(与旧路径逐位 +/// 一致),其余按单元序列比较(对"rope vs 同内容不同指针"也正确)。 #[inline] pub fn string_value_eq(a: JsValue, b: JsValue) -> bool { if a.as_string_ptr() == b.as_string_ptr() { @@ -241,7 +253,10 @@ pub fn string_value_eq(a: JsValue, b: JsValue) -> bool { } let sa = unsafe { &*a.as_string_ptr() }; let sb = unsafe { &*b.as_string_ptr() }; - sa.as_str() == sb.as_str() + if sa.is_flat() && sb.is_flat() { + return sa.as_str() == sb.as_str(); + } + sa.units() == sb.units() } /// BigInt 转 f64 的近似转换(连续整数用 to_u64,大数用 Display 解析)。 @@ -288,8 +303,8 @@ pub fn to_number(val: JsValue) -> f64 { JsType::Null => 0.0, JsType::Undefined => f64::NAN, JsType::String => { - let s = unsafe { string_data(val) }; - parse_js_number(s) + // 孤立 surrogate 的 lossy 文本不含数字/数字前缀,解析结果与单元口径一致(NaN)。 + parse_js_number(&unsafe { string_data(val) }) } JsType::BigInt => { // BigInt → Number 近似转换:i128 超出 f64 精度时舍入为近似值。 @@ -520,7 +535,7 @@ pub fn push_to_string(val: JsValue, buf: &mut String) { return; } if val.is_string() { - unsafe { buf.push_str(string_data(val)) }; + unsafe { buf.push_str(&string_data(val)) }; return; } if val.is_bigint() { @@ -565,6 +580,38 @@ pub fn to_string(val: JsValue) -> String { String::new() } +/// 把任意值转为 UTF-16 单元序列(`ToPrimitive(string hint)` 的单元口径): +/// 字符串值直接借出单元(Flat 惰性编码、rope 扁平化);Symbol 返回 TypeError +/// 错误;其余值经 ToString 原始值路径后 UTF-16 编码。 +pub fn to_units_full(val: JsValue, host: &mut H) -> Result, String> { + if val.is_string() { + return Ok(host.string_units(val).into_owned()); + } + if val.is_symbol() { + return Err(host.error_message_text("TypeError", "Cannot convert a Symbol value to a string")); + } + let prim = to_primitive(val, ToPrimitiveHint::String, host)?; + if prim.is_string() { + return Ok(host.string_units(prim).into_owned()); + } + // 非字符串原始值(数字等):先格式化再编码。 + let s = to_string(prim); + Ok(s.encode_utf16().collect()) +} + +/// 把值追加其 UTF-16 单元到 `buf`:字符串值直接扩单元序列,非字符串值经 +/// ToString 后 UTF-16 编码。字符串拼接的单元通道路径(不可 lossy)。 +pub fn push_units_to(val: JsValue, buf: &mut Vec) { + if val.is_string() { + // SAFETY: val 为字符串值,借用即时消费。 + let s = unsafe { &*val.as_string_ptr() }; + buf.extend_from_slice(s.units().as_ref()); + return; + } + let s = to_string(val); + buf.extend(s.encode_utf16()); +} + /// ToBoolean(ECMA-262 §7.1.2):falsy 值仅限 undefined/null/false/±0/NaN/空串,其余为 true。 pub fn to_boolean(val: JsValue) -> bool { match val.js_type() { @@ -608,13 +655,13 @@ pub fn abstract_eq(lhs: JsValue, rhs: JsValue, host: &mut H) -> Resul (JsType::Int | JsType::Double, JsType::BigInt) => { Ok(to_number(lhs) == bigint_to_f64(unsafe { bigint_data(rhs) })) } - // BigInt 与 String:字符串解析为数字后比较。 + // BigInt 与 String:字符串解析为数字后比较(lossy 文本口径,见 to_number)。 (JsType::BigInt, JsType::String) => { - let r = parse_js_number(unsafe { string_data(rhs) }); + let r = parse_js_number(&unsafe { string_data(rhs) }); Ok(bigint_to_f64(unsafe { bigint_data(lhs) }) == r) } (JsType::String, JsType::BigInt) => { - let l = parse_js_number(unsafe { string_data(lhs) }); + let l = parse_js_number(&unsafe { string_data(lhs) }); Ok(l == bigint_to_f64(unsafe { bigint_data(rhs) })) } // Number 与 String:字符串经 ToNumber 后按严格数值比较。 @@ -651,9 +698,15 @@ fn strict_double_eq(a: f64, b: f64) -> bool { /// 双字符串按字典序;否则转数值比较,任一侧为 NaN 时返回 `None`(表示比较未定义,调用方据此处理 `<`/`>`)。 pub fn relational_compare(lhs: JsValue, rhs: JsValue) -> Option { if lhs.is_string() && rhs.is_string() { - let ls = unsafe { string_data(lhs) }; - let rs = unsafe { string_data(rhs) }; - return Some(ls < rs); + // SAFETY: 两侧均为字符串值,借用即时消费。 + let l = unsafe { &*lhs.as_string_ptr() }; + let r = unsafe { &*rhs.as_string_ptr() }; + if l.is_flat() && r.is_flat() { + // 双 Flat:文本比较与旧路径逐位一致。 + return Some(l.as_str() < r.as_str()); + } + // 单元口径:码元字典序即规范序,Flat 编码与 rope 扁平化统一走单元通道。 + return Some(l.units() < r.units()); } if lhs.is_bigint() && rhs.is_bigint() { let l = unsafe { bigint_data(lhs) }; @@ -935,6 +988,23 @@ pub fn to_string_full(val: JsValue, host: &mut H) -> Result(val: JsValue, host: &mut H) -> Result { + if val.is_string() { + return Ok(val); + } + let primitive = to_primitive(val, ToPrimitiveHint::String, host)?; + if primitive.is_symbol() { + return Err(host.error_message_text("TypeError", "Cannot convert a Symbol value to a string")); + } + if primitive.is_string() { + return Ok(primitive); + } + Ok(host.new_string_owned(to_string(primitive))) +} + /// 带完整对象强制转换的 ToBigInt(input)(§7.1.14):BigInt 原样;Boolean → 0/1; /// String 走 StringToBigInt;Number/Symbol/undefined/null → TypeError;对象经 /// ToPrimitive(default hint)后递归处理。 diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index a5e51306..4042b63d 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -14,79 +14,190 @@ use std::sync::OnceLock; /// /// 字符串*值*以 48 位指针(指向 `JsString`)NaN-box(见 `JsValue::string`)。 /// -/// 两种变体: -/// - `Flat`:整块 UTF-8 文本(String,保留容量)。字面量、方法结果与 CONCAT_N -/// 产物保持此形态; -/// - `Cons`(rope):指向 [`ConsNode`] 载荷(左右子裸指针 + O(1) 字节长 + -/// 惰性扁平化缓存)。二元 `+`/`+=` 拼接 O(1) 链接不拷贝文本,整块文本在 +/// 三种载荷形态(`tag` 区分,载荷并位 @0): +/// - `TAG_FLAT`:整块 UTF-8 文本(`String`,保留容量)。字面量、方法结果与 +/// CONCAT_N 产物保持此形态;内容无孤立 surrogate(UTF-8 合法性天然保证); +/// - `TAG_FLAT_U16`:UTF-16 单元载荷(`Vec`,ptr/len/cap 与 String 同形), +/// 承载含孤立 surrogate 的任意单元序列——`String` 在结构上不可表示该内容; +/// - `TAG_CONS`(rope):指向 [`ConsNode`] 载荷(左右子裸指针 + O(1) 单元长 + +/// 惰性扁平化缓存)。二元 `+`/`+=` 拼接 O(1) 链接不拷贝文本,单元序列在 /// 首次消费时扁平化并原子发布。 /// -/// 结构体大小恒为 32B:`Flat` 内联 `String`(cap@0 / data@8 / len@16), -/// `Cons` 首字为 tag(0xffff_ffff_ffff_ffff)@0、子节点指针@8;`utf16_len` -/// 两形态统一 @24(4B),尾部 4B 填充。`String` 原样存储(零 realloc 收缩); -/// Cons 专属状态(子节点/产物缓存)独立分配在 [`ConsNode`],仅大链持有。 -/// 尺寸锚见下方 size_of/offset_of 断言。 +/// 布局恒为 32B:载荷 24B @0、`utf16_len` @24(4B)、`tag` @28(1B,原 NLL +/// enum 的尾部填充位,显式 repr(C) 钉死)、尾部 3B 填充。`String` 原样存储 +/// (零 realloc 收缩);Cons 专属状态(子节点/产物缓存)独立分配在 [`ConsNode`], +/// 仅大链持有。尺寸锚见下方 size_of/offset_of 断言。 /// /// 生命周期约定: /// - `Cons` 子节点各自独立登记 session 字符串表(或为 perm 串),由 GC 的 /// mark 传播闭包保证随父存活;扁平化产物只挂在 `ConsNode::flat_cache`、不进 /// session 主表,随本节点连带释放。 /// - 地址稳定不搬移(Box 堆分配),GC 无需 forwarding / rewrite。 -#[derive(Debug)] +#[repr(C)] pub struct JsString { - kind: StringKind, - /// JS 语义的字符串长度(UTF-16 code unit 数)懒缓存。 + /// 载荷 24B,按 `tag` 解读:Flat = `String`(data/len/cap)、FlatU16 = + /// `Vec`(ptr/len/cap,与 String 同形)、Cons = 节点指针(首字, + /// 余 16B 未定义填充)。 + payload: [u64; 3], + /// JS 语义的字符串长度(UTF-16 code unit 数)。 /// - /// 构造是热路径(拼接/格式化),长度查询少见,故不预付 O(n) 扫描;首次 - /// `utf16_len()` 访问时计算一次。以 sentinel + `AtomicU32` 实现:perm 字符串 - /// 可被多线程 VM 共享读,计算幂等,relaxed 序即可。 + /// Flat 为懒缓存:构造是热路径(拼接/格式化),长度查询少见,不预付 O(n) + /// 扫描,首次 `utf16_len()` 访问时计算一次(sentinel + `AtomicU32`:perm + /// 串可被多线程 VM 共享读,计算幂等,relaxed 序即可)。FlatU16/Cons 构造时 + /// 预写(单元数 / 子节点归纳,各 O(1))。 utf16_len: AtomicU32, + tag: u8, } -// 布局锚:32B = 载荷 24B(Flat 内联 String / Cons tag+子指针)+ utf16_len 4B -// + 尾部填充 4B。tag 并入载荷首字(Cons 以 0xffff.. marker 占位),无独立索引字—— -// 换变体形状/字段类型可能改变该折叠,布局变更先同步结构体头注释再改本断言。 +// 布局锚:32B = 载荷 24B(三形态同形 ptr/len/cap 或节点指针)+ utf16_len 4B +// + tag 1B + 尾部填充 3B。三变体 NLL enum 需独立 tag 字(撑到 40B、utf16_len@32) +// 故以 repr(C) + 尾部 tag 字节钉死本布局;换载荷形状/字段类型先同步结构体头 +// 注释再改本断言。 const _: () = assert!( - std::mem::size_of::() == 32 && std::mem::offset_of!(JsString, utf16_len) == 24, - "JsString 布局漂移:锚定布局见结构体头注释(32B,utf16_len@24)" + std::mem::size_of::() == 32 + && std::mem::offset_of!(JsString, utf16_len) == 24 + && std::mem::offset_of!(JsString, tag) == 28, + "JsString 布局漂移:锚定布局见结构体头注释(32B,utf16_len@24,tag@28)" ); -/// 字符串内容变体。 -#[derive(Debug)] -enum StringKind { - Flat(String), - Cons(*const ConsNode), +/// 载荷形态标签(`JsString::tag` 取值)。 +const TAG_FLAT: u8 = 0; +const TAG_FLAT_U16: u8 = 1; +const TAG_CONS: u8 = 2; + +impl std::fmt::Debug for JsString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.tag { + TAG_FLAT => write!(f, "JsString::Flat({:?})", self.flat_ref()), + TAG_FLAT_U16 => write!(f, "JsString::FlatU16({:?})", self.flat_u16_ref()), + TAG_CONS => write!(f, "JsString::Cons(len={})", self.utf16_len()), + _ => write!(f, "JsString::Invalid"), + } + } } -/// Cons(rope)节点的载荷:左右子节点指针 + O(1) 拼接字节长 + 扁平化产物缓存。 +impl Drop for JsString { + fn drop(&mut self) { + // 按 tag 恰好释放激活载荷:Flat/FlatU16 释放载荷自身(内部 heap 缓冲), + // Cons 载荷节点由 drop_cons_node 单独释放(见其文档)。 + // + // # Safety + // tag 与载荷内容恒由构造点同步写入;drop 期载荷仍为激活形态。 + unsafe { + match self.tag { + TAG_FLAT => std::ptr::drop_in_place(self.payload.as_mut_ptr() as *mut String), + TAG_FLAT_U16 => std::ptr::drop_in_place(self.payload.as_mut_ptr() as *mut Vec), + TAG_CONS => {} + _ => unreachable!(), + } + } + } +} + +/// Cons(rope)节点的载荷:左右子节点指针 + O(1) 拼接单元长 + 扁平化产物缓存。 /// -/// 独立于 `JsString` 分配,使 Flat 路径的 `JsString` 保持与 rope 前相同的 32B -/// 结构(tag 并入载荷首字,见其尺寸断言);仅超过字节阈值的拼接 -/// (`Vm::new_cons_string`)才创建本节点。 +/// 独立于 `JsString` 分配,使 Flat 路径的 `JsString` 保持 32B 结构(tag 并入 +/// 尾部填充位,见其尺寸断言);仅超过阈值的拼接(`Vm::new_cons_string`)才 +/// 创建本节点。 /// /// 生命周期约定:节点由 `Box::into_raw` 分配,只经 [`JsString::drop_cons_node`] /// 释放(连带扁平化产物);`left`/`right` 由 GC 传播闭包保证随父存活。 -#[derive(Debug)] pub struct ConsNode { left: *const JsString, right: *const JsString, - /// 拼接字节长:构造时 = left.len() + right.len()(各 O(1)),免递归求长。 - byte_len: u32, - /// 扁平化产物缓存:首次文本消费时分配整块 Flat 产物并原子发布(OnceLock)。 + /// 拼接 UTF-16 单元长:构造时 = left.utf16_len() + right.utf16_len()(各 O(1)), + /// 免递归求长、不触发扁平化。 + unit_len: u32, + /// 扁平化产物缓存:首次单元消费时分配整块 FlatU16 产物并原子发布(OnceLock)。 /// 产物不进 session 主表,生命周期挂本节点——GC 传播保证其随父存活, /// 释放节点时连带释放。 flat_cache: OnceLock<*const JsString>, } -/// `utf16_len` 未计算的哨兵值(有效长度不可能为 u32::MAX)。 +/// `utf16_len` 未计算的哨兵值(有效长度不可能为 u32::MAX;仅 Flat 形态使用)。 const UTF16_LEN_UNSET: u32 = u32::MAX; +impl ConsNode { + /// 扁平化产物指针(未扁平化返回 null)。 + pub fn flat_cache_ptr(&self) -> *const JsString { + self.flat_cache.get().copied().unwrap_or(std::ptr::null()) + } +} + impl JsString { - /// 用 UTF-8 数据构造 Flat 字符串。 + /// Flat 载荷借用(载荷即原始 `String`)。 + /// + /// # Safety + /// 仅当 `tag == TAG_FLAT` 时调用(debug_assert 已覆盖常规路径)。 + #[inline(always)] + fn flat_ref(&self) -> &String { + debug_assert!(self.tag == TAG_FLAT, "flat_ref 仅限 Flat 形态"); + // SAFETY: 形态检查通过后载荷即 String(布局与 [u64;3] 逐位一致)。 + unsafe { &*(self.payload.as_ptr() as *const String) } + } + + /// FlatU16 载荷借用(载荷即原始 `Vec`)。 + /// + /// # Safety + /// 仅当 `tag == TAG_FLAT_U16` 时调用(debug_assert 已覆盖常规路径)。 + #[inline(always)] + fn flat_u16_ref(&self) -> &Vec { + debug_assert!(self.tag == TAG_FLAT_U16, "flat_u16_ref 仅限 FlatU16 形态"); + // SAFETY: 形态检查通过后载荷即 Vec(布局与 [u64;3] 逐位一致)。 + unsafe { &*(self.payload.as_ptr() as *const Vec) } + } + + /// Cons 节点指针(载荷首字即节点裸指针)。 + /// + /// # Safety + /// 仅当 `tag == TAG_CONS` 时调用(debug_assert 已覆盖常规路径)。 + #[inline(always)] + fn cons_ptr(&self) -> *const ConsNode { + debug_assert!(self.tag == TAG_CONS, "cons_ptr 仅限 Cons 形态"); + // SAFETY: 形态检查通过后载荷首字即节点指针。 + unsafe { *(self.payload.as_ptr() as *const *const ConsNode) } + } + + /// 用 UTF-8 数据构造 Flat 字符串(内容恒 well-formed,无孤立 surrogate)。 pub fn new(data: String) -> Self { + let mut payload = [0u64; 3]; + // SAFETY: String 与 [u64;3] 布局一致(24B ptr/len/cap);write 不读目的位。 + unsafe { std::ptr::write(payload.as_mut_ptr() as *mut String, data) }; Self { - kind: StringKind::Flat(data), + payload, utf16_len: AtomicU32::new(UTF16_LEN_UNSET), + tag: TAG_FLAT, + } + } + + /// 直接以单元序列构造 FlatU16 字符串(不做 well-formed 判定)。 + /// + /// 供明确知道内容形态的构造点使用(迭代器源物化、单元解码):smart + /// 路由场景一律走 [`Self::from_units`](well-formed 结果落 Flat)。 + pub fn new_flat_u16(units: Vec) -> Self { + let len = units.len() as u32; + let mut payload = [0u64; 3]; + // SAFETY: Vec 与 [u64;3] 布局一致(24B ptr/len/cap);write 不读目的位。 + unsafe { std::ptr::write(payload.as_mut_ptr() as *mut Vec, units) }; + Self { + payload, + utf16_len: AtomicU32::new(len), + tag: TAG_FLAT_U16, + } + } + + /// 智能路由构造:单元序列含孤立 surrogate 落 FlatU16,全 well-formed 落 + /// Flat(内容等价、载荷最小)。 + pub fn from_units(units: Vec) -> Self { + if units_have_lone_surrogate(&units) { + Self::new_flat_u16(units) + } else { + // 已判定无孤立 surrogate,from_utf16 必成功;Err 臂仅为防御 + // (单元判定与 from_utf16 同语义,不可能分歧)。 + match String::from_utf16(&units) { + Ok(s) => Self::new(s), + Err(_) => Self::new_flat_u16(units), + } } } @@ -96,29 +207,37 @@ impl JsString { /// `left`/`right` 必须指向存活的 `JsString`(session 或 perm),且由 GC /// 传播保证随本节点存活——调用方(`Vm::new_cons_string`)负责登记。 pub unsafe fn new_cons(left: *const JsString, right: *const JsString) -> Self { - let byte_len = ((*left).len() + (*right).len()) as u32; + // 子节点单元长各 O(1)(Flat 懒扫描首取后缓存 / FlatU16 直读 / Cons 归纳): + // 链接成本 = 子节点首取扫描,非本链 O(n)。 + let unit_len = (*left).utf16_len() + (*right).utf16_len(); let node = Box::into_raw(Box::new(ConsNode { left, right, - byte_len, + unit_len, flat_cache: OnceLock::new(), })); + let mut payload = [0u64; 3]; + // SAFETY: Cons 载荷首字为节点指针,余 16B 未定义填充;指针按位写入 + // (目标小端,指针→u64 为逐位转换)。 + unsafe { *payload.as_mut_ptr() = node as u64 }; Self { - kind: StringKind::Cons(node), - utf16_len: AtomicU32::new(UTF16_LEN_UNSET), + payload, + utf16_len: AtomicU32::new(unit_len), + tag: TAG_CONS, } } - /// Cons 载荷节点指针(Flat 返回 null)。仅供释放路径使用。 + /// Cons 载荷节点指针(非 Cons 返回 null)。仅供释放路径使用。 pub fn cons_node_ptr(&self) -> *mut ConsNode { - match &self.kind { - StringKind::Flat(_) => std::ptr::null_mut(), - StringKind::Cons(node) => *node as *mut ConsNode, + if self.tag != TAG_CONS { + return std::ptr::null_mut(); } + // SAFETY: tag 为 CONS 时载荷首字即节点指针,由 new_cons 创建且存活。 + self.cons_ptr() as *mut ConsNode } /// 释放 Cons 载荷节点(连带扁平化产物)。须在所属 `JsString` 的 - /// `Box::from_raw` 之前调用且恰好一次;Flat 串传 null 无操作。 + /// `Box::from_raw` 之前调用且恰好一次;非 Cons 串传 null 无操作。 /// /// # Safety /// `node` 必须是 [`JsString::new_cons`] 经 `Box::into_raw` 产生的指针,且 @@ -127,7 +246,7 @@ impl JsString { if node.is_null() { return; } - let flat = (*node).flat_cache.get().copied().unwrap_or(std::ptr::null()); + let flat = (*node).flat_cache_ptr(); if !flat.is_null() { // SAFETY: 产物由本节点 Box::into_raw 创建,且只随本节点释放一次。 drop(Box::from_raw(flat as *mut JsString)); @@ -135,134 +254,212 @@ impl JsString { drop(Box::from_raw(node)); } - /// 字符串的字节长度(非字符数)。Flat 直读;Cons O(1) 取构造时缓存。 - pub fn len(&self) -> usize { - match &self.kind { - StringKind::Flat(data) => data.len(), - StringKind::Cons(node) => { - // SAFETY: node 由 new_cons 创建且存活期覆盖本借用。 - unsafe { (**node).byte_len as usize } - } - } - } - - /// JS 字符串长度(UTF-16 code unit 数)。首次访问计算并缓存,此后 O(1)。 - /// Cons 首次访问触发扁平化(见 [`Self::flat_str`]),取产物长度缓存。 + /// JS 字符串长度(UTF-16 code unit 数)。Flat 首次访问扫描并缓存, + /// 此后恒 O(1);FlatU16/Cons 构造时已预写。 pub fn utf16_len(&self) -> u32 { + if self.tag != TAG_FLAT { + return self.utf16_len.load(Ordering::Relaxed); + } let cached = self.utf16_len.load(Ordering::Relaxed); if cached != UTF16_LEN_UNSET { return cached; } - let len = self.flat_str().encode_utf16().count() as u32; + let s = self.flat_ref(); + let len = if s.is_ascii() { s.len() as u32 } else { s.encode_utf16().count() as u32 }; self.utf16_len.store(len, Ordering::Relaxed); len } /// 是否为空字符串。 pub fn is_empty(&self) -> bool { - self.len() == 0 + self.utf16_len() == 0 } - /// 整块 UTF-8 文本的借用。Cons 未扁平化时惰性扁平化并发布缓存后返回产物文本; - /// 返回的 `&str` 借用期 = `&self` 借用期(产物随本节点存活)。 + /// 整块 UTF-8 文本的借用。**仅 Flat 形态**:FlatU16/Cons 无 `&str` 视图 + /// (内容含孤立 surrogate 或需扁平化),一律改走 [`Self::units`] / + /// [`Self::as_lossy_str`]。 + /// + /// # Panics + /// 非 Flat 形态 panic(调用契约违反,非运行时正常路径)。 pub fn as_str(&self) -> &str { - self.flat_str() + if self.tag != TAG_FLAT { + panic!("JsString::as_str 仅限 Flat 形态"); + } + self.flat_ref() } - /// 整块文本的借用(`as_str` 的显式别名):Flat 直读零开销;Cons 读 - /// `flat_cache`,未命中则迭代展开扁平化(显式栈防深链爆栈)后原子发布。 - pub fn flat_str(&self) -> &str { - match &self.kind { - StringKind::Flat(data) => data.as_str(), - StringKind::Cons(node) => { - // SAFETY: node 由 new_cons 创建且存活期覆盖本借用。 - let node = unsafe { &**node }; - if let Some(flat) = node.flat_cache.get() { - // SAFETY: 产物由本节点 Box::into_raw 创建且只随本节点释放, - // 存活期覆盖本次 &self 借用。 - let flat = unsafe { &**flat }; - return match &flat.kind { - StringKind::Flat(data) => data.as_str(), - StringKind::Cons(_) => unreachable!("扁平化产物恒为 Flat"), - }; - } - let text = self.flatten_text(); - let flat_ptr = Box::into_raw(Box::new(JsString::new(text))); - if node.flat_cache.set(flat_ptr).is_err() { - // 并发首用竞态:另一线程已发布,本线程产物未暴露给任何调用方, - // 恰好释放一次。 - // SAFETY: flat_ptr 来自本线程的 Box::into_raw,无外部引用。 - unsafe { drop(Box::from_raw(flat_ptr)) }; - } - // SAFETY: 上述 set 后缓存必已发布(本线程或他线程)。 - let flat = unsafe { &**node.flat_cache.get().unwrap() }; - match &flat.kind { - StringKind::Flat(data) => data.as_str(), - StringKind::Cons(_) => unreachable!("扁平化产物恒为 Flat"), + /// FlatU16 单元载荷的零拷贝借用;仅 FlatU16 形态返回 Some + /// (Flat 需编码、Cons 需扁平化,热路径经本方法免判形态)。 + pub fn units_borrowed(&self) -> Option<&[u16]> { + (self.tag == TAG_FLAT_U16).then(|| self.flat_u16_ref()).map(|v| &**v) + } + + /// 整块单元序列视图:FlatU16 零拷贝;Flat 即时编码(ASCII 走 u8→u16); + /// Cons 未扁平化时迭代展开(显式栈中序,防深链爆栈)发布 FlatU16 产物缓存 + /// 后返回其零拷贝借用(缓存随本节点存活)。 + pub fn units(&self) -> std::borrow::Cow<'_, [u16]> { + match self.tag { + TAG_FLAT => { + let s = self.flat_ref(); + if s.is_ascii() { + std::borrow::Cow::Owned(s.as_bytes().iter().map(|&b| b as u16).collect()) + } else { + std::borrow::Cow::Owned(s.encode_utf16().collect()) } } + TAG_FLAT_U16 => std::borrow::Cow::Borrowed(self.flat_u16_ref()), + TAG_CONS => { + let node = unsafe { &*self.cons_ptr() }; + if let Some(cached) = node.flat_cache.get() { + // SAFETY: 产物由本节点 Box::into_raw 创建且只随本节点释放, + // 存活期覆盖本次 &self 借用;扁平化产物恒为 FlatU16 形态。 + let cached = unsafe { &**cached }; + return std::borrow::Cow::Borrowed(cached.flat_u16_ref()); + } + let units = self.flatten_units(); + let product = Box::into_raw(Box::new(JsString::new_flat_u16(units))); + if node.flat_cache.set(product).is_err() { + // 并发首用竞态:另一线程已发布,本线程产物未暴露给任何调用方, + // 恰好释放一次。 + // SAFETY: product 来自本线程的 Box::into_raw,无外部引用。 + unsafe { drop(Box::from_raw(product)) }; + } + // 上述 set 后缓存必已发布(本线程或他线程),产物恒 FlatU16。 + let cached = unsafe { &**node.flat_cache.get().unwrap() }; + std::borrow::Cow::Borrowed(cached.flat_u16_ref()) + } + _ => unreachable!(), } } - /// 整块文本的 owned 副本(低频路径:错误消息 / 格式化 / 属性键桥接)。 + /// 整块文本的 lossy 借用:Flat 直读(内容 well-formed,无损);其余形态按 + /// 单元→char 映射(孤立 surrogate 替 U+FFFD)——供展示/解析/错误消息, + /// 语义消费一律走 [`Self::units`]。 + pub fn as_lossy_str(&self) -> std::borrow::Cow<'_, str> { + if self.tag == TAG_FLAT { + return std::borrow::Cow::Borrowed(self.flat_ref()); + } + let units = self.units(); + std::borrow::Cow::Owned(match String::from_utf16(&units) { + Ok(s) => s, + Err(_) => units.iter().map(|&u| char::from_u32(u as u32).unwrap_or('\u{FFFD}')).collect(), + }) + } + + /// 整块文本的 lossy owned 副本(低频路径:错误消息 / 格式化 / 源码桥接)。 pub fn to_owned_string(&self) -> String { - self.flat_str().to_string() + self.as_lossy_str().into_owned() + } + + /// 是否含孤立 surrogate 单元(well-formed 判定的取反)。 + /// + /// Flat 恒 false(不变式:UTF-8 载荷结构上不可承载);FlatU16 恒 true + /// (保守口径:直构造路径可能持有 well-formed 单元,单元消费路径对 + /// well-formed 内容结果仍正确,仅多走单元通道);Cons 扁平化后扫描。 + pub fn has_lone_surrogate(&self) -> bool { + match self.tag { + TAG_FLAT => false, + TAG_FLAT_U16 => true, + TAG_CONS => { + let units = self.units(); + units_have_lone_surrogate(&units) + } + _ => unreachable!(), + } + } + + /// 载荷的记账字节数(GC 账目口径):Flat = UTF-8 内容字节;FlatU16 / + /// Cons = 单元数 × 2。 + pub fn payload_bytes(&self) -> usize { + match self.tag { + TAG_FLAT => self.flat_ref().len(), + TAG_FLAT_U16 => self.flat_u16_ref().len() * 2, + TAG_CONS => (unsafe { (*self.cons_ptr()).unit_len as usize }) * 2, + _ => unreachable!(), + } + } + + /// 是否为 Flat(UTF-8 文本)形态。 + pub fn is_flat(&self) -> bool { + self.tag == TAG_FLAT + } + + /// 是否为 FlatU16(单元载荷)形态。 + pub fn is_flat_u16(&self) -> bool { + self.tag == TAG_FLAT_U16 } /// 是否为 Cons(rope)节点。 pub fn is_cons(&self) -> bool { - matches!(self.kind, StringKind::Cons(_)) + self.tag == TAG_CONS } - /// Cons 左右子节点指针对(Flat 返回双 null)。仅供 GC 传播闭包使用。 + /// Cons 左右子节点指针对(非 Cons 返回双 null)。仅供 GC 传播闭包使用。 pub fn cons_children(&self) -> [*const JsString; 2] { - match &self.kind { - StringKind::Flat(_) => [std::ptr::null(), std::ptr::null()], - StringKind::Cons(node) => { - // SAFETY: node 由 new_cons 创建且存活期覆盖本借用。 - let node = unsafe { &**node }; - [node.left, node.right] - } + if self.tag != TAG_CONS { + return [std::ptr::null(), std::ptr::null()]; } + let node = unsafe { &*self.cons_ptr() }; + [node.left, node.right] } /// 扁平化产物指针(未扁平化返回 null)。仅供 GC 传播与释放路径使用。 pub fn flat_cache_ptr(&self) -> *const JsString { - match &self.kind { - StringKind::Flat(_) => std::ptr::null(), - StringKind::Cons(node) => { - // SAFETY: node 由 new_cons 创建且存活期覆盖本借用。 - unsafe { (**node).flat_cache.get().copied().unwrap_or(std::ptr::null()) } - } + if self.tag != TAG_CONS { + return std::ptr::null(); } + unsafe { (*self.cons_ptr()).flat_cache_ptr() } } - /// 迭代展开 Cons 子树为整块文本(显式栈中序收集,防深链递归爆栈)。 - fn flatten_text(&self) -> String { + /// 迭代展开 Cons 子树为整块单元序列(显式栈中序收集,防深链递归爆栈)。 + fn flatten_units(&self) -> Vec { let mut stack = Vec::with_capacity(8); stack.push(self as *const JsString); - let mut buf = String::with_capacity(self.len()); + let mut buf: Vec = Vec::with_capacity(self.utf16_len() as usize); while let Some(ptr) = stack.pop() { // SAFETY: 树内节点由 GC 传播保证随本节点存活;展开期子树无并发修改。 let node = unsafe { &*ptr }; - match &node.kind { - StringKind::Flat(data) => buf.push_str(data.as_str()), - StringKind::Cons(child) => { + match node.tag { + TAG_FLAT => buf.extend(node.flat_ref().encode_utf16()), + TAG_FLAT_U16 => buf.extend_from_slice(node.flat_u16_ref()), + TAG_CONS => { // 右子先入栈、左子后入,保证左子先出(中序顺序)。 // SAFETY: child 由 new_cons 创建且随父存活。 - let child = unsafe { &**child }; + let child = unsafe { &*node.cons_ptr() }; stack.push(child.right); stack.push(child.left); } + _ => unreachable!(), } } buf } } +/// 扫描单元序列是否含孤立 surrogate(high 后须紧跟 low 成对;low 不得独立出现)。 +fn units_have_lone_surrogate(units: &[u16]) -> bool { + let mut i = 0; + while i < units.len() { + let u = units[i]; + if (0xD800..=0xDBFF).contains(&u) { + if i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) { + i += 2; + } else { + return true; + } + } else if (0xDC00..=0xDFFF).contains(&u) { + return true; + } else { + i += 1; + } + } + false +} + // SAFETY: 与 `StringPtr`(string_forge.rs)同款论证——Cons 节点只存在于单线程 -// session VM 内;跨线程共享的 perm 串恒为 Flat,且其 flat_cache 永不写入 -// (perm 串只读共享,无任何可变路径)。裸指针字段不引入跨线程数据竞争。 +// session VM 内;跨线程共享的 perm 串载荷为 Flat/FlatU16(无 ConsNode 裸指针 +// 写路径),flat_cache 永不写入(perm 串只读共享,无任何可变路径)。裸指针 +// 字段不引入跨线程数据竞争。 unsafe impl Send for JsString {} unsafe impl Sync for JsString {} @@ -1939,19 +2136,60 @@ mod tests { #[test] fn js_string_cons_basic() { - // 左右子 Flat 构造 Cons:len/utf16_len/is_empty/flat 全部按拼接语义。 + // 左右子 Flat 构造 Cons:utf16_len/is_empty/文本视图全部按拼接语义。 let left = Box::new(JsString::new("ab".to_string())); let right = Box::new(JsString::new("cd".to_string())); let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) }); - assert_eq!(cons.len(), 4); - assert!(!cons.is_empty()); assert_eq!(cons.utf16_len(), 4); - assert_eq!(cons.flat_str(), "abcd"); - assert_eq!(cons.as_str(), "abcd"); + assert!(!cons.is_empty()); + assert_eq!(cons.as_lossy_str(), "abcd"); assert_eq!(cons.to_owned_string(), "abcd"); assert!(cons.is_cons()); - // 扁平化产物缓存命中:内容仍一致。 - assert_eq!(cons.flat_str(), "abcd"); + // 扁平化产物缓存命中:内容仍一致(units 视图同样走缓存)。 + assert_eq!(cons.as_lossy_str(), "abcd"); + assert_eq!(cons.units().as_ref(), &['a' as u16, 'b' as u16, 'c' as u16, 'd' as u16]); + } + + #[test] + fn js_string_layout_anchor_and_tags() { + // 32B 锚 + 三形态 tag/utf16_len 预写口径(布局变更先同步头注释再改断言)。 + let flat = JsString::new("ab".to_string()); + assert_eq!(std::mem::size_of::(), 32); + assert!(flat.is_flat() && !flat.is_cons() && !flat.is_flat_u16()); + assert_eq!(flat.tag, TAG_FLAT); + assert_eq!(flat.utf16_len(), 2); + assert_eq!(flat.payload_bytes(), 2); + + let u16 = JsString::new_flat_u16(vec![0xD800, 0xDC00]); + assert!(u16.is_flat_u16()); + assert_eq!(u16.tag, TAG_FLAT_U16); + assert_eq!(u16.utf16_len(), 2); + assert_eq!(u16.payload_bytes(), 4); + assert_eq!(u16.units_borrowed(), Some(&[0xD800, 0xDC00][..])); + // 代理对 well-formed:smart 路由落 Flat 且内容等价。 + let smart = JsString::from_units(vec![0xD800, 0xDC00]); + assert!(smart.is_flat()); + assert_eq!(smart.as_str(), "\u{10000}"); + // 孤立 surrogate:smart 路由落 FlatU16。 + let lone = JsString::from_units(vec![0xD800]); + assert!(lone.is_flat_u16()); + assert!(lone.has_lone_surrogate()); + assert_eq!(lone.units().as_ref(), &[0xD800]); + assert_eq!(lone.as_lossy_str(), "\u{FFFD}"); + + let left = Box::new(JsString::new("ab".to_string())); + let right = Box::new(JsString::new("cd".to_string())); + let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) }); + assert!(cons.is_cons()); + assert_eq!(cons.tag, TAG_CONS); + assert_eq!(cons.utf16_len(), 4, "Cons 单元长构造期 eager 归纳"); + assert_eq!(cons.payload_bytes(), 8); + assert!(!cons.has_lone_surrogate()); + // Cons 扁平化产物恒 FlatU16:units 零拷贝借用来自缓存。 + let u16_node = cons.units().into_owned(); + assert_eq!(u16_node, vec![0x61, 0x62, 0x63, 0x64]); + assert!(!cons.flat_cache_ptr().is_null()); + unsafe { JsString::drop_cons_node(cons.cons_node_ptr()) }; } #[test] @@ -1968,7 +2206,7 @@ mod tests { chain_ptr = cons.as_ref() as *const JsString; nodes.push(cons); } - let text = nodes.last().unwrap().flat_str(); + let text = nodes.last().unwrap().as_lossy_str(); assert_eq!(text.len(), 1 + 2000); assert!(text.starts_with('x')); assert!(text.ends_with('y')); @@ -1981,9 +2219,8 @@ mod tests { let right = Box::new(JsString::new(String::new())); let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) }); assert!(cons.is_empty()); - assert_eq!(cons.len(), 0); - assert_eq!(cons.flat_str(), ""); assert_eq!(cons.utf16_len(), 0); + assert_eq!(cons.as_lossy_str(), ""); } #[test] diff --git a/crates/oxide_vm/src/dispatch/arithmetic.rs b/crates/oxide_vm/src/dispatch/arithmetic.rs index c778534c..195bddce 100644 --- a/crates/oxide_vm/src/dispatch/arithmetic.rs +++ b/crates/oxide_vm/src/dispatch/arithmetic.rs @@ -99,11 +99,11 @@ impl Vm { } /// 字符串拼接接线点(二元 `+`/`+=` 共用):O(1) 链接为 Cons(rope)节点, - /// 消除逐次整串重拷贝。文本在消费时(`.length`/`==`/string_ref 等)惰性扁平化。 + /// 消除逐次整串重拷贝。单元序列在消费时(`.length`/`==`/单元读取等)惰性扁平化。 /// /// # 步骤 - /// 1. 非字符串操作数先转成叶子字符串(`push_to_string` 同文本语义;小整数走 - /// 永久缓存零分配,其余 `to_string` + owned)。 + /// 1. 非字符串操作数先转成叶子字符串(`to_string` 同文本语义;小整数走 + /// 永久缓存零分配,其余 owned)。 /// 2. 两个字符串经 `new_cons_string` 链接(每链接 1 次节点分配,对比原 O(n) /// 拷贝 + 结果串分配)。 /// @@ -112,18 +112,26 @@ impl Vm { /// dispatch 指令边界(结果已写寄存器后检查),链接期子节点天然安全, /// 无需在途保护。 fn concat_strings(&mut self, lhs: JsValue, rhs: JsValue) -> JsValue { - // 小链急切扁平(与 rope 前同款实现):总字节长 ≤ 阈值时单次预分配 - // 写齐,零叶子转换 / Cons 节点 / 惰性扁平化开销——拼接即消费的小串 - // 场景与基线逐字节等价(该场景 rope 的 O(1) 链接收益低于其固定开销)。 - let lbytes = if lhs.is_string() { unsafe { (*lhs.as_string_ptr()).len() } } else { 0 }; - let rbytes = if rhs.is_string() { unsafe { (*rhs.as_string_ptr()).len() } } else { 0 }; - if lbytes + rbytes <= Self::CONS_FLATTEN_BYTES { - let mut buf = String::with_capacity(lbytes + rbytes + 32); - coercion::push_to_string(lhs, &mut buf); - coercion::push_to_string(rhs, &mut buf); - return self.new_string_owned(buf); + // 小链急切扁平:总单元长 ≤ 阈值时单次预分配写齐,零叶子转换 / Cons 节点 / + // 惰性扁平化开销——良形小串场景载荷与旧字节路径逐位一致(该场景 rope 的 + // O(1) 链接收益低于其固定开销)。 + let lu = if lhs.is_string() { + unsafe { (*lhs.as_string_ptr()).utf16_len() as usize } + } else { + 0 + }; + let ru = if rhs.is_string() { + unsafe { (*rhs.as_string_ptr()).utf16_len() as usize } + } else { + 0 + }; + if lu + ru <= Self::CONS_FLATTEN_UNITS { + let mut units = Vec::with_capacity(lu + ru); + coercion::push_units_to(lhs, &mut units); + coercion::push_units_to(rhs, &mut units); + return self.new_string_units_owned(units); } - // 大链走 Cons rope:非字符串操作数先转叶子,再 O(1) 链接(文本惰性扁平化)。 + // 大链走 Cons rope:非字符串操作数先转叶子,再 O(1) 链接(单元惰性扁平化)。 let lv = if lhs.is_string() { lhs } else { self.string_leaf(lhs) }; let rv = if rhs.is_string() { rhs } else { self.string_leaf(rhs) }; self.new_cons_string(lv, rv) @@ -227,9 +235,9 @@ impl Vm { /// /// # 步骤 /// 1. 剩余操作数按序 coerce(对象 ToPrimitive 副作用顺序与左结合一致,抛错点先于分配)。 - /// 2. 精确总长:字符串操作数取字节长,非字符串给 32 字节余量(int/double/bool/null/ - /// undefined 的十进制文本上界;BigInt 超长时 String 自动扩容兜底)。 - /// 3. 单趟 push_to_string + new_string_owned(零二次拷贝)。 + /// 2. 精确总长:字符串操作数取单元长,非字符串给 32 单元余量(int/double/bool/null/ + /// undefined 的十进制文本上界;BigInt 超长时 Vec 自动扩容兜底)。 + /// 3. 单趟单元展开 + 智能路由创建(零二次拷贝;良形内容落 Flat 与旧路径逐位一致)。 /// /// # 副作用 /// 新建一个会话字符串。 @@ -245,16 +253,16 @@ impl Vm { let cap = parts.iter().fold(0usize, |acc, p| { acc + if p.is_string() { // SAFETY: p 是字符串值。 - unsafe { (*p.as_string_ptr()).len() } + unsafe { (*p.as_string_ptr()).utf16_len() as usize } } else { 32 } }); - let mut buf = String::with_capacity(cap); + let mut units = Vec::with_capacity(cap); for p in &parts { - coercion::push_to_string(*p, &mut buf); + coercion::push_units_to(*p, &mut units); } - Ok(self.new_string_owned(buf)) + Ok(self.new_string_units_owned(units)) } #[inline(always)] diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index 14292d87..b6fd7717 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -44,8 +44,9 @@ impl Vm { self.regs[rd] = val; Ok(false) } else { - // SAFETY: key_val 已校验为字符串值,as_str 桥接 JsString 内容。 - let name = unsafe { (*key_val.as_string_ptr()).as_str() }; + // SAFETY: key_val 已校验为字符串值;错误消息走 lossy 文本(不可解析名 + // 含孤立 surrogate 时映射为 FFFD,仅为展示用途)。 + let name = unsafe { (*key_val.as_string_ptr()).as_lossy_str() }; self.raise_error_kind("ReferenceError", &format!("{name} is not defined"))?; Ok(true) } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 705e83af..a71e50d0 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -541,7 +541,7 @@ impl SessionGc { /// `ptr` 必须是 `Box::into_raw(Box::new(JsString))` 产生的非空指针,仍存在 /// 于 `session_string_ptrs`,且恰好释放一次。 unsafe fn drop_dead_session_string(ptr: *mut JsString) -> u64 { - let bytes = (size_of::() + (*ptr).len()) as u64; + let bytes = (size_of::() + (*ptr).payload_bytes()) as u64; Self::drop_session_string_box(ptr); bytes } @@ -701,7 +701,7 @@ impl SessionGc { if self.live_strings.contains(&ptr) { // 存活——地址不变,无需重写。 // SAFETY: ptr 是仍归 VM 所有的存活 session 字符串 box。 - live_bytes += unsafe { size_of::() + (*ptr).len() }; + live_bytes += unsafe { size_of::() + (*ptr).payload_bytes() }; live.push(ptr); } else { // SAFETY: ptr 在 session_string_ptrs 中但不可达,恰好释放一次。 @@ -1032,7 +1032,7 @@ impl SessionGc { continue; } // SAFETY: ptr 在字符串表登记,收尾前有效。 - string_bytes += size_of::() + unsafe { (*ptr).len() }; + string_bytes += size_of::() + unsafe { (*ptr).payload_bytes() }; } let bigint_bytes = vm.gc_state.session_bigint_ptrs.borrow().len() * size_of::(); vm.gc_state.session_bytes_allocated = object_bytes + string_bytes + bigint_bytes; @@ -2311,7 +2311,7 @@ mod tests { vm.maybe_collect_session_strings(); // 账目 = 对象头 + 存活串(size_of::() + len),死串不再计入。 - let expected = size_of::() + (size_of::() + unsafe { (*live_ptr).len() }); + let expected = size_of::() + (size_of::() + unsafe { (*live_ptr).payload_bytes() }); assert_eq!(vm.gc_state.session_bytes_allocated, expected); assert!(!vm.gc_state.session_string_ptrs.contains(&dead.as_string_ptr_mut())); } @@ -2349,7 +2349,7 @@ mod tests { assert!(vm.gc_state.session_string_ptrs.contains(&parent_ptr)); assert!(vm.gc_state.session_string_ptrs.contains(&l_ptr)); assert!(vm.gc_state.session_string_ptrs.contains(&r_ptr)); - assert_eq!(unsafe { (*parent_ptr).as_str() }, "left-partright-part"); + assert_eq!(unsafe { (*parent_ptr).as_lossy_str() }, "left-partright-part"); } #[test] @@ -2373,7 +2373,7 @@ mod tests { let (parent, _, _) = make_cons_pair(&mut vm, "left-part", "right-part"); let parent_ptr = parent.as_string_ptr_mut(); // 触发扁平化:产物发布到 flat_cache。 - assert_eq!(unsafe { (*parent_ptr).flat_str() }, "left-partright-part"); + assert_eq!(unsafe { (*parent_ptr).as_lossy_str() }, "left-partright-part"); let flat_ptr = unsafe { (*parent_ptr).flat_cache_ptr() }; assert!(!flat_ptr.is_null()); @@ -2400,7 +2400,7 @@ mod tests { collect(&mut vm); assert!(vm.gc_state.session_string_ptrs.contains(&chain_ptr)); - assert_eq!(unsafe { (*chain_ptr).as_str() }, format!("root{}", "x".repeat(1024))); + assert_eq!(unsafe { (*chain_ptr).as_lossy_str() }, format!("root{}", "x".repeat(1024))); } #[test] @@ -2418,7 +2418,7 @@ mod tests { // perm 子节点永不释放(不在 session 表);session 子节点随父存活。 assert!(!vm.gc_state.session_string_ptrs.contains(&perm_ptr)); assert!(vm.gc_state.session_string_ptrs.contains(&session_ptr)); - assert_eq!(unsafe { (*parent.as_string_ptr_mut()).as_str() }, "perm-leafsession-leaf"); + assert_eq!(unsafe { (*parent.as_string_ptr_mut()).as_lossy_str() }, "perm-leafsession-leaf"); } #[test] @@ -2426,7 +2426,7 @@ mod tests { let mut vm = Vm::new(); let (parent, _, _) = make_cons_pair(&mut vm, "left-part", "right-part"); // 触发扁平化(产物挂在父上)。 - assert_eq!(unsafe { (*parent.as_string_ptr_mut()).flat_str() }, "left-partright-part"); + assert_eq!(unsafe { (*parent.as_string_ptr_mut()).as_lossy_str() }, "left-partright-part"); vm.regs[0] = parent; // full_reset 清空全部 session 字符串(连带产物):无泄漏、无 double-free。 diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index c8c320e4..cb45c301 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -53,6 +53,33 @@ fn canonical_index_of(s: &str) -> Option { } } +/// 从单元序列判定字符串是否为规范数组下标(无前导零的纯 ASCII 数字串), +/// 并反解其值。口径与 [`canonical_index_of`] 一致,单元口径覆盖非 Flat +/// 载荷的键推导。只覆盖 `[0, INT_KEY_COUNT)`,更大的数字串走普通字符串键。 +fn canonical_index_units(units: &[u16]) -> Option { + if units.is_empty() { + return None; + } + if !matches!(units[0], 0x30..=0x39) { + return None; + } + if units.len() > 1 && units[0] == 0x30 { + return None; + } + if units.len() > 10 { + return None; + } + let mut v: u32 = 0; + for &u in units.iter() { + let d = match u { + 0x30..=0x39 => u - 0x30, + _ => return None, + }; + v = v.checked_mul(10)? + d as u32; + } + (v < INT_KEY_COUNT).then_some(v) +} + /// 将 [`NativeFnPtr`] 转换为可调用的 [`NativeFn`]。 /// /// # Safety @@ -1169,7 +1196,7 @@ impl Vm { } for &ptr in &self.gc_state.session_string_ptrs { // SAFETY: ptr 来自字符串表登记,收尾前始终有效。 - bytes += (std::mem::size_of::() + unsafe { (*ptr).len() }) as u64; + bytes += (std::mem::size_of::() + unsafe { (*ptr).payload_bytes() }) as u64; } bytes += (self.gc_state.session_bigint_ptrs.borrow().len() * std::mem::size_of::()) as u64; bytes += (self.gc_state.session_cell_ptrs.borrow().len() * std::mem::size_of::()) as u64; @@ -1362,12 +1389,18 @@ impl Vm { return Ok(make_int_key(d as u32)); } } else if val.is_string() { - // SAFETY: val 是字符串值,把其内容桥接为永久 key id(rope 经惰性扁平化)。 - let s = unsafe { (*val.as_string_ptr()).as_str() }; - if let Some(i) = canonical_index_of(s) { - return Ok(make_int_key(i)); + // SAFETY: val 是字符串值,按载荷形态桥接为永久 key id。 + let s = unsafe { &*val.as_string_ptr() }; + if s.is_flat() { + // Flat:良形 UTF-8 文本,键推导与旧路径逐位一致。 + let text = s.as_str(); + if let Some(i) = canonical_index_of(text) { + return Ok(make_int_key(i)); + } + return Ok(self.kernel_core.perm_interner().intern(text).0); } - return Ok(self.kernel_core.perm_interner().intern(s).0); + // 单元载荷:键推导同规范(见 string_key_units)。 + return Ok(self.string_key_units(&s.units())); } // Symbol 值直接编码为 Symbol 键(不进字符串 interner,键相互独立)。 if val.is_symbol() { @@ -1380,18 +1413,30 @@ impl Vm { return Ok(make_well_known_symbol_key(id)); } // ToPropertyKey:对象经 ToPrimitive(string hint),结果为 Symbol 时直接作键; - // 其余字符串经规范化(规范数字串映射整数键)与字符串分支统一口径。 + // 其余字符串按单元序列推导键(避免 lossy 文本桥接破坏孤立 surrogate 键)。 let prim = coercion::to_primitive(val, coercion::ToPrimitiveHint::String, self)?; if prim.is_symbol() { return Ok(make_symbol_key(prim.as_symbol_index())); } - let key = coercion::to_string(prim); - return Ok(oxide_runtime_api::VmHost::string_key_si(self, &key)); + let units = oxide_runtime_api::to_units_full(prim, self)?; + return Ok(self.string_key_units(&units)); } // 其它原始值(BigInt 等):ToPropertyKey 一律转字符串并走规范化,避免与 // 数字键区间分裂(`o[5n]` 与 `o["5"]`/`o[5]` 必须同键)。 - let key = coercion::to_string(val); - Ok(oxide_runtime_api::VmHost::string_key_si(self, &key)) + let units = oxide_runtime_api::to_units_full(val, self)?; + Ok(self.string_key_units(&units)) + } + + /// 从单元序列推导属性键 si(`property_key_si` 字符串分支的口径抽取): + /// 规范数组下标 → 整数键,其余以 `encode_key` 形态入键空间(孤立 surrogate + /// 以转义形态区分,不与良形键碰撞)。 + pub(crate) fn string_key_units(&self, units: &[u16]) -> u32 { + if let Some(i) = canonical_index_units(units) { + make_int_key(i) + } else { + let key = oxide_kernel::string_forge::encode_key(units); + self.kernel_core.perm_interner().intern(&key).0 + } } pub(crate) fn array_index_from_property_key(&self, prop_name_si: u32) -> Option { @@ -2496,12 +2541,6 @@ impl oxide_runtime_api::VmHost for Vm { fn new_string_owned(&mut self, s: String) -> JsValue { Vm::new_string_owned(self, s) } - fn string_ref(&self, val: JsValue) -> &str { - // SAFETY: 调用方保证 val 为字符串值。perm 串由内核持有永不释放;session - // 串仅经 &mut self 路径(new_string/new_string_owned/GC)释放,此处 &self - // 借用期间编译器强制不存在 &mut 存续,字符串不会在借用期内回收。 - unsafe { (*val.as_string_ptr()).as_str() } - } fn new_bigint(&mut self, v: num_bigint::BigInt) -> JsValue { Vm::new_bigint(self, v) } @@ -2542,6 +2581,18 @@ impl oxide_runtime_api::VmHost for Vm { self.kernel_core.perm_interner().intern(s).0 } } + fn string_units(&self, val: JsValue) -> std::borrow::Cow<'_, [u16]> { + // SAFETY: 调用方保证 val 为字符串值。perm 串由内核持有永不释放;session + // 串仅经 &mut self 路径(new_string/GC)释放,&self 借用期间编译器强制 + // 不存在 &mut 存续,字符串不会在借用期内回收。 + unsafe { (*val.as_string_ptr()).units() } + } + fn new_string_units(&mut self, units: &[u16]) -> JsValue { + Vm::new_string_units(self, units) + } + fn new_string_units_owned(&mut self, units: Vec) -> JsValue { + Vm::new_string_units_owned(self, units) + } fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option { self.resolve_property(obj, prop_name_si) } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index fe61bcf3..ae27a206 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -1,6 +1,6 @@ use crate::vm::{ForInIter, FrameArgs, FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH}; use crate::vm_trace; -use oxide_runtime_api::{push_to_string, to_boolean, to_string_full, VmHost}; +use oxide_runtime_api::{push_units_to, to_boolean, to_units_full}; use oxide_types::object::{JsObject, PropAttributes}; use oxide_types::private_key::{int_key_value, is_int_key, is_private_name_key, is_symbol_key, make_int_key}; use oxide_types::value::JsValue; @@ -267,7 +267,9 @@ impl Vm { let segment_count = (header >> 16) as usize; let len_hint = (header & 0xFFFF) as usize; - let mut result = String::with_capacity(len_hint.max(16)); + // 单元缓冲:字符串操作数按单元序列展开(lone surrogate 保真), + // len_hint 为单元数口径(emit 侧同口径)。 + let mut units: Vec = Vec::with_capacity(len_hint.max(16)); for _ in 0..segment_count { let seg = self.bytecode[self.pc]; self.pc += 1; @@ -275,17 +277,16 @@ impl Vm { let reg = (seg & 0x7FFF_FFFF) as usize; let val = self.regs[reg]; if val.is_string() { - // SAFETY: val 是字符串值;借用仅在本次 push 内消费,不跨分配点。 - let s = unsafe { (*val.as_string_ptr()).as_str() }; - result.push_str(s); + // SAFETY: val 是字符串值;借用仅在本次展开内消费,不跨分配点。 + let s = unsafe { &*val.as_string_ptr() }; + units.extend_from_slice(s.units().as_ref()); } else if val.is_object() || val.is_symbol() { - // 对象走完整 ToString(ToPrimitive 副作用顺序);Symbol 由 - // to_string_full 抛 TypeError(push_to_string 无 symbol 分支)。 - let s = to_string_full(val, self)?; - result.push_str(&s); + // 对象走完整 ToString(ToPrimitive 副作用顺序,结果保单元); + // Symbol 抛 TypeError(to_units_full 无 symbol 直写分支)。 + units.extend(to_units_full(val, self)?); } else { // 原始值直写结果缓冲,免中间 String。 - push_to_string(val, &mut result); + push_units_to(val, &mut units); } } else { let const_idx = (seg & 0x7FFF_FFFF) as usize; @@ -293,14 +294,14 @@ impl Vm { if const_idx < imm.len() { let val = imm[const_idx]; if val.is_string() { - // SAFETY: val 是字符串值;借用仅在本次 push 内消费,不跨分配点。 - let s = unsafe { (*val.as_string_ptr()).as_str() }; - result.push_str(s); + // SAFETY: val 是字符串值;借用仅在本次展开内消费,不跨分配点。 + let s = unsafe { &*val.as_string_ptr() }; + units.extend_from_slice(s.units().as_ref()); } } } } - self.regs[rd] = self.new_string_owned(result); + self.regs[rd] = self.new_string_units_owned(units); Ok(()) } @@ -825,16 +826,11 @@ impl Vm { JsValue::from_js_object(proto_ptr), )); if src.is_string() { - let code_units: Vec = unsafe { (*src.as_string_ptr()).as_str().encode_utf16().collect() }; + // SAFETY: src 是字符串值;单元序列展开(lone surrogate 保真)。 + let code_units: Vec = unsafe { (*src.as_string_ptr()).units().into_owned() }; for (i, unit) in code_units.iter().enumerate() { let si = make_int_key(i as u32); - let ch_val = match char::from_u32(*unit as u32) { - Some(c) => match self.single_char(c) { - Some(v) => v, - None => self.new_string(&c.to_string()), - }, - None => self.new_string(""), - }; + let ch_val = self.unit_char_value(*unit); let rest = unsafe { &mut *rest_ptr }; self.set_or_create_prop_value(rest, si, ch_val); } @@ -842,23 +838,34 @@ impl Vm { self.regs[rd] = JsValue::from_js_object(rest_ptr); return Ok(()); } + // 排除名单常量按单元序列展开(lone surrogate 名保真),0x0000 分隔。 let excluded_const = self .immutables() .get(excluded_idx) .and_then(|v| { if v.is_string() { // SAFETY: v 是字符串常量值。 - Some(unsafe { (*v.as_string_ptr()).to_owned_string() }) + Some(unsafe { (*v.as_string_ptr()).units().into_owned() }) } else { None } }) .unwrap_or_default(); - let mut excluded: std::collections::HashSet = excluded_const - .split('\0') - .filter(|s| !s.is_empty()) - .map(|s| self.string_key_si(s)) - .collect(); + let mut excluded: std::collections::HashSet = std::collections::HashSet::new(); + let mut seg: Vec = Vec::new(); + for &u in excluded_const.iter() { + if u == 0x0000 { + if !seg.is_empty() { + excluded.insert(self.string_key_units(&seg)); + seg.clear(); + } + continue; + } + seg.push(u); + } + if !seg.is_empty() { + excluded.insert(self.string_key_units(&seg)); + } // 运行时 excluded:b 槽数组(computed key 求值结果)的元素 ToPropertyKey 后排除。 if b != 0 { let arr_val = self.regs[b]; @@ -883,20 +890,14 @@ impl Vm { // 字符串包装对象:索引字符是可枚举自有属性(ToObject("str") 的 0..len-1)。 if src_obj.type_tag == JsObject::OBJ_TYPE_STRING_OBJ { let raw = src_obj.get_prop_at(0); - let s = unsafe { (*raw.as_string_ptr()).to_owned_string() }; - let code_units: Vec = s.encode_utf16().collect(); + // SAFETY: 包装对象槽 0 恒为字符串值;单元序列展开(lone surrogate 保真)。 + let code_units: Vec = unsafe { (*raw.as_string_ptr()).units().into_owned() }; for (i, unit) in code_units.iter().enumerate() { let si = make_int_key(i as u32); if excluded.contains(&si) { continue; } - let ch_val = match char::from_u32(*unit as u32) { - Some(c) => match self.single_char(c) { - Some(v) => v, - None => self.new_string(&c.to_string()), - }, - None => self.new_string(""), - }; + let ch_val = self.unit_char_value(*unit); assignments.push((si, ch_val)); } } @@ -964,19 +965,14 @@ impl Vm { return Ok(()); } - // 字符串源:按索引复制字符(可枚举索引属性)。 + // 字符串源:按单元下标复制(可枚举索引属性,lone surrogate 保真)。 if src.is_string() { let target = unsafe { &mut *target_val.as_js_object_ptr() }; - let code_units: Vec = unsafe { (*src.as_string_ptr()).as_str().encode_utf16().collect() }; + // SAFETY: src 是字符串值。 + let code_units: Vec = unsafe { (*src.as_string_ptr()).units().into_owned() }; for (i, unit) in code_units.iter().enumerate() { let si = make_int_key(i as u32); - let ch_val = match char::from_u32(*unit as u32) { - Some(c) => match self.single_char(c) { - Some(v) => v, - None => self.new_string(&c.to_string()), - }, - None => self.new_string(""), - }; + let ch_val = self.unit_char_value(*unit); self.set_or_create_prop_value(target, si, ch_val); } return Ok(()); diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 660d7086..e1c5a53a 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -16,10 +16,11 @@ use oxide_types::object::{JsObject, JsString, PropAttributes}; use oxide_types::value::JsValue; impl Vm { - /// Cons(rope)节点保留的字节阈值:拼接总长 ≤ 该值时急切扁平为 Flat。 - /// 小链的"链接 + 首次消费扁平化"双重分配高于直接拷贝,直接扁平更优; - /// 超过阈值后 O(n²) 拷贝成本超过节点开销,Cons 的 O(1) 链接才值得。 - pub(crate) const CONS_FLATTEN_BYTES: usize = 128; + /// Cons(rope)节点保留的单元阈值:拼接总长(UTF-16 单元数)≤ 该值时 + /// 急切扁平。小链的"链接 + 首次消费扁平化"双重分配高于直接拷贝,直接 + /// 扁平更优;超过阈值后 O(n²) 拷贝成本超过节点开销,Cons 的 O(1) 链接 + /// 才值得。 + pub(crate) const CONS_FLATTEN_UNITS: usize = 128; /// 替换本 VM 独占的内建 P 对象:先恰好释放旧副本的属性区一次, /// 再让旧 Arc 归零(旧副本无 Drop 口径兜底,full_reset 重初始化 @@ -640,24 +641,47 @@ impl Vm { /// 接线点调用(CONCAT_N 保持急切扁平,不经此路径)。 /// /// # 副作用 - /// - 登记节点到 session 字符串表(账目 = `size_of::() + byte_len`)。 + /// - 登记节点到 session 字符串表(账目 = `size_of::() + payload_bytes`)。 /// /// # 注意事项 /// - 调用方须保证 `left`/`right` 在调用期间存活:子节点(coerce 新鲜字符串 / /// 新建叶子)要么已是执行根,要么在写入根前不被任何回收点触达——当前 /// 回收点仅在指令边界,链接结果写寄存器先于下一次检查,天然满足。 - /// - 调用方(`concat_strings`)已按字节阈值过滤:仅大链(总长 > - /// [`Self::CONS_FLATTEN_BYTES`])进入本方法,小链由其急切扁平。 + /// - 调用方(`concat_strings`)已按单元阈值过滤:仅大链(总长 > + /// [`Self::CONS_FLATTEN_UNITS`])进入本方法,小链由其急切扁平。 #[inline] pub fn new_cons_string(&mut self, left: JsValue, right: JsValue) -> JsValue { debug_assert!(left.is_string() && right.is_string(), "new_cons_string 只接收字符串操作数"); let left_ptr = left.as_string_ptr_mut(); let right_ptr = right.as_string_ptr_mut(); - // SAFETY: 两指针指向存活的 JsString(调用方保证),len() 各为 O(1)。 - let byte_len = unsafe { (*left_ptr).len() + (*right_ptr).len() }; // SAFETY: new_cons 的调用方(本函数)负责保证子节点随节点存活。 let ptr = Box::into_raw(Box::new(unsafe { JsString::new_cons(left_ptr, right_ptr) })); - self.register_session_string(ptr, byte_len) + // SAFETY: ptr 指向刚创建的存活节点,账目口径 = 单元数 × 2。 + let bytes = unsafe { (*ptr).payload_bytes() }; + self.register_session_string(ptr, bytes) + } + + /// 以单元序列分配可被 session GC 回收的字符串值:智能路由(含孤立 + /// surrogate 落 FlatU16,否则 Flat),语义与副作用同 `new_string_owned`。 + pub fn new_string_units_owned(&mut self, units: Vec) -> JsValue { + let ptr = Box::into_raw(Box::new(JsString::from_units(units))); + // SAFETY: ptr 指向刚创建的存活 JsString。 + let bytes = unsafe { (*ptr).payload_bytes() }; + self.register_session_string(ptr, bytes) + } + + /// 同 `new_string_units_owned`,以借用单元序列接收。 + pub fn new_string_units(&mut self, units: &[u16]) -> JsValue { + self.new_string_units_owned(units.to_vec()) + } + + /// 单单元的属性值:ASCII 单元命中共享 perm 串(零分配、可指针短路), + /// 非 ASCII(含孤立 surrogate)物化为 1 单元会话串。 + pub(crate) fn unit_char_value(&mut self, u: u16) -> JsValue { + if let Some(v) = oxide_runtime_api::VmHost::single_unit(self, u) { + return v; + } + self.new_string_units(&[u]) } /// 把字符串 intern 为永久 key id(属性名/方法名),进程生命周期内稳定。 diff --git a/crates/oxide_vm/tests/builtins_iterator_tests.rs b/crates/oxide_vm/tests/builtins_iterator_tests.rs index d4125f88..24f3dfc4 100644 --- a/crates/oxide_vm/tests/builtins_iterator_tests.rs +++ b/crates/oxide_vm/tests/builtins_iterator_tests.rs @@ -528,9 +528,9 @@ fn symbol_dispose_and_async_dispose_registered() { let result = eval(&mut vm, src).unwrap(); assert_eq!(result.as_bool(), expected, "for {}", src); } - // 描述性名称经 String 构造器反射(well_known_symbol_name 表)。 - let desc = eval(&mut vm, "String(Symbol.dispose)").unwrap(); - assert_eq!(to_str(&vm, desc), "Symbol(Symbol.dispose)"); + // 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"); } #[test] diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index 8cbaf749..7daec193 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -940,25 +940,45 @@ fn string_methods_null_receiver_throw_type_error() { #[test] fn string_slice_astral_character_indices() { // UTF-16 单元索引语义:astral 字符占 2 单元,切片边界落在代理对中间时 - // 保留所在 astral 字符(孤立代理无法在 UTF-8 表示,见架构限制)。 + // 单元原样保留(单元载荷可承载孤立 surrogate,规格口径)。 let mut vm = Vm::new(); - let s = eval(&mut vm, "'\\u{1F600}ab'.slice(1, 3)").unwrap(); - assert_eq!(to_str(&vm, s), "\u{1F600}a"); - let s = eval(&mut vm, "'\\u{1F600}ab'.slice(0, 1)").unwrap(); - assert_eq!(to_str(&vm, s), "\u{1F600}"); + let r = eval( + &mut vm, + "var s = '\\u{1F600}ab'.slice(1, 3); s.length === 2 && s.charCodeAt(0) === 0xDE00 && s.charCodeAt(1) === 0x61", + ) + .unwrap(); + assert!(r.as_bool()); + let r = eval( + &mut vm, + "var s = '\\u{1F600}ab'.slice(0, 1); s.length === 1 && s.charCodeAt(0) === 0xD83D", + ) + .unwrap(); + assert!(r.as_bool()); let s = eval(&mut vm, "'\\u{1F600}'.padStart(3, 'x')").unwrap(); assert_eq!(to_str(&vm, s), "xx\u{1F600}"); } #[test] fn string_char_at_ascii_and_astral() { - // charAt 产出:ASCII 走单字符缓存,astral 按 UTF-16 单元定位(落在代理对 - // 中间时返回所在 astral 字符,架构限制),越界空串,缺省参数取首字符。 + // charAt 按码元定位:单单元子串(代理对各出 1 单元,规格口径—— + // 规格返回单元子串而非 Unicode 标量),越界空串,缺省参数取首字符。 let mut vm = Vm::new(); let s = eval(&mut vm, "'abc'.charAt(1)").unwrap(); assert_eq!(to_str(&vm, s), "b"); - let s = eval(&mut vm, "'\\u{1F600}ab'.charAt(0)").unwrap(); - assert_eq!(to_str(&vm, s), "\u{1F600}"); + let r = eval( + &mut vm, + "'\\u{1F600}ab'.charAt(0).length === 1 && '\\u{1F600}ab'.charAt(0).charCodeAt(0) === 0xD83D", + ) + .unwrap(); + assert!(r.as_bool()); + let r = eval( + &mut vm, + "'\\u{1F600}ab'.charAt(1).length === 1 && '\\u{1F600}ab'.charAt(1).charCodeAt(0) === 0xDE00", + ) + .unwrap(); + assert!(r.as_bool()); + let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(2) === 'a'").unwrap(); + assert!(r.as_bool()); let s = eval(&mut vm, "'ab'.charAt(5)").unwrap(); assert_eq!(to_str(&vm, s), ""); let s = eval(&mut vm, "'xy'.charAt()").unwrap(); @@ -975,13 +995,16 @@ fn string_char_at_eq_perm_string() { #[test] fn string_split_empty_separator_chars() { - // 空分隔 split 逐字符产出:ASCII 走单字符缓存,混合 astral 按标量切分。 + // 空分隔 split 逐码元产出:ASCII 走单字符缓存,代理对各出 1 单元元素 + // (规格口径:空分隔按单元切分,孤立 surrogate 各为独立元素)。 let mut vm = Vm::new(); let s = eval(&mut vm, "'abc'.split('').join('-')").unwrap(); assert_eq!(to_str(&vm, s), "a-b-c"); let result = eval(&mut vm, "'\\u{1F600}a'.split('').length").unwrap(); - assert_eq!(result.as_int(), 2); - let result = eval(&mut vm, "'\\u{1F600}a'.split('')[1]").unwrap(); + assert_eq!(result.as_int(), 3); + let r = eval(&mut vm, "'\\u{1F600}a'.split('')[1].charCodeAt(0) === 0xDE00").unwrap(); + assert!(r.as_bool()); + let result = eval(&mut vm, "'\\u{1F600}a'.split('')[2]").unwrap(); assert_eq!(to_str(&vm, result), "a"); } @@ -1016,10 +1039,17 @@ fn string_utf16_length_char_at_code_at() { assert_eq!(r.as_int(), 0xDE00); let r = eval(&mut vm, "'\\u{1F600}a'.charCodeAt(2)").unwrap(); assert_eq!(r.as_int(), 0x61); - let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(2)").unwrap(); - assert_eq!(to_str(&vm, r), "a"); - let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(1)").unwrap(); - assert_eq!(to_str(&vm, r), "\u{1F600}"); + let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(2) === 'a'").unwrap(); + assert!(r.as_bool()); + // charAt 出单单元子串:代理对位置各出 1 单元(高/低 surrogate)。 + let r = eval( + &mut vm, + "'\\u{1F600}ab'.charAt(0).length === 1 && '\\u{1F600}ab'.charAt(0).charCodeAt(0) === 0xD83D", + ) + .unwrap(); + assert!(r.as_bool()); + let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(1).charCodeAt(0) === 0xDE00").unwrap(); + assert!(r.as_bool()); let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(4)").unwrap(); assert_eq!(to_str(&vm, r), ""); } @@ -1040,7 +1070,8 @@ fn string_utf16_at() { #[test] fn string_utf16_slice_substring_substr() { - // 切片族按 UTF-16 单元计数:边界落在代理对中间时保留所在 astral 字符。 + // 切片族按 UTF-16 单元计数:边界落在代理对中间时单元原样保留(孤立 + // surrogate 由单元载荷承载,规格口径)。 let mut vm = Vm::new(); let s = eval(&mut vm, "'\\u{1F600}ab'.slice(2)").unwrap(); assert_eq!(to_str(&vm, s), "ab"); @@ -1054,8 +1085,12 @@ fn string_utf16_slice_substring_substr() { assert_eq!(to_str(&vm, s), "\u{1F600}"); let s = eval(&mut vm, "'\\u{1F600}ab'.substr(2, 2)").unwrap(); assert_eq!(to_str(&vm, s), "ab"); - let s = eval(&mut vm, "'\\u{1F600}ab'.substr(1, 2)").unwrap(); - assert_eq!(to_str(&vm, s), "\u{1F600}a"); + let r = eval( + &mut vm, + "var s = '\\u{1F600}ab'.substr(1, 2); s.length === 2 && s.charCodeAt(0) === 0xDE00 && s.charCodeAt(1) === 0x61", + ) + .unwrap(); + assert!(r.as_bool()); } #[test]