This repository has been archived on 2026-09-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
OxideJS/crates/oxide_builtins/src/regexp.rs
T

545 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_types::object::{JsObject, NativeFnPtr};
use oxide_types::value::JsValue;
use oxide_runtime_api::{NativeResult, VmHost};
fn get_regexp_ptr<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, JsValue> {
let this_val = vm.reg(args[0]);
if !this_val.is_object() {
return Err(crate::error::create_type_error(vm, "RegExp.prototype method called on non-object"));
}
let ptr = this_val.as_js_object_ptr();
if ptr.is_null() {
return Err(crate::error::create_type_error(vm, "null object"));
}
if !unsafe { &*ptr }.is_regexp_obj() {
return Err(crate::error::create_type_error(vm, "RegExp.prototype method called on non-RegExp object"));
}
Ok(ptr)
}
fn parse_flags(flags: &str) -> (bool, bool, bool, bool, bool, bool, bool) {
let mut global = false;
let mut ignore_case = false;
let mut multi_line = false;
let mut dot_all = false;
let mut sticky = false;
let mut unicode = false;
let mut has_indices = false;
for c in flags.chars() {
match c {
'g' => global = true,
'i' => ignore_case = true,
'm' => multi_line = true,
's' => dot_all = true,
'y' => sticky = true,
'u' => unicode = true,
'd' => has_indices = true,
_ => {}
}
}
(global, ignore_case, multi_line, dot_all, sticky, unicode, has_indices)
}
fn set_prop<H: VmHost>(obj: &mut JsObject, name: &str, val: JsValue, vm: &H) {
let si = vm.kernel_core().perm_interner().intern(name).0;
set_prop_by_si(obj, si, val, vm);
}
fn set_prop_by_si<H: VmHost>(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);
}
fn get_prop(obj: &JsObject, idx: usize) -> JsValue {
obj.hash_props_vec()
.and_then(|v| v.get(idx))
.copied()
.unwrap_or(JsValue::undefined())
}
fn set_prop_at(obj: *mut JsObject, idx: usize, val: JsValue) {
unsafe {
let re = &mut *obj;
if let Some(vec) = re.hash_props_vec() {
let ptr = vec.as_ptr() as *mut JsValue;
let v = &mut *ptr.add(idx);
*v = val;
}
}
}
/// `RegExp(pattern, flags)` 构造逻辑:用 regress 引擎编译模式(ECMAScript 语法,
/// 支持 backreference/lookaround/命名组/v-flag);非法模式抛 SyntaxError。
/// 编译结果存于对象的 native_fn 槽。
pub fn regexp_constructor<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (pattern, flags) = if args.len() < 2 {
(String::new(), String::new())
} else if args.len() < 3 {
let pat = oxide_runtime_api::to_string(vm.reg(args[1]));
(pat, String::new())
} else {
let pat = oxide_runtime_api::to_string(vm.reg(args[1]));
let fl = oxide_runtime_api::to_string(vm.reg(args[2]));
(pat, fl)
};
let (global, ignore_case, multi_line, dot_all, sticky, unicode, has_indices) = parse_flags(&flags);
let mut obj = JsObject::new_empty(
EMPTY_SHAPE_ID,
JsValue::from_js_object(vm.session().builtin_world().regexp_proto.as_ptr() as *mut JsObject),
);
// regress 用 JS flag 字符串编译:g/i/m/s/u/y 原样透传(v 由调用方映射为 u+set 语义)。
let compiled = regress::Regex::with_flags(&pattern, flags.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<regress::Regex>` 指针。RegExp 对象把 native_fn 字段复用作
// 已编译 Regex 的存放处——而非 NativeFn 指针。对象存活期间有效;
// VM 重置经 `drop_regexp_native` 释放该 Box。
obj.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(re_ptr as *const ()) }));
}
Err(e) => {
return NativeResult::Err(crate::error::create_syntax_error(
vm,
&format!("Invalid regular expression: {e}"),
));
}
}
set_prop(&mut obj, "lastIndex", JsValue::int(0), vm);
set_prop(&mut obj, "source", vm.new_string(&pattern), vm);
set_prop(&mut obj, "flags", vm.new_string(&flags), vm);
set_prop(&mut obj, "global", JsValue::bool(global), vm);
set_prop(&mut obj, "ignoreCase", JsValue::bool(ignore_case), vm);
set_prop(&mut obj, "multiline", JsValue::bool(multi_line), vm);
set_prop(&mut obj, "dotAll", JsValue::bool(dot_all), vm);
set_prop(&mut obj, "sticky", JsValue::bool(sticky), vm);
set_prop(&mut obj, "unicode", JsValue::bool(unicode), vm);
set_prop(&mut obj, "hasIndices", JsValue::bool(has_indices), vm);
obj.type_tag = JsObject::OBJ_TYPE_REGEXP;
let obj_ptr = vm.alloc_object(obj);
NativeResult::Ok(JsValue::from_js_object(obj_ptr))
}
/// 释放 RegExp 对象 native_fn 槽中编译的 `regress::Regex`,返回释放字节数。
pub fn drop_regexp_native(obj: &mut JsObject) -> u64 {
if !obj.is_regexp_obj() {
return 0;
}
let Some(ptr) = obj.native_fn() else {
return 0;
};
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::<regress::Regex>() as u64
}
/// `RegExp.prototype.test(string)`:判断是否匹配。global 模式下从 lastIndex 开始匹配。
pub fn regexp_test<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let re_ptr = match get_regexp_ptr(vm, args) {
Ok(ptr) => ptr,
Err(err) => return NativeResult::Err(err),
};
let re = unsafe { &*re_ptr };
let fn_ptr = match re.native_fn() {
None => {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid RegExp"));
}
Some(p) => p,
};
// SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box<regress::Regex>` 指针。
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) };
let haystack = match regexp_string_arg(vm, args) {
Ok(s) => s,
Err(err) => return NativeResult::Err(err),
};
let last_index = vm.coerce_number_bounded(get_prop(re, 0)).unwrap_or(f64::NAN) as usize;
let is_global = is_global(re);
if is_global {
let result = regex.find_from(&haystack, last_index).next().is_some();
NativeResult::Ok(JsValue::bool(result))
} else {
NativeResult::Ok(JsValue::bool(regex.find(&haystack).is_some()))
}
}
/// `RegExp.prototype.exec(string)`:执行匹配并返回数组(含捕获组、index、input)。
/// 无匹配返回 null;global 模式推进/重置 lastIndex。
pub fn regexp_exec<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let re_ptr = match get_regexp_ptr(vm, args) {
Ok(ptr) => ptr,
Err(err) => return NativeResult::Err(err),
};
let fn_ptr = {
let re = unsafe { &*re_ptr };
match re.native_fn() {
None => {
return NativeResult::Err(crate::error::create_type_error(vm, "invalid RegExp"));
}
Some(p) => p,
}
};
// SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box<regress::Regex>` 指针。
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) };
let haystack = match regexp_string_arg(vm, args) {
Ok(s) => s,
Err(err) => return NativeResult::Err(err),
};
let (last_index, is_global) = {
let re = unsafe { &*re_ptr };
let li = vm.coerce_number_bounded(get_prop(re, 0)).unwrap_or(f64::NAN) as usize;
let g = is_global(re);
(li, g)
};
let match_result = if is_global {
if last_index > haystack.len() {
return NativeResult::Ok(JsValue::null());
}
regex.find_from(&haystack, last_index).next()
} else {
regex.find(&haystack)
};
if let Some(m) = match_result {
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(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);
}
let index_si = vm.kernel_core().perm_interner().intern("index").0;
vm.set_or_create_prop_value(unsafe { &mut *arr }, index_si, JsValue::int(range.start as i32));
let input_si = vm.kernel_core().perm_interner().intern("input").0;
let haystack_val = vm.new_string(&haystack);
vm.set_or_create_prop_value(unsafe { &mut *arr }, input_si, haystack_val);
let groups_si = vm.kernel_core().perm_interner().intern("groups").0;
vm.set_or_create_prop_value(unsafe { &mut *arr }, groups_si, JsValue::undefined());
if is_global {
set_prop_at(re_ptr, 0, JsValue::int(range.end as i32));
}
NativeResult::Ok(JsValue::from_js_object(arr))
} else {
if is_global {
set_prop_at(re_ptr, 0, JsValue::int(0));
}
NativeResult::Ok(JsValue::null())
}
}
/// `RegExp.prototype.toString`:按 `/source/flags` 形式返回。
pub fn regexp_to_string<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let re_ptr = match get_regexp_ptr(vm, args) {
Ok(ptr) => ptr,
Err(err) => return NativeResult::Err(err),
};
let re = unsafe { &*re_ptr };
let source = {
let val = get_prop(re, 1);
vm.lookup_str(val).unwrap_or_default()
};
let flags = {
let val = get_prop(re, 2);
vm.lookup_str(val).unwrap_or_default()
};
let result = format!("/{}/{}", source, flags);
NativeResult::Ok(vm.new_string(&result))
}
// RegExp 对象 hash 槽中属性的固定下标(构造顺序:lastIndex/source/flags/global/...)。
const PROP_LAST_INDEX: usize = 0;
const PROP_SOURCE: usize = 1;
const PROP_FLAGS: usize = 2;
const PROP_GLOBAL: usize = 3;
/// 读 global 标志;槽值被 accessor 重定义后不再保证是 bool,非 bool 一律视为 false。
fn is_global(re: &JsObject) -> bool {
match get_prop(re, PROP_GLOBAL) {
v if v.is_bool() => v.as_bool(),
_ => false,
}
}
/// 取 this 的已编译 regress 正则与匹配文本;this 非 RegExp 或缺失编译结果时返回 Err。
fn regexp_regex_and_text<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<(&'static regress::Regex, String), JsValue> {
let re_ptr = get_regexp_ptr(vm, args)?;
let fn_ptr = {
let re = unsafe { &*re_ptr };
match re.native_fn() {
None => return Err(crate::error::create_type_error(vm, "invalid RegExp")),
Some(p) => p,
}
};
// SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box<regress::Regex>` 指针。
let regex = unsafe { &*(fn_ptr.as_ptr() as *const regress::Regex) };
let haystack = regexp_string_arg(vm, args)?;
Ok((regex, haystack))
}
/// 取匹配文本参数,按 ToString 语义完整转换(对象经 toString/valueOf);异常原样传播。
fn regexp_string_arg<H: VmHost>(vm: &mut H, args: &[u8]) -> Result<String, JsValue> {
let val = vm.reg(if args.len() > 1 { args[1] } else { args[0] });
match oxide_runtime_api::to_string_full(val, vm) {
Ok(s) => Ok(s),
Err(_) => {
// ToString 触发对象 toString/valueOf 抛出的原生异常须原样传播。
if let Some(exc) = vm.take_uncaught_value() {
return Err(exc);
}
Err(crate::error::create_type_error(vm, "Cannot convert value to a string"))
}
}
}
/// `RegExp.prototype[Symbol.match](string)`:global 正则收集全部匹配串,
/// 非 global 返回单个 exec 匹配数组;无匹配返回 null。
///
/// # 步骤
/// 1. 非 global:直接复用 exec 结果(数组含 index/input/groups)。
/// 2. global:lastIndex 归零后逐匹配收集完整匹配串,末次匹配后推进 lastIndex。
pub fn regexp_symbol_match<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let re_ptr = match get_regexp_ptr(vm, args) {
Ok(ptr) => ptr,
Err(err) => return NativeResult::Err(err),
};
let is_global = is_global(unsafe { &*re_ptr });
if !is_global {
return regexp_exec(vm, args);
}
let (regex, haystack) = match regexp_regex_and_text(vm, args) {
Ok(pair) => pair,
Err(err) => return NativeResult::Err(err),
};
set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(0));
let mut matches: Vec<String> = Vec::new();
let mut last_end = 0;
for m in regex.find_iter(&haystack) {
let range = m.range();
matches.push(haystack[range.start..range.end].to_string());
last_end = range.end;
}
set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(last_end as i32));
if matches.is_empty() {
return NativeResult::Ok(JsValue::null());
}
NativeResult::Ok(crate::string::make_string_array(vm, &matches))
}
/// `RegExp.prototype[Symbol.replace](string, replacement)`:按匹配替换。
/// 字符串 replacement 展开 `$` 引用(`$$`/`$&`/``$` ``/`$'`/`$n`),函数 replacement
/// 逐匹配调用;global 全替换,否则替换首个。
///
/// # 步骤
/// 1. global 时 lastIndex 归零,复用 string 模块的替换逻辑。
/// 2. 结束后把 lastIndex 推进到末次匹配末尾(无匹配保持 0)。
pub fn regexp_symbol_replace<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let re_ptr = match get_regexp_ptr(vm, args) {
Ok(ptr) => ptr,
Err(err) => return NativeResult::Err(err),
};
let is_global = is_global(unsafe { &*re_ptr });
let (regex, haystack) = match regexp_regex_and_text(vm, args) {
Ok(pair) => pair,
Err(err) => return NativeResult::Err(err),
};
if is_global {
set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(0));
}
let result = 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() {
crate::string::regex_replace_fn(vm, regex, &haystack, replacer_val, is_global)
} else {
let replacement = oxide_runtime_api::to_string(replacer_val);
NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual(
regex,
&haystack,
&replacement,
is_global,
)))
}
} else {
let replacement = oxide_runtime_api::to_string(replacer_val);
NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual(
regex,
&haystack,
&replacement,
is_global,
)))
}
} else {
NativeResult::Ok(vm.new_string(&crate::string::regex_replace_manual(regex, &haystack, "", is_global)))
};
if is_global {
let mut last_end = 0;
for m in regex.find_iter(&haystack) {
last_end = m.range().end;
}
set_prop_at(re_ptr, PROP_LAST_INDEX, JsValue::int(last_end as i32));
}
result
}
/// `RegExp.prototype[Symbol.search](string)`:返回首个匹配位置(字符索引),
/// 无匹配返回 -1。global 标志不影响 search。
pub fn regexp_symbol_search<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (regex, haystack) = match regexp_regex_and_text(vm, args) {
Ok(pair) => pair,
Err(err) => return NativeResult::Err(err),
};
if let Some(m) = regex.find(&haystack) {
return NativeResult::Ok(JsValue::int(m.range().start as i32));
}
NativeResult::Ok(JsValue::int(-1))
}
/// `RegExp.prototype[Symbol.split](string, limit)`:按匹配切分,捕获组作为
/// 分隔元素插入结果;limit 限制结果长度,缺省切分全部。
pub fn regexp_symbol_split<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let (regex, haystack) = match regexp_regex_and_text(vm, args) {
Ok(pair) => pair,
Err(err) => return NativeResult::Err(err),
};
let limit = if args.len() > 2 {
let l = oxide_runtime_api::to_integer_or_infinity(vm.reg(args[2]));
if l.is_infinite() {
u32::MAX as usize
} else {
(l.max(0.0).trunc() as u64).min(u32::MAX as u64) as usize
}
} else {
u32::MAX as usize
};
let mut parts: Vec<String> = Vec::new();
let mut last_end = 0;
for m in regex.find_iter(&haystack) {
if parts.len() >= limit {
break;
}
let range = m.range();
parts.push(haystack[last_end..range.start].to_string());
if parts.len() >= limit {
break;
}
for i in 1..=m.captures.len() {
if parts.len() >= limit {
break;
}
match m.group(i) {
Some(g) => parts.push(haystack[g.start..g.end].to_string()),
None => parts.push(String::new()),
}
}
last_end = range.end;
}
if last_end <= haystack.len() && parts.len() < limit {
parts.push(haystack[last_end..].to_string());
}
NativeResult::Ok(crate::string::make_string_array(vm, &parts))
}
/// `RegExp.prototype[Symbol.matchAll](string)`:返回按 global 语义逐个产出
/// 匹配数组的迭代器。global 正则直接复用(初始下标取 lastIndex);
/// 非 global 正则复制一份加 `g` 标志后迭代。
pub fn regexp_symbol_match_all<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let re_ptr = match get_regexp_ptr(vm, args) {
Ok(ptr) => ptr,
Err(err) => return NativeResult::Err(err),
};
let re = unsafe { &*re_ptr };
let is_global = is_global(re);
let last_index = vm.coerce_number_bounded(get_prop(re, PROP_LAST_INDEX)).unwrap_or(f64::NAN) as usize;
let haystack = match regexp_string_arg(vm, args) {
Ok(s) => s,
Err(err) => return NativeResult::Err(err),
};
// 迭代目标:global 用原正则,非 global 复制并补 g 标志。
let iter_re = if is_global {
vm.reg(args[0])
} else {
let source = {
let val = get_prop(re, PROP_SOURCE);
vm.lookup_str(val).unwrap_or_default()
};
let mut flags = {
let val = get_prop(re, PROP_FLAGS);
vm.lookup_str(val).unwrap_or_default()
};
if !flags.contains('g') {
flags.push('g');
}
let compiled = match regress::Regex::with_flags(&source, flags.as_str()) {
Ok(rx) => rx,
Err(e) => {
return NativeResult::Err(crate::error::create_syntax_error(
vm,
&format!("Invalid regular expression: {e}"),
));
}
};
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
let mut stub = JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto));
let raw = Box::into_raw(Box::new(compiled)) as *const u8;
stub.set_native_fn(Some(unsafe { NativeFnPtr::from_raw(raw as *const ()) }));
JsValue::from_js_object(vm.alloc_object(stub))
};
// 复用 String.prototype.matchAll 的迭代器包装(next 经 string_match_all_next 推进)。
let object_proto = vm.session().builtin_world().object_proto.as_ptr() as *mut JsObject;
let wrapper = vm
.epoch()
.alloc(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::from_js_object(object_proto)));
let wrapper_obj = unsafe { &mut *wrapper };
let input_si = vm.kernel_core().perm_interner().intern(crate::string::MALL_INPUT).0;
let index_si = vm.kernel_core().perm_interner().intern(crate::string::MALL_INDEX).0;
let re_si = vm.kernel_core().perm_interner().intern(crate::string::MALL_RE).0;
let next_si = vm.kernel_core().perm_interner().intern("next").0;
let input_val = vm.new_string(&haystack);
vm.set_or_create_prop_value(wrapper_obj, input_si, input_val);
vm.set_or_create_prop_value(wrapper_obj, index_si, JsValue::int(last_index as i32));
vm.set_or_create_prop_value(wrapper_obj, re_si, iter_re);
let next_fn =
crate::string::make_public_native_fn(vm, "next", crate::string::string_match_all_next::<H> as *const (), 0);
vm.set_or_create_prop_value(wrapper_obj, next_si, next_fn);
NativeResult::Ok(JsValue::from_js_object(wrapper))
}