diff --git a/crates/oxide_builtins/src/date.rs b/crates/oxide_builtins/src/date.rs index e9a3c694..a6cc0fd2 100644 --- a/crates/oxide_builtins/src/date.rs +++ b/crates/oxide_builtins/src/date.rs @@ -156,7 +156,7 @@ pub fn date_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let val = vm.reg(args[1]); if val.is_string() { // SAFETY: val 已确认是字符串值。 - let s = unsafe { (*val.as_string_ptr()).data.clone() }; + let s = unsafe { (*val.as_string_ptr()).to_owned_string() }; let formats = ["%Y-%m-%dT%H:%M:%S%.fZ", "%Y-%m-%dT%H:%M:%S%.f"]; let mut ts = f64::NAN; for fmt in &formats { @@ -241,7 +241,7 @@ pub fn date_parse(vm: &mut H, args: &[u8]) -> NativeResult { return NativeResult::Ok(JsValue::float(f64::NAN)); } // SAFETY: val 已确认是字符串值。 - let s = unsafe { (*val.as_string_ptr()).data.clone() }; + let s = unsafe { (*val.as_string_ptr()).to_owned_string() }; let mut ts = f64::NAN; if let Ok(dt) = chrono::DateTime::parse_from_rfc2822(&s) { ts = dt.timestamp_millis() as f64; diff --git a/crates/oxide_builtins/src/json.rs b/crates/oxide_builtins/src/json.rs index 6cf0b3da..e251a2a9 100644 --- a/crates/oxide_builtins/src/json.rs +++ b/crates/oxide_builtins/src/json.rs @@ -22,7 +22,7 @@ pub fn json_parse(vm: &mut H, args: &[u8]) -> NativeResult { } let text = { // SAFETY: val 已确认是字符串值。 - unsafe { (*val.as_string_ptr()).data.clone() } + unsafe { (*val.as_string_ptr()).to_owned_string() } }; let parsed: serde_json::Value = match serde_json::from_str(&text) { @@ -172,7 +172,7 @@ fn process_space(val: JsValue) -> String { let clamped = (n as usize).min(10); " ".repeat(clamped) } else if val.is_string() { - let s = unsafe { (*val.as_string_ptr()).data.clone() }; + let s = unsafe { (*val.as_string_ptr()).to_owned_string() }; s.chars().take(10).collect() } else { let s = oxide_runtime_api::to_string(val); @@ -305,7 +305,7 @@ fn jsvalue_to_json( out.push_str(&oxide_runtime_api::js_number_to_string(n)); } } else if val.is_string() { - let s = unsafe { (*val.as_string_ptr()).data.clone() }; + let s = unsafe { (*val.as_string_ptr()).to_owned_string() }; stringify_string(&s, out); } else if val.is_object() { let obj_ptr = val.as_js_object_ptr(); diff --git a/crates/oxide_builtins/tests/builtins_json_tests.rs b/crates/oxide_builtins/tests/builtins_json_tests.rs index dfdcc48b..67bd0c88 100644 --- a/crates/oxide_builtins/tests/builtins_json_tests.rs +++ b/crates/oxide_builtins/tests/builtins_json_tests.rs @@ -13,7 +13,7 @@ fn eval(source: &str) -> Result<(Vm, JsValue), String> { fn stringify_val(val: &JsValue) -> String { if val.is_string() { - unsafe { (*val.as_string_ptr()).data.clone() } + unsafe { (*val.as_string_ptr()).to_owned_string() } } else { format!("{:?}", val) } diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index 14cd37eb..0655f0d2 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -195,7 +195,7 @@ fn format_js_value( ) -> String { if val.is_string() { // SAFETY: val 已确认是字符串值。 - let s = unsafe { (*val.as_string_ptr()).data.clone() }; + let s = unsafe { (*val.as_string_ptr()).to_owned_string() }; format!("\"{s}\"") } else if val.is_bigint() { format!("{}", vm.bigint_value(val)) diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index 49796406..ef7d4696 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -175,6 +175,33 @@ pub fn single_char_ptr(ch: u8) -> *const JsString { } } +/// 小整数(0..=99)永久 `JsString` 指针表:`s += j` 等数字叶子拼接的高频命中路径, +/// 免每次 `to_string` + 登记 2 次分配。泄漏面严格有界:100 条目 ≈ 4KB,进程生命 +/// 期内不释放(同 `SINGLE_CHAR_TABLE` 论证)。 +static SMALL_INT_TABLE: [OnceLock; 100] = [const { OnceLock::new() }; 100]; + +/// 取 0..=99 小整数的永久 `JsString` 指针,惰性物化一次后恒返回同一地址。 +/// 超出范围返回 `None`,由调用方回落普通字符串创建。 +pub fn small_int_ptr(n: u32) -> Option<*const JsString> { + if n >= 100 { + return None; + } + let slot = &SMALL_INT_TABLE[n as usize]; + if let Some(ptr) = slot.get() { + return Some(ptr.0); + } + let ptr = Box::into_raw(Box::new(JsString::new(n.to_string()))); + match slot.set(StringPtr(ptr)) { + Ok(()) => Some(ptr), + Err(existing) => { + // 并发首用竞态:与 single_char_ptr 同款处置,本线程产物恰好释放一次。 + // SAFETY: ptr 来自本线程的 Box::into_raw,无任何外部引用。 + unsafe { drop(Box::from_raw(ptr)) }; + Some(existing.0) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/oxide_runtime_api/src/lib.rs b/crates/oxide_runtime_api/src/lib.rs index 37f0ebe2..4bb108c6 100644 --- a/crates/oxide_runtime_api/src/lib.rs +++ b/crates/oxide_runtime_api/src/lib.rs @@ -186,7 +186,9 @@ pub(crate) unsafe fn string_data(val: JsValue) -> &'static str { /// 按内容比较两个字符串 `JsValue` 是否相等。 /// -/// 先做指针级短路(同一 interned 字符串必等),否则逐字节比较 `JsString` 内容。 +/// 先做指针级短路(同一 interned 字符串必等),否则比较 `JsString` 的整块文本 +/// (rope 未扁平化时经 `as_str` 惰性扁平化;内容比较对"rope vs 同内容不同指针" +/// 也正确)。 #[inline] pub fn string_value_eq(a: JsValue, b: JsValue) -> bool { if a.as_string_ptr() == b.as_string_ptr() { @@ -194,7 +196,7 @@ 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.data == sb.data + sa.as_str() == sb.as_str() } /// BigInt 转 f64 的近似转换(连续整数用 to_u64,大数用 Display 解析)。 diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 7407109b..a6a7bdb7 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -8,13 +8,31 @@ use crate::value::JsValue; use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::OnceLock; /// 堆分配的 JS 字符串值。 /// /// 字符串*值*以 48 位指针(指向 `JsString`)NaN-box(见 `JsValue::string`)。 +/// +/// 两种变体: +/// - `Flat`:整块 UTF-8 文本(String,保留容量)。字面量、方法结果与 CONCAT_N +/// 产物保持此形态; +/// - `Cons`(rope):指向 [`ConsNode`] 载荷(左右子裸指针 + O(1) 字节长 + +/// 惰性扁平化缓存)。二元 `+`/`+=` 拼接 O(1) 链接不拷贝文本,整块文本在 +/// 首次消费时扁平化并原子发布。 +/// +/// 结构体大小恒为 40B(`Flat(String)` 24B + utf16_len 4B + tag),较 rope 前的 +/// 32B 大一个分配 bin 级,但换来 `String` 原样存储(零 realloc 收缩);Cons +/// 专属状态(子节点/产物缓存)独立分配在 [`ConsNode`],仅大链持有。 +/// +/// 生命周期约定: +/// - `Cons` 子节点各自独立登记 session 字符串表(或为 perm 串),由 GC 的 +/// mark 传播闭包保证随父存活;扁平化产物只挂在 `ConsNode::flat_cache`、不进 +/// session 主表,随本节点连带释放。 +/// - 地址稳定不搬移(Box 堆分配),GC 无需 forwarding / rewrite。 #[derive(Debug)] pub struct JsString { - pub data: String, + kind: StringKind, /// JS 语义的字符串长度(UTF-16 code unit 数)懒缓存。 /// /// 构造是热路径(拼接/格式化),长度查询少见,故不预付 O(n) 扫描;首次 @@ -23,45 +41,220 @@ pub struct JsString { utf16_len: AtomicU32, } +/// 字符串内容变体。 +#[derive(Debug)] +enum StringKind { + Flat(String), + Cons(*const ConsNode), +} + +/// Cons(rope)节点的载荷:左右子节点指针 + O(1) 拼接字节长 + 扁平化产物缓存。 +/// +/// 独立于 `JsString` 分配,使 Flat 路径的 `JsString` 保持与 rope 前相同的 32B +/// 结构;仅超过字节阈值的拼接(`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)。 + /// 产物不进 session 主表,生命周期挂本节点——GC 传播保证其随父存活, + /// 释放节点时连带释放。 + flat_cache: OnceLock<*const JsString>, +} + /// `utf16_len` 未计算的哨兵值(有效长度不可能为 u32::MAX)。 const UTF16_LEN_UNSET: u32 = u32::MAX; impl JsString { - /// 用 UTF-8 数据构造字符串。 + /// 用 UTF-8 数据构造 Flat 字符串。 pub fn new(data: String) -> Self { Self { - data, + kind: StringKind::Flat(data), utf16_len: AtomicU32::new(UTF16_LEN_UNSET), } } - /// 字符串的字节长度(非字符数)。 + /// 以左右子节点构造 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 { + let byte_len = ((*left).len() + (*right).len()) as u32; + let node = Box::into_raw(Box::new(ConsNode { + left, + right, + byte_len, + flat_cache: OnceLock::new(), + })); + Self { + kind: StringKind::Cons(node), + utf16_len: AtomicU32::new(UTF16_LEN_UNSET), + } + } + + /// Cons 载荷节点指针(Flat 返回 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, + } + } + + /// 释放 Cons 载荷节点(连带扁平化产物)。须在所属 `JsString` 的 + /// `Box::from_raw` 之前调用且恰好一次;Flat 串传 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.get().copied().unwrap_or(std::ptr::null()); + if !flat.is_null() { + // SAFETY: 产物由本节点 Box::into_raw 创建,且只随本节点释放一次。 + drop(Box::from_raw(flat as *mut JsString)); + } + drop(Box::from_raw(node)); + } + + /// 字符串的字节长度(非字符数)。Flat 直读;Cons O(1) 取构造时缓存。 pub fn len(&self) -> usize { - self.data.len() + 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`]),取产物长度缓存。 pub fn utf16_len(&self) -> u32 { let cached = self.utf16_len.load(Ordering::Relaxed); if cached != UTF16_LEN_UNSET { return cached; } - let len = self.data.encode_utf16().count() as u32; + let len = self.flat_str().encode_utf16().count() as u32; self.utf16_len.store(len, Ordering::Relaxed); len } /// 是否为空字符串。 pub fn is_empty(&self) -> bool { - self.data.is_empty() + self.len() == 0 } - /// 底层 UTF-8 切片。 + /// 整块 UTF-8 文本的借用。Cons 未扁平化时惰性扁平化并发布缓存后返回产物文本; + /// 返回的 `&str` 借用期 = `&self` 借用期(产物随本节点存活)。 pub fn as_str(&self) -> &str { - self.data.as_str() + self.flat_str() + } + + /// 整块文本的借用(`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"), + } + } + } + } + + /// 整块文本的 owned 副本(低频路径:错误消息 / 格式化 / 属性键桥接)。 + pub fn to_owned_string(&self) -> String { + self.flat_str().to_string() + } + + /// 是否为 Cons(rope)节点。 + pub fn is_cons(&self) -> bool { + matches!(self.kind, StringKind::Cons(_)) + } + + /// Cons 左右子节点指针对(Flat 返回双 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] + } + } + } + + /// 扁平化产物指针(未扁平化返回 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()) } + } + } + } + + /// 迭代展开 Cons 子树为整块文本(显式栈中序收集,防深链递归爆栈)。 + fn flatten_text(&self) -> String { + let mut stack = Vec::with_capacity(8); + stack.push(self as *const JsString); + let mut buf = String::with_capacity(self.len()); + 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) => { + // 右子先入栈、左子后入,保证左子先出(中序顺序)。 + // SAFETY: child 由 new_cons 创建且随父存活。 + let child = unsafe { &**child }; + stack.push(child.right); + stack.push(child.left); + } + } + } + buf } } +// SAFETY: 与 `StringPtr`(string_forge.rs)同款论证——Cons 节点只存在于单线程 +// session VM 内;跨线程共享的 perm 串恒为 Flat,且其 flat_cache 永不写入 +// (perm 串只读共享,无任何可变路径)。裸指针字段不引入跨线程数据竞争。 +unsafe impl Send for JsString {} +unsafe impl Sync for JsString {} + /// 原生函数指针的类型安全不透明包装。 /// /// 以 `*const ()` 存储而非具体 `fn` 类型,使 `oxide_types` 无需依赖 @@ -1615,6 +1808,55 @@ mod tests { assert!(sz <= 256, "JsObject grew unexpectedly: {sz}B"); } + #[test] + fn js_string_cons_basic() { + // 左右子 Flat 构造 Cons:len/utf16_len/is_empty/flat 全部按拼接语义。 + 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_eq!(cons.to_owned_string(), "abcd"); + assert!(cons.is_cons()); + // 扁平化产物缓存命中:内容仍一致。 + assert_eq!(cons.flat_str(), "abcd"); + } + + #[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().flat_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.len(), 0); + assert_eq!(cons.flat_str(), ""); + assert_eq!(cons.utf16_len(), 0); + } + #[test] fn new_empty_defaults() { let obj = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null()); diff --git a/crates/oxide_vm/src/dispatch/arithmetic.rs b/crates/oxide_vm/src/dispatch/arithmetic.rs index 41b65180..04425786 100644 --- a/crates/oxide_vm/src/dispatch/arithmetic.rs +++ b/crates/oxide_vm/src/dispatch/arithmetic.rs @@ -78,22 +78,57 @@ impl Vm { Ok(()) } - /// 字符串拼接热路径:一次预分配写齐两个操作数的 ToString 文本。 + /// 字符串拼接接线点(二元 `+`/`+=` 共用):O(1) 链接为 Cons(rope)节点, + /// 消除逐次整串重拷贝。文本在消费时(`.length`/`==`/string_ref 等)惰性扁平化。 /// /// # 步骤 - /// 1. 按字符串操作数字节长预分配容量(余量覆盖数字/布尔等格式化文本)。 - /// 2. 两个操作数依次走 push_to_string(BigInt 输出十进制,Symbol 由调用方先行拒绝)。 - /// 3. 生成会话字符串写入目标寄存器。 + /// 1. 非字符串操作数先转成叶子字符串(`push_to_string` 同文本语义;小整数走 + /// 永久缓存零分配,其余 `to_string` + owned)。 + /// 2. 两个字符串经 `new_cons_string` 链接(每链接 1 次节点分配,对比原 O(n) + /// 拷贝 + 结果串分配)。 /// /// # 副作用 - /// 新建一个会话字符串,登记到 session 生命周期。 + /// - 新建 Cons 节点 + 可能的数字叶子,登记到 session 生命周期。回收点仅在 + /// 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 }; - let mut buf = String::with_capacity(lbytes + rbytes + 32); - coercion::push_to_string(lhs, &mut buf); - coercion::push_to_string(rhs, &mut buf); - self.new_string_owned(buf) + 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 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) + } + + /// 把非字符串原语转为叶子字符串:0..=99 小整数命中永久缓存(零分配), + /// 其余走 `to_string` + `new_string_owned`(语义与 `push_to_string` 一致)。 + fn string_leaf(&mut self, v: JsValue) -> JsValue { + if v.is_int() { + let n = v.as_int(); + if n >= 0 { + if let Some(ptr) = oxide_kernel::string_forge::small_int_ptr(n as u32) { + return JsValue::string(ptr); + } + } + } + self.new_string_owned(coercion::to_string(v)) } /// CONCAT_N:多操作数拼接(连续 `+` 左结合链摊平)的两阶段 dispatch。 diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index fda41bd5..72687424 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -127,6 +127,53 @@ impl SessionGc { edges } + /// 把字符串指针标记为存活并传播 rope 闭包:Cons 迭代传播左右子节点与 + /// 扁平化产物(显式栈防深链爆栈)。perm 指针进表无害——sweep 只遍历 + /// `session_string_ptrs`。遗漏子节点 = 子节点被 sweep 释放 → 悬垂 UB, + /// 全部字符串 insert 点必须收口此入口。 + fn mark_string_live(live: &mut HashSet<*mut JsString, FxBuildHasher>, root: *mut JsString) { + if root.is_null() { + return; + } + let mut stack = vec![root]; + while let Some(ptr) = stack.pop() { + if ptr.is_null() || !live.insert(ptr) { + continue; + } + // SAFETY: ptr 是合法 JsString 指针(session 或 perm),mark 期存活。 + let node = unsafe { &*ptr }; + if node.is_cons() { + for child in node.cons_children() { + // 子节点与父同属一个 JsString 堆对象族,const 转 mut 仅用于 + // 集合统一,不引入写操作。 + stack.push(child as *mut JsString); + } + let flat = node.flat_cache_ptr(); + if !flat.is_null() { + stack.push(flat as *mut JsString); + } + } + } + } + + /// 释放一个 session `JsString`(`Box::into_raw` 分配),连带释放 rope + /// 扁平化产物。**不递归子节点**——子节点独立登记主表、各自判定存活, + /// 递归即 double-free。 + /// + /// # Safety + /// `ptr` 必须是 `new_string`/`new_cons_string` 中 `Box::into_raw` 产生的非空 + /// 指针,仍登记在 session 字符串表(或由 full_reset 统一清表),且恰好 + /// 释放一次。 + pub(crate) unsafe fn drop_session_string_box(ptr: *mut JsString) { + if ptr.is_null() { + return; + } + // 连带释放 rope 载荷(ConsNode 与其扁平化产物);Flat 串无载荷。 + // SAFETY: cons_node_ptr 由 new_cons 产生,本指针恰好释放一次。 + JsString::drop_cons_node((*ptr).cons_node_ptr()); + drop(Box::from_raw(ptr)); + } + /// 把 `obj` 直接持有的 session 字符串值记入 `live`。JsString 不持有 GC 引用, /// 因此"到达"一个字符串就等于标记它——不存在字符串 DFS 栈。永久字符串也会被 /// 无害地记录;sweep 只遍历 `session_string_ptrs`,`live` 中的非 session 指针 @@ -135,22 +182,22 @@ impl SessionGc { if let Some(elements) = obj.array_elements_vec() { for value in elements.iter() { if value.is_string() { - live.insert(value.as_string_ptr_mut()); + Self::mark_string_live(live, value.as_string_ptr_mut()); } } } if let Some(props) = obj.hash_props_vec() { for value in props.iter() { if value.is_string() { - live.insert(value.as_string_ptr_mut()); + Self::mark_string_live(live, value.as_string_ptr_mut()); } } } if obj.captured_this().is_string() { - live.insert(obj.captured_this().as_string_ptr_mut()); + Self::mark_string_live(live, obj.captured_this().as_string_ptr_mut()); } if obj.home_object().is_string() { - live.insert(obj.home_object().as_string_ptr_mut()); + Self::mark_string_live(live, obj.home_object().as_string_ptr_mut()); } // 扫描 upvalue cell 中的字符串引用。 for cell_ptr in obj.upvalues_slice() { @@ -159,43 +206,43 @@ impl SessionGc { } let cell = unsafe { &**cell_ptr }; if cell.value.is_string() { - live.insert(cell.value.as_string_ptr_mut()); + Self::mark_string_live(live, cell.value.as_string_ptr_mut()); } } if obj.is_map() { for value in map::map_native_edges(obj) { if value.is_string() { - live.insert(value.as_string_ptr_mut()); + Self::mark_string_live(live, value.as_string_ptr_mut()); } } } if obj.is_set() { for value in set::set_native_edges(obj) { if value.is_string() { - live.insert(value.as_string_ptr_mut()); + Self::mark_string_live(live, value.as_string_ptr_mut()); } } } if obj.is_generator_obj() { for ptr in crate::generator::generator_native_string_edges(obj) { - live.insert(ptr); + Self::mark_string_live(live, ptr); } } if obj.is_promise_obj() { for value in crate::promise::promise_native_edges(obj) { if value.is_string() { - live.insert(value.as_string_ptr_mut()); + Self::mark_string_live(live, value.as_string_ptr_mut()); } } } if obj.is_async_obj() { for ptr in crate::async_func::async_native_string_edges(obj) { - live.insert(ptr); + Self::mark_string_live(live, ptr); } } if obj.is_async_generator_obj() { for ptr in crate::async_generator::async_generator_native_string_edges(obj) { - live.insert(ptr); + Self::mark_string_live(live, ptr); } } } @@ -220,9 +267,7 @@ impl SessionGc { stack.clear(); live_strings.clear(); for ptr in string_seeds { - if !ptr.is_null() { - live_strings.insert(ptr); - } + Self::mark_string_live(live_strings, ptr); } for ptr in seeds { @@ -339,16 +384,17 @@ impl SessionGc { Self::drop_session_object_heap_data(obj_ptr) + size_of::() as u64 } - /// 释放一个已死 session `JsString`(由 `Vm::new_string` 经 `Box::into_raw` 分配), - /// 返回释放的字节数。与 `Vm::free_session_string_heap_data` 的释放一致,但只 - /// 选择性作用于单个已死指针。 + /// 释放一个已死 session `JsString`(由 `Vm::new_string`/`new_cons_string` + /// 经 `Box::into_raw` 分配),返回释放的字节数(含 rope 扁平化产物)。 + /// 与 `Vm::free_session_string_heap_data` 的释放一致,但只选择性作用于 + /// 单个已死指针。 /// /// # Safety - /// `ptr` 必须是 `Vm::new_string` 中 `Box::into_raw(Box::new(JsString))` 产生的 - /// 非空指针,仍存在于 `session_string_ptrs`,且恰好释放一次。 + /// `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; - drop(Box::from_raw(ptr)); + Self::drop_session_string_box(ptr); bytes } @@ -1473,4 +1519,112 @@ mod tests { assert_eq!(vm.gc_state.session_gc.total_collections, 0); assert!(vm.gc_state.session_string_ptrs.contains(&s.as_string_ptr_mut())); } + + // ── rope(Cons)GC 传播闭包 ── + + /// 构造 `left + right` 的 Cons 节点(返回 (父, 左, 右) 三指针)。 + fn make_cons_pair(vm: &mut Vm, l: &str, r: &str) -> (JsValue, *mut JsString, *mut JsString) { + let left = vm.new_string(l); + let right = vm.new_string(r); + let parent = vm.new_cons_string(left, right); + (parent, left.as_string_ptr_mut(), right.as_string_ptr_mut()) + } + + #[test] + fn rope_survives_collection_with_children_propagated() { + let mut vm = Vm::new(); + let (parent, l_ptr, r_ptr) = make_cons_pair(&mut vm, "left-part", "right-part"); + vm.regs[0] = parent; + + collect(&mut vm); + + // 父(寄存器根)+ 子节点(经传播闭包)全部存活,内容可读。 + let parent_ptr = parent.as_string_ptr_mut(); + 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"); + } + + #[test] + fn rope_children_swept_when_parent_dead() { + let mut vm = Vm::new(); + let (parent, l_ptr, r_ptr) = make_cons_pair(&mut vm, "left", "right"); + let parent_ptr = parent.as_string_ptr_mut(); + assert!(vm.gc_state.session_string_ptrs.contains(&parent_ptr)); + + // 父与子均无根引用 → 整树回收。 + collect(&mut vm); + + 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)); + } + + #[test] + fn rope_product_freed_with_parent() { + let mut vm = Vm::new(); + 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"); + let flat_ptr = unsafe { (*parent_ptr).flat_cache_ptr() }; + assert!(!flat_ptr.is_null()); + + collect(&mut vm); + + // 父死 → 连带释放产物(不 double-free、不泄漏;产物从不进 session 表)。 + let flat_mut = flat_ptr as *mut JsString; + assert!(!vm.gc_state.session_string_ptrs.contains(&parent_ptr)); + assert!(!vm.gc_state.session_string_ptrs.contains(&flat_mut)); + } + + #[test] + fn rope_deep_chain_mark_iterative() { + let mut vm = Vm::new(); + // 1024 层左倾链:mark 传播用显式栈,不爆栈、不遗漏子节点。 + let mut chain = vm.new_string("root"); + for _ in 0..1024 { + let leaf = vm.new_string("x"); + chain = vm.new_cons_string(chain, leaf); + } + vm.regs[0] = chain; + let chain_ptr = chain.as_string_ptr_mut(); + + 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))); + } + + #[test] + fn rope_perm_child_untouched_by_sweep() { + let mut vm = Vm::new(); + let perm = vm.perm_string("perm-leaf"); + let perm_ptr = perm.as_string_ptr_mut(); + let session = vm.new_string("session-leaf"); + let session_ptr = session.as_string_ptr_mut(); + let parent = vm.new_cons_string(perm, session); + vm.regs[0] = parent; + + collect(&mut vm); + + // 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"); + } + + #[test] + fn full_reset_frees_rope_and_product() { + 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"); + vm.regs[0] = parent; + + // full_reset 清空全部 session 字符串(连带产物):无泄漏、无 double-free。 + vm.full_reset(); + assert!(vm.gc_state.session_string_ptrs.is_empty()); + } } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 668be65f..6c2a2e6d 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -950,7 +950,7 @@ impl Vm { return None; } // SAFETY: val 是字符串值,其 JsString 指针在生命周期内有效。 - Some(unsafe { (*val.as_string_ptr()).data.clone() }) + Some(unsafe { (*val.as_string_ptr()).to_owned_string() }) } pub(crate) fn thrown_error_kind(&self, val: JsValue) -> &'static str { @@ -998,8 +998,8 @@ impl Vm { return Ok(make_int_key(d as u32)); } } else if val.is_string() { - // SAFETY: val 是字符串值,把其内容桥接为永久 key id。 - let s = unsafe { &(*val.as_string_ptr()).data }; + // 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)); } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index cdbbc08e..7148d9b2 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -151,7 +151,7 @@ impl Vm { let val = self.regs[reg]; let s = if val.is_string() { // SAFETY: val 是字符串值。 - unsafe { (*val.as_string_ptr()).data.clone() } + unsafe { (*val.as_string_ptr()).to_owned_string() } } else { to_string_full(val, self)? }; @@ -163,7 +163,7 @@ impl Vm { let val = imm[const_idx]; if val.is_string() { // SAFETY: val 是字符串值。 - let s = unsafe { (*val.as_string_ptr()).data.clone() }; + let s = unsafe { (*val.as_string_ptr()).to_owned_string() }; result.push_str(&s); } } @@ -684,7 +684,7 @@ impl Vm { JsValue::from_js_object(proto_ptr), )); if src.is_string() { - let code_units: Vec = unsafe { (*src.as_string_ptr()).data.encode_utf16().collect() }; + let code_units: Vec = unsafe { (*src.as_string_ptr()).as_str().encode_utf16().collect() }; 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) { @@ -707,7 +707,7 @@ impl Vm { .and_then(|v| { if v.is_string() { // SAFETY: v 是字符串常量值。 - Some(unsafe { (*v.as_string_ptr()).data.clone() }) + Some(unsafe { (*v.as_string_ptr()).to_owned_string() }) } else { None } @@ -742,7 +742,7 @@ 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()).data.clone() }; + let s = unsafe { (*raw.as_string_ptr()).to_owned_string() }; let code_units: Vec = s.encode_utf16().collect(); for (i, unit) in code_units.iter().enumerate() { let si = make_int_key(i as u32); @@ -850,7 +850,7 @@ impl Vm { // 字符串源:按索引复制字符(可枚举索引属性)。 if src.is_string() { let target = unsafe { &mut *target_val.as_js_object_ptr() }; - let code_units: Vec = unsafe { (*src.as_string_ptr()).data.encode_utf16().collect() }; + let code_units: Vec = unsafe { (*src.as_string_ptr()).as_str().encode_utf16().collect() }; 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) { diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index e584e887..26a3b008 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -16,6 +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; + /// 以最小配置创建独立 VM:新建 `KernelCore` + `KernelSession` 并初始化内置对象。 pub fn new() -> Self { let core = KernelCore::new(KernelConfig::minimal()); @@ -383,11 +388,53 @@ impl Vm { pub fn new_string_owned(&mut self, s: String) -> JsValue { let len = s.len(); let ptr = Box::into_raw(Box::new(JsString::new(s))); + self.register_session_string(ptr, len) + } + + /// 登记一个 session 字符串并记账(`new_string_owned` 与 `new_cons_string` + /// 共用),返回字符串值。 + /// + /// # 步骤 + /// 1. 登记指针到 session 字符串表。 + /// 2. 记账 `size_of::() + bytes`。 + /// + /// # 注意事项 + /// - 本方法**不触发**回收:执行期字符串 GC 统一在 dispatch 指令边界检查 + /// 水位触发(此时 builtin 局部值已落地为执行根,分配点触发会误释放 + /// 仅存于局部/构造中的活串)。 + fn register_session_string(&mut self, ptr: *mut JsString, bytes: usize) -> JsValue { self.gc_state.session_string_ptrs.push(ptr); - self.gc_state.session_bytes_allocated += std::mem::size_of::() + len; + self.gc_state.session_bytes_allocated += std::mem::size_of::() + bytes; JsValue::string(ptr) } + /// 分配 Cons(rope)节点:左右子节点 O(1) 链接,不拷贝文本。 + /// + /// # 边界与前提 + /// - `left`/`right` 必须均为字符串值;仅在 `+`/`+=` 的 `concat_strings` + /// 接线点调用(CONCAT_N 保持急切扁平,不经此路径)。 + /// + /// # 副作用 + /// - 登记节点到 session 字符串表(账目 = `size_of::() + byte_len`)。 + /// + /// # 注意事项 + /// - 调用方须保证 `left`/`right` 在调用期间存活:子节点(coerce 新鲜字符串 / + /// 新建叶子)要么已是执行根,要么在写入根前不被任何回收点触达——当前 + /// 回收点仅在指令边界,链接结果写寄存器先于下一次检查,天然满足。 + /// - 调用方(`concat_strings`)已按字节阈值过滤:仅大链(总长 > + /// [`Self::CONS_FLATTEN_BYTES`])进入本方法,小链由其急切扁平。 + #[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) + } + /// 把字符串 intern 为永久 key id(属性名/方法名),进程生命周期内稳定。 pub fn intern_key(&self, s: &str) -> u32 { self.kernel_core.perm_interner().intern(s).0 @@ -407,10 +454,10 @@ impl Vm { /// 会引用它们。较轻量的 `reset()` 刻意保留它们,与 session 对象跨 eval 存活一致。 fn free_session_string_heap_data(&mut self) { for ptr in self.gc_state.session_string_ptrs.drain(..) { - // SAFETY: 每个指针来自 new_string 中的 Box::into_raw(Box::new(JsString)), - // 且只在这里恰好释放一次。 + // SAFETY: 每个指针来自 new_string/new_cons_string 的 Box::into_raw, + // 且只在这里(或 sweep)恰好释放一次;内部连带释放 rope 扁平化产物。 unsafe { - drop(Box::from_raw(ptr)); + crate::session_gc::SessionGc::drop_session_string_box(ptr); } } } diff --git a/crates/oxide_vm/tests/concat_n_tests.rs b/crates/oxide_vm/tests/concat_n_tests.rs index 2af5fe5e..7cc09adf 100644 --- a/crates/oxide_vm/tests/concat_n_tests.rs +++ b/crates/oxide_vm/tests/concat_n_tests.rs @@ -23,7 +23,7 @@ fn to_str(vm: &Vm, val: JsValue) -> String { fn fmt(vm: &Vm, val: JsValue) -> String { if val.is_string() { // SAFETY: val 已确认是字符串值。 - unsafe { (*val.as_string_ptr()).data.clone() } + unsafe { (*val.as_string_ptr()).to_owned_string() } } else if val.is_bigint() { format!("{}", vm.bigint_value(val)) } else { diff --git a/crates/oxide_vm/tests/property_keys_order_tests.rs b/crates/oxide_vm/tests/property_keys_order_tests.rs index cd342329..67c1ace1 100644 --- a/crates/oxide_vm/tests/property_keys_order_tests.rs +++ b/crates/oxide_vm/tests/property_keys_order_tests.rs @@ -24,7 +24,7 @@ fn keys_to_strings(result: JsValue) -> Vec { if v.is_string() { // SAFETY: 字符串值指向 VM arena(调用方保持 vm 存活)。 let s = unsafe { &*v.as_string_ptr() }; - keys.push(s.data.clone()); + keys.push(s.to_owned_string()); } } keys @@ -117,7 +117,7 @@ fn numeric_literal_key_materializes_as_string() { // `{5:1}` 的键是字符串 "5"(键经 ToPropertyKey 字符串化),读 o[5]/o["5"] 同一键。 let r = eval_str("var o={5:1}; Object.getOwnPropertyNames(o)[0]").unwrap(); assert!(r.is_string()); - let s = unsafe { &*r.as_string_ptr() }.data.clone(); + let s = unsafe { &*r.as_string_ptr() }.to_owned_string(); assert_eq!(s, "5"); let r = eval_str("var o={5:1}; o[5] + o['5']").unwrap(); assert!(r.is_int() && r.as_int() == 2); diff --git a/crates/oxide_vm/tests/string_rope_tests.rs b/crates/oxide_vm/tests/string_rope_tests.rs new file mode 100644 index 00000000..5e2d0ba8 --- /dev/null +++ b/crates/oxide_vm/tests/string_rope_tests.rs @@ -0,0 +1,167 @@ +//! 二元 ConsString(rope)语义回归测试。 +//! +//! 覆盖:`+`/`+=` 拼接产物(rope 节点)在各消费点(`==`/`.length`/string 方法/ +//! for-of/属性键/模板/JSON/展开)的惰性扁平化正确性、数字/对象叶子混合、CONCAT_N +//! 产物作 rope 叶子、低 GC 阈值下跨执行期字符串回收的存活与 mark 传播。 + +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {e}"))?; + vm.run(&module) +} + +fn eval_str(source: &str) -> String { + let mut vm = Vm::new(); + let val = eval(&mut vm, source).expect("eval ok"); + fmt(&vm, val) +} + +fn fmt(vm: &Vm, val: JsValue) -> String { + if val.is_string() { + vm.lookup_str(val).unwrap_or_default() + } else if val.is_bigint() { + format!("{}", vm.bigint_value(val)) + } else { + format!("{val}") + } +} + +/// 低 GC 阈值 VM:水位=1 使指令边界检查几乎每指令触发,压 rope 的 mark 传播与 +/// 跨回收存活路径。 +fn vm_with_tiny_threshold() -> Vm { + let mut cfg = KernelConfig::minimal(); + cfg.set_session_gc_threshold(1); + let core = KernelCore::new(cfg); + Vm::with_kernel_core(core) +} + +#[test] +fn rope_binary_concat_content_and_length() { + // 二元 `+` 产物为 Cons 节点:内容与长度经惰性扁平化与缓存 O(1) 读取。 + assert_eq!(eval_str("\"ab\" + \"cd\""), "abcd"); + assert_eq!(eval_str("(\"ab\" + \"cd\").length"), "4"); + assert_eq!(eval_str("(\"ab\" + \"cd\" + \"ef\").length"), "6"); + assert_eq!(eval_str("(\"ab\" + \"cd\").charCodeAt(1)"), "98"); +} + +#[test] +fn rope_equality_with_literal_and_rope() { + // `==`/`===` 走 string_value_eq 内容比较:rope 与同内容字面量/另一 rope 相等, + // 与不同内容不等。 + assert_eq!(eval_str("(\"ab\" + \"cd\") === \"abcd\""), "true"); + assert_eq!(eval_str("(\"ab\" + \"cd\") == \"abcd\""), "true"); + assert_eq!(eval_str("(\"ab\" + \"cd\") === (\"a\" + \"bcd\")"), "true"); + assert_eq!(eval_str("(\"ab\" + \"cd\") === \"abce\""), "false"); +} + +#[test] +fn rope_compound_loop_builds_correctly() { + // string_build 主模式:`s += j` 循环 50 次(数字叶子 + Cons 链接), + // 末次 `.length` 扁平化结果正确(0..49 拼接:10 个单字符 + 40 个双字符 = 90)。 + assert_eq!( + eval_str( + "var s = ''; for (var j = 0; j < 50; j++) { s += j } \ + s + '|' + s.length" + ), + "012345678910111213141516171819202122232425262728293031323334353637383940414243444546474849|90" + ); +} + +#[test] +fn rope_mixed_operand_leaf_conversion() { + // 非字符串操作数先转叶子再链接:数字/布尔/null/undefined/对象与字符串混拼。 + assert_eq!(eval_str("var s = 'a'; s += 1; s += 2.5; s += true; s"), "a12.5true"); + assert_eq!(eval_str("'x' + null + undefined"), "xnullundefined"); + assert_eq!(eval_str("var s = 'a'; s += { toString: function () { return 'obj' } }; s"), "aobj"); +} + +#[test] +fn rope_concat_n_rhs_becomes_leaf() { + // `s += a+b+c`:rhs 摊平为 CONCAT_N(急切 Flat 产物),外层 COMPOUND_ADD 把 + // Flat 产物链接成 Cons 子节点——两层机制叠加内容正确。 + assert_eq!(eval_str("var s = ''; s += 'a' + 'b' + 'c'; s + s"), "abcabc"); + assert_eq!(eval_str("var s = 'z'; for (var i = 0; i < 5; i++) { s += 'x' + 'y' } s"), "zxyxyxyxyxy"); +} + +#[test] +fn rope_as_property_key() { + // rope 作属性键:ToPropertyKey → flat 桥接 intern,键内容一致。 + assert_eq!(eval_str("var s = 'a' + 'b'; var o = {}; o[s] = 42; o['ab']"), "42"); + assert_eq!(eval_str("var s = '5'; var o = {}; o[s] = 7; o[5]"), "7"); +} + +#[test] +fn rope_for_of_iterates_chars() { + // for-of 字符串迭代 rope:首次访问扁平化后游标推进,逐字符正确。 + assert_eq!(eval_str("var s = 'ab' + 'cd'; var out = ''; for (var c of s) { out += c } out"), "abcd"); + assert_eq!(eval_str("var s = 'x'; s += 'y'; s += 'z'; [...s].join('-')"), "x-y-z"); +} + +#[test] +fn rope_consumed_by_string_methods() { + // string 方法族(this_string 借用 + 只读扫描)消费 rope 时惰性扁平化。 + assert_eq!(eval_str("('hello' + 'world').indexOf('wo')"), "5"); + assert_eq!(eval_str("('hello' + 'world').substring(2, 7)"), "llowo"); + assert_eq!(eval_str("('ab' + 'cd').repeat(2)"), "abcdabcd"); + assert_eq!(eval_str("('aB' + 'cD').toUpperCase()"), "ABCD"); + assert_eq!(eval_str("('a,b' + ',c').split(',').length"), "3"); +} + +#[test] +fn rope_in_template_and_json() { + // 模板串与 JSON.stringify 消费 rope 结果一致。 + assert_eq!(eval_str("var s = 'a' + 'b'; `${s}-x`"), "ab-x"); + assert_eq!(eval_str("var s = 'x' + 'y'; JSON.stringify([s])"), "[\"xy\"]"); +} + +#[test] +fn rope_numeric_coercion_of_content() { + // rope 文本参与数值转换:ToNumber 走 string_data 扁平化文本。 + assert_eq!(eval_str("('1' + '0') * 2"), "20"); + assert_eq!(eval_str("Number('4' + '2')"), "42"); +} + +#[test] +fn rope_survives_runtime_string_gc() { + // 低阈值下指令边界检查几乎每指令触发:循环拼接全程(链接结果写寄存器先于 + // 下次检查)不被误回收;大量中间垃圾触发多轮回收后 rope 仍正确消费。 + let mut vm = vm_with_tiny_threshold(); + let val = eval( + &mut vm, + "var s = 'a'; for (var i = 0; i < 300; i++) { s += 'b' } \ + var garbage = ''; for (var g = 0; g < 500; g++) { garbage += 'x' } \ + s.length + '|' + s.charAt(150)", + ) + .expect("eval ok"); + assert_eq!(fmt(&vm, val), "301|b"); +} + +#[test] +fn rope_in_object_holder_survives_gc() { + // rope 挂在全局对象属性上:mark 经对象字符串边传播 rope 闭包,子节点与 + // 产物随父存活(跨多轮低阈值回收)。 + let mut vm = vm_with_tiny_threshold(); + let val = eval( + &mut vm, + "var o = {}; o.s = 'x'; for (var i = 0; i < 200; i++) { o.s += 'y' } \ + var t = ''; for (var g = 0; g < 300; g++) { t += 'z' } \ + o.s.length + '|' + o.s.charAt(200) + '|' + t.length", + ) + .expect("eval ok"); + assert_eq!(fmt(&vm, val), "201|y|300"); +} + +#[test] +fn rope_dead_after_scope_released() { + // rope 离开作用域成为垃圾:低阈值下被后续分配回收,不影响新值。 + let mut vm = vm_with_tiny_threshold(); + eval(&mut vm, "var t = ''; for (var i = 0; i < 200; i++) { t += 'z' }").expect("eval ok"); + let val = eval(&mut vm, "'fresh' + 'value'").expect("eval ok"); + assert_eq!(fmt(&vm, val), "freshvalue"); +}