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
+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");
}