use std::ops::ControlFlow;
use oxc_allocator::Allocator;
use oxc_ast::ast::RegExpFlags;
use oxc_regular_expression::{LiteralParser, Options};
use crate::{
ecmascript::{
Agent, ArgumentsList, Array, BUILTIN_STRING_MEMORY, ExceptionType, Function, JsResult,
Number, Object, PropertyKey, PropertyLookupCache, ProtoIntrinsics, RegExp, RegExpHeapData,
RegExpLastIndex, String, TryError, TryGetResult, Value, array_create, call_function,
handle_try_get_result, is_callable, ordinary_create_from_constructor,
ordinary_object_create_null, throw_set_error, to_length, to_string,
try_create_data_property_or_throw, try_get, try_result_into_js, try_to_length, unwrap_try,
unwrap_try_get_value,
},
engine::{Bindable, GcScope, NoGcScope, Scopable, Scoped, bindable_handle},
heap::{ArenaAccess, ArenaAccessMut, CreateHeapData, DirectArenaAccessMut},
};
pub(crate) fn reg_exp_create<'a>(
agent: &mut Agent,
p: Scoped<Value>,
f: Option<String>,
gc: GcScope<'a, '_>,
) -> JsResult<'a, RegExp<'a>> {
let f = f.map_or(Ok(RegExpFlags::empty()), |f| {
Err(Value::from(f).scope(agent, gc.nogc()))
});
let obj = agent.heap.create(RegExpHeapData::default()).bind(gc.nogc());
reg_exp_initialize(agent, obj.unbind(), p, f, gc)
}
pub(crate) fn reg_exp_create_literal<'a>(
agent: &mut Agent,
p: String,
f: Option<RegExpFlags>,
gc: NoGcScope<'a, '_>,
) -> RegExp<'a> {
let f = f.unwrap_or(RegExpFlags::empty());
agent.heap.create(RegExpHeapData::new(agent, p, f)).bind(gc)
}
pub(crate) fn reg_exp_alloc<'a>(
agent: &mut Agent,
new_target: Function,
gc: GcScope<'a, '_>,
) -> JsResult<'a, RegExp<'a>> {
let obj = RegExp::try_from(ordinary_create_from_constructor(
agent,
new_target,
ProtoIntrinsics::RegExp,
gc,
)?)
.unwrap();
Ok(obj)
}
pub(crate) fn reg_exp_initialize<'a>(
agent: &mut Agent,
obj: RegExp,
scoped_pattern: Scoped<Value>,
scoped_flags: Result<RegExpFlags, Scoped<Value>>,
mut gc: GcScope<'a, '_>,
) -> JsResult<'a, RegExp<'a>> {
let obj = obj.bind(gc.nogc());
let pattern = unsafe { scoped_pattern.take(agent).bind(gc.nogc()) };
let flags = scoped_flags
.as_ref()
.map(|f| *f)
.map_err(|v| v.get(agent).bind(gc.nogc()));
let quick_pattern = if pattern.is_undefined() {
Some(None)
} else if let Ok(pattern) = String::try_from(pattern) {
Some(Some(pattern))
} else {
None
};
let quick_flags = match flags {
Ok(f) => Some(Ok(f)),
Err(flags) => {
if flags.is_undefined() {
Some(Ok(RegExpFlags::empty()))
} else if let Ok(f) = String::try_from(flags) {
Some(Err(f))
} else {
None
}
}
};
let (obj, p, f) = if let (Some(p), Some(f)) = (quick_pattern, quick_flags) {
let p = p.unwrap_or(String::EMPTY_STRING);
(obj, p, f)
} else {
let obj = obj.scope(agent, gc.nogc());
let flags = scoped_flags;
let mut p = if pattern.is_undefined() {
String::EMPTY_STRING
} else {
to_string(agent, pattern.unbind(), gc.reborrow())
.unbind()?
.bind(gc.nogc())
};
let f = match flags {
Ok(f) => Ok(f),
Err(flags) => {
let flags = unsafe { flags.take(agent) }.bind(gc.nogc());
if flags.is_undefined() {
Ok(RegExpFlags::empty())
} else {
let scoped_p = p.scope(agent, gc.nogc());
let f = to_string(agent, flags.unbind(), gc.reborrow())
.unbind()?
.bind(gc.nogc());
p = unsafe { scoped_p.take(agent) }.bind(gc.nogc());
Err(f)
}
}
};
let obj = unsafe { obj.take(agent) }.bind(gc.nogc());
(obj, p, f)
};
let f_str = f.map(|f| f.to_inline_string());
let f_str = match &f_str {
Ok(f) => f.as_str().into(),
Err(f) => f.to_string_lossy_(agent),
};
let flags: Option<&str> = if f_str.is_empty() {
None
} else {
Some(f_str.as_ref())
};
let allocator = Allocator::new();
match LiteralParser::new(
&allocator,
&p.to_string_lossy_(agent),
flags,
Options::default(),
)
.parse()
{
Ok(_) => {
}
Err(err) => {
return Err(agent.throw_exception(
ExceptionType::SyntaxError,
err.message.to_string(),
gc.into_nogc(),
));
}
};
let f = f.unwrap_or_else(|f| parse_flags(&f.to_string_lossy_(agent)).unwrap());
let reg_exp_matcher = RegExpHeapData::compile_pattern(&p.to_string_lossy_(agent), f);
{
let data = obj.get_mut(agent);
data.original_source = p.unbind();
data.original_flags = f;
data.reg_exp_matcher = reg_exp_matcher;
}
if !obj.set_last_index(agent, RegExpLastIndex::ZERO, gc.nogc()) {
return throw_set_error(
agent,
BUILTIN_STRING_MEMORY.lastIndex.to_property_key(),
gc.into_nogc(),
)
.into();
}
Ok(obj.unbind())
}
fn parse_flags(f: &str) -> Option<RegExpFlags> {
let mut flags: u8 = 0;
for cu in f.as_bytes() {
match cu {
b'd' => flags |= RegExpFlags::D.bits(),
b'g' => flags |= RegExpFlags::G.bits(),
b'i' => flags |= RegExpFlags::I.bits(),
b'm' => flags |= RegExpFlags::M.bits(),
b's' => flags |= RegExpFlags::S.bits(),
b'u' => flags |= RegExpFlags::U.bits(),
b'v' => flags |= RegExpFlags::V.bits(),
b'y' => flags |= RegExpFlags::Y.bits(),
_ => return None,
}
}
Some(RegExpFlags::from_bits_retain(flags))
}
#[inline]
pub(crate) fn require_internal_slot_reg_exp<'a>(
agent: &mut Agent,
o: Value,
gc: NoGcScope<'a, '_>,
) -> JsResult<'a, RegExp<'a>> {
let Ok(o) = Object::try_from(o) else {
let error_message = format!(
"{} is not an object",
o.unbind()
.try_string_repr(agent, gc)
.to_string_lossy_(agent)
);
return Err(agent.throw_exception(ExceptionType::TypeError, error_message, gc));
};
require_internal_slot_reg_exp_object(agent, o, gc)
}
#[inline]
pub(crate) fn require_internal_slot_reg_exp_object<'a>(
agent: &mut Agent,
o: Object,
gc: NoGcScope<'a, '_>,
) -> JsResult<'a, RegExp<'a>> {
match o {
Object::RegExp(reg_exp) => Ok(reg_exp.unbind().bind(gc)),
_ => Err(agent.throw_exception_with_static_message(
ExceptionType::TypeError,
"Expected this to be RegExp",
gc,
)),
}
}
pub(crate) fn reg_exp_exec<'a>(
agent: &mut Agent,
r: Object,
s: String,
mut gc: GcScope<'a, '_>,
) -> JsResult<'a, Option<Object<'a>>> {
let (r, s) = match reg_exp_exec_prepare(agent, r, s, gc.reborrow()) {
ControlFlow::Continue(r) => r,
ControlFlow::Break(result) => return result.unbind().bind(gc.into_nogc()),
};
reg_exp_builtin_exec(agent, r.unbind(), s.unbind(), gc).map(|o| o.map(|o| o.into()))
}
fn reg_exp_exec_prepare<'a>(
agent: &mut Agent,
r: Object,
s: String,
mut gc: GcScope<'a, '_>,
) -> ControlFlow<JsResult<'a, Option<Object<'a>>>, (RegExp<'a>, String<'a>)> {
let mut s = s.bind(gc.nogc());
let mut r = r.bind(gc.nogc());
let key = BUILTIN_STRING_MEMORY.exec.to_property_key();
let exec = try_get(
agent,
r,
key,
PropertyLookupCache::get(agent, key),
gc.nogc(),
);
let exec = match exec {
ControlFlow::Continue(TryGetResult::Unset) => Value::Undefined,
ControlFlow::Continue(TryGetResult::Value(v)) => v,
ControlFlow::Break(TryError::Err(e)) => {
return ControlFlow::Break(Err(e.unbind().bind(gc.into_nogc())));
}
_ => {
let scoped_r = r.scope(agent, gc.nogc());
let scoped_s = s.scope(agent, gc.nogc());
let exec = handle_try_get_result(
agent,
r.unbind(),
BUILTIN_STRING_MEMORY.exec.to_property_key(),
exec.unbind(),
gc.reborrow(),
)
.unbind()
.bind(gc.nogc());
let exec = match exec {
Ok(e) => e,
Err(err) => return ControlFlow::Break(Err(err.unbind())),
};
let gc = gc.nogc();
unsafe {
s = scoped_s.take(agent).bind(gc);
r = scoped_r.take(agent).bind(gc);
}
exec
}
};
if let Object::RegExp(r) = r
&& exec
== agent
.current_realm_record()
.intrinsics()
.reg_exp_prototype_exec()
.into()
{
return ControlFlow::Continue((r.unbind(), s.unbind()));
}
if let Some(exec) = is_callable(exec, gc.nogc()) {
let result = call_function(
agent,
exec.unbind(),
r.unbind().into(),
Some(ArgumentsList::from_mut_value(&mut s.unbind().into())),
gc.reborrow(),
)
.unbind();
let gc = gc.into_nogc();
let result = result.bind(gc);
let result = match result {
Ok(r) => r,
Err(err) => return ControlFlow::Break(Err(err)),
};
let result = if let Ok(result) = Object::try_from(result) {
Some(result)
} else if result.is_null() {
None
} else {
return ControlFlow::Break(Err(agent.throw_exception_with_static_message(
ExceptionType::TypeError,
"'exec' function result was not object or null",
gc,
)));
};
return ControlFlow::Break(Ok(result));
}
let r = require_internal_slot_reg_exp_object(agent, r, gc.nogc()).unbind();
let s = s.unbind();
let gc = gc.into_nogc();
let r = r.bind(gc);
let s = s.bind(gc);
let r = match r {
Ok(r) => r,
Err(err) => return ControlFlow::Break(Err(err)),
};
ControlFlow::Continue((r, s))
}
pub(crate) fn reg_exp_test<'a>(
agent: &mut Agent,
r: Object,
s: String,
mut gc: GcScope<'a, '_>,
) -> JsResult<'a, bool> {
let (r, s) = match reg_exp_exec_prepare(agent, r, s, gc.reborrow()) {
ControlFlow::Continue(r) => r,
ControlFlow::Break(result) => {
return result.unbind().bind(gc.into_nogc()).map(|r| r.is_some());
}
};
reg_exp_builtin_test(agent, r.unbind(), s.unbind(), gc)
}
pub(crate) struct RegExpExecBase<'gc> {
pub(crate) r: RegExp<'gc>,
pub(crate) s: String<'gc>,
pub(crate) last_index: usize,
pub(crate) global: bool,
pub(crate) sticky: bool,
pub(crate) has_indices: bool,
#[expect(dead_code)]
pub(crate) full_unicode: bool,
}
bindable_handle!(RegExpExecBase);
pub(crate) fn reg_exp_builtin_exec_prepare<'a>(
agent: &mut Agent,
r: RegExp,
s: String,
mut gc: GcScope<'a, '_>,
) -> JsResult<'a, RegExpExecBase<'a>> {
let mut r = r.bind(gc.nogc());
let mut s = s.bind(gc.nogc());
let mut last_index = if let Some(last_index) = r.try_get_last_index(agent) {
last_index as usize
} else {
let last_index = unwrap_try_get_value(try_get(
agent,
r,
BUILTIN_STRING_MEMORY.lastIndex.to_property_key(),
None,
gc.nogc(),
));
if let Some(last_index) =
try_result_into_js(try_to_length(agent, last_index, gc.nogc())).unbind()?
{
last_index as usize
} else {
let scoped_r = r.scope(agent, gc.nogc());
let scoped_s = s.scope(agent, gc.nogc());
let last_index =
to_length(agent, last_index.unbind(), gc.reborrow()).unbind()? as usize;
unsafe {
s = scoped_s.take(agent).bind(gc.nogc());
r = scoped_r.take(agent).bind(gc.nogc());
}
last_index
}
};
let r = r.unbind();
let s = s.unbind();
let gc = gc.into_nogc();
let r = r.bind(gc);
let s = s.bind(gc);
let flags = r.original_flags(agent);
let global = (flags & RegExpFlags::G).bits() > 0;
let sticky = (flags & RegExpFlags::Y).bits() > 0;
let has_indices = (flags & RegExpFlags::D).bits() > 0;
if !global && !sticky {
last_index = 0;
}
let last_index = if last_index > s.len_(agent) {
last_index
} else {
s.utf8_index_(agent, last_index).unwrap_or(last_index)
};
if let Err(err) = &r.get(agent).reg_exp_matcher {
return Err(agent.throw_exception(ExceptionType::SyntaxError, err.to_string(), gc));
};
let full_unicode = (flags & (RegExpFlags::U | RegExpFlags::V)).bits() > 0;
Ok(RegExpExecBase {
r,
s,
last_index,
global,
sticky,
has_indices,
full_unicode,
})
}
pub(crate) fn reg_exp_builtin_exec<'a>(
agent: &mut Agent,
r: RegExp,
s: String,
mut gc: GcScope<'a, '_>,
) -> JsResult<'a, Option<Array<'a>>> {
let r = r.bind(gc.nogc());
let s = s.bind(gc.nogc());
let result =
reg_exp_builtin_exec_prepare(agent, r.unbind(), s.unbind(), gc.reborrow()).unbind()?;
let gc = gc.into_nogc();
let RegExpExecBase {
r,
s,
last_index,
global,
sticky,
has_indices,
full_unicode: _,
} = result.bind(gc);
let length = s.len_(agent);
let r_data = r.get_direct_mut(&mut agent.heap.regexps);
let s_bytes = s.as_bytes_(&agent.heap.strings);
let matcher = unsafe { r_data.reg_exp_matcher.as_mut().unwrap_unchecked() };
if last_index > length {
if global || sticky {
r_data.last_index = RegExpLastIndex::ZERO;
}
return Ok(None);
}
let input_index = last_index;
let result = matcher.captures_at(s_bytes, input_index);
let Some(result) = result else {
if global || sticky {
r_data.last_index = RegExpLastIndex::ZERO;
}
return Ok(None);
};
let full_match = unsafe { result.get(0).unwrap_unchecked() };
if sticky && full_match.start() != last_index {
r_data.last_index = RegExpLastIndex::ZERO;
return Ok(None);
}
let last_index = full_match.start();
let e = full_match.end();
let e = s.utf16_index_(&agent.heap.strings, e);
if global || sticky {
r_data.last_index = e.into();
}
let n = result.len();
debug_assert_eq!(n, matcher.captures_len());
debug_assert!(n < 2usize.pow(32) - 1);
let has_group_name = matcher.capture_names().any(|n| n.is_some());
let a = array_create(agent, n, n, None, gc).unwrap();
debug_assert_eq!(a.len(agent) as usize, n);
unwrap_try(try_create_data_property_or_throw(
agent,
a,
BUILTIN_STRING_MEMORY.index.to_property_key(),
Number::try_from(last_index).unwrap().into(),
None,
gc,
));
let input = String::from_static_str(agent, "input", gc).to_property_key();
unwrap_try(try_create_data_property_or_throw(
agent,
a,
input,
s.into(),
None,
gc,
));
let groups = if has_group_name {
Some(ordinary_object_create_null(agent, gc))
} else {
None
};
let key = String::from_static_str(agent, "groups", gc).to_property_key();
unwrap_try(try_create_data_property_or_throw(
agent,
a,
key,
groups.map_or(Value::Undefined, |g| g.into()),
None,
gc,
));
for (i, capture_i) in result.iter().enumerate() {
if has_indices {
}
let captured_value = if let Some(capture_i) = capture_i {
match std::string::String::from_utf8_lossy(capture_i.as_bytes()) {
std::borrow::Cow::Borrowed(str) => String::from_str(agent, str, gc).into(),
std::borrow::Cow::Owned(string) => String::from_string(agent, string, gc).into(),
}
} else {
Value::Undefined
};
unwrap_try(try_create_data_property_or_throw(
agent,
a,
PropertyKey::try_from(i).unwrap(),
captured_value,
None,
gc,
));
}
Ok(Some(a))
}
pub(crate) fn reg_exp_builtin_test<'a>(
agent: &mut Agent,
r: RegExp,
s: String,
mut gc: GcScope<'a, '_>,
) -> JsResult<'a, bool> {
let r = r.bind(gc.nogc());
let s = s.bind(gc.nogc());
let result =
reg_exp_builtin_exec_prepare(agent, r.unbind(), s.unbind(), gc.reborrow()).unbind()?;
let gc = gc.into_nogc();
let RegExpExecBase {
r,
s,
last_index,
sticky,
global,
..
} = result.bind(gc);
let length = s.len_(agent);
let r_data = r.get_direct_mut(&mut agent.heap.regexps);
if last_index > length {
if global || sticky {
r_data.last_index = RegExpLastIndex::ZERO;
}
return Ok(false);
}
let s_bytes = s.as_bytes_(&agent.heap.strings);
let matcher = unsafe { r_data.reg_exp_matcher.as_mut().unwrap_unchecked() };
if global || sticky {
let result = matcher.find_at(s_bytes, last_index);
if let Some(result) = result {
if sticky && result.start() != last_index {
r_data.last_index = RegExpLastIndex::ZERO;
Ok(false)
} else {
let e = result.end();
let e = s.utf16_index_(&agent.heap.strings, e);
r_data.last_index = e.into();
Ok(true)
}
} else {
r_data.last_index = RegExpLastIndex::ZERO;
Ok(false)
}
} else {
let result = matcher.is_match_at(s_bytes, last_index);
r_data.last_index = RegExpLastIndex::ZERO;
Ok(result)
}
}
pub(crate) fn advance_string_index(agent: &Agent, s: String, index: usize, unicode: bool) -> usize {
assert!(index < 2usize.pow(53));
if !unicode {
return index + 1;
}
let length = s.utf16_len_(agent);
if index + 1 >= length {
return index + 1;
}
let cp = s.code_point_at_(agent, index);
let code = cp.to_u32();
if (code & !0xFFFF) > 0 {
index + 2
} else {
index + 1
}
}