diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 88f2b731..224af8eb 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -7,461 +7,10 @@ //! 闭包共享 cell (`Cell`)。 use crate::value::JsValue; -use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::OnceLock; -/// 堆分配的 JS 字符串值。 -/// -/// 字符串*值*以 48 位指针(指向 `JsString`)NaN-box(见 `JsValue::string`)。 -/// -/// 三种载荷形态(`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:载荷 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。 -#[repr(C)] -pub struct JsString { - /// 载荷 24B,按 `tag` 解读:Flat = `String`(data/len/cap)、FlatU16 = - /// `Vec`(ptr/len/cap,与 String 同形)、Cons = 节点指针(首字, - /// 余 16B 未定义填充)。 - payload: [u64; 3], - /// JS 语义的字符串长度(UTF-16 code unit 数)。 - /// - /// Flat 为懒缓存:构造是热路径(拼接/格式化),长度查询少见,不预付 O(n) - /// 扫描,首次 `utf16_len()` 访问时计算一次(sentinel + `AtomicU32`:perm - /// 串可被多线程 VM 共享读,计算幂等,relaxed 序即可)。FlatU16/Cons 构造时 - /// 预写(单元数 / 子节点归纳,各 O(1))。 - utf16_len: AtomicU32, - tag: u8, -} +mod string; -// 布局锚: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 - && std::mem::offset_of!(JsString, tag) == 28, - "JsString 布局漂移:锚定布局见结构体头注释(32B,utf16_len@24,tag@28)" -); - -/// 载荷形态标签(`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"), - } - } -} - -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` 保持 32B 结构(tag 并入 -/// 尾部填充位,见其尺寸断言);仅超过阈值的拼接(`Vm::new_cons_string`)才 -/// 创建本节点。 -/// -/// 生命周期约定:节点由 `Box::into_raw` 分配,只经 [`JsString::drop_cons_node`] -/// 释放(连带扁平化产物);`left`/`right` 由 GC 传播闭包保证随父存活。 -pub struct ConsNode { - left: *const JsString, - right: *const JsString, - /// 拼接 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;仅 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 { - /// 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 { - 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), - } - } - } - - /// 以左右子节点构造 Cons(rope)节点,O(1) 链接不拷贝文本。 - /// - /// # Safety - /// `left`/`right` 必须指向存活的 `JsString`(session 或 perm),且由 GC - /// 传播保证随本节点存活——调用方(`Vm::new_cons_string`)负责登记。 - pub unsafe fn new_cons(left: *const JsString, right: *const JsString) -> Self { - // 子节点单元长各 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, - unit_len, - flat_cache: OnceLock::new(), - })); - let mut payload = [0u64; 3]; - // SAFETY: Cons 载荷首字为节点指针,余 16B 未定义填充;指针按位写入 - // (目标小端,指针→u64 为逐位转换)。 - unsafe { *payload.as_mut_ptr() = node as u64 }; - Self { - payload, - utf16_len: AtomicU32::new(unit_len), - tag: TAG_CONS, - } - } - - /// Cons 载荷节点指针(非 Cons 返回 null)。仅供释放路径使用。 - pub fn cons_node_ptr(&self) -> *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` 之前调用且恰好一次;非 Cons 串传 null 无操作。 - /// - /// # Safety - /// `node` 必须是 [`JsString::new_cons`] 经 `Box::into_raw` 产生的指针,且 - /// 尚未被释放。 - pub unsafe fn drop_cons_node(node: *mut ConsNode) { - if node.is_null() { - return; - } - let flat = (*node).flat_cache_ptr(); - if !flat.is_null() { - // SAFETY: 产物由本节点 Box::into_raw 创建,且只随本节点释放一次。 - drop(Box::from_raw(flat as *mut JsString)); - } - drop(Box::from_raw(node)); - } - - /// 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 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.utf16_len() == 0 - } - - /// 整块 UTF-8 文本的借用。**仅 Flat 形态**:FlatU16/Cons 无 `&str` 视图 - /// (内容含孤立 surrogate 或需扁平化),一律改走 [`Self::units`] / - /// [`Self::as_lossy_str`]。 - /// - /// # Panics - /// 非 Flat 形态 panic(调用契约违反,非运行时正常路径)。 - pub fn as_str(&self) -> &str { - if self.tag != TAG_FLAT { - panic!("JsString::as_str 仅限 Flat 形态"); - } - self.flat_ref() - } - - /// 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!(), - } - } - - /// 整块文本的 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.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 { - self.tag == TAG_CONS - } - - /// Cons 左右子节点指针对(非 Cons 返回双 null)。仅供 GC 传播闭包使用。 - pub fn cons_children(&self) -> [*const JsString; 2] { - 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 { - if self.tag != TAG_CONS { - return std::ptr::null(); - } - unsafe { (*self.cons_ptr()).flat_cache_ptr() } - } - - /// 迭代展开 Cons 子树为整块单元序列(显式栈中序收集,防深链递归爆栈)。 - fn flatten_units(&self) -> Vec { - let mut stack = Vec::with_capacity(8); - stack.push(self as *const JsString); - 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.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 { &*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/FlatU16(无 ConsNode 裸指针 -// 写路径),flat_cache 永不写入(perm 串只读共享,无任何可变路径)。裸指针 -// 字段不引入跨线程数据竞争。 -unsafe impl Send for JsString {} -unsafe impl Sync for JsString {} +pub use string::{ConsNode, JsString}; /// 原生函数指针的类型安全不透明包装。 /// diff --git a/crates/oxide_types/src/object/string.rs b/crates/oxide_types/src/object/string.rs new file mode 100644 index 00000000..e9d67857 --- /dev/null +++ b/crates/oxide_types/src/object/string.rs @@ -0,0 +1,465 @@ +//! `JsString` 三形态载荷(Flat UTF-8 文本 / FlatU16 单元 / Cons rope)与 rope 节点 `ConsNode`。 +//! +//! - 布局恒为 32B:载荷 24B @0、`utf16_len` @24、`tag` @28,由文件内 size_of/ +//! offset_of 断言锚定; +//! - `Cons` 子节点各自独立登记 session 字符串表(或为 perm 串),扁平化产物只挂 +//! `ConsNode::flat_cache`、随节点连带释放; +//! - 地址稳定不搬移(Box 堆分配),GC 无需 forwarding / rewrite。 + +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::OnceLock; +/// 堆分配的 JS 字符串值。 +/// +/// 字符串*值*以 48 位指针(指向 `JsString`)NaN-box(见 `JsValue::string`)。 +/// +/// 三种载荷形态(`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:载荷 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。 +#[repr(C)] +pub struct JsString { + /// 载荷 24B,按 `tag` 解读:Flat = `String`(data/len/cap)、FlatU16 = + /// `Vec`(ptr/len/cap,与 String 同形)、Cons = 节点指针(首字, + /// 余 16B 未定义填充)。 + payload: [u64; 3], + /// JS 语义的字符串长度(UTF-16 code unit 数)。 + /// + /// Flat 为懒缓存:构造是热路径(拼接/格式化),长度查询少见,不预付 O(n) + /// 扫描,首次 `utf16_len()` 访问时计算一次(sentinel + `AtomicU32`:perm + /// 串可被多线程 VM 共享读,计算幂等,relaxed 序即可)。FlatU16/Cons 构造时 + /// 预写(单元数 / 子节点归纳,各 O(1))。 + utf16_len: AtomicU32, + tag: u8, +} + +// 布局锚: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 + && std::mem::offset_of!(JsString, tag) == 28, + "JsString 布局漂移:锚定布局见结构体头注释(32B,utf16_len@24,tag@28)" +); + +/// 载荷形态标签(`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"), + } + } +} + +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` 保持 32B 结构(tag 并入 +/// 尾部填充位,见其尺寸断言);仅超过阈值的拼接(`Vm::new_cons_string`)才 +/// 创建本节点。 +/// +/// 生命周期约定:节点由 `Box::into_raw` 分配,只经 [`JsString::drop_cons_node`] +/// 释放(连带扁平化产物);`left`/`right` 由 GC 传播闭包保证随父存活。 +pub struct ConsNode { + left: *const JsString, + right: *const JsString, + /// 拼接 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;仅 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 { + /// 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 { + 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), + } + } + } + + /// 以左右子节点构造 Cons(rope)节点,O(1) 链接不拷贝文本。 + /// + /// # Safety + /// `left`/`right` 必须指向存活的 `JsString`(session 或 perm),且由 GC + /// 传播保证随本节点存活——调用方(`Vm::new_cons_string`)负责登记。 + pub unsafe fn new_cons(left: *const JsString, right: *const JsString) -> Self { + // 子节点单元长各 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, + unit_len, + flat_cache: OnceLock::new(), + })); + let mut payload = [0u64; 3]; + // SAFETY: Cons 载荷首字为节点指针,余 16B 未定义填充;指针按位写入 + // (目标小端,指针→u64 为逐位转换)。 + unsafe { *payload.as_mut_ptr() = node as u64 }; + Self { + payload, + utf16_len: AtomicU32::new(unit_len), + tag: TAG_CONS, + } + } + + /// Cons 载荷节点指针(非 Cons 返回 null)。仅供释放路径使用。 + pub fn cons_node_ptr(&self) -> *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` 之前调用且恰好一次;非 Cons 串传 null 无操作。 + /// + /// # Safety + /// `node` 必须是 [`JsString::new_cons`] 经 `Box::into_raw` 产生的指针,且 + /// 尚未被释放。 + pub unsafe fn drop_cons_node(node: *mut ConsNode) { + if node.is_null() { + return; + } + let flat = (*node).flat_cache_ptr(); + if !flat.is_null() { + // SAFETY: 产物由本节点 Box::into_raw 创建,且只随本节点释放一次。 + drop(Box::from_raw(flat as *mut JsString)); + } + drop(Box::from_raw(node)); + } + + /// 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 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.utf16_len() == 0 + } + + /// 整块 UTF-8 文本的借用。**仅 Flat 形态**:FlatU16/Cons 无 `&str` 视图 + /// (内容含孤立 surrogate 或需扁平化),一律改走 [`Self::units`] / + /// [`Self::as_lossy_str`]。 + /// + /// # Panics + /// 非 Flat 形态 panic(调用契约违反,非运行时正常路径)。 + pub fn as_str(&self) -> &str { + if self.tag != TAG_FLAT { + panic!("JsString::as_str 仅限 Flat 形态"); + } + self.flat_ref() + } + + /// 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!(), + } + } + + /// 整块文本的 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.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 { + self.tag == TAG_CONS + } + + /// Cons 左右子节点指针对(非 Cons 返回双 null)。仅供 GC 传播闭包使用。 + pub fn cons_children(&self) -> [*const JsString; 2] { + 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 { + if self.tag != TAG_CONS { + return std::ptr::null(); + } + unsafe { (*self.cons_ptr()).flat_cache_ptr() } + } + + /// 迭代展开 Cons 子树为整块单元序列(显式栈中序收集,防深链递归爆栈)。 + fn flatten_units(&self) -> Vec { + let mut stack = Vec::with_capacity(8); + stack.push(self as *const JsString); + 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.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 { &*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/FlatU16(无 ConsNode 裸指针 +// 写路径),flat_cache 永不写入(perm 串只读共享,无任何可变路径)。裸指针 +// 字段不引入跨线程数据竞争。 +unsafe impl Send for JsString {} +unsafe impl Sync for JsString {} + +#[cfg(test)] +mod tests; diff --git a/crates/oxide_types/src/object/string/tests.rs b/crates/oxide_types/src/object/string/tests.rs new file mode 100644 index 00000000..0c398c8e --- /dev/null +++ b/crates/oxide_types/src/object/string/tests.rs @@ -0,0 +1,90 @@ +use super::*; + +#[test] +fn js_string_cons_basic() { + // 左右子 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.utf16_len(), 4); + assert!(!cons.is_empty()); + assert_eq!(cons.as_lossy_str(), "abcd"); + assert_eq!(cons.to_owned_string(), "abcd"); + assert!(cons.is_cons()); + // 扁平化产物缓存命中:内容仍一致(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] +fn js_string_cons_deep_chain_flattens_iteratively() { + // 2000 层左倾链:显式栈展开不爆栈,产物与逐段拼接一致。 + let mut nodes: Vec> = Vec::new(); + nodes.push(Box::new(JsString::new("x".to_string()))); + for _ in 0..2000 { + nodes.push(Box::new(JsString::new("y".to_string()))); + } + let mut chain_ptr = nodes[0].as_ref() as *const JsString; + for i in 1..nodes.len() { + let cons = Box::new(unsafe { JsString::new_cons(chain_ptr, nodes[i].as_ref() as *const JsString) }); + chain_ptr = cons.as_ref() as *const JsString; + nodes.push(cons); + } + let text = nodes.last().unwrap().as_lossy_str(); + assert_eq!(text.len(), 1 + 2000); + assert!(text.starts_with('x')); + assert!(text.ends_with('y')); +} + +#[test] +fn js_string_cons_empty_parts() { + // 双空串 Cons:空判定与长度均为 0,扁平化为空文本。 + let left = Box::new(JsString::new(String::new())); + 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.utf16_len(), 0); + assert_eq!(cons.as_lossy_str(), ""); +} diff --git a/crates/oxide_types/src/object/tests.rs b/crates/oxide_types/src/object/tests.rs index ca4463f7..17915c4e 100644 --- a/crates/oxide_types/src/object/tests.rs +++ b/crates/oxide_types/src/object/tests.rs @@ -7,95 +7,6 @@ fn object_size_bounds() { assert!(sz <= 256, "JsObject grew unexpectedly: {sz}B"); } -#[test] -fn js_string_cons_basic() { - // 左右子 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.utf16_len(), 4); - assert!(!cons.is_empty()); - assert_eq!(cons.as_lossy_str(), "abcd"); - assert_eq!(cons.to_owned_string(), "abcd"); - assert!(cons.is_cons()); - // 扁平化产物缓存命中:内容仍一致(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] -fn js_string_cons_deep_chain_flattens_iteratively() { - // 2000 层左倾链:显式栈展开不爆栈,产物与逐段拼接一致。 - let mut nodes: Vec> = Vec::new(); - nodes.push(Box::new(JsString::new("x".to_string()))); - for _ in 0..2000 { - nodes.push(Box::new(JsString::new("y".to_string()))); - } - let mut chain_ptr = nodes[0].as_ref() as *const JsString; - for i in 1..nodes.len() { - let cons = Box::new(unsafe { JsString::new_cons(chain_ptr, nodes[i].as_ref() as *const JsString) }); - chain_ptr = cons.as_ref() as *const JsString; - nodes.push(cons); - } - let text = nodes.last().unwrap().as_lossy_str(); - assert_eq!(text.len(), 1 + 2000); - assert!(text.starts_with('x')); - assert!(text.ends_with('y')); -} - -#[test] -fn js_string_cons_empty_parts() { - // 双空串 Cons:空判定与长度均为 0,扁平化为空文本。 - let left = Box::new(JsString::new(String::new())); - 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.utf16_len(), 0); - assert_eq!(cons.as_lossy_str(), ""); -} - #[test] fn new_empty_defaults() { let obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null());