Skip to content

feat(ecmascript): get RegExp.prototype.source and RegExp.escape #830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion nova_vm/src/ecmascript/builtins/regexp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
agent::{TryResult, unwrap_try},
},
types::{
BUILTIN_STRING_MEMORY, InternalMethods, InternalSlots, IntoObject, Object,
BUILTIN_STRING_MEMORY, InternalMethods, InternalSlots, IntoObject, IntoValue, Object,
OrdinaryObject, PropertyDescriptor, PropertyKey, SetResult, String, TryGetResult,
TryHasResult, Value,
},
Expand Down Expand Up @@ -62,6 +62,44 @@ impl<'a> RegExp<'a> {
agent[self].original_flags
}

pub(crate) fn set_last_index(
self,
agent: &mut Agent,
last_index: RegExpLastIndex,
gc: NoGcScope,
) -> bool {
debug_assert!(last_index.is_valid());
// If we're setting the last index and we have a backing object,
// then we set the value there first and observe the result.
if self.get_backing_object(agent).is_some() {
// Note: The lastIndex is an unconfigurable data property: It
// cannot be turned into a getter or setter and will thus never
// call into JavaScript.
let success = unwrap_try(ordinary_try_set(
agent,
self.into_object(),
BUILTIN_STRING_MEMORY.lastIndex.to_property_key(),
last_index.get_value().unwrap().into_value(),
self.into_value(),
None,
gc,
))
.into_boolean()
.unwrap();
if success {
// We successfully set the value, so set it in our direct
// data as well.
agent[self].last_index = last_index;
}
success
} else {
// Note: lastIndex property is writable, so setting its value
// always succeeds. We can just set this directly here.
agent[self].last_index = last_index;
true
}
}

/// ### \[\[LastIndex]]
///
/// This is a custom internal slot that stores the "lastIndex" property of
Expand Down
20 changes: 13 additions & 7 deletions nova_vm/src/ecmascript/builtins/regexp/abstract_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use oxc_regular_expression::{LiteralParser, Options};
use crate::{
ecmascript::{
abstract_operations::{
operations_on_objects::{call_function, try_create_data_property_or_throw, try_get},
operations_on_objects::{
call_function, throw_set_error, try_create_data_property_or_throw, try_get,
},
testing_and_comparison::is_callable,
type_conversion::{to_length, to_string, try_to_length},
},
Expand Down Expand Up @@ -242,6 +244,14 @@ pub(crate) fn reg_exp_initialize<'a>(
agent[obj].reg_exp_matcher = reg_exp_matcher;

// 22. Perform ? Set(obj, "lastIndex", +0𝔽, true).
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();
}
// 23. Return obj.
Ok(obj.unbind())
}
Expand Down Expand Up @@ -546,12 +556,8 @@ pub(crate) fn reg_exp_builtin_exec_prepare<'a>(
s.utf8_index(agent, last_index).unwrap_or(last_index)
};
// 8. Let matcher be R.[[RegExpMatcher]].
if agent[r].reg_exp_matcher.is_none() {
return Err(agent.throw_exception_with_static_message(
ExceptionType::SyntaxError,
"RegExp engine failed to compile the given pattern",
gc,
));
if let Err(err) = &agent[r].reg_exp_matcher {
return Err(agent.throw_exception(ExceptionType::SyntaxError, err.to_string(), gc));
};
// 9. If flags contains "u" or flags contains "v", let fullUnicode be true;
// else let fullUnicode be false.
Expand Down
10 changes: 6 additions & 4 deletions nova_vm/src/ecmascript/builtins/regexp/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,22 +121,24 @@ impl From<usize> for RegExpLastIndex {
#[derive(Debug)]
pub struct RegExpHeapData<'a> {
pub(super) object_index: Option<OrdinaryObject<'a>>,
pub(super) reg_exp_matcher: Option<Regex>,
pub(super) reg_exp_matcher: Result<Regex, regex::Error>,
pub(super) original_source: String<'a>,
pub(super) original_flags: RegExpFlags,
pub(super) last_index: RegExpLastIndex,
}

impl<'a> RegExpHeapData<'a> {
pub(crate) fn compile_pattern(pattern: &str, flags: RegExpFlags) -> Option<Regex> {
pub(crate) fn compile_pattern(
pattern: &str,
flags: RegExpFlags,
) -> Result<Regex, regex::Error> {
RegexBuilder::new(pattern)
.dot_matches_new_line((flags & RegExpFlags::M).bits() > 0)
.case_insensitive((flags & RegExpFlags::I).bits() > 0)
.unicode((flags & (RegExpFlags::U | RegExpFlags::V)).bits() > 0)
.dot_matches_new_line((flags & RegExpFlags::S).bits() > 0)
.octal(false) // TODO: !strict
.build()
.ok()
}

pub(crate) fn new(agent: &Agent, source: String<'a>, flags: RegExpFlags) -> Self {
Expand Down Expand Up @@ -170,7 +172,7 @@ impl Default for RegExpHeapData<'_> {
fn default() -> Self {
Self {
object_index: Default::default(),
reg_exp_matcher: None,
reg_exp_matcher: Err(regex::Error::CompiledTooBig(usize::MAX)),
original_source: String::EMPTY_STRING,
original_flags: RegExpFlags::empty(),
last_index: Default::default(),
Expand Down
10 changes: 5 additions & 5 deletions nova_vm/src/ecmascript/builtins/structured_data/json_object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,13 +889,13 @@ fn quote_json_string(agent: &Agent, product: &mut Wtf8Buf, value: String) {
// | U+0008 | Backspace | \b |
0x0008 => product.push_str("\\b"),
// | U+0009 | CHARACTER TABULATION | \t |
0x0009 => product.push_str("\\t'"),
0x0009 => product.push_str("\\t"),
// | U+000A | LINE FEED (LF) | \n |
0x000A => product.push_str("\\n'"),
0x000A => product.push_str("\\n"),
// | U+000C | FORM FEED (FF) | \f |
0x000C => product.push_str("\\f'"),
0x000C => product.push_str("\\f"),
// | U+000D | CARRIAGE RETURN (CR) | \r |
0x000D => product.push_str("\\r'"),
0x000D => product.push_str("\\r"),
// | U+0022 | QUOTATION MARK | \" |
0x0022 => product.push_str("\\\""),
// | U+005C | REVERSE SOLIDUS | \\ |
Expand All @@ -918,7 +918,7 @@ fn quote_json_string(agent: &Agent, product: &mut Wtf8Buf, value: String) {
if v < 10 { v as u8 + 48 } else { v as u8 + 87 }
}

buf[0] = 0x005C;
buf[0] = b'\\';
buf[1] = b'u';
buf[5] = u16_to_char_code(unit % 16);
buf[4] = u16_to_char_code((unit >> 4) % 16);
Expand Down
Loading
Loading