perf: 字符串 +/+= 二元拼接改 Cons rope 惰性扁平化消除 O(n²)

JsString 拆 Flat/Cons 变体:Cons 经独立 ConsNode 保存左右子节点 + O(1)
字节长 + 惰性扁平化产物缓存(OnceLock 原子发布,产物挂节点生命周期)。
concat_strings 接线:总长 ≤ 128B 的小链急切扁平(与 rope 前同款,拼接即
消费场景零开销),大链 O(1) 链接——大段重复拼接从 O(n²) 拷贝降为 O(n)
链接,文本在消费点(.length/==/string_ref/方法族)首次扁平化后缓存。

GC 兼容:mark 收口 mark_string_live 传播 Cons 子节点与产物闭包(显式栈
防深链爆栈);sweep/full_reset 共用 drop 辅助连带释放产物;节点字节记账
用 O(1) byte_len。rope 节点登记 session 字符串表,与执行期字符串 GC
(指令边界触发)天然兼容。JsString 结构 32B 与 rope 前同 bin,小串路径
零结构回退;unsafe impl Send/Sync 论证同 StringPtr(perm 恒 Flat)。
This commit is contained in:
2026-08-15 14:32:08 +08:00
parent f25a3a1b95
commit f2535ffc77
15 changed files with 737 additions and 63 deletions
+44 -9
View File
@@ -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。
+174 -20
View File
@@ -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::<JsObject>() 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::<JsString>() + (*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());
}
}
+3 -3
View File
@@ -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));
}
+6 -6
View File
@@ -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<u16> = unsafe { (*src.as_string_ptr()).data.encode_utf16().collect() };
let code_units: Vec<u16> = 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<u16> = 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<u16> = unsafe { (*src.as_string_ptr()).data.encode_utf16().collect() };
let code_units: Vec<u16> = 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) {
+51 -4
View File
@@ -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::<JsString>() + 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::<JsString>() + len;
self.gc_state.session_bytes_allocated += std::mem::size_of::<JsString>() + bytes;
JsValue::string(ptr)
}
/// 分配 Cons(rope)节点:左右子节点 O(1) 链接,不拷贝文本。
///
/// # 边界与前提
/// - `left`/`right` 必须均为字符串值;仅在 `+`/`+=` 的 `concat_strings`
/// 接线点调用(CONCAT_N 保持急切扁平,不经此路径)。
///
/// # 副作用
/// - 登记节点到 session 字符串表(账目 = `size_of::<JsString>() + 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);
}
}
}
+1 -1
View File
@@ -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 {
@@ -24,7 +24,7 @@ fn keys_to_strings(result: JsValue) -> Vec<String> {
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);
+167
View File
@@ -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<JsValue, String> {
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");
}