mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
352 lines
12 KiB
Rust
352 lines
12 KiB
Rust
use std::sync::Arc;
|
||
|
||
use oxide_compiler::compiler::Compiler;
|
||
use oxide_types::value::JsValue;
|
||
use oxide_vm::vm::Vm;
|
||
|
||
fn eval(vm: &mut Vm, source: &str) -> Result<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))?;
|
||
vm.run(&Arc::new(module))
|
||
}
|
||
|
||
fn to_str(vm: &Vm, val: JsValue) -> String {
|
||
vm.lookup_str(val).unwrap_or_default()
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_survives_gc_and_still_matches() {
|
||
// 回归:正则存入全局对象触发晋升/GC 搬移后,native_fn 槽的已编译 Box
|
||
// 必须深拷贝到新对象(而非共享指针),否则 epoch 释放与 teardown 双重释放。
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "var r = /ab+c/g; globalThis.r = r; r.test('xxabbcx')").unwrap();
|
||
assert!(result.as_bool());
|
||
|
||
// 触发完整收集:存活正则克隆进新 arena,旧 Box 由 sweep 释放。
|
||
vm.reset();
|
||
|
||
let result = eval(&mut vm, "globalThis.r.test('xxabbcx')").unwrap();
|
||
assert!(result.as_bool(), "GC 后正则应仍可匹配");
|
||
let result = eval(&mut vm, "globalThis.r.exec('xabbbcx')[0]").unwrap();
|
||
assert_eq!(to_str(&vm, result), "abbbc");
|
||
}
|
||
|
||
// --- RegExp constructor ---
|
||
|
||
#[test]
|
||
fn regexp_constructor_creates_object() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "typeof new RegExp('abc')").unwrap();
|
||
assert_eq!(to_str(&vm, result), "object");
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_constructor_with_flags() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('abc', 'gi').ignoreCase").unwrap();
|
||
assert!(result.as_bool());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_constructor_invalid_pattern_syntax_error() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('[')");
|
||
assert!(result.is_err() || (result.is_ok() && result.unwrap().is_string()));
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_source_property() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('hello', 'g').source").unwrap();
|
||
assert_eq!(to_str(&vm, result), "hello");
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_flags_property() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('abc', 'gi').flags").unwrap();
|
||
assert_eq!(to_str(&vm, result), "gi");
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_global_property() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('abc', 'g').global").unwrap();
|
||
assert!(result.as_bool());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_multiline_property() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('abc', 'm').multiline").unwrap();
|
||
assert!(result.as_bool());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_last_index_property() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('abc', 'g').lastIndex").unwrap();
|
||
assert_eq!(result.as_int(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_dotall_sticky_unicode_default_false() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('abc').dotAll").unwrap();
|
||
assert!(!result.as_bool());
|
||
}
|
||
|
||
// --- RegExp.prototype.test ---
|
||
|
||
#[test]
|
||
fn regexp_test_basic() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('abc').test('abc')").unwrap();
|
||
assert!(result.as_bool());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_test_no_match() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('abc').test('xyz')").unwrap();
|
||
assert!(!result.as_bool());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_test_case_insensitive() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "new RegExp('abc', 'i').test('ABC')").unwrap();
|
||
assert!(result.as_bool());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_test_global_flag_iteration() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "var re = /a/g; re.test('a')").unwrap();
|
||
assert!(result.as_bool());
|
||
}
|
||
|
||
// --- RegExp.prototype.exec ---
|
||
|
||
#[test]
|
||
fn regexp_exec_basic() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "/hello/.exec('hello world') !== null").unwrap();
|
||
assert!(result.as_bool());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_exec_no_match_returns_null() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "/xyz/.exec('hello')").unwrap();
|
||
assert!(result.is_null());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_exec_returns_index() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "/ell/.exec('hello')").unwrap();
|
||
assert!(result.is_object());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_exec_returns_input() {
|
||
let mut vm = Vm::new();
|
||
let m = eval(&mut vm, "/ell/.exec('hello')").unwrap();
|
||
assert!(m.is_object());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_exec_global_last_index_advances() {
|
||
let mut vm = Vm::new();
|
||
let re = eval(&mut vm, "var re = /a/g; re.exec('aba'); re.lastIndex").unwrap();
|
||
assert_eq!(re.as_int(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_exec_global_resets_on_exhaustion() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "var re = /a/g; re.exec('a'); re.exec('a') === null").unwrap();
|
||
assert!(result.as_bool());
|
||
}
|
||
|
||
// --- RegExp literal compilation ---
|
||
|
||
#[test]
|
||
fn regexp_literal_basic() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "/hello/").unwrap();
|
||
assert!(result.is_object());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_literal_with_flags() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "/abc/i.test('ABC')").unwrap();
|
||
assert!(result.as_bool());
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_literal_global_exec() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "var re = /a/g; re.exec('aba'); re.lastIndex").unwrap();
|
||
assert_eq!(result.as_int(), 1);
|
||
}
|
||
|
||
// --- String integration ---
|
||
|
||
#[test]
|
||
fn string_match_with_regexp() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "'hello world'.match(/ell/)").unwrap();
|
||
assert!(result.is_object());
|
||
}
|
||
|
||
#[test]
|
||
fn string_replace_with_regexp() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "'hello'.replace(/ell/, 'ipp')").unwrap();
|
||
assert_eq!(to_str(&vm, result), "hippo");
|
||
}
|
||
|
||
#[test]
|
||
fn string_replace_with_regexp_global() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "'aba'.replace(/a/g, 'c')").unwrap();
|
||
assert_eq!(to_str(&vm, result), "cbc");
|
||
}
|
||
|
||
#[test]
|
||
fn string_search_with_regexp() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "'hello'.search(/ell/)").unwrap();
|
||
assert_eq!(result.as_int(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn string_search_not_found() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "'hello'.search(/xyz/)").unwrap();
|
||
assert_eq!(result.as_int(), -1);
|
||
}
|
||
|
||
#[test]
|
||
fn string_split_with_regexp() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "'a,b,c'.split(/,/)").unwrap();
|
||
assert!(result.is_object());
|
||
let obj = unsafe { &*result.as_js_object_ptr() };
|
||
assert_eq!(obj.prop_count(), 3);
|
||
assert_eq!(to_str(&vm, obj.get_prop_at(1)), "b");
|
||
}
|
||
|
||
// --- toString ---
|
||
|
||
#[test]
|
||
fn regexp_to_string() {
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "/abc/gi.toString()").unwrap();
|
||
assert_eq!(to_str(&vm, result), "/abc/gi");
|
||
}
|
||
|
||
// --- Unicode property escapes:Script/Script_Extensions 的 Unknown(Zzzz)取值 ---
|
||
// vendor 的 Unicode 表须含 Unknown(未分配码点集),否则合法模式编译期 SyntaxError。
|
||
|
||
#[test]
|
||
fn regexp_unicode_property_script_unknown_matches_unassigned() {
|
||
let mut vm = Vm::new();
|
||
// U+038B 未分配:Script 与 Script_Extensions 均为 Unknown
|
||
let result = eval(&mut vm, "/\\p{Script=Unknown}/u.test(String.fromCodePoint(0x038B))").unwrap();
|
||
assert!(result.as_bool(), "未分配码点应匹配 Script=Unknown");
|
||
let result = eval(&mut vm, "/\\p{Script_Extensions=Unknown}/u.test(String.fromCodePoint(0x038B))").unwrap();
|
||
assert!(result.as_bool(), "未分配码点应匹配 Script_Extensions=Unknown");
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_unicode_property_script_unknown_rejects_assigned() {
|
||
let mut vm = Vm::new();
|
||
// 已分配拉丁字母不属于 Unknown
|
||
let result = eval(&mut vm, "/\\p{Script=Unknown}/u.test('d')").unwrap();
|
||
assert!(!result.as_bool(), "已分配码点不应匹配 Script=Unknown");
|
||
let result = eval(&mut vm, "/\\P{Script=Unknown}/u.test('d')").unwrap();
|
||
assert!(result.as_bool(), "取反后已分配码点应匹配");
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_unicode_property_script_unknown_aliases() {
|
||
let mut vm = Vm::new();
|
||
// 单字母缩写与符号名须同义解析
|
||
for pattern in [r"\p{sc=Unknown}", r"\p{scx=Unknown}", r"\p{Script=Zzzz}", r"\p{scx=Zzzz}"] {
|
||
let source = format!("/{pattern}/u.test(String.fromCodePoint(0x038B))");
|
||
let result = eval(&mut vm, &source).unwrap();
|
||
assert!(result.as_bool(), "别名 {pattern} 应匹配未分配码点");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_unicode_property_script_existing_values_unchanged() {
|
||
// 补齐 Unknown 不得扰动既有取值:Adlam 等已分配 Script 行为保持
|
||
let mut vm = Vm::new();
|
||
let result = eval(&mut vm, "/\\p{Script=Adlam}/u.test(String.fromCodePoint(0x1E900))").unwrap();
|
||
assert!(result.as_bool(), "既有 Script 取值行为应保持不变");
|
||
let result = eval(&mut vm, "/\\p{Script=Adlam}/u.test('d')").unwrap();
|
||
assert!(!result.as_bool(), "既有 Script 取值行为应保持不变");
|
||
}
|
||
|
||
// --- UTF-16 单元向匹配入口:gate 运行时化 + 惰性 units IR 回归钉 ---
|
||
// 引擎构建已启用 regress 的 utf16 feature(纯加性 API)。字节字面量 pass 改为
|
||
// 逐编译运行时 gate 后,str 路径编译与执行不变;单元向 IR 在首次
|
||
// find_from_utf16 时惰性编译(关字节 pass)并缓存。以下钉直接走 regress API,
|
||
// 不依赖 JS 侧单元视图接线(后续批次才落地)。
|
||
|
||
fn units_of(text: &str) -> Vec<u16> {
|
||
text.encode_utf16().collect()
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_utf16_entry_surrogate_property() {
|
||
// 孤立 surrogate 单元命中 \p{General_Category=Surrogate};普通字符不命中;
|
||
// 代理对(单码点)不命中。
|
||
let re = regress::Regex::with_flags(r"\p{General_Category=Surrogate}", "u").unwrap();
|
||
let m = re.find_from_utf16(&[0xD800], 0).next();
|
||
assert!(m.is_some(), "孤立 surrogate 单元应命中 GC=Surrogate");
|
||
assert_eq!(m.unwrap().range(), 0..1);
|
||
assert!(re.find_from_utf16(&[0x41], 0).next().is_none(), "A 不应命中");
|
||
assert!(
|
||
re.find_from_utf16(&[0xD834, 0xDE00], 0).next().is_none(),
|
||
"代理对是单码点,不应按两个孤立 surrogate 命中"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_utf16_entry_surrogate_pair_equals_str_path() {
|
||
// U+1D11E = 代理对 [0xD834, 0xDE00]:单元路径按单码点匹配,与 str 路径等价。
|
||
let ch = char::from_u32(0x1D11E).unwrap();
|
||
let re = regress::Regex::with_flags(&ch.to_string(), "").unwrap();
|
||
let text: String = [ch, 'a', 'b', 'c'].iter().copied().collect();
|
||
let m_str = re.find(&text).expect("str 路径应命中");
|
||
assert_eq!(m_str.range(), 0..4, "str 路径命中 1 个码点(4 字节)");
|
||
|
||
let units = units_of(&text);
|
||
let m_u16 = re.find_from_utf16(&units, 0).next().expect("单元路径应命中");
|
||
assert_eq!(m_u16.range(), 0..2, "单元路径命中 1 个码点(2 个单元)");
|
||
}
|
||
|
||
#[test]
|
||
fn regexp_utf16_entry_well_formed_equals_str_path() {
|
||
// well-formed 语料:单元路径与 str 路径匹配结果逐一相等(含非 ASCII)。
|
||
for (pattern, text) in [(r"\d+", "abc123x456"), (r"a[bc]d", "xabcdyabd"), (r"\w+\s+\w+", "héllo wörld")] {
|
||
let re = regress::Regex::new(pattern).unwrap();
|
||
let str_m = re.find(text).map(|m| m.as_str(text).to_string());
|
||
let units = units_of(text);
|
||
let u16_m = re.find_from_utf16(&units, 0).next().map(|m| {
|
||
units[m.range()]
|
||
.iter()
|
||
.flat_map(|&u| char::from_u32(u as u32).map(|c| c.to_string()))
|
||
.collect::<String>()
|
||
});
|
||
assert_eq!(str_m, u16_m, "pattern {pattern}");
|
||
}
|
||
}
|