feat: 补 String.fromCodePoint/codePointAt/isWellFormed/toWellFormed

This commit is contained in:
2026-08-08 20:05:49 +08:00
parent dc6eed44f8
commit 71c9f78338
3 changed files with 131 additions and 14 deletions
+122 -13
View File
@@ -22,8 +22,21 @@ fn this_string<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<String, JsValue> {
if this_val.is_null() || this_val.is_undefined() {
return Err(crate::error::create_type_error(vm, "String.prototype method called on null or undefined"));
}
if this_val.is_symbol() {
return Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string"));
}
// 对象 this 须经 ToString 完整转换(boxed Number/String 等取内部原始值)。
oxide_runtime_api::to_string_full(this_val, vm).map_err(|e| crate::error::create_error(vm, &e))
match oxide_runtime_api::to_string_full(this_val, vm) {
Ok(s) => Ok(s),
Err(_) => {
// ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播,
// 否则会被展平为普通 Error 丢失原始异常对象。
if let Some(exc) = vm.take_uncaught_value() {
return Err(exc);
}
Err(crate::error::create_type_error(vm, "Cannot convert this value to a string"))
}
}
}
/// `String.fromCharCode(...codes)`:把各参数按低 16 位转成字符拼接为字符串。
@@ -41,6 +54,39 @@ pub fn string_from_char_code<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
NativeResult::Ok(vm.new_string(&out))
}
/// `String.fromCodePoint(...codes)`:把各参数按 ToNumber 语义转成 code point
/// (0..0x10FFFF 的整数)拼接为字符串;非整数、NaN 或越界抛 RangeError,
/// Symbol 抛 TypeError。surrogate 区间按现行规范接受(孤立 surrogate 无法在
/// UTF-8 表示,与字面量编码一致输出 U+FFFD + 四位小写 hex 文本)。
pub fn string_from_code_point<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.fromCodePoint called with {} args", args.len());
let mut out = String::new();
for &arg_reg in args.iter().skip(1) {
let n = match oxide_runtime_api::to_number_full(vm.reg(arg_reg), vm) {
Ok(n) => n,
Err(_) => {
// ToNumber 触发对象 valueOf/toString 抛出的原生异常须原样传播,
// 否则会被展平为普通 Error 丢失原始异常对象。
if let Some(exc) = vm.take_uncaught_value() {
return NativeResult::Err(exc);
}
return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert value to a number"));
}
};
if !n.is_finite() || n.trunc() != n || n < 0.0 || n > 0x10FFFF as f64 {
return NativeResult::Err(crate::error::create_range_error(vm, "Invalid code point"));
}
let code = n as u32;
if (0xD800..=0xDFFF).contains(&code) {
out.push('\u{FFFD}');
out.push_str(&format!("{code:04x}"));
} else {
out.push(char::from_u32(code).unwrap());
}
}
NativeResult::Ok(vm.new_string(&out))
}
/// `String.prototype.valueOf`:返回包装对象的原始字符串;其它 this 抛 TypeError。
pub fn string_value_of<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.valueOf called with {} args", args.len());
@@ -846,29 +892,92 @@ pub fn string_trim_end<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
NativeResult::Ok(vm.new_string(s.trim_end()))
}
/// `String.prototype.codePointAt(pos)`:返回 code point(surrogate pair 会合并),
/// 越界返回 undefined。
/// `String.prototype.codePointAt(pos)`:按 UTF-16 code unit 位置取 code point
/// (surrogate pair 合并);越界或孤立代理返回 undefined。
pub fn string_code_point_at<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.codePointAt called with {} args", args.len());
let s = try_string!(this_string(vm, args));
let pos = if args.len() > 1 {
vm.coerce_number_bounded(vm.reg(args[1])).unwrap_or(f64::NAN) as usize
let pos_val = vm.reg(args[1]);
if pos_val.is_symbol() {
return NativeResult::Err(crate::error::create_type_error(
vm,
"Cannot convert a Symbol value to a number",
));
}
match vm.coerce_number_bounded(pos_val) {
Ok(n) => n,
Err(_) => {
// pos 对象 valueOf/toString 抛出的异常须原样传播。
if let Some(exc) = vm.take_uncaught_value() {
return NativeResult::Err(exc);
}
return NativeResult::Err(crate::error::create_type_error(
vm,
"Cannot convert argument to a number",
));
}
}
} else {
0
0.0
};
let chars: Vec<char> = s.chars().collect();
if pos >= chars.len() {
let pos = if pos.is_nan() { 0.0 } else { pos.trunc() };
if pos < 0.0 || pos > u32::MAX as f64 {
return NativeResult::Ok(JsValue::undefined());
}
let c = chars[pos] as u32;
if (0xD800..=0xDBFF).contains(&c) && pos + 1 < chars.len() {
let next = chars[pos + 1] as u32;
if (0xDC00..=0xDFFF).contains(&next) {
let cp = 0x10000 + ((c - 0xD800) << 10) + (next - 0xDC00);
let pos = pos as usize;
// JS 规范索引是 UTF-16 code unit 位置,须按代理对展开定位(astral 字符占两单元)。
let units: Vec<u16> = s.encode_utf16().collect();
if pos >= units.len() {
return NativeResult::Ok(JsValue::undefined());
}
let first = units[pos];
if (0xD800..=0xDBFF).contains(&first) && pos + 1 < units.len() {
let second = units[pos + 1];
if (0xDC00..=0xDFFF).contains(&second) {
let cp = 0x10000 + (((first - 0xD800) as u32) << 10) + (second - 0xDC00) as u32;
return NativeResult::Ok(JsValue::int(cp as i32));
}
}
NativeResult::Ok(JsValue::int(c as i32))
NativeResult::Ok(JsValue::int(first as i32))
}
/// `String.prototype.isWellFormed()`:字符串无孤立 surrogate(每个 UTF-16
/// 码元要么是合法字符,要么与相邻码元成代理对)时返回 true。
pub fn string_is_well_formed<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.isWellFormed called with {} args", args.len());
let s = try_string!(this_string(vm, args));
let mut iter = s.encode_utf16().peekable();
while let Some(&first) = iter.peek() {
if (0xDC00..=0xDFFF).contains(&first) {
return NativeResult::Ok(JsValue::bool(false));
}
if (0xD800..=0xDBFF).contains(&first) {
if !matches!(iter.nth(1), Some(second) if (0xDC00..=0xDFFF).contains(&second)) {
return NativeResult::Ok(JsValue::bool(false));
}
} else {
iter.next();
}
}
NativeResult::Ok(JsValue::bool(true))
}
/// `String.prototype.toWellFormed()`:把孤立 surrogate 替换为 U+FFFD 返回新字符串。
pub fn string_to_well_formed<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.prototype.toWellFormed called with {} args", args.len());
let s = try_string!(this_string(vm, args));
// 引擎字符串为合法 UTF-8,Rust char 不可能落在 surrogate 区间;遍历保留
// 通用来正确编码未来可能出现的替换路径。
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if (0xD800..=0xDFFF).contains(&(c as u32)) {
out.push('\u{FFFD}');
} else {
out.push(c);
}
}
NativeResult::Ok(vm.new_string(&out))
}
/// `String.prototype.normalize(form)`:按 NFC/NFD/NFKC/NFKD 规范化为 Unicode 规范形式。
+6 -1
View File
@@ -147,6 +147,9 @@ pub struct StringMethods {
pub substr: *const (),
pub at: *const (),
pub last_index_of: *const (),
pub from_code_point: *const (),
pub is_well_formed: *const (),
pub to_well_formed: *const (),
}
/// RegExp 原型方法的 native 函数指针集合。
@@ -1093,7 +1096,7 @@ impl BuiltinWorld {
pub fn bind_string_methods(&self, methods: &StringMethods, string_forge: &PermInterner, shape_forge: &ShapeForge) {
let ctor_ptr = P::as_ptr(&self.string_constructor) as *mut JsObject;
let ctor = unsafe { &mut *ctor_ptr };
bind_methods!(self, ctor, string_forge, shape_forge, ("fromCharCode", methods.from_char_code, 1),);
bind_methods!(self, ctor, string_forge, shape_forge, ("fromCharCode", methods.from_char_code, 1), ("fromCodePoint", methods.from_code_point, 1),);
let proto_ptr = P::as_ptr(&self.string_proto) as *mut JsObject;
let proto = unsafe { &mut *proto_ptr };
@@ -1131,6 +1134,8 @@ impl BuiltinWorld {
("substr", methods.substr, 2),
("at", methods.at, 1),
("lastIndexOf", methods.last_index_of, 1),
("isWellFormed", methods.is_well_formed, 0),
("toWellFormed", methods.to_well_formed, 0),
);
}
@@ -39,6 +39,9 @@ pub fn bind_string(core: &Arc<KernelCore>, session: &KernelSession, global: &mut
substr: oxide_builtins::string::string_substr::<crate::vm::Vm> as *const (),
at: oxide_builtins::string::string_at::<crate::vm::Vm> as *const (),
last_index_of: oxide_builtins::string::string_last_index_of::<crate::vm::Vm> as *const (),
from_code_point: oxide_builtins::string::string_from_code_point::<crate::vm::Vm> as *const (),
is_well_formed: oxide_builtins::string::string_is_well_formed::<crate::vm::Vm> as *const (),
to_well_formed: oxide_builtins::string::string_to_well_formed::<crate::vm::Vm> as *const (),
};
session.builtin_world().bind_string_methods(
&string_methods,