From 61f590f118c0a3e85c2b0a9daf5f2aa355d2e841 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 16 Sep 2026 00:31:44 +0000 Subject: [PATCH] =?UTF-8?q?test:=20oxide=5Fkernel=20forge=20=E5=86=85?= =?UTF-8?q?=E8=81=94=E6=B5=8B=E8=AF=95=E7=A7=BB=E5=85=A5=E9=9B=86=E6=88=90?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_kernel/src/prop_forge.rs | 79 ------ crates/oxide_kernel/src/string_forge.rs | 246 ------------------ crates/oxide_kernel/tests/prop_forge_tests.rs | 75 ++++++ .../oxide_kernel/tests/string_forge_tests.rs | 246 ++++++++++++++++++ 4 files changed, 321 insertions(+), 325 deletions(-) create mode 100644 crates/oxide_kernel/tests/prop_forge_tests.rs create mode 100644 crates/oxide_kernel/tests/string_forge_tests.rs diff --git a/crates/oxide_kernel/src/prop_forge.rs b/crates/oxide_kernel/src/prop_forge.rs index 221e6432..50a3b4e3 100644 --- a/crates/oxide_kernel/src/prop_forge.rs +++ b/crates/oxide_kernel/src/prop_forge.rs @@ -75,82 +75,3 @@ impl Default for PropForge { Self::new() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_get_nonexistent() { - let forge = PropForge::new(); - assert!(forge.get_template(999).is_none()); - } - - #[test] - fn test_upsert_and_get() { - let forge = PropForge::new(); - let t = PropTemplate { - shape_id: 1, - prop_name: 11, - position: 3, - generation: 10, - }; - forge.upsert(1, t); - let got = forge.get_template(1).unwrap(); - assert_eq!(got.shape_id, 1); - assert_eq!(got.prop_name, 11); - assert_eq!(got.position, 3); - assert_eq!(got.generation, 10); - } - - #[test] - fn test_upsert_if_better_high_gen_wins() { - let forge = PropForge::new(); - forge.upsert_if_better( - 1, - PropTemplate { - shape_id: 1, - prop_name: 11, - position: 3, - generation: 10, - }, - ); - forge.upsert_if_better( - 1, - PropTemplate { - shape_id: 1, - prop_name: 12, - position: 7, - generation: 5, - }, - ); - let got = forge.get_template(1).unwrap(); - assert_eq!(got.generation, 10); - } - - #[test] - fn test_upsert_if_better_low_replaced() { - let forge = PropForge::new(); - forge.upsert_if_better( - 1, - PropTemplate { - shape_id: 1, - prop_name: 11, - position: 3, - generation: 5, - }, - ); - forge.upsert_if_better( - 1, - PropTemplate { - shape_id: 1, - prop_name: 12, - position: 7, - generation: 10, - }, - ); - let got = forge.get_template(1).unwrap(); - assert_eq!(got.generation, 10); - assert_eq!(got.position, 7); - } -} diff --git a/crates/oxide_kernel/src/string_forge.rs b/crates/oxide_kernel/src/string_forge.rs index ac747348..7a589526 100644 --- a/crates/oxide_kernel/src/string_forge.rs +++ b/crates/oxide_kernel/src/string_forge.rs @@ -456,249 +456,3 @@ pub fn typeof_string_ptr(kind: u8) -> *const JsString { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn intern_dedup() { - let interner = PermInterner::new(); - let (i1, h1) = interner.intern("abc"); - let (i2, h2) = interner.intern("abc"); - assert_eq!(i1, i2); - assert_eq!(h1, h2); - } - - #[test] - fn intern_different() { - let interner = PermInterner::new(); - let (i1, _) = interner.intern("x"); - let (i2, _) = interner.intern("y"); - assert_ne!(i1, i2); - } - - #[test] - fn lookup_zero_clone() { - let interner = PermInterner::new(); - let (id, _) = interner.intern("hello"); - assert_eq!(interner.lookup(id), Some("hello")); - } - - #[test] - fn lookup_not_found() { - let interner = PermInterner::new(); - assert_eq!(interner.lookup(99999), None); - } - - #[test] - fn entry_count_monotonic() { - let interner = PermInterner::new(); - assert_eq!(interner.entry_count(), 0); - interner.intern("a"); - interner.intern("b"); - interner.intern("a"); - assert_eq!(interner.entry_count(), 2); - } - - #[test] - fn many_unique_no_collision() { - let interner = PermInterner::new(); - for i in 0..10_000 { - let s = format!("key{i}"); - let (id, _) = interner.intern(&s); - assert_eq!(interner.lookup(id), Some(&*Box::leak(s.into_boxed_str()))); - } - assert_eq!(interner.entry_count(), 10_000); - } - - #[test] - fn string_ptr_roundtrip() { - 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); - } - - #[test] - fn single_char_ptr_idempotent() { - let a = single_char_ptr(b'a'); - assert_eq!(unsafe { (*a).as_str() }, "a"); - // 二次调用返回同一稳定指针(仅物化一次)。 - assert_eq!(single_char_ptr(b'a'), a); - } - - #[test] - fn single_char_table_full_ascii() { - // 全表 128 条目均可物化且内容为对应的单字符文本。 - for b in 0u8..=127 { - let ptr = single_char_ptr(b); - assert_eq!(unsafe { (*ptr).as_str() }, (b as char).to_string()); - } - } - - #[test] - fn typeof_table_content_and_stability() { - // 8 个 typeof 结果串与下标约定一一对应,且二次调用返回同一稳定指针。 - let texts = ["undefined", "object", "boolean", "number", "string", "symbol", "bigint", "function"]; - for (i, t) in texts.iter().enumerate() { - let ptr = typeof_string_ptr(i as u8); - assert_eq!(unsafe { (*ptr).as_str() }, *t); - assert_eq!(typeof_string_ptr(i as u8), ptr, "下标 {i} 二次调用应返回同一指针"); - } - } - - #[test] - fn encode_key_identity_and_escapes() { - // 良形且无 FFFD(含超平面良配对)编码为恒等文本。 - assert_eq!(encode_key(&"abc🚀".encode_utf16().collect::>()), "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])); - // 文本 [FFFD,'d','8','0','0'] 与单元 [D800] 的键必须可区分(防物化串键)。 - assert_ne!(encode_key(&[0xFFFD, 0x64, 0x38, 0x30, 0x30]), encode_key(&[0xD800])); - } - - #[test] - fn encode_decode_roundtrip() { - let cases: Vec> = 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 source_escape_marker_forms() { - // 孤立 surrogate / FFFD → 源码域 \u 转义形态;配对与 BMP 恒等。 - assert_eq!(source_escape(&[0xD800]), "\\ud800"); - assert_eq!(source_escape(&[0xDFFF]), "\\udfff"); - assert_eq!(source_escape(&[0xFFFD]), "\\ufffd"); - assert_eq!(source_escape(&[0x61, 0xD800, 0x62]), "a\\ud800b"); - assert_eq!(source_escape(&[0xD83D, 0xDE00]), "\u{1F600}"); - } - - /// 源码是 JS 程序,反斜杠为语法字符:原样透传,转义文本形态逐字保留 - /// (正则/字符串字面量内 `\u{1d306}`、`\\` 等序列不得被改写)。 - #[test] - fn source_escape_backslash_passthrough() { - // 正则字面量源文本(u 模式 \u{..} 转义):逐字恒等。 - let re_src: Vec = "/\\u{1d306}/u".encode_utf16().collect(); - assert_eq!(source_escape(&re_src), "/\\u{1d306}/u"); - // 转义文本(反斜杠 + "ud800" 字面文本)与孤立 surrogate 单元同形: - // 源码域中 `\ud800` 文本即孤立 surrogate 的源码表示,两者编码同一 - // 源码文本是设计使然(源码即程序,oxc 只见转义文本)。 - let text: Vec = "\\ud800".encode_utf16().collect(); - assert_eq!(source_escape(&text), "\\ud800"); - assert_eq!(source_escape(&[0xD800]), source_escape(&text)); - // 双反斜杠(字面量内转义反斜杠)恒等。 - let dbl: Vec = "\\\\".encode_utf16().collect(); - assert_eq!(source_escape(&dbl), "\\\\"); - } - - /// 键域闭环:`decode_key ∘ source_escape_to_key ∘ source_escape` 对单元 - /// 序列恒等(注入 marker 还原、用户转义文本透传、双反斜杠与 astral 对)。 - #[test] - fn source_escape_roundtrip_via_key() { - let cases: Vec> = vec![ - vec![0xD800], - vec![0xDFFF], - vec![0xFFFD], - vec![0x5C], - // 用户字面转义文本(非 marker 值域 / \u{..} 形态):逐字透传。 - vec![0x5C, 0x75, 0x30, 0x30, 0x34, 0x31], - vec![0x5C, 0x75, 0x7B, 0x31, 0x64, 0x33, 0x30, 0x36, 0x7D], - vec![0x5C, 0x5C], - // 数据反斜杠紧邻注入 marker(`\\ud800`/`\\ufffd` 拼接形态)。 - vec![0x5C, 0xD800], - vec![0x5C, 0xFFFD], - // FFFD 单元后跟 "d800" 文本:marker 消费恰 4 字符,不吞后续。 - vec![0xFFFD, 0x64, 0x38, 0x30, 0x30], - vec![0x66, 0x66, 0x66, 0x64], - vec![0xD83D, 0xDE00], - ]; - for units in &cases { - let escaped = source_escape(units); - let key = source_escape_to_key(&escaped); - assert_eq!(&decode_key(&key), units.as_slice(), "roundtrip 失败: escaped={escaped:?}"); - } - } - - #[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 线程同一时刻命中冷表槽,强制多线程同入慢路径。 - // 所有调用方必须拿到同一稳定指针,且该指针恒指向存活的 JsString。 - use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::Arc; - - const N: usize = 96; - let go = Arc::new(AtomicBool::new(false)); - let mut handles = Vec::with_capacity(N); - for _ in 0..N { - let go = Arc::clone(&go); - handles.push(std::thread::spawn(move || { - while !go.load(Ordering::Acquire) { - std::hint::spin_loop(); - } - // 裸指针 !Send,跨线程以 usize 传递,出口再转回。 - let (i, c, t) = (small_int_ptr(42).unwrap(), single_char_ptr(b'x'), typeof_string_ptr(1)); - (i as usize, c as usize, t as usize) - })); - } - go.store(true, Ordering::Release); - - let mut results = Vec::with_capacity(N); - for h in handles { - results.push(h.join().expect("竞态线程")); - } - let (i0, c0, t0) = results[0]; - for (i, c, t) in &results[1..] { - assert_eq!(*i, i0, "小整数表首用应全局唯一指针"); - assert_eq!(*c, c0, "单字符表首用应全局唯一指针"); - assert_eq!(*t, t0, "typeof 表首用应全局唯一指针"); - } - - // 返回指针必须指向存活 JsString:内容与长度可回读。 - assert_eq!(unsafe { (*(i0 as *const JsString)).as_str() }, "42"); - 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).utf16_len() }, 6); - } -} diff --git a/crates/oxide_kernel/tests/prop_forge_tests.rs b/crates/oxide_kernel/tests/prop_forge_tests.rs new file mode 100644 index 00000000..765346db --- /dev/null +++ b/crates/oxide_kernel/tests/prop_forge_tests.rs @@ -0,0 +1,75 @@ +use oxide_kernel::prop_forge::{PropForge, PropTemplate}; + +#[test] +fn test_get_nonexistent() { + let forge = PropForge::new(); + assert!(forge.get_template(999).is_none()); +} + +#[test] +fn test_upsert_and_get() { + let forge = PropForge::new(); + let t = PropTemplate { + shape_id: 1, + prop_name: 11, + position: 3, + generation: 10, + }; + forge.upsert(1, t); + let got = forge.get_template(1).unwrap(); + assert_eq!(got.shape_id, 1); + assert_eq!(got.prop_name, 11); + assert_eq!(got.position, 3); + assert_eq!(got.generation, 10); +} + +#[test] +fn test_upsert_if_better_high_gen_wins() { + let forge = PropForge::new(); + forge.upsert_if_better( + 1, + PropTemplate { + shape_id: 1, + prop_name: 11, + position: 3, + generation: 10, + }, + ); + forge.upsert_if_better( + 1, + PropTemplate { + shape_id: 1, + prop_name: 12, + position: 7, + generation: 5, + }, + ); + let got = forge.get_template(1).unwrap(); + assert_eq!(got.generation, 10); +} + +#[test] +fn test_upsert_if_better_low_replaced() { + let forge = PropForge::new(); + forge.upsert_if_better( + 1, + PropTemplate { + shape_id: 1, + prop_name: 11, + position: 3, + generation: 5, + }, + ); + forge.upsert_if_better( + 1, + PropTemplate { + shape_id: 1, + prop_name: 12, + position: 7, + generation: 10, + }, + ); + let got = forge.get_template(1).unwrap(); + assert_eq!(got.generation, 10); + assert_eq!(got.position, 7); +} diff --git a/crates/oxide_kernel/tests/string_forge_tests.rs b/crates/oxide_kernel/tests/string_forge_tests.rs new file mode 100644 index 00000000..32e1baba --- /dev/null +++ b/crates/oxide_kernel/tests/string_forge_tests.rs @@ -0,0 +1,246 @@ +use oxide_kernel::string_forge::{ + decode_key, encode_key, single_char_ptr, small_int_ptr, source_escape, source_escape_to_key, typeof_string_ptr, + PermInterner, +}; +use oxide_types::object::JsString; + +#[test] +fn intern_dedup() { + let interner = PermInterner::new(); + let (i1, h1) = interner.intern("abc"); + let (i2, h2) = interner.intern("abc"); + assert_eq!(i1, i2); + assert_eq!(h1, h2); +} + +#[test] +fn intern_different() { + let interner = PermInterner::new(); + let (i1, _) = interner.intern("x"); + let (i2, _) = interner.intern("y"); + assert_ne!(i1, i2); +} + +#[test] +fn lookup_zero_clone() { + let interner = PermInterner::new(); + let (id, _) = interner.intern("hello"); + assert_eq!(interner.lookup(id), Some("hello")); +} + +#[test] +fn lookup_not_found() { + let interner = PermInterner::new(); + assert_eq!(interner.lookup(99999), None); +} + +#[test] +fn entry_count_monotonic() { + let interner = PermInterner::new(); + assert_eq!(interner.entry_count(), 0); + interner.intern("a"); + interner.intern("b"); + interner.intern("a"); + assert_eq!(interner.entry_count(), 2); +} + +#[test] +fn many_unique_no_collision() { + let interner = PermInterner::new(); + for i in 0..10_000 { + let s = format!("key{i}"); + let (id, _) = interner.intern(&s); + assert_eq!(interner.lookup(id), Some(&*Box::leak(s.into_boxed_str()))); + } + assert_eq!(interner.entry_count(), 10_000); +} + +#[test] +fn string_ptr_roundtrip() { + 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); +} + +#[test] +fn single_char_ptr_idempotent() { + let a = single_char_ptr(b'a'); + assert_eq!(unsafe { (*a).as_str() }, "a"); + // 二次调用返回同一稳定指针(仅物化一次)。 + assert_eq!(single_char_ptr(b'a'), a); +} + +#[test] +fn single_char_table_full_ascii() { + // 全表 128 条目均可物化且内容为对应的单字符文本。 + for b in 0u8..=127 { + let ptr = single_char_ptr(b); + assert_eq!(unsafe { (*ptr).as_str() }, (b as char).to_string()); + } +} + +#[test] +fn typeof_table_content_and_stability() { + // 8 个 typeof 结果串与下标约定一一对应,且二次调用返回同一稳定指针。 + let texts = ["undefined", "object", "boolean", "number", "string", "symbol", "bigint", "function"]; + for (i, t) in texts.iter().enumerate() { + let ptr = typeof_string_ptr(i as u8); + assert_eq!(unsafe { (*ptr).as_str() }, *t); + assert_eq!(typeof_string_ptr(i as u8), ptr, "下标 {i} 二次调用应返回同一指针"); + } +} + +#[test] +fn encode_key_identity_and_escapes() { + // 良形且无 FFFD(含超平面良配对)编码为恒等文本。 + assert_eq!(encode_key(&"abc🚀".encode_utf16().collect::>()), "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])); + // 文本 [FFFD,'d','8','0','0'] 与单元 [D800] 的键必须可区分(防物化串键)。 + assert_ne!(encode_key(&[0xFFFD, 0x64, 0x38, 0x30, 0x30]), encode_key(&[0xD800])); +} + +#[test] +fn encode_decode_roundtrip() { + let cases: Vec> = 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 source_escape_marker_forms() { + // 孤立 surrogate / FFFD → 源码域 \u 转义形态;配对与 BMP 恒等。 + assert_eq!(source_escape(&[0xD800]), "\\ud800"); + assert_eq!(source_escape(&[0xDFFF]), "\\udfff"); + assert_eq!(source_escape(&[0xFFFD]), "\\ufffd"); + assert_eq!(source_escape(&[0x61, 0xD800, 0x62]), "a\\ud800b"); + assert_eq!(source_escape(&[0xD83D, 0xDE00]), "\u{1F600}"); +} + +/// 源码是 JS 程序,反斜杠为语法字符:原样透传,转义文本形态逐字保留 +/// (正则/字符串字面量内 `\u{1d306}`、`\\` 等序列不得被改写)。 +#[test] +fn source_escape_backslash_passthrough() { + // 正则字面量源文本(u 模式 \u{..} 转义):逐字恒等。 + let re_src: Vec = "/\\u{1d306}/u".encode_utf16().collect(); + assert_eq!(source_escape(&re_src), "/\\u{1d306}/u"); + // 转义文本(反斜杠 + "ud800" 字面文本)与孤立 surrogate 单元同形: + // 源码域中 `\ud800` 文本即孤立 surrogate 的源码表示,两者编码同一 + // 源码文本是设计使然(源码即程序,oxc 只见转义文本)。 + let text: Vec = "\\ud800".encode_utf16().collect(); + assert_eq!(source_escape(&text), "\\ud800"); + assert_eq!(source_escape(&[0xD800]), source_escape(&text)); + // 双反斜杠(字面量内转义反斜杠)恒等。 + let dbl: Vec = "\\\\".encode_utf16().collect(); + assert_eq!(source_escape(&dbl), "\\\\"); +} + +/// 键域闭环:`decode_key ∘ source_escape_to_key ∘ source_escape` 对单元 +/// 序列恒等(注入 marker 还原、用户转义文本透传、双反斜杠与 astral 对)。 +#[test] +fn source_escape_roundtrip_via_key() { + let cases: Vec> = vec![ + vec![0xD800], + vec![0xDFFF], + vec![0xFFFD], + vec![0x5C], + // 用户字面转义文本(非 marker 值域 / \u{..} 形态):逐字透传。 + vec![0x5C, 0x75, 0x30, 0x30, 0x34, 0x31], + vec![0x5C, 0x75, 0x7B, 0x31, 0x64, 0x33, 0x30, 0x36, 0x7D], + vec![0x5C, 0x5C], + // 数据反斜杠紧邻注入 marker(`\\ud800`/`\\ufffd` 拼接形态)。 + vec![0x5C, 0xD800], + vec![0x5C, 0xFFFD], + // FFFD 单元后跟 "d800" 文本:marker 消费恰 4 字符,不吞后续。 + vec![0xFFFD, 0x64, 0x38, 0x30, 0x30], + vec![0x66, 0x66, 0x66, 0x64], + vec![0xD83D, 0xDE00], + ]; + for units in &cases { + let escaped = source_escape(units); + let key = source_escape_to_key(&escaped); + assert_eq!(&decode_key(&key), units.as_slice(), "roundtrip 失败: escaped={escaped:?}"); + } +} + +#[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 线程同一时刻命中冷表槽,强制多线程同入慢路径。 + // 所有调用方必须拿到同一稳定指针,且该指针恒指向存活的 JsString。 + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + + const N: usize = 96; + let go = Arc::new(AtomicBool::new(false)); + let mut handles = Vec::with_capacity(N); + for _ in 0..N { + let go = Arc::clone(&go); + handles.push(std::thread::spawn(move || { + while !go.load(Ordering::Acquire) { + std::hint::spin_loop(); + } + // 裸指针 !Send,跨线程以 usize 传递,出口再转回。 + let (i, c, t) = (small_int_ptr(42).unwrap(), single_char_ptr(b'x'), typeof_string_ptr(1)); + (i as usize, c as usize, t as usize) + })); + } + go.store(true, Ordering::Release); + + let mut results = Vec::with_capacity(N); + for h in handles { + results.push(h.join().expect("竞态线程")); + } + let (i0, c0, t0) = results[0]; + for (i, c, t) in &results[1..] { + assert_eq!(*i, i0, "小整数表首用应全局唯一指针"); + assert_eq!(*c, c0, "单字符表首用应全局唯一指针"); + assert_eq!(*t, t0, "typeof 表首用应全局唯一指针"); + } + + // 返回指针必须指向存活 JsString:内容与长度可回读。 + assert_eq!(unsafe { (*(i0 as *const JsString)).as_str() }, "42"); + 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).utf16_len() }, 6); +}