From c3322d6a3cda07bf25ac560bcfd63fb60ae62a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=89=B9=E6=AF=8F=E7=BE=8A=E9=98=9F?= Date: Sat, 8 Aug 2026 20:05:49 +0800 Subject: [PATCH] feat: implement String.fromCodePoint, codePointAt UTF-16 indexing, isWellFormed/toWellFormed --- crates/oxide_builtins/src/string.rs | 135 ++++++++++++++++++-- crates/oxide_kernel/src/builtin.rs | 7 +- crates/oxide_vm/src/bindings/bind_string.rs | 3 + 3 files changed, 131 insertions(+), 14 deletions(-) diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index 290fa27..643db6a 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -22,8 +22,21 @@ fn this_string(vm: &mut H, args: &[u8]) -> Result { 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(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(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(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(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(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 = 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 = 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(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(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 规范形式。 diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index c362c0f..e98c9a1 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -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), ); } diff --git a/crates/oxide_vm/src/bindings/bind_string.rs b/crates/oxide_vm/src/bindings/bind_string.rs index 5ca3f2c..0b19112 100644 --- a/crates/oxide_vm/src/bindings/bind_string.rs +++ b/crates/oxide_vm/src/bindings/bind_string.rs @@ -39,6 +39,9 @@ pub fn bind_string(core: &Arc, session: &KernelSession, global: &mut substr: oxide_builtins::string::string_substr:: as *const (), at: oxide_builtins::string::string_at:: as *const (), last_index_of: oxide_builtins::string::string_last_index_of:: as *const (), + from_code_point: oxide_builtins::string::string_from_code_point:: as *const (), + is_well_formed: oxide_builtins::string::string_is_well_formed:: as *const (), + to_well_formed: oxide_builtins::string::string_to_well_formed:: as *const (), }; session.builtin_world().bind_string_methods( &string_methods,