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
+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]