types/kernel/vm/builtins: JsString UTF-16 载荷层(FlatU16 变体 + 单元入口)——原 UTF-8 载荷只存 well-formed 串、孤立 surrogate lossy 转 U+FFFD 回写,FromCodePoint/属性转义与字符串面码点语义皆破;现载荷直存 [u64;3](Flat well-formed / FlatU16 单元序列 / Cons 拼接),utf16_len 独立 32 位字,32B 布局锚保持;from_units 智能路由(含孤立→FlatU16),as_str 仅限 Flat,units/units_borrowed 单元借用,as_lossy_str 宽容读;string_forge 编解码 surrogate 感知(孤立 surrogate 逃逸 hex4 编码、解码展开 astral 代理对、string_ptr 由单元物化);VmHost 单元物化/分配/查询入口 + runtime_api 单元 API;字符串内置全法单元化(charAt/slice/substr/split/replace/search/match/matchAll 单元定位;空模式边界:search→0、match→[""]、replace 插 0 位、matchAll 游标耗尽守卫;接收者 ToString 错误原样传播);正则内置单元化(exec/test/search/symbol_* 单元文本,替换串 \u{...} 与孤立 surrogate 文本处理,lastIndex 与 matchAll 游标单元化);字符串迭代器码点化(高代理+后续低代理成对产出,astral 值回真实码点);String(Symbol) 抛 TypeError;回归钉更新五处(charAt/split/slice/substr 单元精确语义 + dispose 钉改 TypeError 断言);四 crate 全绿(oxide_vm 98 套件 2403 测)+ clippy/fmt 零新增

This commit is contained in:
2026-09-13 22:42:10 +00:00
parent 8a35dcd06e
commit 3acac721b0
17 changed files with 2177 additions and 1186 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ pub fn eval<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if !arg.is_string() {
return NativeResult::Ok(arg);
}
let code = vm.string_ref(arg).to_string();
let code = vm.lookup_str(arg).unwrap_or_default();
match vm.create_dynamic_script(&code) {
Ok(func) => {
let global = JsValue::from_js_object(vm.session().global_object().as_ptr() as *mut JsObject);
+64 -11
View File
@@ -1487,6 +1487,30 @@ fn get_iterator<H: VmHost>(vm: &mut H, value: JsValue) -> Result<Option<(JsValue
Ok(None)
}
/// 字符串迭代的一个产出元素:单个码元,或整个合法代理对(超平面字符,
/// 按 Unicode 标量产出单元素)。
#[derive(Clone, Copy)]
enum StrIterElem {
One(u16),
Pair(u16, u16),
}
/// 单元序列的字符串迭代步进(Unicode 标量口径):合法代理对整体产出一个
/// 2 单元元素(游标推进 2),否则产出单单元元素(推进 1);越界产出 None。
fn str_iter_step(units: &[u16], at: usize) -> (usize, Option<StrIterElem>) {
let Some(&u) = units.get(at) else {
return (0, None);
};
if (0xD800..=0xDBFF).contains(&u) {
if let Some(lo) = units.get(at + 1) {
if (0xDC00..=0xDFFF).contains(lo) {
return (at + 2, Some(StrIterElem::Pair(u, *lo)));
}
}
}
(at + 1, Some(StrIterElem::One(u)))
}
fn next_array_like<H: VmHost>(
vm: &mut H, wrapper: &mut JsObject, inner: JsValue, index_si: u32,
) -> Result<Option<JsValue>, JsValue> {
@@ -1513,18 +1537,47 @@ fn next_array_like<H: VmHost>(
}
if inner.is_string() {
// index 槽在字符串分支存字节游标(每步推进一个 char 的 UTF-8 宽度)而非
// 元素序号:该槽只被 next 内部读写、无外部消费者,包装器创建后类型固定
// 不跨类型复用,故可安全借用语义(字符串=字节偏移)。
let byteoff = current_index(vm, wrapper, index_si);
// 源串裸指针借用压缩到单个表达式:ch 是 Copy 的 char,不携带借用,
// index 槽在字符串分支存游标(按载荷形态钉死):单元形态(FlatU16/Cons)
// 存单元偏移;Flat 形态存 字节偏移×2(与单元口径对齐,每字符步长按其
// UTF-16 单元数)。游标只被 next 内部读写、无外部消费者,包装器创建后
// 类型固定不跨类型复用。
//
// 产出按 Unicode 标量(规格口径):合法代理对整体产出一个 2 单元元素,
// 孤立 surrogate 各产出 1 单元元素。
let cursor = current_index(vm, wrapper, index_si);
// 源串裸指针借用压缩到单个表达式:产出元素为 Copy 值,不携带借用,
// 之后对 VM 状态的可变访问不再与源串借用共存。
let ch = unsafe { &*inner.as_string_ptr() }.as_str()[byteoff..].chars().next();
if let Some(ch) = ch {
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int((byteoff + ch.len_utf8()) as i32));
let value = match vm.single_char(ch) {
Some(v) => v,
None => vm.new_string(&ch.to_string()),
let (next_cursor, elem): (usize, Option<StrIterElem>) = unsafe {
let sp = &*inner.as_string_ptr();
if sp.is_flat() {
let byteoff = cursor / 2;
match sp.as_str().get(byteoff..).unwrap_or("").chars().next() {
// 超平面字符:出整个代理对(单元素),游标跳越字符。
Some(c) if c.len_utf8() == 4 => {
let v = c as u32;
let hi = (0xD800 + (((v - 0x10000) >> 10) & 0x3FF)) as u16;
let lo = (0xDC00 + ((v - 0x10000) & 0x3FF)) as u16;
((byteoff + 4) * 2, Some(StrIterElem::Pair(hi, lo)))
}
Some(c) => ((byteoff + c.len_utf8()) * 2, Some(StrIterElem::One(c as u16))),
None => (0, None),
}
} else if let Some(units) = sp.units_borrowed() {
str_iter_step(units, cursor)
} else {
let units = sp.units();
str_iter_step(&units, cursor)
}
};
if let Some(elem) = elem {
vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(next_cursor as i32));
let value = match elem {
// ASCII 单元走单字符缓存零分配,其余(含孤立 surrogate)落串创建。
StrIterElem::One(u) => match vm.single_unit(u) {
Some(v) => v,
None => vm.new_string_units(&[u]),
},
StrIterElem::Pair(hi, lo) => vm.new_string_units(&[hi, lo]),
};
return Ok(Some(make_iter_result(vm, value, false)));
}
+2 -2
View File
@@ -196,8 +196,8 @@ pub fn module_data<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
}
"text" => {
// 经借用路径拷贝出 owned String 后再入堆,避免借用跨过 new_string 分配点。
let text = vm.string_ref(content_val).to_string();
// lossy 文本拷贝出 owned String 再入堆(模块数据文本桥接,131.2 边界)。
let text = vm.lookup_str(content_val).unwrap_or_default();
vm.new_string_owned(text)
}
_ => return type_error(vm, "__moduleData: unsupported data kind"),
+113 -63
View File
@@ -4,6 +4,8 @@ use oxide_types::value::JsValue;
use oxide_runtime_api::{NativeResult, VmHost};
use crate::string::{make_units_array, regex_replace_fn, regex_replace_manual_units, OwnedText};
fn get_regexp_ptr<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, JsValue> {
let this_val = vm.reg(args[0]);
if !this_val.is_object() {
@@ -199,19 +201,21 @@ pub fn regexp_test<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
// SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box<regress::Regex>` 指针。
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) };
let haystack = match regexp_string_arg(vm, args) {
let haystack = match regexp_text_arg(vm, args) {
Ok(s) => s,
Err(err) => return NativeResult::Err(err),
};
let last_index = vm.coerce_number_bounded(get_prop(re, 0)).unwrap_or(f64::NAN) as usize;
let is_global = is_global(re);
if is_global {
let result = regex.find_from(&haystack, last_index).next().is_some();
NativeResult::Ok(JsValue::bool(result))
// lastIndex 为码元口径;Str 臂的内部字节换算在 find_from_units 内完成。
let text = haystack.as_match_text();
let found = if is_global {
text.find_from_units(regex, last_index).is_some()
} else {
NativeResult::Ok(JsValue::bool(regex.find(&haystack).is_some()))
}
text.find_from_units(regex, 0).is_some()
};
NativeResult::Ok(JsValue::bool(found))
}
/// `RegExp.prototype.exec(string)`:执行匹配并返回数组(含捕获组、index、input)。
@@ -234,7 +238,7 @@ pub fn regexp_exec<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
// SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box<regress::Regex>` 指针。
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) };
let haystack = match regexp_string_arg(vm, args) {
let haystack = match regexp_text_arg(vm, args) {
Ok(s) => s,
Err(err) => return NativeResult::Err(err),
};
@@ -246,13 +250,16 @@ pub fn regexp_exec<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
(li, g)
};
// 匹配范围取臂原生命径(Str 臂字节、Units 臂码元);index/lastIndex 落盘
// 前统一换算到码元口径(规格口径)。
let text = haystack.as_match_text();
let match_result = if is_global {
if last_index > haystack.len() {
if last_index > text.len_units() {
return NativeResult::Ok(JsValue::null());
}
regex.find_from(&haystack, last_index).next()
text.find_from_units(regex, last_index)
} else {
regex.find(&haystack)
text.find_from_units(regex, 0)
};
if let Some(m) = match_result {
@@ -263,11 +270,13 @@ pub fn regexp_exec<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let arr =
vm.alloc_object(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(proto), n, vm.epoch().bump()));
unsafe {
(*arr).set_prop_at(0, vm.new_string(&haystack[range.start..range.end]));
(*arr).set_prop_at(0, vm.new_string_units_owned(text.slice(range.start, range.end).into_owned()));
// 捕获组:未参与匹配的组为 undefined。
for i in 1..=group_count {
match m.group(i) {
Some(g) => (*arr).set_prop_at(i, vm.new_string(&haystack[g.start..g.end])),
Some(g) => {
(*arr).set_prop_at(i, vm.new_string_units_owned(text.slice(g.start, g.end).into_owned()))
}
None => (*arr).set_prop_at(i, JsValue::undefined()),
}
}
@@ -275,14 +284,14 @@ pub fn regexp_exec<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
let index_si = vm.kernel_core().perm_interner().intern("index").0;
vm.set_or_create_prop_value(unsafe { &mut *arr }, index_si, JsValue::int(range.start as i32));
vm.set_or_create_prop_value(unsafe { &mut *arr }, index_si, JsValue::int(text.unit_pos(range.start) as i32));
let input_val = haystack.to_value(vm);
let input_si = vm.kernel_core().perm_interner().intern("input").0;
let haystack_val = vm.new_string(&haystack);
vm.set_or_create_prop_value(unsafe { &mut *arr }, input_si, haystack_val);
vm.set_or_create_prop_value(unsafe { &mut *arr }, input_si, input_val);
let groups_si = vm.kernel_core().perm_interner().intern("groups").0;
vm.set_or_create_prop_value(unsafe { &mut *arr }, groups_si, JsValue::undefined());
if is_global {
set_prop_at(re_ptr, 0, JsValue::int(range.end as i32));
set_prop_at(re_ptr, 0, JsValue::int(text.unit_pos(range.end) as i32));
}
NativeResult::Ok(JsValue::from_js_object(arr))
@@ -327,8 +336,33 @@ fn is_global(re: &JsObject) -> bool {
}
}
/// 取匹配文本参数,按 ToString 语义完整转换(对象经 toString/valueOf);
/// 结果按载荷形态 owned:良形文本走 Str 臂,含孤立 surrogate 等走 Units 臂。
/// 对象 ToString 抛出的原生异常原样传播。
fn regexp_text_arg<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<OwnedText, JsValue> {
let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] });
match oxide_runtime_api::to_string_value_full(val, vm) {
Ok(v) => {
// SAFETY: v 为字符串值,借用即时消费。
let sp = unsafe { &*v.as_string_ptr() };
if sp.is_flat() {
Ok(OwnedText::Str(sp.as_str().to_string()))
} else {
Ok(OwnedText::Units(sp.units().into_owned()))
}
}
Err(_) => {
// ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播。
if let Some(exc) = vm.take_uncaught_value() {
return Err(exc);
}
Err(crate::error::create_type_error(vm, "Cannot convert value to a string"))
}
}
}
/// 取 this 的已编译 regress 正则与匹配文本;this 非 RegExp 或缺失编译结果时返回 Err。
fn regexp_regex_and_text<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(&'static regress::Regex, String), JsValue> {
fn regexp_regex_and_text<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(&'static regress::Regex, OwnedText), JsValue> {
let re_ptr = get_regexp_ptr(vm, args)?;
let fn_ptr = {
let re = unsafe { &*re_ptr };
@@ -339,21 +373,20 @@ fn regexp_regex_and_text<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(&'static
};
// SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box<regress::Regex>` 指针。
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) };
let haystack = regexp_string_arg(vm, args)?;
let haystack = regexp_text_arg(vm, args)?;
Ok((regex, haystack))
}
/// 取匹配文本参数,按 ToString 语义完整转换(对象经 toString/valueOf);异常原样传播。
fn regexp_string_arg<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<String, JsValue> {
let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] });
match oxide_runtime_api::to_string_full(val, vm) {
Ok(s) => Ok(s),
/// 替换参数转单元序列:字符串值按载荷形态原样借出,其余经完整 ToString;
/// Symbol 按规范抛 TypeError;对象 ToString 抛出的原生异常原样传播。
fn replacement_units<H: VmHost>(vm: &mut H, val: JsValue) -> Result<Vec<u16>, JsValue> {
match oxide_runtime_api::to_string_value_full(val, vm) {
Ok(v) => Ok(vm.string_units(v).into_owned()),
Err(_) => {
// ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播。
if let Some(exc) = vm.take_uncaught_value() {
return Err(exc);
}
Err(crate::error::create_type_error(vm, "Cannot convert value to a string"))
Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string"))
}
}
}
@@ -379,22 +412,23 @@ pub fn regexp_symbol_match<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
Err(err) => return NativeResult::Err(err),
};
set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(0));
let mut matches: Vec<String> = Vec::new();
let mut last_end = 0;
for m in regex.find_iter(&haystack) {
let text = haystack.as_match_text();
let mut matches: Vec<Vec<u16>> = Vec::new();
let mut last_end = 0usize;
text.for_each_match(regex, |m| {
let range = m.range();
matches.push(haystack[range.start..range.end].to_string());
last_end = range.end;
}
matches.push(text.slice(range.start, range.end).into_owned());
last_end = text.unit_pos(range.end);
});
set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(last_end as i32));
if matches.is_empty() {
return NativeResult::Ok(JsValue::null());
}
NativeResult::Ok(crate::string::make_string_array(vm, matches))
NativeResult::Ok(make_units_array(vm, matches))
}
/// `RegExp.prototype[Symbol.replace](string, replacement)`:按匹配替换。
/// 字符串 replacement 展开 `$` 引用(`$$`/`$&`/``$` ``/`$'`/`$n`),函数 replacement
/// 字符串 replacement 展开 `$` 引用(`$$`/`$&`/`` $` ``/`$'`/`$n`),函数 replacement
/// 逐匹配调用;global 全替换,否则替换首个。
///
/// # 步骤
@@ -414,6 +448,10 @@ pub fn regexp_symbol_replace<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(0));
}
// 替换文本统一走单元口径($ 展开/回调参数/拼接全在单元序列上进行)。
let text = haystack.as_match_text();
let units = text.units();
let result = if args.len() > 2 {
let replacer_val = vm.reg(args[2]);
if replacer_val.is_object() {
@@ -422,49 +460,56 @@ pub fn regexp_symbol_replace<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
// 回调第 4 参(原字符串)预构一次:原始字符串参数直接复用零拷贝,
// 对象参数复用已转换的 haystack 建单个会话串(原每匹配整串复制)。
let text_val = vm.reg(if args.len() > 1 { args[1] } else { args[0] });
let text_arg = if text_val.is_string() { text_val } else { vm.new_string(&haystack) };
crate::string::regex_replace_fn(vm, regex, &haystack, replacer_val, is_global, text_arg)
let text_arg = if text_val.is_string() { text_val } else { haystack.to_value(vm) };
regex_replace_fn(vm, regex, &units, replacer_val, is_global, text_arg)
} else {
let replacement = oxide_runtime_api::to_string(replacer_val);
NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual(
let replacement = match replacement_units(vm, replacer_val) {
Ok(u) => u,
Err(err) => return NativeResult::Err(err),
};
NativeResult::Ok(vm.new_string_units_owned(regex_replace_manual_units(
regex,
&haystack,
&units,
&replacement,
is_global,
)))
}
} else {
let replacement = oxide_runtime_api::to_string(replacer_val);
NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual(
let replacement = match replacement_units(vm, replacer_val) {
Ok(u) => u,
Err(err) => return NativeResult::Err(err),
};
NativeResult::Ok(vm.new_string_units_owned(regex_replace_manual_units(
regex,
&haystack,
&units,
&replacement,
is_global,
)))
}
} else {
NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual(regex, &haystack, "", is_global)))
NativeResult::Ok(vm.new_string_units_owned(regex_replace_manual_units(regex, &units, &[], is_global)))
};
if is_global {
let mut last_end = 0;
for m in regex.find_iter(&haystack) {
last_end = m.range().end;
}
let mut last_end = 0usize;
text.for_each_match(regex, |m| {
last_end = text.unit_pos(m.range().end);
});
set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(last_end as i32));
}
result
}
/// `RegExp.prototype[Symbol.search](string)`:返回首个匹配位置(字符索引),
/// `RegExp.prototype[Symbol.search](string)`:返回首个匹配位置(码元索引),
/// 无匹配返回 -1。global 标志不影响 search。
pub fn regexp_symbol_search<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (regex, haystack) = match regexp_regex_and_text(vm, args) {
Ok(pair) => pair,
Err(err) => return NativeResult::Err(err),
};
if let Some(m) = regex.find(&haystack) {
return NativeResult::Ok(JsValue::int(m.range().start as i32));
let text = haystack.as_match_text();
if let Some(m) = text.find_from_units(regex, 0) {
return NativeResult::Ok(JsValue::int(text.unit_pos(m.range().start) as i32));
}
NativeResult::Ok(JsValue::int(-1))
}
@@ -487,32 +532,35 @@ pub fn regexp_symbol_split<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
u32::MAX as usize
};
let mut parts: Vec<String> = Vec::new();
let mut last_end = 0;
for m in regex.find_iter(&haystack) {
// 片段切片取臂原生命径(Str 臂字节、Units 臂码元);limit 触顶后提前停推
// 但保持游标语义一致(尾段仅在未触顶时追加)。
let text = haystack.as_match_text();
let mut parts: Vec<Vec<u16>> = Vec::new();
let mut last_end = 0usize;
text.for_each_match(regex, |m| {
if parts.len() >= limit {
break;
return;
}
let range = m.range();
parts.push(haystack[last_end..range.start].to_string());
parts.push(text.slice(last_end, range.start).into_owned());
if parts.len() >= limit {
break;
return;
}
for i in 1..=m.captures.len() {
if parts.len() >= limit {
break;
}
match m.group(i) {
Some(g) => parts.push(haystack[g.start..g.end].to_string()),
None => parts.push(String::new()),
Some(g) => parts.push(text.slice(g.start, g.end).into_owned()),
None => parts.push(Vec::new()),
}
}
last_end = range.end;
});
if parts.len() < limit {
parts.push(text.slice(last_end, text.raw_len()).into_owned());
}
if last_end <= haystack.len() && parts.len() < limit {
parts.push(haystack[last_end..].to_string());
}
NativeResult::Ok(crate::string::make_string_array(vm, parts))
NativeResult::Ok(make_units_array(vm, parts))
}
/// `RegExp.prototype[Symbol.matchAll](string)`:返回按 global 语义逐个产出
@@ -526,7 +574,7 @@ pub fn regexp_symbol_match_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResu
let re = unsafe { &*re_ptr };
let is_global = is_global(re);
let last_index = vm.coerce_number_bounded(get_prop(re, PROP_LAST_INDEX)).unwrap_or(f64::NAN) as usize;
let haystack = match regexp_string_arg(vm, args) {
let haystack = match regexp_text_arg(vm, args) {
Ok(s) => s,
Err(err) => return NativeResult::Err(err),
};
@@ -573,7 +621,9 @@ pub fn regexp_symbol_match_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResu
let input_si = vm.kernel_core().perm_interner().intern(crate::string::MALL_INPUT).0;
let index_si = vm.kernel_core().perm_interner().intern(crate::string::MALL_INDEX).0;
let re_si = vm.kernel_core().perm_interner().intern(crate::string::MALL_RE).0;
let input_val = vm.new_string(&haystack);
// input 属性存完整转换后的字符串值(单元保真);index 游标为码元口径,
// next 按 input 载荷形态各自消费。
let input_val = haystack.to_value(vm);
vm.set_or_create_prop_value(wrapper_obj, input_si, input_val);
vm.set_or_create_prop_value(wrapper_obj, index_si, JsValue::int(last_index as i32));
vm.set_or_create_prop_value(wrapper_obj, re_si, iter_re);
+3 -2
View File
@@ -44,12 +44,13 @@ impl Hash for SetKey {
let bits = if d == 0.0 { 0.0f64.to_bits() } else { JsValue::float(d).to_bits() };
bits.hash(state);
}
// String:内容字节(rope 经 `as_str` 惰性扁平化,幂等)。
// String:单元序列(rope 经扁平化缓存,幂等;单元口径与
// same_value_zero 的内容比较同域,同值同哈希)。
JsType::String => {
1u8.hash(state);
// SAFETY: 串指针在 native 调用期间存活(session 串不在调用中途释放),
// 借出在本哈希调用内即时消费。
unsafe { (*v.as_string_ptr()).as_str().as_bytes().hash(state) };
unsafe { (*v.as_string_ptr()).units().as_ref().hash(state) };
}
// BigInt:`to_bytes_le` 规范形(零号统一 NoZero,同值同字节)。
JsType::BigInt => {
File diff suppressed because it is too large Load Diff
+162 -2
View File
@@ -15,6 +15,111 @@ fn hash64(s: &str) -> u64 {
h.finish()
}
/// 把字符串单元序列编码为 interner 键文本。配对感知:
/// - 良配 surrogate 对(高+低)编码为对应单字符(超平面字符逐字保留,
/// 键文本保持恒等);
/// - 孤立 surrogate 编码为 U+FFFD 后跟 4 位小写十六进制;
/// - 单元 U+FFFD 编码为 U+FFFD 后跟字面文本 `fffd`(与任何 surrogate
/// 的转义文本可区分)。
///
/// 编码无损:`decode_key` 是其精确逆变换(对无 FFFD 的良形文本为恒等)。
pub fn encode_key(units: &[u16]) -> String {
let mut out = String::with_capacity(units.len());
let mut i = 0;
while i < units.len() {
let u = units[i];
if (0xD800..=0xDBFF).contains(&u) && i + 1 < units.len() {
let lo = units[i + 1];
if (0xDC00..=0xDFFF).contains(&lo) {
let cp = 0x10000u32 + (((u as u32) - 0xD800) << 10) + (lo as u32 - 0xDC00);
out.push(char::from_u32(cp).expect("良配 surrogate 对必映射为合法码点"));
i += 2;
continue;
}
}
if (0xD800..=0xDFFF).contains(&u) {
out.push('\u{FFFD}');
out.push_str(&format!("{:04x}", u));
} else if u == 0xFFFD {
out.push('\u{FFFD}');
out.push_str("fffd");
} else {
// 非 surrogate 单元恒可单字符表示(BMP 或超平面字符由对分支处理)。
out.push(char::from_u32(u as u32).expect("非 surrogate 单元必为合法码点"));
}
i += 1;
}
out
}
/// `encode_key` 的逆变换:把键文本还原为单元序列。
///
/// 转义约定:U+FFFD 后跟 `fffd` 还原为单元 U+FFFD;U+FFFD 后跟 surrogate
/// 段内的 4 位小写十六进制还原为对应孤立 surrogate 单元。FFFD 后跟其他
/// 文本是防御性兜底(`encode_key` 不产生该形态):输出 FFFD 单元并把后续
/// 已消费字符原样补回。
///
/// 已知边界:用户来源的键文本若含裸 FFFD 且其后恰为 `fffd` 或 surrogate
/// 段 4 位十六进制,物化时该 4 字符会被当作转义吞掉。物化面仅限 builtin
/// 永久键(恒为良形恒等文本),故该歧义不实际发生。
pub fn decode_key(text: &str) -> Vec<u16> {
let mut out: Vec<u16> = Vec::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c != '\u{FFFD}' {
if (c as u32) > 0xFFFF {
// 超平面字符还原为其良配 surrogate 对。
let v = (c as u32) - 0x10000;
out.push(0xD800 + (v >> 10) as u16);
out.push(0xDC00 + (v & 0x3FF) as u16);
} else {
out.push(c as u16);
}
continue;
}
// FFFD:消费其后最多 4 字符判定转义形态。
let mut tail: [char; 4] = [' '; 4];
let mut n = 0;
while n < 4 {
if let Some(&ch) = chars.peek() {
tail[n] = ch;
chars.next();
n += 1;
} else {
break;
}
}
if n == 4 && tail == ['f', 'f', 'f', 'd'] {
out.push(0xFFFD);
continue;
}
if n == 4 {
let mut v: u16 = 0;
let mut ok = true;
for &ch in tail.iter() {
// 编码端恒产小写十六进制(0-9 或 a-f),大写/非 hex 一律视为裸文本。
match ch.to_digit(16) {
Some(d) if matches!(ch, '0'..='9' | 'a'..='f') => v = v * 16 + d as u16,
_ => {
ok = false;
break;
}
}
}
if ok && (0xD800..=0xDFFF).contains(&v) {
out.push(v);
continue;
}
}
// 防御性兜底:裸 FFFD,后续字符原样补回。
out.push(0xFFFD);
for ch in tail.iter().take(n) {
out.push(*ch as u16);
}
}
out
}
/// 一条 intern 过的键。`data` 是泄漏的 `&'static str`——永久键从不释放
/// (按设计 append-only),所以泄漏即存储模型,而非 bug。键 id 与 64 位
/// 哈希经 `DashMap` 的哈希键→候选 id 表寻址,条目自身不存哈希。
@@ -129,7 +234,10 @@ impl PermInterner {
perm.resize_with(id as usize + 1, || None);
}
if perm[id as usize].is_none() {
perm[id as usize] = Some(Box::new(JsString::new(text.to_string())));
// 键文本经逆编码还原为单元序列再物化:良形键为逐字恒等,
// 含转义的键(见 encode_key 约定)还原出孤立 surrogate 单元。
let units = decode_key(text);
perm[id as usize] = Some(Box::new(JsString::from_units(units)));
}
perm[id as usize].as_ref().unwrap().as_ref() as *const JsString
}
@@ -326,6 +434,58 @@ mod tests {
}
}
#[test]
fn encode_key_identity_and_escapes() {
// 良形且无 FFFD(含超平面良配对)编码为恒等文本。
assert_eq!(encode_key(&"abc🚀".encode_utf16().collect::<Vec<u16>>()), "abc🚀");
// 孤立 surrogate:FFFD + 4 位小写十六进制。
assert_eq!(encode_key(&[0xD800]), "\u{FFFD}d800");
assert_eq!(encode_key(&[0xDFFF]), "\u{FFFD}dfff");
assert_eq!(encode_key(&[0xDBFF, 0x61]), "\u{FFFD}dbffa");
// FFFD 单元:FFFD + 字面 "fffd"。
assert_eq!(encode_key(&[0xFFFD]), "\u{FFFD}fffd");
// 两种不同单元序列的键文本互异(解码可区分)。
assert_ne!(encode_key(&[0xFFFD]), encode_key(&[0xD800]));
}
#[test]
fn encode_decode_roundtrip() {
let cases: Vec<Vec<u16>> = vec![
Vec::new(),
"a".encode_utf16().collect(),
"abc🚀😀".encode_utf16().collect(),
vec![0xD800, 0x42, 0xDC00],
vec![0xFFFD, 0xFFFD, 0x41, 0xFFFD, 0x66, 0x66, 0x66, 0x64],
vec![0xFFFF, 0x0041, 0xDBFF, 0xDC00, 0xD800],
];
for units in &cases {
let key = encode_key(units);
assert_eq!(decode_key(&key), units.as_slice(), "roundtrip 失败: key={key:?}");
}
}
#[test]
fn decode_key_defensive_raw_fffd() {
// 裸 FFFD(文本末尾或后跟非转义文本)还原为 FFFD 单元,后续字符不被吞。
assert_eq!(decode_key("a\u{FFFD}"), &[0x61, 0xFFFD]);
assert_eq!(decode_key("a\u{FFFD}zz"), &[0x61, 0xFFFD, 0x7A, 0x7A]);
}
#[test]
fn string_ptr_materializes_units() {
let interner = PermInterner::new();
// 良形键:物化内容与旧行为逐位一致,且二次调用返回同一稳定指针。
let (id, _) = interner.intern("perm");
let ptr = interner.string_ptr(id);
assert_eq!(unsafe { (*ptr).as_str() }, "perm");
assert_eq!(interner.string_ptr(id), ptr);
// 含转义的键:物化出孤立 surrogate 单元形态。
let (id2, _) = interner.intern(&encode_key(&[0xD800]));
let ptr2 = interner.string_ptr(id2);
assert!(unsafe { (*ptr2).has_lone_surrogate() });
assert_eq!(unsafe { (*ptr2).units() }, &[0xD800][..]);
}
#[test]
fn perm_table_concurrent_first_use_unique_ptr() {
// 并发首用竞态回归:96 线程同一时刻命中冷表槽,强制多线程同入慢路径。
@@ -365,6 +525,6 @@ mod tests {
assert_eq!(unsafe { (*(c0 as *const JsString)).as_str() }, "x");
let t0 = t0 as *const JsString;
assert_eq!(unsafe { (*t0).as_str() }, "object");
assert_eq!(unsafe { (*t0).len() }, 6);
assert_eq!(unsafe { (*t0).utf16_len() }, 6);
}
}
+96 -26
View File
@@ -120,13 +120,23 @@ pub trait VmHost {
None
}
}
/// 借出字符串值的文本内容,生命周期绑定到 `&self` 借用。
///
/// perm 字符串由内核持有、永不释放;session 字符串只在 `&mut self` 路径
/// (`new_string`/`new_string_owned`/`maybe_collect_session_gc`)释放,`&self`
/// 借用与 `&mut` 互斥由编译器强制,借用期内该字符串不会回收。实现内部
/// `unsafe` 解引用,调用方须保证 `val` 为字符串值。
fn string_ref(&self, val: JsValue) -> &str;
/// 借出字符串值的 UTF-16 单元序列(Flat 惰性编码 / FlatU16 直接借用 /
/// Cons 扁平化缓存),生命周期绑定到 `&self` 借用。调用方须保证
/// `val` 为字符串值。
fn string_units(&self, val: JsValue) -> std::borrow::Cow<'_, [u16]>;
/// 取单单元的永久字符串值:ASCII 单元命中共享 perm 串(零分配、可指针
/// 短路),非 ASCII 返回 `None` 由调用方回落单元创建。
fn single_unit(&self, u: u16) -> Option<JsValue> {
if u < 0x80 {
Some(JsValue::string(oxide_kernel::string_forge::single_char_ptr(u as u8)))
} else {
None
}
}
/// 以单元序列创建会话字符串(智能路由:含孤立 surrogate 落 FlatU16)。
fn new_string_units(&mut self, units: &[u16]) -> JsValue;
/// 同 `new_string_units`,以 owned 单元序列接收,避免一次克隆。
fn new_string_units_owned(&mut self, units: Vec<u16>) -> JsValue;
/// 分配 BigInt 值(num_bigint::BigInt box 登记到 VM,返回携带指针的 `JsValue`)。
fn new_bigint(&mut self, v: num_bigint::BigInt) -> JsValue;
/// 读取 BigInt 值;调用方须保证 `val.is_bigint()`。
@@ -217,23 +227,25 @@ pub fn format_error_message(name: &str, msg: &str) -> String {
}
}
/// 借出字符串值的文本内容,生命周期为 `'static`。仅限同一函数内即时消费。
/// 取字符串值的 lossy 文本(孤立 surrogate 单元映射为 U+FFFD),生命周期为
/// `'static`。仅限同一函数内即时消费。
///
/// # Safety
/// 调用方必须保证:从取用返回值到最后一次使用之间,不发生任何可能释放该字符串
/// 调用方必须保证:从取用返回值到最后一次消费之间,不发生任何可能释放该字符串
/// 的操作(分配、GC、`full_reset`)——session 字符串在执行期 GC 或 reset 时会被
/// 释放,跨分配点持有引用即悬垂。需要跨分配点消费的场景改用 [`VmHost::string_ref`]
/// 或公开的 [`to_string`] owned 路径。
/// 释放,跨分配点持有即悬垂。需要单元语义(不可 lossy)的场景走
/// [`VmHost::string_units`] / [`to_units_full`]。
#[inline]
pub(crate) unsafe fn string_data(val: JsValue) -> &'static str {
(*val.as_string_ptr()).as_str()
pub(crate) unsafe fn string_data(val: JsValue) -> std::borrow::Cow<'static, str> {
// Borrowed 臂借用期名义绑定解引用临时;与旧 `&'static str` 版本同款契约
// (同函数内即时消费),生命周期统一升为 'static。
std::mem::transmute((*val.as_string_ptr()).as_lossy_str())
}
/// 按内容比较两个字符串 `JsValue` 是否相等。
///
/// 先做指针级短路(同一 interned 字符串必等),否则比较 `JsString` 的整块文本
/// (rope 未扁平化时经 `as_str` 惰性扁平化;内容比较对"rope vs 同内容不同指针"
/// 也正确)。
/// 先做指针级短路(同一 interned 字符串必等);双 Flat 比文本(与旧路径逐位
/// 一致),其余按单元序列比较(对"rope vs 同内容不同指针"也正确)。
#[inline]
pub fn string_value_eq(a: JsValue, b: JsValue) -> bool {
if a.as_string_ptr() == b.as_string_ptr() {
@@ -241,7 +253,10 @@ 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.as_str() == sb.as_str()
if sa.is_flat() && sb.is_flat() {
return sa.as_str() == sb.as_str();
}
sa.units() == sb.units()
}
/// BigInt 转 f64 的近似转换(连续整数用 to_u64,大数用 Display 解析)。
@@ -288,8 +303,8 @@ pub fn to_number(val: JsValue) -> f64 {
JsType::Null => 0.0,
JsType::Undefined => f64::NAN,
JsType::String => {
let s = unsafe { string_data(val) };
parse_js_number(s)
// 孤立 surrogate 的 lossy 文本不含数字/数字前缀,解析结果与单元口径一致(NaN)。
parse_js_number(&unsafe { string_data(val) })
}
JsType::BigInt => {
// BigInt → Number 近似转换:i128 超出 f64 精度时舍入为近似值。
@@ -520,7 +535,7 @@ pub fn push_to_string(val: JsValue, buf: &mut String) {
return;
}
if val.is_string() {
unsafe { buf.push_str(string_data(val)) };
unsafe { buf.push_str(&string_data(val)) };
return;
}
if val.is_bigint() {
@@ -565,6 +580,38 @@ pub fn to_string(val: JsValue) -> String {
String::new()
}
/// 把任意值转为 UTF-16 单元序列(`ToPrimitive(string hint)` 的单元口径):
/// 字符串值直接借出单元(Flat 惰性编码、rope 扁平化);Symbol 返回 TypeError
/// 错误;其余值经 ToString 原始值路径后 UTF-16 编码。
pub fn to_units_full<H: VmHost>(val: JsValue, host: &mut H) -> Result<Vec<u16>, String> {
if val.is_string() {
return Ok(host.string_units(val).into_owned());
}
if val.is_symbol() {
return Err(host.error_message_text("TypeError", "Cannot convert a Symbol value to a string"));
}
let prim = to_primitive(val, ToPrimitiveHint::String, host)?;
if prim.is_string() {
return Ok(host.string_units(prim).into_owned());
}
// 非字符串原始值(数字等):先格式化再编码。
let s = to_string(prim);
Ok(s.encode_utf16().collect())
}
/// 把值追加其 UTF-16 单元到 `buf`:字符串值直接扩单元序列,非字符串值经
/// ToString 后 UTF-16 编码。字符串拼接的单元通道路径(不可 lossy)。
pub fn push_units_to(val: JsValue, buf: &mut Vec<u16>) {
if val.is_string() {
// SAFETY: val 为字符串值,借用即时消费。
let s = unsafe { &*val.as_string_ptr() };
buf.extend_from_slice(s.units().as_ref());
return;
}
let s = to_string(val);
buf.extend(s.encode_utf16());
}
/// ToBoolean(ECMA-262 §7.1.2):falsy 值仅限 undefined/null/false/±0/NaN/空串,其余为 true。
pub fn to_boolean(val: JsValue) -> bool {
match val.js_type() {
@@ -608,13 +655,13 @@ pub fn abstract_eq<H: VmHost>(lhs: JsValue, rhs: JsValue, host: &mut H) -> Resul
(JsType::Int | JsType::Double, JsType::BigInt) => {
Ok(to_number(lhs) == bigint_to_f64(unsafe { bigint_data(rhs) }))
}
// BigInt 与 String:字符串解析为数字后比较。
// BigInt 与 String:字符串解析为数字后比较(lossy 文本口径,见 to_number)。
(JsType::BigInt, JsType::String) => {
let r = parse_js_number(unsafe { string_data(rhs) });
let r = parse_js_number(&unsafe { string_data(rhs) });
Ok(bigint_to_f64(unsafe { bigint_data(lhs) }) == r)
}
(JsType::String, JsType::BigInt) => {
let l = parse_js_number(unsafe { string_data(lhs) });
let l = parse_js_number(&unsafe { string_data(lhs) });
Ok(l == bigint_to_f64(unsafe { bigint_data(rhs) }))
}
// Number 与 String:字符串经 ToNumber 后按严格数值比较。
@@ -651,9 +698,15 @@ fn strict_double_eq(a: f64, b: f64) -> bool {
/// 双字符串按字典序;否则转数值比较,任一侧为 NaN 时返回 `None`(表示比较未定义,调用方据此处理 `<`/`>`)。
pub fn relational_compare(lhs: JsValue, rhs: JsValue) -> Option<bool> {
if lhs.is_string() && rhs.is_string() {
let ls = unsafe { string_data(lhs) };
let rs = unsafe { string_data(rhs) };
return Some(ls < rs);
// SAFETY: 两侧均为字符串值,借用即时消费。
let l = unsafe { &*lhs.as_string_ptr() };
let r = unsafe { &*rhs.as_string_ptr() };
if l.is_flat() && r.is_flat() {
// 双 Flat:文本比较与旧路径逐位一致。
return Some(l.as_str() < r.as_str());
}
// 单元口径:码元字典序即规范序,Flat 编码与 rope 扁平化统一走单元通道。
return Some(l.units() < r.units());
}
if lhs.is_bigint() && rhs.is_bigint() {
let l = unsafe { bigint_data(lhs) };
@@ -935,6 +988,23 @@ pub fn to_string_full<H: VmHost>(val: JsValue, host: &mut H) -> Result<String, S
Ok(to_string(primitive))
}
/// ToPrimitive(string hint) 完整转换并返回字符串*值*:字符串值原样返回(载荷
/// 形态保留,非 Flat 不 lossy);Symbol 返回 TypeError;其余值经 ToString 后
/// 以新字符串值返回(非 Flat 源的转换结果落良形 Flat)。
pub fn to_string_value_full<H: VmHost>(val: JsValue, host: &mut H) -> Result<JsValue, String> {
if val.is_string() {
return Ok(val);
}
let primitive = to_primitive(val, ToPrimitiveHint::String, host)?;
if primitive.is_symbol() {
return Err(host.error_message_text("TypeError", "Cannot convert a Symbol value to a string"));
}
if primitive.is_string() {
return Ok(primitive);
}
Ok(host.new_string_owned(to_string(primitive)))
}
/// 带完整对象强制转换的 ToBigInt(input)(§7.1.14):BigInt 原样;Boolean → 0/1;
/// String 走 StringToBigInt;Number/Symbol/undefined/null → TypeError;对象经
/// ToPrimitive(default hint)后递归处理。
+368 -131
View File
@@ -14,79 +14,190 @@ use std::sync::OnceLock;
///
/// 字符串*值*以 48 位指针(指向 `JsString`)NaN-box(见 `JsValue::string`)。
///
/// 两种变体:
/// - `Flat`:整块 UTF-8 文本(String,保留容量)。字面量、方法结果与 CONCAT_N
/// 产物保持此形态;
/// - `Cons`(rope):指向 [`ConsNode`] 载荷(左右子裸指针 + O(1) 字节长 +
/// 惰性扁平化缓存)。二元 `+`/`+=` 拼接 O(1) 链接不拷贝文本,整块文本在
/// 三种载荷形态(`tag` 区分,载荷并位 @0):
/// - `TAG_FLAT`:整块 UTF-8 文本(`String`,保留容量)。字面量、方法结果与
/// CONCAT_N 产物保持此形态;内容无孤立 surrogate(UTF-8 合法性天然保证);
/// - `TAG_FLAT_U16`:UTF-16 单元载荷(`Vec<u16>`,ptr/len/cap 与 String 同形),
/// 承载含孤立 surrogate 的任意单元序列——`String` 在结构上不可表示该内容;
/// - `TAG_CONS`(rope):指向 [`ConsNode`] 载荷(左右子裸指针 + O(1) 单元长 +
/// 惰性扁平化缓存)。二元 `+`/`+=` 拼接 O(1) 链接不拷贝文本,单元序列在
/// 首次消费时扁平化并原子发布。
///
/// 结构体大小恒为 32B:`Flat` 内联 `String`(cap@0 / data@8 / len@16),
/// `Cons` 首字为 tag(0xffff_ffff_ffff_ffff)@0、子节点指针@8;`utf16_len`
/// 两形态统一 @24(4B),尾部 4B 填充。`String` 原样存储(零 realloc 收缩);
/// Cons 专属状态(子节点/产物缓存)独立分配在 [`ConsNode`],仅大链持有。
/// 尺寸锚见下方 size_of/offset_of 断言。
/// 布局恒为 32B:载荷 24B @0、`utf16_len` @24(4B)、`tag` @28(1B,原 NLL
/// enum 的尾部填充位,显式 repr(C) 钉死)、尾部 3B 填充。`String` 原样存储
/// (零 realloc 收缩);Cons 专属状态(子节点/产物缓存)独立分配在 [`ConsNode`],
/// 仅大链持有。尺寸锚见下方 size_of/offset_of 断言。
///
/// 生命周期约定:
/// - `Cons` 子节点各自独立登记 session 字符串表(或为 perm 串),由 GC 的
/// mark 传播闭包保证随父存活;扁平化产物只挂在 `ConsNode::flat_cache`、不进
/// session 主表,随本节点连带释放。
/// - 地址稳定不搬移(Box 堆分配),GC 无需 forwarding / rewrite。
#[derive(Debug)]
#[repr(C)]
pub struct JsString {
kind: StringKind,
/// JS 语义的字符串长度(UTF-16 code unit 数)懒缓存。
/// 载荷 24B,按 `tag` 解读:Flat = `String`(data/len/cap)、FlatU16 =
/// `Vec<u16>`(ptr/len/cap,与 String 同形)、Cons = 节点指针(首字,
/// 余 16B 未定义填充)。
payload: [u64; 3],
/// JS 语义的字符串长度(UTF-16 code unit 数)。
///
/// 构造是热路径(拼接/格式化),长度查询少见,故不预付 O(n) 扫描;首次
/// `utf16_len()` 访问时计算一次。以 sentinel + `AtomicU32` 实现:perm 字符串
/// 可被多线程 VM 共享读,计算幂等,relaxed 序即可。
/// Flat 为懒缓存:构造是热路径(拼接/格式化),长度查询少见,不预付 O(n)
/// 扫描,首次 `utf16_len()` 访问时计算一次(sentinel + `AtomicU32`:perm
/// 串可被多线程 VM 共享读,计算幂等,relaxed 序即可)。FlatU16/Cons 构造时
/// 预写(单元数 / 子节点归纳,各 O(1))。
utf16_len: AtomicU32,
tag: u8,
}
// 布局锚:32B = 载荷 24B(Flat 内联 String / Cons tag+子指针)+ utf16_len 4B
// + 尾部填充 4B。tag 并入载荷首字(Cons 以 0xffff.. marker 占位),无独立索引字——
// 换变体形状/字段类型可能改变该折叠,布局变更先同步结构体头注释再改本断言。
// 布局锚:32B = 载荷 24B(三形态同形 ptr/len/cap 或节点指针)+ utf16_len 4B
// + tag 1B + 尾部填充 3B。三变体 NLL enum 需独立 tag 字(撑到 40B、utf16_len@32)
// 故以 repr(C) + 尾部 tag 字节钉死本布局;换载荷形状/字段类型先同步结构体头
// 注释再改本断言。
const _: () = assert!(
std::mem::size_of::<JsString>() == 32 && std::mem::offset_of!(JsString, utf16_len) == 24,
"JsString 布局漂移:锚定布局见结构体头注释(32B,utf16_len@24)"
std::mem::size_of::<JsString>() == 32
&& std::mem::offset_of!(JsString, utf16_len) == 24
&& std::mem::offset_of!(JsString, tag) == 28,
"JsString 布局漂移:锚定布局见结构体头注释(32B,utf16_len@24,tag@28)"
);
/// 字符串内容变体。
#[derive(Debug)]
enum StringKind {
Flat(String),
Cons(*const ConsNode),
/// 载荷形态标签(`JsString::tag` 取值)。
const TAG_FLAT: u8 = 0;
const TAG_FLAT_U16: u8 = 1;
const TAG_CONS: u8 = 2;
impl std::fmt::Debug for JsString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.tag {
TAG_FLAT => write!(f, "JsString::Flat({:?})", self.flat_ref()),
TAG_FLAT_U16 => write!(f, "JsString::FlatU16({:?})", self.flat_u16_ref()),
TAG_CONS => write!(f, "JsString::Cons(len={})", self.utf16_len()),
_ => write!(f, "JsString::Invalid"),
}
}
}
/// Cons(rope)节点的载荷:左右子节点指针 + O(1) 拼接字节长 + 扁平化产物缓存。
impl Drop for JsString {
fn drop(&mut self) {
// 按 tag 恰好释放激活载荷:Flat/FlatU16 释放载荷自身(内部 heap 缓冲),
// Cons 载荷节点由 drop_cons_node 单独释放(见其文档)。
//
// # Safety
// tag 与载荷内容恒由构造点同步写入;drop 期载荷仍为激活形态。
unsafe {
match self.tag {
TAG_FLAT => std::ptr::drop_in_place(self.payload.as_mut_ptr() as *mut String),
TAG_FLAT_U16 => std::ptr::drop_in_place(self.payload.as_mut_ptr() as *mut Vec<u16>),
TAG_CONS => {}
_ => unreachable!(),
}
}
}
}
/// Cons(rope)节点的载荷:左右子节点指针 + O(1) 拼接单元长 + 扁平化产物缓存。
///
/// 独立于 `JsString` 分配,使 Flat 路径的 `JsString` 保持与 rope 前相同的 32B
/// 结构(tag 并入载荷首字,见其尺寸断言);仅超过字节阈值的拼接
/// (`Vm::new_cons_string`)才创建本节点。
/// 独立于 `JsString` 分配,使 Flat 路径的 `JsString` 保持 32B 结构(tag 并入
/// 尾部填充位,见其尺寸断言);仅超过阈值的拼接(`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)。
/// 拼接 UTF-16 单元长:构造时 = left.utf16_len() + right.utf16_len()(各 O(1)),
/// 免递归求长、不触发扁平化。
unit_len: u32,
/// 扁平化产物缓存:首次单元消费时分配整块 FlatU16 产物并原子发布(OnceLock)。
/// 产物不进 session 主表,生命周期挂本节点——GC 传播保证其随父存活,
/// 释放节点时连带释放。
flat_cache: OnceLock<*const JsString>,
}
/// `utf16_len` 未计算的哨兵值(有效长度不可能为 u32::MAX)。
/// `utf16_len` 未计算的哨兵值(有效长度不可能为 u32::MAX;仅 Flat 形态使用)。
const UTF16_LEN_UNSET: u32 = u32::MAX;
impl ConsNode {
/// 扁平化产物指针(未扁平化返回 null)。
pub fn flat_cache_ptr(&self) -> *const JsString {
self.flat_cache.get().copied().unwrap_or(std::ptr::null())
}
}
impl JsString {
/// 用 UTF-8 数据构造 Flat 字符串。
/// Flat 载荷借用(载荷即原始 `String`)。
///
/// # Safety
/// 仅当 `tag == TAG_FLAT` 时调用(debug_assert 已覆盖常规路径)。
#[inline(always)]
fn flat_ref(&self) -> &String {
debug_assert!(self.tag == TAG_FLAT, "flat_ref 仅限 Flat 形态");
// SAFETY: 形态检查通过后载荷即 String(布局与 [u64;3] 逐位一致)。
unsafe { &*(self.payload.as_ptr() as *const String) }
}
/// FlatU16 载荷借用(载荷即原始 `Vec<u16>`)。
///
/// # Safety
/// 仅当 `tag == TAG_FLAT_U16` 时调用(debug_assert 已覆盖常规路径)。
#[inline(always)]
fn flat_u16_ref(&self) -> &Vec<u16> {
debug_assert!(self.tag == TAG_FLAT_U16, "flat_u16_ref 仅限 FlatU16 形态");
// SAFETY: 形态检查通过后载荷即 Vec<u16>(布局与 [u64;3] 逐位一致)。
unsafe { &*(self.payload.as_ptr() as *const Vec<u16>) }
}
/// Cons 节点指针(载荷首字即节点裸指针)。
///
/// # Safety
/// 仅当 `tag == TAG_CONS` 时调用(debug_assert 已覆盖常规路径)。
#[inline(always)]
fn cons_ptr(&self) -> *const ConsNode {
debug_assert!(self.tag == TAG_CONS, "cons_ptr 仅限 Cons 形态");
// SAFETY: 形态检查通过后载荷首字即节点指针。
unsafe { *(self.payload.as_ptr() as *const *const ConsNode) }
}
/// 用 UTF-8 数据构造 Flat 字符串(内容恒 well-formed,无孤立 surrogate)。
pub fn new(data: String) -> Self {
let mut payload = [0u64; 3];
// SAFETY: String 与 [u64;3] 布局一致(24B ptr/len/cap);write 不读目的位。
unsafe { std::ptr::write(payload.as_mut_ptr() as *mut String, data) };
Self {
kind: StringKind::Flat(data),
payload,
utf16_len: AtomicU32::new(UTF16_LEN_UNSET),
tag: TAG_FLAT,
}
}
/// 直接以单元序列构造 FlatU16 字符串(不做 well-formed 判定)。
///
/// 供明确知道内容形态的构造点使用(迭代器源物化、单元解码):smart
/// 路由场景一律走 [`Self::from_units`](well-formed 结果落 Flat)。
pub fn new_flat_u16(units: Vec<u16>) -> Self {
let len = units.len() as u32;
let mut payload = [0u64; 3];
// SAFETY: Vec<u16> 与 [u64;3] 布局一致(24B ptr/len/cap);write 不读目的位。
unsafe { std::ptr::write(payload.as_mut_ptr() as *mut Vec<u16>, units) };
Self {
payload,
utf16_len: AtomicU32::new(len),
tag: TAG_FLAT_U16,
}
}
/// 智能路由构造:单元序列含孤立 surrogate 落 FlatU16,全 well-formed 落
/// Flat(内容等价、载荷最小)。
pub fn from_units(units: Vec<u16>) -> Self {
if units_have_lone_surrogate(&units) {
Self::new_flat_u16(units)
} else {
// 已判定无孤立 surrogate,from_utf16 必成功;Err 臂仅为防御
// (单元判定与 from_utf16 同语义,不可能分歧)。
match String::from_utf16(&units) {
Ok(s) => Self::new(s),
Err(_) => Self::new_flat_u16(units),
}
}
}
@@ -96,29 +207,37 @@ impl JsString {
/// `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;
// 子节点单元长各 O(1)(Flat 懒扫描首取后缓存 / FlatU16 直读 / Cons 归纳):
// 链接成本 = 子节点首取扫描,非本链 O(n)。
let unit_len = (*left).utf16_len() + (*right).utf16_len();
let node = Box::into_raw(Box::new(ConsNode {
left,
right,
byte_len,
unit_len,
flat_cache: OnceLock::new(),
}));
let mut payload = [0u64; 3];
// SAFETY: Cons 载荷首字为节点指针,余 16B 未定义填充;指针按位写入
// (目标小端,指针→u64 为逐位转换)。
unsafe { *payload.as_mut_ptr() = node as u64 };
Self {
kind: StringKind::Cons(node),
utf16_len: AtomicU32::new(UTF16_LEN_UNSET),
payload,
utf16_len: AtomicU32::new(unit_len),
tag: TAG_CONS,
}
}
/// Cons 载荷节点指针(Flat 返回 null)。仅供释放路径使用。
/// Cons 载荷节点指针(非 Cons 返回 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,
if self.tag != TAG_CONS {
return std::ptr::null_mut();
}
// SAFETY: tag 为 CONS 时载荷首字即节点指针,由 new_cons 创建且存活。
self.cons_ptr() as *mut ConsNode
}
/// 释放 Cons 载荷节点(连带扁平化产物)。须在所属 `JsString` 的
/// `Box::from_raw` 之前调用且恰好一次;Flat 串传 null 无操作。
/// `Box::from_raw` 之前调用且恰好一次;非 Cons 串传 null 无操作。
///
/// # Safety
/// `node` 必须是 [`JsString::new_cons`] 经 `Box::into_raw` 产生的指针,且
@@ -127,7 +246,7 @@ impl JsString {
if node.is_null() {
return;
}
let flat = (*node).flat_cache.get().copied().unwrap_or(std::ptr::null());
let flat = (*node).flat_cache_ptr();
if !flat.is_null() {
// SAFETY: 产物由本节点 Box::into_raw 创建,且只随本节点释放一次。
drop(Box::from_raw(flat as *mut JsString));
@@ -135,134 +254,212 @@ impl JsString {
drop(Box::from_raw(node));
}
/// 字符串的字节长度(非字符数)。Flat 直读;Cons O(1) 取构造时缓存。
pub fn len(&self) -> usize {
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`]),取产物长度缓存。
/// JS 字符串长度(UTF-16 code unit 数)。Flat 首次访问扫描并缓存,
/// 此后恒 O(1);FlatU16/Cons 构造时已预写。
pub fn utf16_len(&self) -> u32 {
if self.tag != TAG_FLAT {
return self.utf16_len.load(Ordering::Relaxed);
}
let cached = self.utf16_len.load(Ordering::Relaxed);
if cached != UTF16_LEN_UNSET {
return cached;
}
let len = self.flat_str().encode_utf16().count() as u32;
let s = self.flat_ref();
let len = if s.is_ascii() { s.len() as u32 } else { s.encode_utf16().count() as u32 };
self.utf16_len.store(len, Ordering::Relaxed);
len
}
/// 是否为空字符串。
pub fn is_empty(&self) -> bool {
self.len() == 0
self.utf16_len() == 0
}
/// 整块 UTF-8 文本的借用。Cons 未扁平化时惰性扁平化并发布缓存后返回产物文本;
/// 返回的 `&str` 借用期 = `&self` 借用期(产物随本节点存活)。
/// 整块 UTF-8 文本的借用。**仅 Flat 形态**:FlatU16/Cons 无 `&str` 视图
/// (内容含孤立 surrogate 或需扁平化),一律改走 [`Self::units`] /
/// [`Self::as_lossy_str`]。
///
/// # Panics
/// 非 Flat 形态 panic(调用契约违反,非运行时正常路径)。
pub fn as_str(&self) -> &str {
self.flat_str()
if self.tag != TAG_FLAT {
panic!("JsString::as_str 仅限 Flat 形态");
}
self.flat_ref()
}
/// 整块文本的借用(`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"),
/// FlatU16 单元载荷的零拷贝借用;仅 FlatU16 形态返回 Some
/// (Flat 需编码、Cons 需扁平化,热路径经本方法免判形态)。
pub fn units_borrowed(&self) -> Option<&[u16]> {
(self.tag == TAG_FLAT_U16).then(|| self.flat_u16_ref()).map(|v| &**v)
}
/// 整块单元序列视图:FlatU16 零拷贝;Flat 即时编码(ASCII 走 u8→u16);
/// Cons 未扁平化时迭代展开(显式栈中序,防深链爆栈)发布 FlatU16 产物缓存
/// 后返回其零拷贝借用(缓存随本节点存活)。
pub fn units(&self) -> std::borrow::Cow<'_, [u16]> {
match self.tag {
TAG_FLAT => {
let s = self.flat_ref();
if s.is_ascii() {
std::borrow::Cow::Owned(s.as_bytes().iter().map(|&b| b as u16).collect())
} else {
std::borrow::Cow::Owned(s.encode_utf16().collect())
}
}
TAG_FLAT_U16 => std::borrow::Cow::Borrowed(self.flat_u16_ref()),
TAG_CONS => {
let node = unsafe { &*self.cons_ptr() };
if let Some(cached) = node.flat_cache.get() {
// SAFETY: 产物由本节点 Box::into_raw 创建且只随本节点释放,
// 存活期覆盖本次 &self 借用;扁平化产物恒为 FlatU16 形态。
let cached = unsafe { &**cached };
return std::borrow::Cow::Borrowed(cached.flat_u16_ref());
}
let units = self.flatten_units();
let product = Box::into_raw(Box::new(JsString::new_flat_u16(units)));
if node.flat_cache.set(product).is_err() {
// 并发首用竞态:另一线程已发布,本线程产物未暴露给任何调用方,
// 恰好释放一次。
// SAFETY: product 来自本线程的 Box::into_raw,无外部引用。
unsafe { drop(Box::from_raw(product)) };
}
// 上述 set 后缓存必已发布(本线程或他线程),产物恒 FlatU16。
let cached = unsafe { &**node.flat_cache.get().unwrap() };
std::borrow::Cow::Borrowed(cached.flat_u16_ref())
}
_ => unreachable!(),
}
}
/// 整块文本的 owned 副本(低频路径:错误消息 / 格式化 / 属性键桥接)。
/// 整块文本的 lossy 借用:Flat 直读(内容 well-formed,无损);其余形态按
/// 单元→char 映射(孤立 surrogate 替 U+FFFD)——供展示/解析/错误消息,
/// 语义消费一律走 [`Self::units`]。
pub fn as_lossy_str(&self) -> std::borrow::Cow<'_, str> {
if self.tag == TAG_FLAT {
return std::borrow::Cow::Borrowed(self.flat_ref());
}
let units = self.units();
std::borrow::Cow::Owned(match String::from_utf16(&units) {
Ok(s) => s,
Err(_) => units.iter().map(|&u| char::from_u32(u as u32).unwrap_or('\u{FFFD}')).collect(),
})
}
/// 整块文本的 lossy owned 副本(低频路径:错误消息 / 格式化 / 源码桥接)。
pub fn to_owned_string(&self) -> String {
self.flat_str().to_string()
self.as_lossy_str().into_owned()
}
/// 是否含孤立 surrogate 单元(well-formed 判定的取反)。
///
/// Flat 恒 false(不变式:UTF-8 载荷结构上不可承载);FlatU16 恒 true
/// (保守口径:直构造路径可能持有 well-formed 单元,单元消费路径对
/// well-formed 内容结果仍正确,仅多走单元通道);Cons 扁平化后扫描。
pub fn has_lone_surrogate(&self) -> bool {
match self.tag {
TAG_FLAT => false,
TAG_FLAT_U16 => true,
TAG_CONS => {
let units = self.units();
units_have_lone_surrogate(&units)
}
_ => unreachable!(),
}
}
/// 载荷的记账字节数(GC 账目口径):Flat = UTF-8 内容字节;FlatU16 /
/// Cons = 单元数 × 2。
pub fn payload_bytes(&self) -> usize {
match self.tag {
TAG_FLAT => self.flat_ref().len(),
TAG_FLAT_U16 => self.flat_u16_ref().len() * 2,
TAG_CONS => (unsafe { (*self.cons_ptr()).unit_len as usize }) * 2,
_ => unreachable!(),
}
}
/// 是否为 Flat(UTF-8 文本)形态。
pub fn is_flat(&self) -> bool {
self.tag == TAG_FLAT
}
/// 是否为 FlatU16(单元载荷)形态。
pub fn is_flat_u16(&self) -> bool {
self.tag == TAG_FLAT_U16
}
/// 是否为 Cons(rope)节点。
pub fn is_cons(&self) -> bool {
matches!(self.kind, StringKind::Cons(_))
self.tag == TAG_CONS
}
/// Cons 左右子节点指针对(Flat 返回双 null)。仅供 GC 传播闭包使用。
/// Cons 左右子节点指针对(非 Cons 返回双 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]
}
if self.tag != TAG_CONS {
return [std::ptr::null(), std::ptr::null()];
}
let node = unsafe { &*self.cons_ptr() };
[node.left, node.right]
}
/// 扁平化产物指针(未扁平化返回 null)。仅供 GC 传播与释放路径使用。
pub fn flat_cache_ptr(&self) -> *const JsString {
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()) }
}
if self.tag != TAG_CONS {
return std::ptr::null();
}
unsafe { (*self.cons_ptr()).flat_cache_ptr() }
}
/// 迭代展开 Cons 子树为整块文本(显式栈中序收集,防深链递归爆栈)。
fn flatten_text(&self) -> String {
/// 迭代展开 Cons 子树为整块单元序列(显式栈中序收集,防深链递归爆栈)。
fn flatten_units(&self) -> Vec<u16> {
let mut stack = Vec::with_capacity(8);
stack.push(self as *const JsString);
let mut buf = String::with_capacity(self.len());
let mut buf: Vec<u16> = Vec::with_capacity(self.utf16_len() as usize);
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) => {
match node.tag {
TAG_FLAT => buf.extend(node.flat_ref().encode_utf16()),
TAG_FLAT_U16 => buf.extend_from_slice(node.flat_u16_ref()),
TAG_CONS => {
// 右子先入栈、左子后入,保证左子先出(中序顺序)。
// SAFETY: child 由 new_cons 创建且随父存活。
let child = unsafe { &**child };
let child = unsafe { &*node.cons_ptr() };
stack.push(child.right);
stack.push(child.left);
}
_ => unreachable!(),
}
}
buf
}
}
/// 扫描单元序列是否含孤立 surrogate(high 后须紧跟 low 成对;low 不得独立出现)。
fn units_have_lone_surrogate(units: &[u16]) -> bool {
let mut i = 0;
while i < units.len() {
let u = units[i];
if (0xD800..=0xDBFF).contains(&u) {
if i + 1 < units.len() && (0xDC00..=0xDFFF).contains(&units[i + 1]) {
i += 2;
} else {
return true;
}
} else if (0xDC00..=0xDFFF).contains(&u) {
return true;
} else {
i += 1;
}
}
false
}
// SAFETY: 与 `StringPtr`(string_forge.rs)同款论证——Cons 节点只存在于单线程
// session VM 内;跨线程共享的 perm 串恒为 Flat,且其 flat_cache 永不写入
// (perm 串只读共享,无任何可变路径)。裸指针字段不引入跨线程数据竞争。
// session VM 内;跨线程共享的 perm 串载荷为 Flat/FlatU16(无 ConsNode 裸指针
// 写路径),flat_cache 永不写入(perm 串只读共享,无任何可变路径)。裸指针
// 字段不引入跨线程数据竞争。
unsafe impl Send for JsString {}
unsafe impl Sync for JsString {}
@@ -1939,19 +2136,60 @@ mod tests {
#[test]
fn js_string_cons_basic() {
// 左右子 Flat 构造 Cons:len/utf16_len/is_empty/flat 全部按拼接语义。
// 左右子 Flat 构造 Cons:utf16_len/is_empty/文本视图全部按拼接语义。
let left = Box::new(JsString::new("ab".to_string()));
let right = Box::new(JsString::new("cd".to_string()));
let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) });
assert_eq!(cons.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!(!cons.is_empty());
assert_eq!(cons.as_lossy_str(), "abcd");
assert_eq!(cons.to_owned_string(), "abcd");
assert!(cons.is_cons());
// 扁平化产物缓存命中:内容仍一致。
assert_eq!(cons.flat_str(), "abcd");
// 扁平化产物缓存命中:内容仍一致(units 视图同样走缓存)。
assert_eq!(cons.as_lossy_str(), "abcd");
assert_eq!(cons.units().as_ref(), &['a' as u16, 'b' as u16, 'c' as u16, 'd' as u16]);
}
#[test]
fn js_string_layout_anchor_and_tags() {
// 32B 锚 + 三形态 tag/utf16_len 预写口径(布局变更先同步头注释再改断言)。
let flat = JsString::new("ab".to_string());
assert_eq!(std::mem::size_of::<JsString>(), 32);
assert!(flat.is_flat() && !flat.is_cons() && !flat.is_flat_u16());
assert_eq!(flat.tag, TAG_FLAT);
assert_eq!(flat.utf16_len(), 2);
assert_eq!(flat.payload_bytes(), 2);
let u16 = JsString::new_flat_u16(vec![0xD800, 0xDC00]);
assert!(u16.is_flat_u16());
assert_eq!(u16.tag, TAG_FLAT_U16);
assert_eq!(u16.utf16_len(), 2);
assert_eq!(u16.payload_bytes(), 4);
assert_eq!(u16.units_borrowed(), Some(&[0xD800, 0xDC00][..]));
// 代理对 well-formed:smart 路由落 Flat 且内容等价。
let smart = JsString::from_units(vec![0xD800, 0xDC00]);
assert!(smart.is_flat());
assert_eq!(smart.as_str(), "\u{10000}");
// 孤立 surrogate:smart 路由落 FlatU16。
let lone = JsString::from_units(vec![0xD800]);
assert!(lone.is_flat_u16());
assert!(lone.has_lone_surrogate());
assert_eq!(lone.units().as_ref(), &[0xD800]);
assert_eq!(lone.as_lossy_str(), "\u{FFFD}");
let left = Box::new(JsString::new("ab".to_string()));
let right = Box::new(JsString::new("cd".to_string()));
let cons = Box::new(unsafe { JsString::new_cons(&*left, &*right) });
assert!(cons.is_cons());
assert_eq!(cons.tag, TAG_CONS);
assert_eq!(cons.utf16_len(), 4, "Cons 单元长构造期 eager 归纳");
assert_eq!(cons.payload_bytes(), 8);
assert!(!cons.has_lone_surrogate());
// Cons 扁平化产物恒 FlatU16:units 零拷贝借用来自缓存。
let u16_node = cons.units().into_owned();
assert_eq!(u16_node, vec![0x61, 0x62, 0x63, 0x64]);
assert!(!cons.flat_cache_ptr().is_null());
unsafe { JsString::drop_cons_node(cons.cons_node_ptr()) };
}
#[test]
@@ -1968,7 +2206,7 @@ mod tests {
chain_ptr = cons.as_ref() as *const JsString;
nodes.push(cons);
}
let text = nodes.last().unwrap().flat_str();
let text = nodes.last().unwrap().as_lossy_str();
assert_eq!(text.len(), 1 + 2000);
assert!(text.starts_with('x'));
assert!(text.ends_with('y'));
@@ -1981,9 +2219,8 @@ mod tests {
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);
assert_eq!(cons.as_lossy_str(), "");
}
#[test]
+29 -21
View File
@@ -99,11 +99,11 @@ impl Vm {
}
/// 字符串拼接接线点(二元 `+`/`+=` 共用):O(1) 链接为 Cons(rope)节点,
/// 消除逐次整串重拷贝。文本在消费时(`.length`/`==`/string_ref 等)惰性扁平化。
/// 消除逐次整串重拷贝。单元序列在消费时(`.length`/`==`/单元读取等)惰性扁平化。
///
/// # 步骤
/// 1. 非字符串操作数先转成叶子字符串(`push_to_string` 同文本语义;小整数走
/// 永久缓存零分配,其余 `to_string` + owned)。
/// 1. 非字符串操作数先转成叶子字符串(`to_string` 同文本语义;小整数走
/// 永久缓存零分配,其余 owned)。
/// 2. 两个字符串经 `new_cons_string` 链接(每链接 1 次节点分配,对比原 O(n)
/// 拷贝 + 结果串分配)。
///
@@ -112,18 +112,26 @@ impl Vm {
/// 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);
return self.new_string_owned(buf);
// 小链急切扁平:总单元长 ≤ 阈值时单次预分配写齐,零叶子转换 / Cons 节点 /
// 惰性扁平化开销——良形小串场景载荷与旧字节路径逐位一致(该场景 rope 的
// O(1) 链接收益低于其固定开销)。
let lu = if lhs.is_string() {
unsafe { (*lhs.as_string_ptr()).utf16_len() as usize }
} else {
0
};
let ru = if rhs.is_string() {
unsafe { (*rhs.as_string_ptr()).utf16_len() as usize }
} else {
0
};
if lu + ru <= Self::CONS_FLATTEN_UNITS {
let mut units = Vec::with_capacity(lu + ru);
coercion::push_units_to(lhs, &mut units);
coercion::push_units_to(rhs, &mut units);
return self.new_string_units_owned(units);
}
// 大链走 Cons rope:非字符串操作数先转叶子,再 O(1) 链接(文本惰性扁平化)。
// 大链走 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)
@@ -227,9 +235,9 @@ impl Vm {
///
/// # 步骤
/// 1. 剩余操作数按序 coerce(对象 ToPrimitive 副作用顺序与左结合一致,抛错点先于分配)。
/// 2. 精确总长:字符串操作数取字节长,非字符串给 32 字节余量(int/double/bool/null/
/// undefined 的十进制文本上界;BigInt 超长时 String 自动扩容兜底)。
/// 3. 单趟 push_to_string + new_string_owned(零二次拷贝)。
/// 2. 精确总长:字符串操作数取单元长,非字符串给 32 单元余量(int/double/bool/null/
/// undefined 的十进制文本上界;BigInt 超长时 Vec 自动扩容兜底)。
/// 3. 单趟单元展开 + 智能路由创建(零二次拷贝;良形内容落 Flat 与旧路径逐位一致)。
///
/// # 副作用
/// 新建一个会话字符串。
@@ -245,16 +253,16 @@ impl Vm {
let cap = parts.iter().fold(0usize, |acc, p| {
acc + if p.is_string() {
// SAFETY: p 是字符串值。
unsafe { (*p.as_string_ptr()).len() }
unsafe { (*p.as_string_ptr()).utf16_len() as usize }
} else {
32
}
});
let mut buf = String::with_capacity(cap);
let mut units = Vec::with_capacity(cap);
for p in &parts {
coercion::push_to_string(*p, &mut buf);
coercion::push_units_to(*p, &mut units);
}
Ok(self.new_string_owned(buf))
Ok(self.new_string_units_owned(units))
}
#[inline(always)]
+3 -2
View File
@@ -44,8 +44,9 @@ impl Vm {
self.regs[rd] = val;
Ok(false)
} else {
// SAFETY: key_val 已校验为字符串值,as_str 桥接 JsString 内容。
let name = unsafe { (*key_val.as_string_ptr()).as_str() };
// SAFETY: key_val 已校验为字符串值;错误消息走 lossy 文本(不可解析名
// 含孤立 surrogate 时映射为 FFFD,仅为展示用途)。
let name = unsafe { (*key_val.as_string_ptr()).as_lossy_str() };
self.raise_error_kind("ReferenceError", &format!("{name} is not defined"))?;
Ok(true)
}
+9 -9
View File
@@ -541,7 +541,7 @@ impl SessionGc {
/// `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;
let bytes = (size_of::<JsString>() + (*ptr).payload_bytes()) as u64;
Self::drop_session_string_box(ptr);
bytes
}
@@ -701,7 +701,7 @@ impl SessionGc {
if self.live_strings.contains(&ptr) {
// 存活——地址不变,无需重写。
// SAFETY: ptr 是仍归 VM 所有的存活 session 字符串 box。
live_bytes += unsafe { size_of::<JsString>() + (*ptr).len() };
live_bytes += unsafe { size_of::<JsString>() + (*ptr).payload_bytes() };
live.push(ptr);
} else {
// SAFETY: ptr 在 session_string_ptrs 中但不可达,恰好释放一次。
@@ -1032,7 +1032,7 @@ impl SessionGc {
continue;
}
// SAFETY: ptr 在字符串表登记,收尾前有效。
string_bytes += size_of::<JsString>() + unsafe { (*ptr).len() };
string_bytes += size_of::<JsString>() + unsafe { (*ptr).payload_bytes() };
}
let bigint_bytes = vm.gc_state.session_bigint_ptrs.borrow().len() * size_of::<num_bigint::BigInt>();
vm.gc_state.session_bytes_allocated = object_bytes + string_bytes + bigint_bytes;
@@ -2311,7 +2311,7 @@ mod tests {
vm.maybe_collect_session_strings();
// 账目 = 对象头 + 存活串(size_of::<JsString>() + len),死串不再计入。
let expected = size_of::<JsObject>() + (size_of::<JsString>() + unsafe { (*live_ptr).len() });
let expected = size_of::<JsObject>() + (size_of::<JsString>() + unsafe { (*live_ptr).payload_bytes() });
assert_eq!(vm.gc_state.session_bytes_allocated, expected);
assert!(!vm.gc_state.session_string_ptrs.contains(&dead.as_string_ptr_mut()));
}
@@ -2349,7 +2349,7 @@ mod tests {
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");
assert_eq!(unsafe { (*parent_ptr).as_lossy_str() }, "left-partright-part");
}
#[test]
@@ -2373,7 +2373,7 @@ mod tests {
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");
assert_eq!(unsafe { (*parent_ptr).as_lossy_str() }, "left-partright-part");
let flat_ptr = unsafe { (*parent_ptr).flat_cache_ptr() };
assert!(!flat_ptr.is_null());
@@ -2400,7 +2400,7 @@ mod tests {
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)));
assert_eq!(unsafe { (*chain_ptr).as_lossy_str() }, format!("root{}", "x".repeat(1024)));
}
#[test]
@@ -2418,7 +2418,7 @@ mod tests {
// 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");
assert_eq!(unsafe { (*parent.as_string_ptr_mut()).as_lossy_str() }, "perm-leafsession-leaf");
}
#[test]
@@ -2426,7 +2426,7 @@ mod tests {
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");
assert_eq!(unsafe { (*parent.as_string_ptr_mut()).as_lossy_str() }, "left-partright-part");
vm.regs[0] = parent;
// full_reset 清空全部 session 字符串(连带产物):无泄漏、无 double-free。
+68 -17
View File
@@ -53,6 +53,33 @@ fn canonical_index_of(s: &str) -> Option<u32> {
}
}
/// 从单元序列判定字符串是否为规范数组下标(无前导零的纯 ASCII 数字串),
/// 并反解其值。口径与 [`canonical_index_of`] 一致,单元口径覆盖非 Flat
/// 载荷的键推导。只覆盖 `[0, INT_KEY_COUNT)`,更大的数字串走普通字符串键。
fn canonical_index_units(units: &[u16]) -> Option<u32> {
if units.is_empty() {
return None;
}
if !matches!(units[0], 0x30..=0x39) {
return None;
}
if units.len() > 1 && units[0] == 0x30 {
return None;
}
if units.len() > 10 {
return None;
}
let mut v: u32 = 0;
for &u in units.iter() {
let d = match u {
0x30..=0x39 => u - 0x30,
_ => return None,
};
v = v.checked_mul(10)? + d as u32;
}
(v < INT_KEY_COUNT).then_some(v)
}
/// 将 [`NativeFnPtr`] 转换为可调用的 [`NativeFn`]。
///
/// # Safety
@@ -1169,7 +1196,7 @@ impl Vm {
}
for &ptr in &self.gc_state.session_string_ptrs {
// SAFETY: ptr 来自字符串表登记,收尾前始终有效。
bytes += (std::mem::size_of::<oxide_types::object::JsString>() + unsafe { (*ptr).len() }) as u64;
bytes += (std::mem::size_of::<oxide_types::object::JsString>() + unsafe { (*ptr).payload_bytes() }) as u64;
}
bytes += (self.gc_state.session_bigint_ptrs.borrow().len() * std::mem::size_of::<num_bigint::BigInt>()) as u64;
bytes += (self.gc_state.session_cell_ptrs.borrow().len() * std::mem::size_of::<Cell>()) as u64;
@@ -1362,12 +1389,18 @@ impl Vm {
return Ok(make_int_key(d as u32));
}
} else if val.is_string() {
// 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));
// SAFETY: val 是字符串值,按载荷形态桥接为永久 key id。
let s = unsafe { &*val.as_string_ptr() };
if s.is_flat() {
// Flat:良形 UTF-8 文本,键推导与旧路径逐位一致。
let text = s.as_str();
if let Some(i) = canonical_index_of(text) {
return Ok(make_int_key(i));
}
return Ok(self.kernel_core.perm_interner().intern(text).0);
}
return Ok(self.kernel_core.perm_interner().intern(s).0);
// 单元载荷:键推导同规范(见 string_key_units)。
return Ok(self.string_key_units(&s.units()));
}
// Symbol 值直接编码为 Symbol 键(不进字符串 interner,键相互独立)。
if val.is_symbol() {
@@ -1380,18 +1413,30 @@ impl Vm {
return Ok(make_well_known_symbol_key(id));
}
// ToPropertyKey:对象经 ToPrimitive(string hint),结果为 Symbol 时直接作键;
// 其余字符串经规范化(规范数字串映射整数键)与字符串分支统一口径。
// 其余字符串按单元序列推导键(避免 lossy 文本桥接破坏孤立 surrogate 键)。
let prim = coercion::to_primitive(val, coercion::ToPrimitiveHint::String, self)?;
if prim.is_symbol() {
return Ok(make_symbol_key(prim.as_symbol_index()));
}
let key = coercion::to_string(prim);
return Ok(oxide_runtime_api::VmHost::string_key_si(self, &key));
let units = oxide_runtime_api::to_units_full(prim, self)?;
return Ok(self.string_key_units(&units));
}
// 其它原始值(BigInt 等):ToPropertyKey 一律转字符串并走规范化,避免与
// 数字键区间分裂(`o[5n]` 与 `o["5"]`/`o[5]` 必须同键)。
let key = coercion::to_string(val);
Ok(oxide_runtime_api::VmHost::string_key_si(self, &key))
let units = oxide_runtime_api::to_units_full(val, self)?;
Ok(self.string_key_units(&units))
}
/// 从单元序列推导属性键 si(`property_key_si` 字符串分支的口径抽取):
/// 规范数组下标 → 整数键,其余以 `encode_key` 形态入键空间(孤立 surrogate
/// 以转义形态区分,不与良形键碰撞)。
pub(crate) fn string_key_units(&self, units: &[u16]) -> u32 {
if let Some(i) = canonical_index_units(units) {
make_int_key(i)
} else {
let key = oxide_kernel::string_forge::encode_key(units);
self.kernel_core.perm_interner().intern(&key).0
}
}
pub(crate) fn array_index_from_property_key(&self, prop_name_si: u32) -> Option<u32> {
@@ -2496,12 +2541,6 @@ impl oxide_runtime_api::VmHost for Vm {
fn new_string_owned(&mut self, s: String) -> JsValue {
Vm::new_string_owned(self, s)
}
fn string_ref(&self, val: JsValue) -> &str {
// SAFETY: 调用方保证 val 为字符串值。perm 串由内核持有永不释放;session
// 串仅经 &mut self 路径(new_string/new_string_owned/GC)释放,此处 &self
// 借用期间编译器强制不存在 &mut 存续,字符串不会在借用期内回收。
unsafe { (*val.as_string_ptr()).as_str() }
}
fn new_bigint(&mut self, v: num_bigint::BigInt) -> JsValue {
Vm::new_bigint(self, v)
}
@@ -2542,6 +2581,18 @@ impl oxide_runtime_api::VmHost for Vm {
self.kernel_core.perm_interner().intern(s).0
}
}
fn string_units(&self, val: JsValue) -> std::borrow::Cow<'_, [u16]> {
// SAFETY: 调用方保证 val 为字符串值。perm 串由内核持有永不释放;session
// 串仅经 &mut self 路径(new_string/GC)释放,&self 借用期间编译器强制
// 不存在 &mut 存续,字符串不会在借用期内回收。
unsafe { (*val.as_string_ptr()).units() }
}
fn new_string_units(&mut self, units: &[u16]) -> JsValue {
Vm::new_string_units(self, units)
}
fn new_string_units_owned(&mut self, units: Vec<u16>) -> JsValue {
Vm::new_string_units_owned(self, units)
}
fn resolve_property(&self, obj: &JsObject, prop_name_si: u32) -> Option<JsValue> {
self.resolve_property(obj, prop_name_si)
}
+42 -46
View File
@@ -1,6 +1,6 @@
use crate::vm::{ForInIter, FrameArgs, FrameContinuation, Vm, MAX_PROTO_CHAIN_DEPTH};
use crate::vm_trace;
use oxide_runtime_api::{push_to_string, to_boolean, to_string_full, VmHost};
use oxide_runtime_api::{push_units_to, to_boolean, to_units_full};
use oxide_types::object::{JsObject, PropAttributes};
use oxide_types::private_key::{int_key_value, is_int_key, is_private_name_key, is_symbol_key, make_int_key};
use oxide_types::value::JsValue;
@@ -267,7 +267,9 @@ impl Vm {
let segment_count = (header >> 16) as usize;
let len_hint = (header & 0xFFFF) as usize;
let mut result = String::with_capacity(len_hint.max(16));
// 单元缓冲:字符串操作数按单元序列展开(lone surrogate 保真),
// len_hint 为单元数口径(emit 侧同口径)。
let mut units: Vec<u16> = Vec::with_capacity(len_hint.max(16));
for _ in 0..segment_count {
let seg = self.bytecode[self.pc];
self.pc += 1;
@@ -275,17 +277,16 @@ impl Vm {
let reg = (seg & 0x7FFF_FFFF) as usize;
let val = self.regs[reg];
if val.is_string() {
// SAFETY: val 是字符串值;借用仅在本次 push 内消费,不跨分配点。
let s = unsafe { (*val.as_string_ptr()).as_str() };
result.push_str(s);
// SAFETY: val 是字符串值;借用仅在本次展开内消费,不跨分配点。
let s = unsafe { &*val.as_string_ptr() };
units.extend_from_slice(s.units().as_ref());
} else if val.is_object() || val.is_symbol() {
// 对象走完整 ToString(ToPrimitive 副作用顺序);Symbol 由
// to_string_full 抛 TypeError(push_to_string 无 symbol 分支)。
let s = to_string_full(val, self)?;
result.push_str(&s);
// 对象走完整 ToString(ToPrimitive 副作用顺序,结果保单元);
// Symbol 抛 TypeError(to_units_full 无 symbol 直写分支)。
units.extend(to_units_full(val, self)?);
} else {
// 原始值直写结果缓冲,免中间 String。
push_to_string(val, &mut result);
push_units_to(val, &mut units);
}
} else {
let const_idx = (seg & 0x7FFF_FFFF) as usize;
@@ -293,14 +294,14 @@ impl Vm {
if const_idx < imm.len() {
let val = imm[const_idx];
if val.is_string() {
// SAFETY: val 是字符串值;借用仅在本次 push 内消费,不跨分配点。
let s = unsafe { (*val.as_string_ptr()).as_str() };
result.push_str(s);
// SAFETY: val 是字符串值;借用仅在本次展开内消费,不跨分配点。
let s = unsafe { &*val.as_string_ptr() };
units.extend_from_slice(s.units().as_ref());
}
}
}
}
self.regs[rd] = self.new_string_owned(result);
self.regs[rd] = self.new_string_units_owned(units);
Ok(())
}
@@ -825,16 +826,11 @@ impl Vm {
JsValue::from_js_object(proto_ptr),
));
if src.is_string() {
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).as_str().encode_utf16().collect() };
// SAFETY: src 是字符串值;单元序列展开(lone surrogate 保真)。
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).units().into_owned() };
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) {
Some(c) => match self.single_char(c) {
Some(v) => v,
None => self.new_string(&c.to_string()),
},
None => self.new_string(""),
};
let ch_val = self.unit_char_value(*unit);
let rest = unsafe { &mut *rest_ptr };
self.set_or_create_prop_value(rest, si, ch_val);
}
@@ -842,23 +838,34 @@ impl Vm {
self.regs[rd] = JsValue::from_js_object(rest_ptr);
return Ok(());
}
// 排除名单常量按单元序列展开(lone surrogate 名保真),0x0000 分隔。
let excluded_const = self
.immutables()
.get(excluded_idx)
.and_then(|v| {
if v.is_string() {
// SAFETY: v 是字符串常量值。
Some(unsafe { (*v.as_string_ptr()).to_owned_string() })
Some(unsafe { (*v.as_string_ptr()).units().into_owned() })
} else {
None
}
})
.unwrap_or_default();
let mut excluded: std::collections::HashSet<u32> = excluded_const
.split('\0')
.filter(|s| !s.is_empty())
.map(|s| self.string_key_si(s))
.collect();
let mut excluded: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut seg: Vec<u16> = Vec::new();
for &u in excluded_const.iter() {
if u == 0x0000 {
if !seg.is_empty() {
excluded.insert(self.string_key_units(&seg));
seg.clear();
}
continue;
}
seg.push(u);
}
if !seg.is_empty() {
excluded.insert(self.string_key_units(&seg));
}
// 运行时 excluded:b 槽数组(computed key 求值结果)的元素 ToPropertyKey 后排除。
if b != 0 {
let arr_val = self.regs[b];
@@ -883,20 +890,14 @@ 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()).to_owned_string() };
let code_units: Vec<u16> = s.encode_utf16().collect();
// SAFETY: 包装对象槽 0 恒为字符串值;单元序列展开(lone surrogate 保真)。
let code_units: Vec<u16> = unsafe { (*raw.as_string_ptr()).units().into_owned() };
for (i, unit) in code_units.iter().enumerate() {
let si = make_int_key(i as u32);
if excluded.contains(&si) {
continue;
}
let ch_val = match char::from_u32(*unit as u32) {
Some(c) => match self.single_char(c) {
Some(v) => v,
None => self.new_string(&c.to_string()),
},
None => self.new_string(""),
};
let ch_val = self.unit_char_value(*unit);
assignments.push((si, ch_val));
}
}
@@ -964,19 +965,14 @@ impl Vm {
return Ok(());
}
// 字符串源:按索引复制字符(可枚举索引属性)。
// 字符串源:按单元下标复制(可枚举索引属性,lone surrogate 保真)。
if src.is_string() {
let target = unsafe { &mut *target_val.as_js_object_ptr() };
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).as_str().encode_utf16().collect() };
// SAFETY: src 是字符串值。
let code_units: Vec<u16> = unsafe { (*src.as_string_ptr()).units().into_owned() };
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) {
Some(c) => match self.single_char(c) {
Some(v) => v,
None => self.new_string(&c.to_string()),
},
None => self.new_string(""),
};
let ch_val = self.unit_char_value(*unit);
self.set_or_create_prop_value(target, si, ch_val);
}
return Ok(());
+34 -10
View File
@@ -16,10 +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;
/// Cons(rope)节点保留的单元阈值:拼接总长(UTF-16 单元数)≤ 该值时
/// 急切扁平。小链的"链接 + 首次消费扁平化"双重分配高于直接拷贝,直接
/// 扁平更优;超过阈值后 O(n²) 拷贝成本超过节点开销,Cons 的 O(1) 链接
/// 才值得。
pub(crate) const CONS_FLATTEN_UNITS: usize = 128;
/// 替换本 VM 独占的内建 P 对象:先恰好释放旧副本的属性区一次,
/// 再让旧 Arc 归零(旧副本无 Drop 口径兜底,full_reset 重初始化
@@ -640,24 +641,47 @@ impl Vm {
/// 接线点调用(CONCAT_N 保持急切扁平,不经此路径)。
///
/// # 副作用
/// - 登记节点到 session 字符串表(账目 = `size_of::<JsString>() + byte_len`)。
/// - 登记节点到 session 字符串表(账目 = `size_of::<JsString>() + payload_bytes`)。
///
/// # 注意事项
/// - 调用方须保证 `left`/`right` 在调用期间存活:子节点(coerce 新鲜字符串 /
/// 新建叶子)要么已是执行根,要么在写入根前不被任何回收点触达——当前
/// 回收点仅在指令边界,链接结果写寄存器先于下一次检查,天然满足。
/// - 调用方(`concat_strings`)已按字节阈值过滤:仅大链(总长 >
/// [`Self::CONS_FLATTEN_BYTES`])进入本方法,小链由其急切扁平。
/// - 调用方(`concat_strings`)已按单元阈值过滤:仅大链(总长 >
/// [`Self::CONS_FLATTEN_UNITS`])进入本方法,小链由其急切扁平。
#[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)
// SAFETY: ptr 指向刚创建的存活节点,账目口径 = 单元数 × 2。
let bytes = unsafe { (*ptr).payload_bytes() };
self.register_session_string(ptr, bytes)
}
/// 以单元序列分配可被 session GC 回收的字符串值:智能路由(含孤立
/// surrogate 落 FlatU16,否则 Flat),语义与副作用同 `new_string_owned`。
pub fn new_string_units_owned(&mut self, units: Vec<u16>) -> JsValue {
let ptr = Box::into_raw(Box::new(JsString::from_units(units)));
// SAFETY: ptr 指向刚创建的存活 JsString。
let bytes = unsafe { (*ptr).payload_bytes() };
self.register_session_string(ptr, bytes)
}
/// 同 `new_string_units_owned`,以借用单元序列接收。
pub fn new_string_units(&mut self, units: &[u16]) -> JsValue {
self.new_string_units_owned(units.to_vec())
}
/// 单单元的属性值:ASCII 单元命中共享 perm 串(零分配、可指针短路),
/// 非 ASCII(含孤立 surrogate)物化为 1 单元会话串。
pub(crate) fn unit_char_value(&mut self, u: u16) -> JsValue {
if let Some(v) = oxide_runtime_api::VmHost::single_unit(self, u) {
return v;
}
self.new_string_units(&[u])
}
/// 把字符串 intern 为永久 key id(属性名/方法名),进程生命周期内稳定。
@@ -528,9 +528,9 @@ fn symbol_dispose_and_async_dispose_registered() {
let result = eval(&mut vm, src).unwrap();
assert_eq!(result.as_bool(), expected, "for {}", src);
}
// 描述性名称经 String 构造器反射(well_known_symbol_name 表)。
let desc = eval(&mut vm, "String(Symbol.dispose)").unwrap();
assert_eq!(to_str(&vm, desc), "Symbol(Symbol.dispose)");
// ToString(Symbol) 按规格抛 TypeError(String 构造器无描述性名称通道)。
let err = eval(&mut vm, "try { String(Symbol.dispose); 'no-throw' } catch (e) { e.name }").unwrap();
assert_eq!(to_str(&vm, err), "TypeError");
}
#[test]
+54 -19
View File
@@ -940,25 +940,45 @@ fn string_methods_null_receiver_throw_type_error() {
#[test]
fn string_slice_astral_character_indices() {
// UTF-16 单元索引语义:astral 字符占 2 单元,切片边界落在代理对中间时
// 保留所在 astral 字符(孤立代理无法在 UTF-8 表示,见架构限制)。
// 单元原样保留(单元载荷可承载孤立 surrogate,规格口径)。
let mut vm = Vm::new();
let s = eval(&mut vm, "'\\u{1F600}ab'.slice(1, 3)").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}a");
let s = eval(&mut vm, "'\\u{1F600}ab'.slice(0, 1)").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}");
let r = eval(
&mut vm,
"var s = '\\u{1F600}ab'.slice(1, 3); s.length === 2 && s.charCodeAt(0) === 0xDE00 && s.charCodeAt(1) === 0x61",
)
.unwrap();
assert!(r.as_bool());
let r = eval(
&mut vm,
"var s = '\\u{1F600}ab'.slice(0, 1); s.length === 1 && s.charCodeAt(0) === 0xD83D",
)
.unwrap();
assert!(r.as_bool());
let s = eval(&mut vm, "'\\u{1F600}'.padStart(3, 'x')").unwrap();
assert_eq!(to_str(&vm, s), "xx\u{1F600}");
}
#[test]
fn string_char_at_ascii_and_astral() {
// charAt 产出:ASCII 走单字符缓存,astral 按 UTF-16 单元定位(落在代理对
// 中间时返回所在 astral 字符,架构限制),越界空串,缺省参数取首字符。
// charAt 按码元定位:单单元子串(代理对各出 1 单元,规格口径——
// 规格返回单元子串而非 Unicode 标量),越界空串,缺省参数取首字符。
let mut vm = Vm::new();
let s = eval(&mut vm, "'abc'.charAt(1)").unwrap();
assert_eq!(to_str(&vm, s), "b");
let s = eval(&mut vm, "'\\u{1F600}ab'.charAt(0)").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}");
let r = eval(
&mut vm,
"'\\u{1F600}ab'.charAt(0).length === 1 && '\\u{1F600}ab'.charAt(0).charCodeAt(0) === 0xD83D",
)
.unwrap();
assert!(r.as_bool());
let r = eval(
&mut vm,
"'\\u{1F600}ab'.charAt(1).length === 1 && '\\u{1F600}ab'.charAt(1).charCodeAt(0) === 0xDE00",
)
.unwrap();
assert!(r.as_bool());
let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(2) === 'a'").unwrap();
assert!(r.as_bool());
let s = eval(&mut vm, "'ab'.charAt(5)").unwrap();
assert_eq!(to_str(&vm, s), "");
let s = eval(&mut vm, "'xy'.charAt()").unwrap();
@@ -975,13 +995,16 @@ fn string_char_at_eq_perm_string() {
#[test]
fn string_split_empty_separator_chars() {
// 空分隔 split 逐字符产出:ASCII 走单字符缓存,混合 astral 按标量切分。
// 空分隔 split 逐码元产出:ASCII 走单字符缓存,代理对各出 1 单元元素
// (规格口径:空分隔按单元切分,孤立 surrogate 各为独立元素)。
let mut vm = Vm::new();
let s = eval(&mut vm, "'abc'.split('').join('-')").unwrap();
assert_eq!(to_str(&vm, s), "a-b-c");
let result = eval(&mut vm, "'\\u{1F600}a'.split('').length").unwrap();
assert_eq!(result.as_int(), 2);
let result = eval(&mut vm, "'\\u{1F600}a'.split('')[1]").unwrap();
assert_eq!(result.as_int(), 3);
let r = eval(&mut vm, "'\\u{1F600}a'.split('')[1].charCodeAt(0) === 0xDE00").unwrap();
assert!(r.as_bool());
let result = eval(&mut vm, "'\\u{1F600}a'.split('')[2]").unwrap();
assert_eq!(to_str(&vm, result), "a");
}
@@ -1016,10 +1039,17 @@ fn string_utf16_length_char_at_code_at() {
assert_eq!(r.as_int(), 0xDE00);
let r = eval(&mut vm, "'\\u{1F600}a'.charCodeAt(2)").unwrap();
assert_eq!(r.as_int(), 0x61);
let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(2)").unwrap();
assert_eq!(to_str(&vm, r), "a");
let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(1)").unwrap();
assert_eq!(to_str(&vm, r), "\u{1F600}");
let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(2) === 'a'").unwrap();
assert!(r.as_bool());
// charAt 出单单元子串:代理对位置各出 1 单元(高/低 surrogate)。
let r = eval(
&mut vm,
"'\\u{1F600}ab'.charAt(0).length === 1 && '\\u{1F600}ab'.charAt(0).charCodeAt(0) === 0xD83D",
)
.unwrap();
assert!(r.as_bool());
let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(1).charCodeAt(0) === 0xDE00").unwrap();
assert!(r.as_bool());
let r = eval(&mut vm, "'\\u{1F600}ab'.charAt(4)").unwrap();
assert_eq!(to_str(&vm, r), "");
}
@@ -1040,7 +1070,8 @@ fn string_utf16_at() {
#[test]
fn string_utf16_slice_substring_substr() {
// 切片族按 UTF-16 单元计数:边界落在代理对中间时保留所在 astral 字符。
// 切片族按 UTF-16 单元计数:边界落在代理对中间时单元原样保留(孤立
// surrogate 由单元载荷承载,规格口径)。
let mut vm = Vm::new();
let s = eval(&mut vm, "'\\u{1F600}ab'.slice(2)").unwrap();
assert_eq!(to_str(&vm, s), "ab");
@@ -1054,8 +1085,12 @@ fn string_utf16_slice_substring_substr() {
assert_eq!(to_str(&vm, s), "\u{1F600}");
let s = eval(&mut vm, "'\\u{1F600}ab'.substr(2, 2)").unwrap();
assert_eq!(to_str(&vm, s), "ab");
let s = eval(&mut vm, "'\\u{1F600}ab'.substr(1, 2)").unwrap();
assert_eq!(to_str(&vm, s), "\u{1F600}a");
let r = eval(
&mut vm,
"var s = '\\u{1F600}ab'.substr(1, 2); s.length === 2 && s.charCodeAt(0) === 0xDE00 && s.charCodeAt(1) === 0x61",
)
.unwrap();
assert!(r.as_bool());
}
#[test]