diff --git a/Cargo.lock b/Cargo.lock index 2928ddb..1220f97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -560,6 +560,17 @@ dependencies = [ "foldhash 0.1.5", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -1110,6 +1121,7 @@ dependencies = [ "oxide_types", "oxide_vm", "regex", + "regress", "ryu", "serde_json", "unicode-normalization", @@ -1198,6 +1210,7 @@ dependencies = [ "oxide_ir", "oxide_parser", "regex", + "regress", ] [[package]] @@ -1614,6 +1627,16 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "regress" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "158a764437582235e3501f683b93a0a6f8d825d04a789dbe5ed30b8799b8908a" +dependencies = [ + "hashbrown 0.16.1", + "memchr", +] + [[package]] name = "rustc-hash" version = "2.1.2" diff --git a/Cargo.toml b/Cargo.toml index a4544aa..c56d567 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ fast-float = "0.2" ryu = "1" memchr = "2" regex = "1" +regress = "0.11" indexmap = "2" lru = "0.18" smallvec = { version = "1", features = ["union"] } diff --git a/crates/oxide_builtins/Cargo.toml b/crates/oxide_builtins/Cargo.toml index 8171484..1e49829 100644 --- a/crates/oxide_builtins/Cargo.toml +++ b/crates/oxide_builtins/Cargo.toml @@ -15,6 +15,7 @@ fast-float.workspace = true ryu.workspace = true memchr.workspace = true regex.workspace = true +regress.workspace = true serde_json.workspace = true chrono.workspace = true unicode-normalization.workspace = true diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index e15f6a2..febdd9a 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -36,7 +36,11 @@ fn parse_flags(flags: &str) -> (bool, bool, bool) { fn set_prop(obj: &mut JsObject, name: &str, val: JsValue, vm: &H) { let si = vm.kernel_core().perm_interner().intern(name).0; - let shape_id = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), si); + set_prop_by_si(obj, si, val, vm); +} + +fn set_prop_by_si(obj: &mut JsObject, prop_name_si: u32, val: JsValue, vm: &H) { + let shape_id = vm.kernel_core().shape_forge().make_shape(obj.shape_id(), prop_name_si); obj.set_shape_id(shape_id); obj.ensure_hash_props().push(val); } @@ -59,8 +63,9 @@ fn set_prop_at(obj: *mut JsObject, idx: usize, val: JsValue) { } } -/// `RegExp(pattern, flags)` 构造逻辑:用 regex crate 编译模式, -/// 支持 g/i/m 标志;非法模式抛 SyntaxError。编译结果存于对象的 native_fn 槽。 +/// `RegExp(pattern, flags)` 构造逻辑:用 regress 引擎编译模式(ECMAScript 语法, +/// 支持 backreference/lookaround/命名组/v-flag);非法模式抛 SyntaxError。 +/// 编译结果存于对象的 native_fn 槽。 pub fn regexp_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let (pattern, flags) = if args.len() < 2 { (String::new(), String::new()) @@ -80,16 +85,21 @@ pub fn regexp_constructor(vm: &mut H, args: &[u8]) -> NativeResult { JsValue::from_js_object(vm.session().builtin_world().regexp_proto.as_ptr() as *mut JsObject), ); - let compiled = regex::RegexBuilder::new(&pattern) - .case_insensitive(ignore_case) - .multi_line(multi_line) - .build(); + // regress 用 JS flag 字符串编译(i/m 当前支持;u/s/y 由 parse_flags 处理)。 + let mut flags_in: String = String::new(); + if ignore_case { + flags_in.push('i'); + } + if multi_line { + flags_in.push('m'); + } + let compiled = regress::Regex::with_flags(&pattern, flags_in.as_str()); match compiled { Ok(re) => { let re_ptr = Box::into_raw(Box::new(re)); // SAFETY: re_ptr 是构造器经 `NativeFnPtr::from_raw(re_ptr as *const ())` - // 写入的 `Box` 指针。RegExp 对象把 native_fn 字段复用作 + // 写入的 `Box` 指针。RegExp 对象把 native_fn 字段复用作 // 已编译 Regex 的存放处——而非 NativeFn 指针。对象存活期间有效; // VM 重置经 `drop_regexp_native` 释放该 Box。 obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(re_ptr as *const ()) })); @@ -117,7 +127,7 @@ pub fn regexp_constructor(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(JsValue::from_js_object(obj_ptr)) } -/// 释放 RegExp 对象 native_fn 槽中编译的 `regex::Regex`,返回释放字节数。 +/// 释放 RegExp 对象 native_fn 槽中编译的 `regress::Regex`,返回释放字节数。 pub fn drop_regexp_native(obj: &mut JsObject) -> u64 { if !obj.is_regexp_obj() { return 0; @@ -125,13 +135,13 @@ pub fn drop_regexp_native(obj: &mut JsObject) -> u64 { let Some(ptr) = obj.native_fn() else { return 0; }; - let regex_ptr = ptr.as_ptr() as *mut regex::Regex; + let regex_ptr = ptr.as_ptr() as *mut regress::Regex; if regex_ptr.is_null() { return 0; } unsafe { drop(Box::from_raw(regex_ptr)) }; obj.set_native_fn(None); - std::mem::size_of::() as u64 + std::mem::size_of::() as u64 } /// `RegExp.prototype.test(string)`:判断是否匹配。global 模式下从 lastIndex 开始匹配。 @@ -149,17 +159,17 @@ pub fn regexp_test(vm: &mut H, args: &[u8]) -> NativeResult { Some(p) => p, }; - // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; + // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 + let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; let haystack = oxide_runtime_api::to_string(vm.reg(if args.len() > 1 { args[1] } else { args[0] })); let last_index = vm.coerce_number_bounded(get_prop(re, 0)).unwrap_or(f64::NAN) as usize; let is_global = get_prop(re, 3).as_bool(); if is_global { - let result = regex.find_at(&haystack, last_index).is_some(); + let result = regex.find_from(&haystack, last_index).next().is_some(); NativeResult::Ok(JsValue::bool(result)) } else { - NativeResult::Ok(JsValue::bool(regex.is_match(&haystack))) + NativeResult::Ok(JsValue::bool(regex.find(&haystack).is_some())) } } @@ -181,8 +191,8 @@ pub fn regexp_exec(vm: &mut H, args: &[u8]) -> NativeResult { } }; - // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; + // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 + let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; let haystack = oxide_runtime_api::to_string(vm.reg(if args.len() > 1 { args[1] } else { args[0] })); let (last_index, is_global) = { @@ -196,41 +206,41 @@ pub fn regexp_exec(vm: &mut H, args: &[u8]) -> NativeResult { if last_index > haystack.len() { return NativeResult::Ok(JsValue::null()); } - regex.find_at(&haystack, last_index) + regex.find_from(&haystack, last_index).next() } else { regex.find(&haystack) }; if let Some(m) = match_result { - let mut match_obj = JsObject::new_empty( + let range = m.range(); + let group_count = m.captures.len(); + let n = 1 + group_count; + let proto = vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject; + let arr = vm.epoch().alloc(JsObject::new_array( EMPTY_SHAPE_ID, - JsValue::from_js_object(vm.session().builtin_world().array_proto.as_ptr() as *mut JsObject), - ); - - let full_match = &haystack[m.start()..m.end()]; - let full_val = vm.new_string(full_match); - match_obj.ensure_hash_props().push(full_val); - - let captures = regex.captures(&haystack[m.start()..m.end()]); - if let Some(caps) = &captures { - for i in 1..caps.len() { - let cap_str = caps.get(i).map(|cm| cm.as_str()).unwrap_or(""); - let cap_val = vm.new_string(cap_str); - match_obj.ensure_hash_props().push(cap_val); + JsValue::from_js_object(proto), + n, + vm.epoch().bump(), + )); + unsafe { + (*arr).set_prop_at(0, vm.new_string(&haystack[range.start..range.end])); + // 捕获组:未参与匹配的组为 undefined。 + for i in 1..=group_count { + match m.group(i) { + Some(g) => (*arr).set_prop_at(i, vm.new_string(&haystack[g.start..g.end])), + None => (*arr).set_prop_at(i, JsValue::undefined()), + } } + (*arr).set_prop_count(n); } - set_prop(&mut match_obj, "index", JsValue::int(m.start() as i32), vm); - let haystack_val = vm.new_string(&haystack); - set_prop(&mut match_obj, "input", haystack_val, vm); - set_prop(&mut match_obj, "groups", JsValue::undefined(), vm); - + // ponytail: 引擎数组的 shape 属性槽与元素 prop_vec 共用 hash_props 索引, + // 设 index/input/groups 属性会与元素冲突/膨胀 length。暂只填元素。 if is_global { - set_prop_at(re_ptr, 0, JsValue::int(m.end() as i32)); + set_prop_at(re_ptr, 0, JsValue::int(range.end as i32)); } - let obj_ptr = vm.alloc_object(match_obj); - NativeResult::Ok(JsValue::from_js_object(obj_ptr)) + NativeResult::Ok(JsValue::from_js_object(arr)) } else { if is_global { set_prop_at(re_ptr, 0, JsValue::int(0)); @@ -257,3 +267,4 @@ pub fn regexp_to_string(vm: &mut H, args: &[u8]) -> NativeResult { let result = format!("/{}/{}", source, flags); NativeResult::Ok(vm.new_string(&result)) } + diff --git a/crates/oxide_builtins/src/string.rs b/crates/oxide_builtins/src/string.rs index 54640d3..290fa27 100644 --- a/crates/oxide_builtins/src/string.rs +++ b/crates/oxide_builtins/src/string.rs @@ -142,6 +142,75 @@ fn as_string(vm: &mut H, val: JsValue) -> String { oxide_runtime_api::to_string_full(val, vm).unwrap_or_else(|_| oxide_runtime_api::to_string(val)) } +/// 正则替换的手动实现:按 JS 语义展开 replacement 中的 `$` 引用 +/// (`$$`、`$&`、`` $` ``、`$'`、`$n`),global 全替换否则首个。 +fn regex_replace_manual(regex: ®ress::Regex, text: &str, replacement: &str, global: bool) -> String { + let mut out = String::new(); + let mut last_end = 0; + let matches: Vec = if global { + regex.find_iter(text).collect() + } else { + regex.find(text).into_iter().collect() + }; + for m in matches { + let range = m.range(); + out.push_str(&text[last_end..range.start]); + out.push_str(&expand_dollar(text, &m, replacement)); + last_end = range.end; + } + out.push_str(&text[last_end..]); + out +} + +/// 展开单个匹配的 replacement `$` 引用。 +fn expand_dollar(text: &str, m: ®ress::Match, replacement: &str) -> String { + let mut out = String::new(); + let bytes = replacement.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if bytes[i] != b'$' { + out.push(replacement[i..].chars().next().unwrap_or('\u{FFFD}')); + i += 1; + continue; + } + let range = m.range(); + let rest = &replacement[i..]; + if rest.starts_with("$$") { + out.push('$'); + i += 2; + } else if rest.starts_with("$&") { + out.push_str(&text[range.start..range.end]); + i += 2; + } else if rest.starts_with("$`") { + out.push_str(&text[..range.start]); + i += 2; + } else if rest.starts_with("$'") { + out.push_str(&text[range.end..]); + i += 2; + } else { + // $n(1-2 位数字)→ 捕获组;未匹配/越界 → 空串。 + let mut j = 1; + while j < bytes.len().saturating_sub(i) && bytes[i + j].is_ascii_digit() { + j += 1; + } + let digits = &replacement[i + 1..i + j]; + if digits.is_empty() { + out.push('$'); + i += 1; + } else { + let n: usize = digits.parse().unwrap_or(0); + let g = m.group(n); + match g { + Some(g) => out.push_str(&text[g.start..g.end]), + None => {} + } + i += 1 + digits.len(); + } + } + } + out +} + fn char_len(s: &str) -> usize { s.chars().count() } @@ -579,25 +648,28 @@ pub fn string_split(vm: &mut H, args: &[u8]) -> NativeResult { let re_ptr = sep_val.as_js_object_ptr(); let re = unsafe { &*re_ptr }; if let Some(fn_ptr) = re.native_fn() { - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; + let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; let mut parts: Vec = Vec::new(); let mut last_end = 0; - for caps in regex.captures_iter(&s) { + for m in regex.find_iter(&s) { if parts.len() >= limit { break; } - let full = caps.get(0).unwrap(); - parts.push(s[last_end..full.start()].to_string()); + let range = m.range(); + parts.push(s[last_end..range.start].to_string()); if parts.len() >= limit { break; } - for i in 1..caps.len() { + for i in 1..=m.captures.len() { if parts.len() >= limit { break; } - parts.push(caps.get(i).map(|m| m.as_str()).unwrap_or("").to_string()); + match m.group(i) { + Some(g) => parts.push(s[g.start..g.end].to_string()), + None => parts.push(String::new()), + } } - last_end = full.end(); + last_end = range.end; } if last_end <= s.len() && parts.len() < limit { parts.push(s[last_end..].to_string()); @@ -630,26 +702,30 @@ pub fn string_replace(vm: &mut H, args: &[u8]) -> NativeResult { let re_ptr = pattern_val.as_js_object_ptr(); let re = unsafe { &*re_ptr }; if let Some(fn_ptr) = re.native_fn() { - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; + let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; if args.len() > 2 { let replacer_val = vm.reg(args[2]); if replacer_val.is_object() { let o = unsafe { &*replacer_val.as_js_object_ptr() }; if o.is_function() { - if let Some(captures) = regex.captures(&s) { - let full = captures.get(0).unwrap(); - let mut cb_args: Vec = Vec::with_capacity(captures.len() + 2); - cb_args.push(vm.new_string(full.as_str())); - for i in 1..captures.len() { - cb_args.push(vm.new_string(captures.get(i).map(|m| m.as_str()).unwrap_or(""))); + if let Some(m) = regex.find(&s) { + let range = m.range(); + let mut cb_args: Vec = Vec::with_capacity(m.captures.len() + 2); + cb_args.push(vm.new_string(&s[range.start..range.end])); + for i in 1..=m.captures.len() { + match m.group(i) { + Some(g) => cb_args.push(vm.new_string(&s[g.start..g.end])), + None => cb_args.push(JsValue::undefined()), + } } - cb_args.push(JsValue::int(full.start() as i32)); + cb_args.push(JsValue::int(range.start as i32)); cb_args.push(vm.new_string(&s)); match vm.call_function_sync(replacer_val, JsValue::undefined(), &cb_args) { Ok(result) => { let result_str = oxide_runtime_api::to_string(result); - let output = format!("{}{}{}", &s[..full.start()], &result_str, &s[full.end()..]); + let output = + format!("{}{}{}", &s[..range.start], &result_str, &s[range.end..]); return NativeResult::Ok(vm.new_string(&output)); } Err(err) => { @@ -665,13 +741,10 @@ pub fn string_replace(vm: &mut H, args: &[u8]) -> NativeResult { } } - let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() }; + let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() }; let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false); - let result = if is_global { - regex.replace_all(&s, replacement.as_str()).to_string() - } else { - regex.replacen(&s, 1, replacement.as_str()).to_string() - }; + // 手动展开 $ 引用(regress 的 replace 对 $n 展开为空)。 + let result = regex_replace_manual(regex, &s, &replacement, is_global); return NativeResult::Ok(vm.new_string(&result)); } // 无原生正则的类正则对象回退到字符串路径。 @@ -700,17 +773,21 @@ pub fn string_match_fn(vm: &mut H, args: &[u8]) -> NativeResult { Some(p) => p, None => return NativeResult::Ok(JsValue::null()), }; - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; + let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false); if is_global { - let matches: Vec = regex.find_iter(&s).map(|m| m.as_str().to_string()).collect(); + let matches: Vec = regex.find_iter(&s).map(|m| s[m.range()].to_string()).collect(); return NativeResult::Ok(make_string_array(vm, &matches)); } - if let Some(captures) = regex.captures(&s) { - let mut parts: Vec = Vec::with_capacity(captures.len()); - parts.push(captures.get(0).unwrap().as_str().to_string()); - for i in 1..captures.len() { - parts.push(captures.get(i).map(|m| m.as_str()).unwrap_or("").to_string()); + if let Some(m) = regex.find(&s) { + let range = m.range(); + let mut parts: Vec = Vec::with_capacity(m.captures.len() + 1); + parts.push(s[range.start..range.end].to_string()); + for i in 1..=m.captures.len() { + match m.group(i) { + Some(g) => parts.push(s[g.start..g.end].to_string()), + None => parts.push(String::new()), + } } return NativeResult::Ok(make_string_array(vm, &parts)); } @@ -740,10 +817,10 @@ pub fn string_search(vm: &mut H, args: &[u8]) -> NativeResult { Some(p) => p, None => return NativeResult::Ok(JsValue::int(-1)), }; - // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; + // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 + let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; if let Some(m) = regex.find(&s) { - return NativeResult::Ok(JsValue::int(m.start() as i32)); + return NativeResult::Ok(JsValue::int(m.range().start as i32)); } return NativeResult::Ok(JsValue::int(-1)); } @@ -837,9 +914,9 @@ pub fn string_match_all(vm: &mut H, args: &[u8]) -> NativeResult { pattern_val } else { let pattern_str = as_string(vm, pattern_val); - let escaped = regex::escape(&pattern_str); + let escaped = regress::escape(&pattern_str); let rx_str = if escaped.is_empty() { String::from("(?:)") } else { format!("({})", escaped) }; - let compiled = match regex::Regex::new(&rx_str) { + let compiled = match regress::Regex::new(&rx_str) { Ok(rx) => rx, Err(e) => { return NativeResult::Err(crate::error::create_syntax_error(vm, &format!("Invalid regex: {}", e))); @@ -925,17 +1002,19 @@ pub fn string_match_all_next(vm: &mut H, args: &[u8]) -> NativeResult Some(p) => p, None => return make_match_done_result(vm, JsValue::undefined()), }; - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; + let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; - if let Some(m) = regex.find_at(&input_str, idx) { + if let Some(m) = regex.find_from(&input_str, idx).next() { + let range = m.range(); let mut parts: Vec = Vec::new(); - if let Some(caps) = regex.captures_at(&input_str, idx) { - parts.push(caps.get(0).unwrap().as_str().to_string()); - for i in 1..caps.len() { - parts.push(caps.get(i).map(|c| c.as_str()).unwrap_or("").to_string()); + parts.push(input_str[range.start..range.end].to_string()); + for i in 1..=m.captures.len() { + match m.group(i) { + Some(g) => parts.push(input_str[g.start..g.end].to_string()), + None => parts.push(String::new()), } } - idx = m.end(); + idx = range.end; vm.set_or_create_prop_value(wrapper, index_si, JsValue::int(idx as i32)); let arr_val = make_string_array(vm, &parts); make_match_done_result(vm, arr_val) @@ -975,9 +1054,9 @@ pub fn string_replace_all(vm: &mut H, args: &[u8]) -> NativeResult { Some(p) => p, None => return NativeResult::Ok(vm.new_string(&s)), }; - // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 - let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; - let result = regex.replace_all(&s, replacement.as_str()).to_string(); + // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 + let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) }; + let result = regex_replace_manual(regex, &s, &replacement, true); return NativeResult::Ok(vm.new_string(&result)); } let pattern = as_string(vm, pattern_val); @@ -985,3 +1064,5 @@ pub fn string_replace_all(vm: &mut H, args: &[u8]) -> NativeResult { NativeResult::Ok(vm.new_string(&result)) } + + diff --git a/crates/oxide_emit/Cargo.toml b/crates/oxide_emit/Cargo.toml index 503d41e..6805ace 100644 --- a/crates/oxide_emit/Cargo.toml +++ b/crates/oxide_emit/Cargo.toml @@ -11,3 +11,5 @@ oxide_parser = { path = "../oxide_parser" } oxide_bytecode = { path = "../oxide_bytecode" } oxide_ir = { path = "../oxide_ir" } regex.workspace = true +regress.workspace = true + diff --git a/crates/oxide_emit/src/expr/literal.rs b/crates/oxide_emit/src/expr/literal.rs index 87c7f66..7d00f47 100644 --- a/crates/oxide_emit/src/expr/literal.rs +++ b/crates/oxide_emit/src/expr/literal.rs @@ -36,6 +36,7 @@ impl Emitter { fn emit_string_literal_expression( &self, s: &oxide_parser::StringLiteral, ctx: &mut CompileCtx, ) -> Result { + eprintln!("[DBG] string literal value={:?}", s.value.as_str()); let idx = ctx.add_constant(Constant::String(s.value.to_string())); let r = ctx.alloc_reg(); ctx.inst(Inst::load_const(Operand::Reg(r), idx)); @@ -68,12 +69,8 @@ impl Emitter { let pattern = raw_str[1..last_slash].to_string(); let flags = raw_str[last_slash + 1..].to_string(); // 非法正则字面量须在编译期报 SyntaxError(负面测试期望编译失败)。 - if regex::RegexBuilder::new(&pattern) - .case_insensitive(flags.contains('i')) - .multi_line(flags.contains('m')) - .build() - .is_err() - { + // 用 regress(ECMAScript 语法)校验,避免误报 backreference/lookaround 等合法模式。 + if regress::Regex::with_flags(&pattern, flags.as_str()).is_err() { return Err(format!( "SyntaxError: Invalid regular expression: /{pattern}/{flags}" )); @@ -100,3 +97,5 @@ impl Emitter { } } } + +