diff --git a/crates/oxide_builtins/src/regexp.rs b/crates/oxide_builtins/src/regexp.rs index d9aad29c..75db4794 100644 --- a/crates/oxide_builtins/src/regexp.rs +++ b/crates/oxide_builtins/src/regexp.rs @@ -21,29 +21,6 @@ fn get_regexp_ptr(vm: &mut H, args: &[u8]) -> Result<*mut JsObject, J 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(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); @@ -100,14 +77,21 @@ pub fn regexp_constructor(vm: &mut H, args: &[u8]) -> NativeResult { let pattern = String::from_utf16_lossy(&pattern_units); let flags = String::from_utf16_lossy(&flags_units); - let (global, ignore_case, multi_line, dot_all, sticky, unicode, has_indices) = parse_flags(&flags); + // u 与 v 互斥(regress 不校验此约束):编译前置抛 SyntaxError。 + if flags.contains('u') && flags.contains('v') { + return NativeResult::Err(crate::error::create_syntax_error( + vm, + "Invalid regular expression flags: 'u' and 'v' are mutually exclusive", + )); + } 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 语义)。 + // regress 用 JS flag 字符串编译:g/i/m/s/y/u/v 原样透传(vendored fork + // 原生识别 v 标志,u/v 互斥已在上方前置校验)。 let compiled = regress::Regex::with_flags(&pattern, flags.as_str()); match compiled { @@ -128,16 +112,10 @@ pub fn regexp_constructor(vm: &mut H, args: &[u8]) -> NativeResult { } set_prop(&mut obj, "lastIndex", JsValue::int(0), vm); - // 单元口径物化 source/flags(孤立 surrogate 单元保持往返)。 + // 单元口径物化 source/flags(孤立 surrogate 单元保持往返)。7 个 flag 布尔 + // 不再作实例数据属性:读侧走原型只读访问器,以 flags 串为唯一数据源。 set_prop(&mut obj, "source", vm.new_string_units_owned(pattern_units), vm); set_prop(&mut obj, "flags", vm.new_string_units_owned(flags_units), 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); @@ -219,7 +197,7 @@ pub fn regexp_test(vm: &mut H, args: &[u8]) -> NativeResult { 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); + let is_global = regexp_has_flag(vm, re, 'g'); // lastIndex 为码元口径;Str 臂的内部字节换算在 find_from_units 内完成。 let text = haystack.as_match_text(); @@ -259,7 +237,7 @@ pub fn regexp_exec(vm: &mut H, args: &[u8]) -> NativeResult { 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); + let g = regexp_has_flag(vm, re, 'g'); (li, g) }; @@ -445,20 +423,87 @@ const HEX_DIGITS: [u16; 16] = [ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, ]; -// RegExp 对象 hash 槽中属性的固定下标(构造顺序:lastIndex/source/flags/global/...)。 +// RegExp 对象 hash 槽中属性的固定下标(构造顺序:lastIndex/source/flags)。 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, +/// 读实例 flags 串([[OriginalFlags]] 的物化形态)判单个 flag 码元。 +/// +/// # 边界与前提 +/// - `re` 须为持有编译正则的对象(RegExp 实例或 matchAll 载体);flags 缺失 +/// 时各 flag 一律 false。 +/// - 经自身+原型链解析 flags 键:flags 是实例数据属性,原型链回退仅覆盖 +/// 用户删改自身属性后的读法。 +pub(crate) fn regexp_has_flag(vm: &H, re: &JsObject, unit: char) -> bool { + let flags_si = vm.kernel_core().perm_interner().intern("flags").0; + match vm.resolve_property(re, flags_si) { + Some(val) => vm.lookup_str(val).unwrap_or_default().contains(unit), + None => false, } } +/// 8 个单 flag 只读访问器的公共核:this 为 RegExp.prototype 本身返回 +/// undefined;this 非 RegExp 对象抛 TypeError;其余读实例 flags 串判码元。 +fn regexp_flag_of(vm: &mut H, args: &[u8], unit: char) -> NativeResult { + // proto 恒等判定先于 RegExp 校验:规范对 prototype 本身返回 undefined + // 而非抛错,顺序不可互换。 + let this_val = vm.reg(args[0]); + if this_val.is_object() { + let this_ptr = this_val.as_js_object_ptr(); + let proto_ptr = vm.session().builtin_world().regexp_proto.as_ptr() as *mut JsObject; + if !this_ptr.is_null() && this_ptr == proto_ptr { + return NativeResult::Ok(JsValue::undefined()); + } + } + let re_ptr = match get_regexp_ptr(vm, args) { + Ok(ptr) => ptr, + Err(err) => return NativeResult::Err(err), + }; + let re = unsafe { &*re_ptr }; + NativeResult::Ok(JsValue::bool(regexp_has_flag(vm, re, unit))) +} + +/// `RegExp.prototype.global` getter。 +pub fn regexp_get_global(vm: &mut H, args: &[u8]) -> NativeResult { + regexp_flag_of(vm, args, 'g') +} + +/// `RegExp.prototype.ignoreCase` getter。 +pub fn regexp_get_ignore_case(vm: &mut H, args: &[u8]) -> NativeResult { + regexp_flag_of(vm, args, 'i') +} + +/// `RegExp.prototype.multiline` getter。 +pub fn regexp_get_multiline(vm: &mut H, args: &[u8]) -> NativeResult { + regexp_flag_of(vm, args, 'm') +} + +/// `RegExp.prototype.dotAll` getter。 +pub fn regexp_get_dot_all(vm: &mut H, args: &[u8]) -> NativeResult { + regexp_flag_of(vm, args, 's') +} + +/// `RegExp.prototype.sticky` getter。 +pub fn regexp_get_sticky(vm: &mut H, args: &[u8]) -> NativeResult { + regexp_flag_of(vm, args, 'y') +} + +/// `RegExp.prototype.unicode` getter。 +pub fn regexp_get_unicode(vm: &mut H, args: &[u8]) -> NativeResult { + regexp_flag_of(vm, args, 'u') +} + +/// `RegExp.prototype.hasIndices` getter。 +pub fn regexp_get_has_indices(vm: &mut H, args: &[u8]) -> NativeResult { + regexp_flag_of(vm, args, 'd') +} + +/// `RegExp.prototype.unicodeSets` getter。 +pub fn regexp_get_unicode_sets(vm: &mut H, args: &[u8]) -> NativeResult { + regexp_flag_of(vm, args, 'v') +} + /// 取匹配文本参数,按 ToString 语义完整转换(对象经 toString/valueOf); /// 结果按载荷形态 owned:良形文本走 Str 臂,含孤立 surrogate 等走 Units 臂。 /// 对象 ToString 抛出的原生异常原样传播。 @@ -525,7 +570,7 @@ pub fn regexp_symbol_match(vm: &mut H, args: &[u8]) -> NativeResult { Ok(ptr) => ptr, Err(err) => return NativeResult::Err(err), }; - let is_global = is_global(unsafe { &*re_ptr }); + let is_global = regexp_has_flag(vm, unsafe { &*re_ptr }, 'g'); if !is_global { return regexp_exec(vm, args); } @@ -562,7 +607,7 @@ pub fn regexp_symbol_replace(vm: &mut H, args: &[u8]) -> NativeResult Ok(ptr) => ptr, Err(err) => return NativeResult::Err(err), }; - let is_global = is_global(unsafe { &*re_ptr }); + let is_global = regexp_has_flag(vm, unsafe { &*re_ptr }, 'g'); let (regex, haystack) = match regexp_regex_and_text(vm, args) { Ok(pair) => pair, Err(err) => return NativeResult::Err(err), @@ -695,7 +740,7 @@ pub fn regexp_symbol_match_all(vm: &mut H, args: &[u8]) -> NativeResu Err(err) => return NativeResult::Err(err), }; let re = unsafe { &*re_ptr }; - let is_global = is_global(re); + let is_global = regexp_has_flag(vm, re, 'g'); let last_index = vm.coerce_number_bounded(get_prop(re, PROP_LAST_INDEX)).unwrap_or(f64::NAN) as usize; let haystack = match regexp_text_arg(vm, args) { Ok(s) => s, diff --git a/crates/oxide_builtins/src/string/regex.rs b/crates/oxide_builtins/src/string/regex.rs index 72daa295..80b321bf 100644 --- a/crates/oxide_builtins/src/string/regex.rs +++ b/crates/oxide_builtins/src/string/regex.rs @@ -411,7 +411,7 @@ fn string_replace_impl(vm: &mut H, args: &[u8], all: bool) -> NativeR // SAFETY: is_re 已保证 pattern_val 为非空对象且 proto 恒等 RegExp.prototype。 let re = unsafe { &*re_ptr }; if re.native_fn().is_some() { - let g = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false); + let g = crate::regexp::regexp_has_flag(vm, re, 'g'); (true, g) } else { (false, false) @@ -562,6 +562,15 @@ pub fn string_match_fn(vm: &mut H, args: &[u8]) -> NativeResult { Some(v) if !is_re => try_string!(as_units(vm, v)).into_owned(), _ => Vec::new(), }; + // global 标志前置读:flags 串共享借用与下方 text 转换的 &mut 借用不可重叠。 + let is_global = if is_re { + let re_ptr = pattern_val.unwrap().as_js_object_ptr(); + // SAFETY: is_re 已保证 pattern_val 为非空对象且 proto 恒等 RegExp.prototype。 + let re = unsafe { &*re_ptr }; + re.native_fn().is_some() && crate::regexp::regexp_has_flag(vm, re, 'g') + } else { + false + }; let text = try_string!(this_text(vm, args)); if args.len() < 2 { return NativeResult::Ok(JsValue::null()); @@ -576,7 +585,6 @@ pub fn string_match_fn(vm: &mut H, args: &[u8]) -> NativeResult { }; // SAFETY: fn_ptr 持有 regexp_constructor 存放的 `Box` 指针。 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 mut matches: Vec> = Vec::new(); text.for_each_match(regex, |m| { @@ -764,7 +772,7 @@ pub fn string_match_all(vm: &mut H, args: &[u8]) -> NativeResult { let re_obj = if is_regexp_obj(pattern_val, vm) { let re_ptr = pattern_val.as_js_object_ptr(); let re = unsafe { &*re_ptr }; - let is_global = re.hash_props_vec().and_then(|v| v.get(3)).map(|v| v.as_bool()).unwrap_or(false); + let is_global = crate::regexp::regexp_has_flag(vm, re, 'g'); if !is_global { return NativeResult::Err(crate::error::create_type_error( vm, diff --git a/crates/oxide_vm/src/bindings/bind_regexp.rs b/crates/oxide_vm/src/bindings/bind_regexp.rs index fe531674..1af1ef85 100644 --- a/crates/oxide_vm/src/bindings/bind_regexp.rs +++ b/crates/oxide_vm/src/bindings/bind_regexp.rs @@ -1,6 +1,8 @@ use std::sync::Arc; -use crate::bindings::{apply_binding_table, bind_well_known_method, configure_native_constructor}; +use crate::bindings::{ + apply_binding_table, bind_accessor_getter, bind_well_known_method, configure_native_constructor, +}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::object::JsObject; @@ -34,6 +36,64 @@ pub fn bind_regexp(core: &Arc, session: &KernelSession, global: &mut ], ); + // 8 个单 flag 只读访问器(set 恒 undefined,getter 读实例 flags 串判码元)。 + bind_accessor_getter( + core, + session, + proto, + "global", + oxide_builtins::regexp::regexp_get_global:: as *const (), + ); + bind_accessor_getter( + core, + session, + proto, + "ignoreCase", + oxide_builtins::regexp::regexp_get_ignore_case:: as *const (), + ); + bind_accessor_getter( + core, + session, + proto, + "multiline", + oxide_builtins::regexp::regexp_get_multiline:: as *const (), + ); + bind_accessor_getter( + core, + session, + proto, + "dotAll", + oxide_builtins::regexp::regexp_get_dot_all:: as *const (), + ); + bind_accessor_getter( + core, + session, + proto, + "sticky", + oxide_builtins::regexp::regexp_get_sticky:: as *const (), + ); + bind_accessor_getter( + core, + session, + proto, + "unicode", + oxide_builtins::regexp::regexp_get_unicode:: as *const (), + ); + bind_accessor_getter( + core, + session, + proto, + "hasIndices", + oxide_builtins::regexp::regexp_get_has_indices:: as *const (), + ); + bind_accessor_getter( + core, + session, + proto, + "unicodeSets", + oxide_builtins::regexp::regexp_get_unicode_sets:: as *const (), + ); + // Symbol.match 等 well-known symbol 方法按 Symbol 键安装,供 `re[Symbol.match]` 等读取。 let world = session.builtin_world(); bind_well_known_method( diff --git a/crates/oxide_vm/tests/builtins_regexp_tests.rs b/crates/oxide_vm/tests/builtins_regexp_tests.rs index 03b44c77..5e9e03df 100644 --- a/crates/oxide_vm/tests/builtins_regexp_tests.rs +++ b/crates/oxide_vm/tests/builtins_regexp_tests.rs @@ -408,3 +408,86 @@ fn regexp_utf16_entry_well_formed_equals_str_path() { assert_eq!(str_m, u16_m, "pattern {pattern}"); } } + +// --- RegExp flag 只读访问器(M11)--- + +#[test] +fn regexp_flag_accessors_read_values() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var r = new RegExp('a', 'gimsyud'); [r.global, r.ignoreCase, r.multiline, r.dotAll, r.sticky, r.unicode, r.hasIndices, r.unicodeSets].join(',')", + ) + .unwrap(); + assert_eq!(to_str(&vm, result), "true,true,true,true,true,true,true,false"); + let result = eval(&mut vm, "new RegExp('a', 'v').unicodeSets").unwrap(); + assert!(result.as_bool()); + let result = + eval(&mut vm, "new RegExp('a', 'u').unicodeSets === false && new RegExp('a').unicode === false").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_flag_accessor_descriptor_and_identity() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var d = Object.getOwnPropertyDescriptor(RegExp.prototype, 'global'); d.set === undefined && typeof d.get === 'function' && d.enumerable === false && d.configurable === true && d.get.length === 0 && d.get.name === 'get global'", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_instance_has_no_own_flag_props() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Object.getOwnPropertyNames(new RegExp('a', 'g')).sort().join(',')").unwrap(); + assert_eq!(to_str(&vm, result), "flags,lastIndex,source"); +} + +#[test] +fn regexp_flag_write_is_noop_sloppy() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "var r = new RegExp('a'); r.global = true; r.ignoreCase = false; r.global === false", + ) + .unwrap(); + assert!(result.as_bool(), "无 setter 访问器在 sloppy 写应静默 no-op"); +} + +#[test] +fn regexp_uv_flags_mutual_exclusive() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "(() => { try { new RegExp('.', 'uv'); return 'no-throw'; } catch (e) { return e instanceof SyntaxError ? 'SyntaxError' : 'other'; } })()", + ) + .unwrap(); + assert_eq!(to_str(&vm, result), "SyntaxError"); + // u 与 v 单独使用不受影响。 + let result = eval(&mut vm, "new RegExp('.', 'u').unicode + ',' + new RegExp('.', 'v').unicodeSets").unwrap(); + assert_eq!(to_str(&vm, result), "true,true"); +} + +#[test] +fn regexp_flag_getter_proto_this_returns_undefined() { + let mut vm = Vm::new(); + let result = eval( + &mut vm, + "Object.getOwnPropertyDescriptor(RegExp.prototype, 'dotAll').get.call(RegExp.prototype) === undefined", + ) + .unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_flag_getter_non_regexp_this_throws_type_error() { + let mut vm = Vm::new(); + let source = "(() => { var get = Object.getOwnPropertyDescriptor(RegExp.prototype, 'global').get; try { get.call({}); return 'no-throw'; } catch (e) { return e instanceof TypeError ? 'TypeError' : 'other'; } })()"; + let result = eval(&mut vm, source).unwrap(); + assert_eq!(to_str(&vm, result), "TypeError"); + let source = "(() => { var get = Object.getOwnPropertyDescriptor(RegExp.prototype, 'global').get; try { get.call(null); return 'no-throw'; } catch (e) { return e instanceof TypeError ? 'TypeError' : 'other'; } })()"; + let result = eval(&mut vm, source).unwrap(); + assert_eq!(to_str(&vm, result), "TypeError"); +}