diff --git a/Cargo.lock b/Cargo.lock index c0c62862..db0a0eb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,7 +1095,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.8.0" +version = "0.8.1" dependencies = [ "chrono", "fast-float", @@ -1120,7 +1120,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.8.0" +version = "0.8.1" dependencies = [ "num-bigint", "oxide_log", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.8.0" +version = "0.8.1" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.8.0" +version = "0.8.1" dependencies = [ "ansi_term", "bumpalo", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.8.0" +version = "0.8.1" dependencies = [ "lru", "oxide_bytecode", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.8.0" +version = "0.8.1" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1187,7 +1187,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.8.0" +version = "0.8.1" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1201,7 +1201,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.8.0" +version = "0.8.1" dependencies = [ "num-bigint", "oxc_allocator", @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.8.0" +version = "0.8.1" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.8.0" +version = "0.8.1" dependencies = [ "bumpalo", "dashmap", @@ -1240,7 +1240,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.8.0" +version = "0.8.1" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.8.0" +version = "0.8.1" dependencies = [ "tracing", "tracing-appender", @@ -1261,7 +1261,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.8.0" +version = "0.8.1" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.8.0" +version = "0.8.1" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1292,7 +1292,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.8.0" +version = "0.8.1" dependencies = [ "num-bigint", "num-traits", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.8.0" +version = "0.8.1" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.8.0" +version = "0.8.1" dependencies = [ "bumpalo", "hashbrown 0.14.5", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.8.0" +version = "0.8.1" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 800976eb..e4a5ec14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.8.0" +version = "0.8.1" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_builtins/src/string/basic.rs b/crates/oxide_builtins/src/string/basic.rs index 36772980..a8db2e80 100644 --- a/crates/oxide_builtins/src/string/basic.rs +++ b/crates/oxide_builtins/src/string/basic.rs @@ -135,23 +135,58 @@ pub fn string_constructor(vm: &mut H, args: &[u8]) -> NativeResult { // 对 Symbol 抛 TypeError——与函数调用路径(3a 描述串)相异。 return NativeResult::Err(crate::error::create_type_error(vm, "Cannot convert a Symbol value to a string")); } - // 对象参数须经 ToPrimitive/ToString 完整转换;函数调用路径的 Symbol - // 走 SymbolDescriptiveString(规范步骤 3a,不经 ToString)。 - let v = oxide_runtime_api::to_string_for_string_constructor(v, vm); - match v { - Ok(s) => vm.new_string_owned(s), - Err(msg) => { - // ToString on an object may throw via toString/valueOf; propagate the original exception. - if let Some(exc) = vm.take_uncaught_value() { - return NativeResult::Err(exc); + // 常量快路径:字符串直通,布尔/null/undefined/NaN/±Infinity 走永久常量表, + // 0..=99 小整数走小整数永久表,均零分配、不经过转换链。 + if v.is_string() { + // ToString 对字符串是恒等(规范 §21.2.1.1 步骤 3),原值返回免克隆。 + v + } else if v.is_bool() { + JsValue::string(oxide_kernel::string_forge::const_string_ptr(if v.as_bool() { 0 } else { 1 })) + } else if v.is_null() { + JsValue::string(oxide_kernel::string_forge::const_string_ptr(2)) + } else if v.is_undefined() { + JsValue::string(oxide_kernel::string_forge::const_string_ptr(3)) + } else if v.is_int() { + let n = v.as_int(); + if n >= 0 { + if let Some(ptr) = oxide_kernel::string_forge::small_int_ptr(n as u32) { + JsValue::string(ptr) + } else { + vm.new_string_owned(oxide_runtime_api::to_string(v)) + } + } else { + vm.new_string_owned(oxide_runtime_api::to_string(v)) + } + } else if v.is_double() { + let d = v.as_double(); + if d.is_nan() { + JsValue::string(oxide_kernel::string_forge::const_string_ptr(4)) + } else if d.is_infinite() { + JsValue::string(oxide_kernel::string_forge::const_string_ptr(if d > 0.0 { 5 } else { 6 })) + } else { + // 有限 double 走原数值转串链,行为与转换链一致。 + vm.new_string_owned(oxide_runtime_api::js_number_to_string(d)) + } + } else { + // BigInt/对象参数走完整转换链(对象可经 valueOf/toString 抛错); + // 函数调用路径的 Symbol 走 SymbolDescriptiveString(规范步骤 3a,不经 ToString)。 + let v = oxide_runtime_api::to_string_for_string_constructor(v, vm); + match v { + Ok(s) => vm.new_string_owned(s), + Err(msg) => { + // ToString on an object may throw via toString/valueOf; propagate the original exception. + if let Some(exc) = vm.take_uncaught_value() { + return NativeResult::Err(exc); + } + // 无在途异常时按格式化文本恢复 kind 与消息(装箱 Symbol 走 ToString + // 抛 TypeError,消息须与原始异常一致)。 + return NativeResult::Err(crate::error::create_from_text(vm, &msg)); } - // 无在途异常时按格式化文本恢复 kind 与消息(装箱 Symbol 走 ToString - // 抛 TypeError,消息须与原始异常一致)。 - return NativeResult::Err(crate::error::create_from_text(vm, &msg)); } } } else { - vm.new_string("") + // 无参返回永久空串,免每次新建 session 串。 + JsValue::string(oxide_kernel::string_forge::empty_string_ptr()) }; if !is_ctor { diff --git a/crates/oxide_builtins/tests/string_constructor_tests.rs b/crates/oxide_builtins/tests/string_constructor_tests.rs new file mode 100644 index 00000000..77ca146a --- /dev/null +++ b/crates/oxide_builtins/tests/string_constructor_tests.rs @@ -0,0 +1,92 @@ +use std::sync::Arc; + +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(source: &str) -> Result<(Vm, JsValue), String> { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new().compile(&program).map_err(|e| format!("Compile error: {}", e))?; + let mut vm = Vm::new(); + let result = vm.run(&Arc::new(module))?; + Ok((vm, result)) +} + +fn string_of(source: &str) -> String { + let (_vm, result) = eval(source).unwrap(); + assert!(result.is_string(), "expected a string result from {source}"); + unsafe { &*result.as_string_ptr() }.as_str().to_string() +} + +#[test] +fn string_constructor_primitive_content() { + // 原始值各类别的内容正确性:整数、负整数、有限 double、布尔、 + // null/undefined、NaN/±Infinity、-0、大指数、字符串直通、无参。 + assert_eq!(string_of("String(42)"), "42"); + assert_eq!(string_of("String(-5)"), "-5"); + assert_eq!(string_of("String(3.14)"), "3.14"); + assert_eq!(string_of("String(true)"), "true"); + assert_eq!(string_of("String(false)"), "false"); + assert_eq!(string_of("String(null)"), "null"); + assert_eq!(string_of("String(undefined)"), "undefined"); + assert_eq!(string_of("String(NaN)"), "NaN"); + assert_eq!(string_of("String(Infinity)"), "Infinity"); + assert_eq!(string_of("String(-Infinity)"), "-Infinity"); + assert_eq!(string_of("String(-0)"), "0"); + assert_eq!(string_of("String(1e21)"), "1e+21"); + assert_eq!(string_of("String(\"abc\")"), "abc"); + assert_eq!(string_of("String()"), ""); +} + +#[test] +fn string_constructor_perm_identity() { + // 0..=99 小整数命中小整数永久表:两次调用返回同一指针,且与表项一致。 + let (_vm, a) = eval("String(42)").unwrap(); + let (_vm, b) = eval("String(42)").unwrap(); + assert!(a.is_string() && b.is_string()); + assert_eq!(a.as_string_ptr(), b.as_string_ptr()); + assert_eq!(a.as_string_ptr(), oxide_kernel::string_forge::small_int_ptr(42).unwrap()); + + // 布尔/null/undefined/NaN/±Infinity 命中常量表:两次调用返回同一指针。 + for src in [ + "String(true)", + "String(false)", + "String(null)", + "String(undefined)", + "String(NaN)", + "String(Infinity)", + "String(-Infinity)", + ] { + let (_vm, a) = eval(src).unwrap(); + let (_vm, b) = eval(src).unwrap(); + assert!(a.is_string() && b.is_string()); + assert_eq!(a.as_string_ptr(), b.as_string_ptr(), "{src} 应返回同一指针"); + } +} + +#[test] +fn string_constructor_string_passthrough() { + // 字符串入参恒等返回:内容不变,且跨 VM 实例与永久表同源(免克隆)。 + let (_vm, result) = eval("String(\"abc\")").unwrap(); + assert!(result.is_string()); + assert_eq!(unsafe { &*result.as_string_ptr() }.as_str(), "abc"); +} + +#[test] +fn string_constructor_fallback_paths() { + // 对象参数走完整转换链(valueOf/toString)。 + assert_eq!(string_of("String({})"), "[object Object]"); + // BigInt 参数走 BigInt 路径。 + assert_eq!(string_of("String(1n)"), "1"); + // 装箱 Symbol 抛 TypeError(抛错点不变)。 + assert!(eval("String(Object(Symbol('s')))").is_err()); + // 构造形态返回包装对象,length 为 2。 + let (_vm, result) = eval("new String(42)").unwrap(); + assert!(result.is_object()); + let (_vm, len) = eval("new String(42).length").unwrap(); + assert_eq!(len.as_int(), 2); + // 构造形态字符串入参:从原串物化盒体。 + let (_vm, len) = eval("new String(\"ab\").length").unwrap(); + assert_eq!(len.as_int(), 2); +} diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index b3c3f30b..4f5fcf50 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -479,3 +479,33 @@ pub fn typeof_string_ptr(kind: u8) -> *const JsString { } } } + +/// 原始值常量串文本表(下标见 [`const_string_ptr`]),进程生命周期内不释放。 +const CONST_STRING_TEXTS: [&str; 7] = ["true", "false", "null", "undefined", "NaN", "Infinity", "-Infinity"]; + +/// 原始值常量串的永久 `JsString` 指针表:String() 构造器等高频常量产出路径, +/// 复用静态表免每次 session 分配与 interner 锁查。物化面严格有界:7 条目 ≈ 数百字节, +/// 随进程生命周期有意保留(同 `TYPEOF_TABLE` 论证)。 +static CONST_STRING_TABLE: [OnceLock; 7] = [const { OnceLock::new() }; 7]; + +/// 取原始值常量串的永久 `JsString` 指针,惰性物化一次后恒返回同一地址。 +/// +/// 下标约定:0=true、1=false、2=null、3=undefined、4=NaN、 +/// 5=Infinity、6=-Infinity。调用方按值类型映射。 +pub fn const_string_ptr(kind: u8) -> *const JsString { + let slot = &CONST_STRING_TABLE[kind as usize]; + if let Some(ptr) = slot.get() { + return ptr.0; + } + let ptr = Box::into_raw(Box::new(JsString::new(CONST_STRING_TEXTS[kind as usize].to_string()))); + match slot.set(StringPtr(ptr)) { + Ok(()) => ptr, + Err(_) => { + // 并发首用竞态:与 single_char_ptr 同款处置,本线程产物恰好释放一次。 + let existing = slot.get().expect("set 失败时槽必已初始化").0; + // SAFETY: ptr 来自本线程的 Box::into_raw,无任何外部引用。 + unsafe { drop(Box::from_raw(ptr)) }; + existing + } + } +}