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(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) { let mut global = false; let mut ignore_case = false; let mut multi_line = false; for c in flags.chars() { match c { 'g' => global = true, 'i' => ignore_case = true, 'm' => multi_line = true, _ => {} } } (global, ignore_case, multi_line) } 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); 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; } } } pub fn regexp_constructor(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) = 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), ); let compiled = regex::RegexBuilder::new(&pattern) .case_insensitive(ignore_case) .multi_line(multi_line) .build(); match compiled { Ok(re) => { let re_ptr = Box::into_raw(Box::new(re)); // SAFETY: re_ptr is a Box pointer stored by the constructor via // NativeFnPtr::from_raw(re_ptr as *const ()). RegExp objects repurpose the native_fn // field to hold the compiled Regex — not a NativeFn pointer. Valid for the object's // lifetime; VM reset drops the Box through `drop_regexp_native`. 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(false), vm); set_prop(&mut obj, "sticky", JsValue::bool(false), vm); set_prop(&mut obj, "unicode", JsValue::bool(false), vm); obj.type_tag = JsObject::OBJ_TYPE_REGEXP; let obj_ptr = vm.alloc_object(obj); NativeResult::Ok(JsValue::from_js_object(obj_ptr)) } 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 regex::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 } pub fn regexp_test(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 holds a Box pointer stored by regexp_constructor. let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::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(); NativeResult::Ok(JsValue::bool(result)) } else { NativeResult::Ok(JsValue::bool(regex.is_match(&haystack))) } } pub fn regexp_exec(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 holds a Box pointer stored by regexp_constructor. let regex = unsafe { &*(fn_ptr.as_ptr() as *const regex::Regex) }; let haystack = oxide_runtime_api::to_string(vm.reg(if args.len() > 1 { args[1] } else { args[0] })); 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 = get_prop(re, 3).as_bool(); (li, g) }; let match_result = if is_global { if last_index > haystack.len() { return NativeResult::Ok(JsValue::null()); } regex.find_at(&haystack, last_index) } else { regex.find(&haystack) }; if let Some(m) = match_result { let mut match_obj = JsObject::new_empty( 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); } } 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); if is_global { set_prop_at(re_ptr, 0, JsValue::int(m.end() as i32)); } let obj_ptr = vm.alloc_object(match_obj); NativeResult::Ok(JsValue::from_js_object(obj_ptr)) } else { if is_global { set_prop_at(re_ptr, 0, JsValue::int(0)); } NativeResult::Ok(JsValue::null()) } } pub fn regexp_to_string(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)) }