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:
@@ -156,7 +156,7 @@ pub fn date_constructor<H: VmHost>(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<H: VmHost>(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;
|
||||
|
||||
@@ -22,7 +22,7 @@ pub fn json_parse<H: VmHost>(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<H: VmHost>(
|
||||
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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<StringPtr>; 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::*;
|
||||
|
||||
@@ -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 解析)。
|
||||
|
||||
@@ -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,44 +41,219 @@ 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 {}
|
||||
|
||||
/// 原生函数指针的类型安全不透明包装。
|
||||
///
|
||||
@@ -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<Box<JsString>> = 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());
|
||||
|
||||
@@ -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 };
|
||||
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);
|
||||
self.new_string_owned(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。
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user