Skip to main content

boa_engine/builtins/regexp/
mod.rs

1//! Boa's implementation of ECMAScript's global `RegExp` object.
2//!
3//! The `RegExp` object is used for matching text with a pattern.
4//!
5//! More information:
6//!  - [ECMAScript reference][spec]
7//!  - [MDN documentation][mdn]
8//!
9//! [spec]: https://tc39.es/ecma262/#sec-regexp-constructor
10//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
11
12use crate::{
13    Context, JsArgs, JsData, JsResult, JsString,
14    builtins::{BuiltInObject, array::Array, string},
15    context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors},
16    error::JsNativeError,
17    js_string,
18    native_function::NativeFunctionObject,
19    object::{CONSTRUCTOR, JsObject, internal_methods::get_prototype_from_constructor},
20    property::Attribute,
21    realm::Realm,
22    string::{CodePoint, CommonJsStringBuilder, JsStrVariant, StaticJsStrings},
23    symbol::JsSymbol,
24    value::JsValue,
25};
26use arrayvec::ArrayVec;
27use boa_gc::{Finalize, Trace};
28use boa_macros::{js_str, utf16};
29use boa_parser::lexer::regex::RegExpFlags;
30use regress::{Flags, Range, Regex};
31use std::{cell::RefCell, collections::HashMap, rc::Rc, str::FromStr};
32
33thread_local! {
34    /// Compiled matchers by source and flags, so a regular expression literal evaluated in a
35    /// loop compiles its pattern once.
36    static MATCHERS: RefCell<HashMap<(JsString, u8), Rc<Regex>>> = RefCell::default();
37}
38
39use super::{BuiltInBuilder, BuiltInConstructor, IntrinsicObject};
40
41mod regexp_string_iterator;
42pub(crate) use regexp_string_iterator::RegExpStringIterator;
43#[cfg(test)]
44mod tests;
45
46/// The internal representation of a `RegExp` object.
47#[derive(Debug, Clone, Trace, Finalize, JsData)]
48// Safety: `RegExp` does not contain any objects which needs to be traced, so this is safe.
49#[boa_gc(unsafe_empty_trace)]
50pub struct RegExp {
51    /// Regex matcher.
52    matcher: Rc<Regex>,
53    flags: RegExpFlags,
54    original_source: JsString,
55    original_flags: JsString,
56}
57
58impl RegExp {
59    /// Returns the original source string of the regex (e.g. `"regex-test"`).
60    pub(crate) fn original_source(&self) -> &JsString {
61        &self.original_source
62    }
63
64    /// Returns the original flags string of the regex (e.g. `"gi"`).
65    pub(crate) fn original_flags(&self) -> &JsString {
66        &self.original_flags
67    }
68}
69
70impl IntrinsicObject for RegExp {
71    fn init(realm: &Realm) {
72        let get_species = BuiltInBuilder::callable(realm, Self::get_species)
73            .name(js_string!("get [Symbol.species]"))
74            .build();
75
76        let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE;
77
78        let get_has_indices = BuiltInBuilder::callable(realm, Self::get_has_indices)
79            .name(js_string!("get hasIndices"))
80            .build();
81        let get_global = BuiltInBuilder::callable(realm, Self::get_global)
82            .name(js_string!("get global"))
83            .build();
84        let get_ignore_case = BuiltInBuilder::callable(realm, Self::get_ignore_case)
85            .name(js_string!("get ignoreCase"))
86            .build();
87        let get_multiline = BuiltInBuilder::callable(realm, Self::get_multiline)
88            .name(js_string!("get multiline"))
89            .build();
90        let get_dot_all = BuiltInBuilder::callable(realm, Self::get_dot_all)
91            .name(js_string!("get dotAll"))
92            .build();
93        let get_unicode = BuiltInBuilder::callable(realm, Self::get_unicode)
94            .name(js_string!("get unicode"))
95            .build();
96        let get_unicode_sets = BuiltInBuilder::callable(realm, Self::get_unicode_sets)
97            .name(js_string!("get unicodeSets"))
98            .build();
99        let get_sticky = BuiltInBuilder::callable(realm, Self::get_sticky)
100            .name(js_string!("get sticky"))
101            .build();
102        let get_flags = BuiltInBuilder::callable(realm, Self::get_flags)
103            .name(js_string!("get flags"))
104            .build();
105        let get_source = BuiltInBuilder::callable(realm, Self::get_source)
106            .name(js_string!("get source"))
107            .build();
108        let regexp = BuiltInBuilder::from_standard_constructor::<Self>(realm)
109            .static_method(Self::escape, js_string!("escape"), 1)
110            .static_accessor(
111                JsSymbol::species(),
112                Some(get_species),
113                None,
114                Attribute::CONFIGURABLE,
115            )
116            .property(js_string!("lastIndex"), 0, Attribute::all())
117            .method(Self::test, js_string!("test"), 1)
118            .method(Self::exec, js_string!("exec"), 1)
119            .method(Self::to_string, js_string!("toString"), 0)
120            .method(Self::r#match, JsSymbol::r#match(), 1)
121            .method(Self::match_all, JsSymbol::match_all(), 1)
122            .method(Self::replace, JsSymbol::replace(), 2)
123            .method(Self::search, JsSymbol::search(), 1)
124            .method(Self::split, JsSymbol::split(), 2)
125            .accessor(
126                js_string!("hasIndices"),
127                Some(get_has_indices),
128                None,
129                flag_attributes,
130            )
131            .accessor(
132                js_string!("global"),
133                Some(get_global),
134                None,
135                flag_attributes,
136            )
137            .accessor(
138                js_string!("ignoreCase"),
139                Some(get_ignore_case),
140                None,
141                flag_attributes,
142            )
143            .accessor(
144                js_string!("multiline"),
145                Some(get_multiline),
146                None,
147                flag_attributes,
148            )
149            .accessor(
150                js_string!("dotAll"),
151                Some(get_dot_all),
152                None,
153                flag_attributes,
154            )
155            .accessor(
156                js_string!("unicode"),
157                Some(get_unicode),
158                None,
159                flag_attributes,
160            )
161            .accessor(
162                js_string!("unicodeSets"),
163                Some(get_unicode_sets),
164                None,
165                flag_attributes,
166            )
167            .accessor(
168                js_string!("sticky"),
169                Some(get_sticky),
170                None,
171                flag_attributes,
172            )
173            .accessor(js_string!("flags"), Some(get_flags), None, flag_attributes)
174            .accessor(
175                js_string!("source"),
176                Some(get_source),
177                None,
178                flag_attributes,
179            );
180
181        #[cfg(feature = "annex-b")]
182        let regexp = regexp.method(Self::compile, js_string!("compile"), 2);
183
184        regexp.build();
185    }
186
187    fn get(intrinsics: &Intrinsics) -> JsObject {
188        Self::STANDARD_CONSTRUCTOR(intrinsics.constructors()).constructor()
189    }
190}
191
192impl BuiltInObject for RegExp {
193    const NAME: JsString = StaticJsStrings::REG_EXP;
194}
195
196impl BuiltInConstructor for RegExp {
197    const CONSTRUCTOR_ARGUMENTS: usize = 2;
198    const PROTOTYPE_STORAGE_SLOTS: usize = 30;
199    const CONSTRUCTOR_STORAGE_SLOTS: usize = 3;
200
201    const STANDARD_CONSTRUCTOR: fn(&StandardConstructors) -> &StandardConstructor =
202        StandardConstructors::regexp;
203
204    /// `22.2.3.1 RegExp ( pattern, flags )`
205    ///
206    /// More information:
207    ///  - [ECMAScript reference][spec]
208    ///
209    /// [spec]: https://tc39.es/ecma262/#sec-regexp-pattern-flags
210    fn constructor(
211        new_target: &JsValue,
212        args: &[JsValue],
213        context: &mut Context,
214    ) -> JsResult<JsValue> {
215        let pattern = args.get_or_undefined(0);
216        let flags = args.get_or_undefined(1);
217
218        // 1. Let patternIsRegExp be ? IsRegExp(pattern).
219        let pattern_is_regexp = Self::is_reg_exp(pattern, context)?;
220
221        // 2. If NewTarget is undefined, then
222        // 3. Else, let newTarget be NewTarget.
223        if new_target.is_undefined() {
224            // a. Let newTarget be the active function object.
225            let new_target = context
226                .active_function_object()
227                .map_or(JsValue::undefined(), JsValue::new);
228
229            // b. If patternIsRegExp is true and flags is undefined, then
230            if let Some(pattern) = &pattern_is_regexp
231                && flags.is_undefined()
232            {
233                // i. Let patternConstructor be ? Get(pattern, "constructor").
234                let pattern_constructor = pattern.get(CONSTRUCTOR, context)?;
235
236                // ii. If SameValue(newTarget, patternConstructor) is true, return pattern.
237                if JsValue::same_value(&new_target, &pattern_constructor) {
238                    return Ok(pattern.clone().into());
239                }
240            }
241        }
242
243        // 4. If pattern is an Object and pattern has a [[RegExpMatcher]] internal slot, then
244        let object = pattern.clone().as_object();
245        let (p, f) =
246            if let Some(pattern) = object.as_ref().and_then(JsObject::downcast_ref::<RegExp>) {
247                // a. Let P be pattern.[[OriginalSource]].
248                let p = pattern.original_source.clone().into();
249
250                // b. If flags is undefined, let F be pattern.[[OriginalFlags]].
251                let f = if flags.is_undefined() {
252                    pattern.original_flags.clone().into()
253                // c. Else, let F be flags.
254                } else {
255                    flags.clone()
256                };
257
258                (p, f)
259            } else if let Some(pattern) = &pattern_is_regexp {
260                // a. Let P be ? Get(pattern, "source").
261                let p = pattern.get(js_string!("source"), context)?;
262
263                // b. If flags is undefined, then
264                let f = if flags.is_undefined() {
265                    // i. Let F be ? Get(pattern, "flags").
266                    pattern.get(js_string!("flags"), context)?
267                // c. Else,
268                } else {
269                    // i. Let F be flags.
270                    flags.clone()
271                };
272
273                (p, f)
274            // 6. Else,
275            } else {
276                // a. Let P be pattern.
277                // b. Let F be flags.
278                (pattern.clone(), flags.clone())
279            };
280
281        // 7. Let O be ? RegExpAlloc(newTarget).
282        let proto =
283            get_prototype_from_constructor(new_target, StandardConstructors::regexp, context)?;
284
285        // 8.Return ? RegExpInitialize(O, P, F).
286        Self::initialize(Some(proto), &p, &f, context)
287    }
288}
289
290impl RegExp {
291    /// `7.2.8 IsRegExp ( argument )`
292    ///
293    /// This modified to return the object if it's `true`, [`None`] otherwise.
294    ///
295    /// More information:
296    ///  - [ECMAScript reference][spec]
297    ///
298    /// [spec]: https://tc39.es/ecma262/#sec-isregexp
299    pub(crate) fn is_reg_exp(
300        argument: &JsValue,
301        context: &mut Context,
302    ) -> JsResult<Option<JsObject>> {
303        // 1. If argument is not an Object, return false.
304        let Some(argument) = argument.as_object() else {
305            return Ok(None);
306        };
307
308        // 2. Let matcher be ? Get(argument, @@match).
309        let matcher = argument.get(JsSymbol::r#match(), context)?;
310
311        // 3. If matcher is not undefined, return ToBoolean(matcher).
312        if !matcher.is_undefined() {
313            return Ok(matcher.to_boolean().then_some(argument));
314        }
315
316        // 4. If argument has a [[RegExpMatcher]] internal slot, return true.
317        if argument.is::<RegExp>() {
318            return Ok(Some(argument));
319        }
320
321        // 5. Return false.
322        Ok(None)
323    }
324
325    /// Compiles a `RegExp` from the provided pattern and flags.
326    ///
327    /// Equivalent to the beginning of [`RegExpInitialize ( obj, pattern, flags )`][spec]
328    ///
329    /// [spec]: https://tc39.es/ecma262/#sec-regexpinitialize
330    fn compile_native_regexp(
331        pattern: &JsValue,
332        flags: &JsValue,
333        context: &mut Context,
334    ) -> JsResult<RegExp> {
335        // 1. If pattern is undefined, let P be the empty String.
336        // 2. Else, let P be ? ToString(pattern).
337        let p = if pattern.is_undefined() {
338            js_string!()
339        } else {
340            pattern.to_string(context)?
341        };
342
343        // 3. If flags is undefined, let F be the empty String.
344        // 4. Else, let F be ? ToString(flags).
345        let f = if flags.is_undefined() {
346            js_string!()
347        } else {
348            flags.to_string(context)?
349        };
350
351        // 5. If F contains any code unit other than "g", "i", "m", "s", "u", "v", or "y"
352        //    or if it contains the same code unit more than once, throw a SyntaxError exception.
353        // TODO: Should directly parse the JsString instead of converting to String
354        let flags = match RegExpFlags::from_str(&f.to_std_string_escaped()) {
355            Err(msg) => return Err(JsNativeError::syntax().with_message(msg).into()),
356            Ok(result) => result,
357        };
358
359        // 13. Let parseResult be ParsePattern(patternText, u, v).
360        // 14. If parseResult is a non-empty List of SyntaxError objects, throw a SyntaxError exception.
361
362        if let Some(matcher) =
363            MATCHERS.with(|matchers| matchers.borrow().get(&(p.clone(), flags.bits())).cloned())
364        {
365            return Ok(RegExp {
366                matcher,
367                flags,
368                original_source: p,
369                original_flags: f,
370            });
371        }
372
373        // If u or v flag is set, fullUnicode is true — compile as full codepoints.
374        let full_unicode =
375            flags.contains(RegExpFlags::UNICODE) || flags.contains(RegExpFlags::UNICODE_SETS);
376
377        let matcher = if full_unicode {
378            // Unicode mode (u/v flag) OR pattern has named groups:
379            // compile as full Unicode codepoints.
380            Regex::from_unicode(p.code_points().map(CodePoint::as_u32), Flags::from(flags))
381                .map_err(|error| {
382                    JsNativeError::syntax()
383                        .with_message(format!("failed to create matcher: {}", error.text))
384                })?
385        } else {
386            // Non-Unicode mode with no named groups:
387            // compile as raw UTF-16 code units so that surrogate pairs
388            // (e.g. 𠮷 = [0xD842, 0xDFB7]) are matched correctly by find_from_ucs2.
389            let utf16_units = p.code_points().flat_map(|cp| {
390                let mut buf = [0u16; 2];
391                match cp {
392                    CodePoint::Unicode(c) => c
393                        .encode_utf16(&mut buf)
394                        .iter()
395                        .map(|&u| u32::from(u))
396                        .collect::<Vec<_>>(),
397                    CodePoint::UnpairedSurrogate(s) => vec![u32::from(s)],
398                }
399            });
400            Regex::from_unicode(utf16_units, Flags::from(flags)).map_err(|error| {
401                JsNativeError::syntax()
402                    .with_message(format!("failed to create matcher: {}", error.text))
403            })?
404        };
405
406        // 15. Assert: parseResult is a Pattern Parse Node.
407        // 16. Set obj.[[OriginalSource]] to P.
408        // 17. Set obj.[[OriginalFlags]] to F.
409        // 18. Let capturingGroupsCount be CountLeftCapturingParensWithin(parseResult).
410        // 19. Let rer be the RegExp Record { [[IgnoreCase]]: i, [[Multiline]]: m,
411        //     [[DotAll]]: s, [[Unicode]]: u, [[UnicodeSets]]: v,
412        //     [[CapturingGroupsCount]]: capturingGroupsCount }.
413        // 20. Set obj.[[RegExpRecord]] to rer.
414        // 21. Set obj.[[RegExpMatcher]] to CompilePattern of parseResult with argument rer.
415        let matcher = Rc::new(matcher);
416        MATCHERS.with(|matchers| {
417            let mut matchers = matchers.borrow_mut();
418            if matchers.len() >= 1024 {
419                matchers.clear();
420            }
421            matchers.insert((p.clone(), flags.bits()), matcher.clone());
422        });
423        Ok(RegExp {
424            matcher,
425            flags,
426            original_source: p,
427            original_flags: f,
428        })
429    }
430
431    /// `RegExpInitialize ( obj, pattern, flags )`
432    ///
433    /// If prototype is `None`, initializes the prototype to `%RegExp%.prototype`.
434    ///
435    /// More information:
436    ///  - [ECMAScript reference][spec]
437    ///
438    /// [spec]: https://tc39.es/ecma262/#sec-regexpinitialize
439    pub(crate) fn initialize(
440        prototype: Option<JsObject>,
441        pattern: &JsValue,
442        flags: &JsValue,
443        context: &mut Context,
444    ) -> JsResult<JsValue> {
445        // Has the steps  of `RegExpInitialize`.
446        let regexp = Self::compile_native_regexp(pattern, flags, context)?;
447
448        // 22. Perform ? Set(obj, "lastIndex", +0𝔽, true).
449        let obj = if let Some(prototype) = prototype {
450            let mut template = context
451                .intrinsics()
452                .templates()
453                .regexp_without_proto()
454                .clone();
455            template.set_prototype(prototype);
456            template.create(regexp, vec![0.into()])
457        } else {
458            context
459                .intrinsics()
460                .templates()
461                .regexp()
462                .create(regexp, vec![0.into()])
463        };
464
465        // 23. Return obj.
466        Ok(obj.into())
467    }
468
469    /// `22.2.3.2.4 RegExpCreate ( P, F )`
470    ///
471    /// More information:
472    ///  - [ECMAScript reference][spec]
473    ///
474    /// [spec]: https://tc39.es/ecma262/#sec-regexpcreate
475    pub(crate) fn create(p: &JsValue, f: &JsValue, context: &mut Context) -> JsResult<JsValue> {
476        // 1. Let obj be ? RegExpAlloc(%RegExp%).
477        // 2. Return ? RegExpInitialize(obj, P, F).
478        Self::initialize(None, p, f, context)
479    }
480
481    /// `get RegExp [ @@species ]`
482    ///
483    /// The `RegExp [ @@species ]` accessor property returns the `RegExp` constructor.
484    ///
485    /// More information:
486    ///  - [ECMAScript reference][spec]
487    ///  - [MDN documentation][mdn]
488    ///
489    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp-@@species
490    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@species
491    #[allow(clippy::unnecessary_wraps)]
492    fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
493        // 1. Return the this value.
494        Ok(this.clone())
495    }
496
497    fn regexp_has_flag(this: &JsValue, flag: u8, context: &mut Context) -> JsResult<JsValue> {
498        if let Some(object) = this.as_object() {
499            if let Some(regexp) = object.downcast_ref::<RegExp>() {
500                return Ok(JsValue::new(match flag {
501                    b'd' => regexp.flags.contains(RegExpFlags::HAS_INDICES),
502                    b'g' => regexp.flags.contains(RegExpFlags::GLOBAL),
503                    b'm' => regexp.flags.contains(RegExpFlags::MULTILINE),
504                    b's' => regexp.flags.contains(RegExpFlags::DOT_ALL),
505                    b'i' => regexp.flags.contains(RegExpFlags::IGNORE_CASE),
506                    b'u' => regexp.flags.contains(RegExpFlags::UNICODE),
507                    b'v' => regexp.flags.contains(RegExpFlags::UNICODE_SETS),
508                    b'y' => regexp.flags.contains(RegExpFlags::STICKY),
509                    _ => unreachable!(),
510                }));
511            }
512
513            if JsObject::equals(
514                &object,
515                &context.intrinsics().constructors().regexp().prototype(),
516            ) {
517                return Ok(JsValue::undefined());
518            }
519        }
520
521        let name = match flag {
522            b'd' => "hasIndices",
523            b'g' => "global",
524            b'm' => "multiline",
525            b's' => "dotAll",
526            b'i' => "ignoreCase",
527            b'u' => "unicode",
528            b'v' => "unicodeSets",
529            b'y' => "sticky",
530            _ => unreachable!(),
531        };
532
533        Err(JsNativeError::typ()
534            .with_message(format!(
535                "RegExp.prototype.{name} getter called on non-RegExp object",
536            ))
537            .into())
538    }
539
540    /// `get RegExp.prototype.hasIndices`
541    ///
542    /// More information:
543    ///  - [ECMAScript reference][spec]
544    ///  - [MDN documentation][mdn]
545    ///
546    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.hasindices
547    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global
548    pub(crate) fn get_has_indices(
549        this: &JsValue,
550        _: &[JsValue],
551        context: &mut Context,
552    ) -> JsResult<JsValue> {
553        Self::regexp_has_flag(this, b'd', context)
554    }
555
556    /// `get RegExp.prototype.global`
557    ///
558    /// The `global` property indicates whether or not the "`g`" flag is used with the regular expression.
559    ///
560    /// More information:
561    ///  - [ECMAScript reference][spec]
562    ///  - [MDN documentation][mdn]
563    ///
564    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.global
565    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global
566    pub(crate) fn get_global(
567        this: &JsValue,
568        _: &[JsValue],
569        context: &mut Context,
570    ) -> JsResult<JsValue> {
571        Self::regexp_has_flag(this, b'g', context)
572    }
573
574    /// `get RegExp.prototype.ignoreCase`
575    ///
576    /// The `ignoreCase` property indicates whether or not the "`i`" flag is used with the regular expression.
577    ///
578    /// More information:
579    ///  - [ECMAScript reference][spec]
580    ///  - [MDN documentation][mdn]
581    ///
582    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.ignorecase
583    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/ignoreCase
584    pub(crate) fn get_ignore_case(
585        this: &JsValue,
586        _: &[JsValue],
587        context: &mut Context,
588    ) -> JsResult<JsValue> {
589        Self::regexp_has_flag(this, b'i', context)
590    }
591
592    /// `get RegExp.prototype.multiline`
593    ///
594    /// The multiline property indicates whether or not the "m" flag is used with the regular expression.
595    ///
596    /// More information:
597    ///  - [ECMAScript reference][spec]
598    ///  - [MDN documentation][mdn]
599    ///
600    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.multiline
601    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/multiline
602    pub(crate) fn get_multiline(
603        this: &JsValue,
604        _: &[JsValue],
605        context: &mut Context,
606    ) -> JsResult<JsValue> {
607        Self::regexp_has_flag(this, b'm', context)
608    }
609
610    /// `get RegExp.prototype.dotAll`
611    ///
612    /// The `dotAll` property indicates whether or not the "`s`" flag is used with the regular expression.
613    ///
614    /// More information:
615    ///  - [ECMAScript reference][spec]
616    ///  - [MDN documentation][mdn]
617    ///
618    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.dotAll
619    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/dotAll
620    pub(crate) fn get_dot_all(
621        this: &JsValue,
622        _: &[JsValue],
623        context: &mut Context,
624    ) -> JsResult<JsValue> {
625        Self::regexp_has_flag(this, b's', context)
626    }
627
628    /// `get RegExp.prototype.unicode`
629    ///
630    /// The unicode property indicates whether or not the "`u`" flag is used with a regular expression.
631    /// unicode is a read-only property of an individual regular expression instance.
632    ///
633    /// More information:
634    ///  - [ECMAScript reference][spec]
635    ///  - [MDN documentation][mdn]
636    ///
637    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.unicode
638    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode
639    pub(crate) fn get_unicode(
640        this: &JsValue,
641        _: &[JsValue],
642        context: &mut Context,
643    ) -> JsResult<JsValue> {
644        Self::regexp_has_flag(this, b'u', context)
645    }
646
647    /// `get RegExp.prototype.unicodeSets`
648    ///
649    /// More information:
650    ///  - [ECMAScript reference][spec]
651    ///  - [MDN documentation][mdn]
652    ///
653    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.unicodesets
654    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicodeSets
655    pub(crate) fn get_unicode_sets(
656        this: &JsValue,
657        _: &[JsValue],
658        context: &mut Context,
659    ) -> JsResult<JsValue> {
660        Self::regexp_has_flag(this, b'v', context)
661    }
662
663    /// `get RegExp.prototype.sticky`
664    ///
665    /// This flag indicates that it matches only from the index indicated by the `lastIndex` property
666    /// of this regular expression in the target string (and does not attempt to match from any later indexes).
667    ///
668    /// More information:
669    ///  - [ECMAScript reference][spec]
670    ///  - [MDN documentation][mdn]
671    ///
672    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.sticky
673    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky
674    pub(crate) fn get_sticky(
675        this: &JsValue,
676        _: &[JsValue],
677        context: &mut Context,
678    ) -> JsResult<JsValue> {
679        Self::regexp_has_flag(this, b'y', context)
680    }
681
682    /// `get RegExp.prototype.flags`
683    ///
684    /// The `flags` property returns a string consisting of the [`flags`][flags] of the current regular expression object.
685    ///
686    /// More information:
687    ///  - [ECMAScript reference][spec]
688    ///  - [MDN documentation][mdn]
689    ///
690    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.flags
691    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags
692    /// [flags]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Advanced_searching_with_flags_2
693    pub(crate) fn get_flags(
694        this: &JsValue,
695        _: &[JsValue],
696        context: &mut Context,
697    ) -> JsResult<JsValue> {
698        // 1. Let R be the this value.
699        // 2. If R is not an Object, throw a TypeError exception.
700        let Some(object) = this.as_object() else {
701            return Err(JsNativeError::typ()
702                .with_message("RegExp.prototype.flags getter called on non-object")
703                .into());
704        };
705
706        // 3. Let codeUnits be a new empty List.
707        let mut code_units = String::new();
708
709        // 4. Let hasIndices be ToBoolean(? Get(R, "hasIndices")).
710        // 5. If hasIndices is true, append the code unit 0x0064 (LATIN SMALL LETTER D) to codeUnits.
711        if object.get(js_string!("hasIndices"), context)?.to_boolean() {
712            code_units.push('d');
713        }
714
715        // 6. Let global be ToBoolean(? Get(R, "global")).
716        // 7. If global is true, append the code unit 0x0067 (LATIN SMALL LETTER G) to codeUnits.
717        if object.get(js_string!("global"), context)?.to_boolean() {
718            code_units.push('g');
719        }
720
721        // 8. Let ignoreCase be ToBoolean(? Get(R, "ignoreCase")).
722        // 9. If ignoreCase is true, append the code unit 0x0069 (LATIN SMALL LETTER I) to codeUnits.
723        if object.get(js_string!("ignoreCase"), context)?.to_boolean() {
724            code_units.push('i');
725        }
726
727        // 10. Let multiline be ToBoolean(? Get(R, "multiline")).
728        // 11. If multiline is true, append the code unit 0x006D (LATIN SMALL LETTER M) to codeUnits.
729        if object.get(js_string!("multiline"), context)?.to_boolean() {
730            code_units.push('m');
731        }
732
733        // 12. Let dotAll be ToBoolean(? Get(R, "dotAll")).
734        // 13. If dotAll is true, append the code unit 0x0073 (LATIN SMALL LETTER S) to codeUnits.
735        if object.get(js_string!("dotAll"), context)?.to_boolean() {
736            code_units.push('s');
737        }
738
739        // 14. Let unicode be ToBoolean(? Get(R, "unicode")).
740        // 15. If unicode is true, append the code unit 0x0075 (LATIN SMALL LETTER U) to codeUnits.
741        if object.get(js_string!("unicode"), context)?.to_boolean() {
742            code_units.push('u');
743        }
744
745        // 16. Let unicodeSets be ToBoolean(? Get(R, "unicodeSets")).
746        // 17. If unicodeSets is true, append the code unit 0x0076 (LATIN SMALL LETTER V) to codeUnits.
747        if object.get(js_string!("unicodeSets"), context)?.to_boolean() {
748            code_units.push('v');
749        }
750
751        // 18. Let sticky be ToBoolean(? Get(R, "sticky")).
752        // 19. If sticky is true, append the code unit 0x0079 (LATIN SMALL LETTER Y) to codeUnits.
753        if object.get(js_string!("sticky"), context)?.to_boolean() {
754            code_units.push('y');
755        }
756
757        // 20. Return the String value whose code units are the elements of the List codeUnits.
758        //     If codeUnits has no elements, the empty String is returned.
759        Ok(JsString::from(code_units).into())
760    }
761
762    /// `get RegExp.prototype.source`
763    ///
764    /// The `source` property returns a `String` containing the source text of the regexp object,
765    /// and it doesn't contain the two forward slashes on both sides and any flags.
766    ///
767    /// More information:
768    ///  - [ECMAScript reference][spec]
769    ///  - [MDN documentation][mdn]
770    ///
771    /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.source
772    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source
773    pub(crate) fn get_source(
774        this: &JsValue,
775        _: &[JsValue],
776        context: &mut Context,
777    ) -> JsResult<JsValue> {
778        // 1. Let R be the this value.
779        // 2. If Type(R) is not Object, throw a TypeError exception.
780        let Some(object) = this.as_object() else {
781            return Err(JsNativeError::typ()
782                .with_message("RegExp.prototype.source method called on incompatible value")
783                .into());
784        };
785
786        let casted = object.downcast_ref::<RegExp>();
787        match casted {
788            // 3. If R does not have an [[OriginalSource]] internal slot, then
789            None => {
790                // a. If SameValue(R, %RegExp.prototype%) is true, return "(?:)".
791                // b. Otherwise, throw a TypeError exception.
792                if JsValue::same_value(
793                    this,
794                    &JsValue::new(context.intrinsics().constructors().regexp().prototype()),
795                ) {
796                    Ok(JsValue::new(js_string!("(?:)")))
797                } else {
798                    Err(JsNativeError::typ()
799                        .with_message("RegExp.prototype.source method called on incompatible value")
800                        .into())
801                }
802            }
803            // 4. Assert: R has an [[OriginalFlags]] internal slot.
804            Some(re) => {
805                // 5. Let src be R.[[OriginalSource]].
806                // 6. Let flags be R.[[OriginalFlags]].
807                // 7. Return EscapeRegExpPattern(src, flags).
808                Ok(Self::escape_pattern(
809                    &re.original_source,
810                    &re.original_flags,
811                ))
812            }
813        }
814    }
815
816    /// `22.2.3.2.5 EscapeRegExpPattern ( P, F )`
817    ///
818    /// More information:
819    ///  - [ECMAScript reference][spec]
820    ///
821    /// [spec]: https://tc39.es/ecma262/#sec-escaperegexppattern
822    fn escape_pattern(src: &JsString, _flags: &JsString) -> JsValue {
823        if src.is_empty() {
824            js_string!("(?:)").into()
825        } else {
826            let mut s = Vec::with_capacity(src.len());
827            let mut buf = [0; 2];
828            for c in src.code_points() {
829                match c {
830                    CodePoint::Unicode('/') => s.extend_from_slice(utf16!(r"\/")),
831                    CodePoint::Unicode('\n') => s.extend_from_slice(utf16!(r"\n")),
832                    CodePoint::Unicode('\r') => s.extend_from_slice(utf16!(r"\r")),
833                    CodePoint::Unicode('\u{2028}') => s.extend_from_slice(utf16!(r"\u2028")),
834                    CodePoint::Unicode('\u{2029}') => s.extend_from_slice(utf16!(r"\u2029")),
835                    CodePoint::Unicode(c) => s.extend_from_slice(c.encode_utf16(&mut buf)),
836                    CodePoint::UnpairedSurrogate(surr) => s.push(surr),
837                }
838            }
839
840            JsValue::new(js_string!(&s[..]))
841        }
842    }
843
844    /// `RegExp.escape( string )`
845    ///
846    /// The `RegExp.escape()` static method escapes any potential regex syntax characters in a string,
847    /// and returns a new string that can be safely used as a literal pattern for the `RegExp()` constructor.
848    ///
849    /// More information:
850    ///  - [ECMAScript reference][spec]
851    ///  - [MDN documentation][mdn]
852    ///
853    /// [spec]: https://tc39.es/proposal-regex-escaping/#sec-regexp.escape
854    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape
855    ///
856    /// Helper function to check if a character is a `WhiteSpace` character
857    fn is_whitespace(ch: char) -> bool {
858        matches!(
859            ch,
860            '\u{0009}' | // <TAB>
861            '\u{000B}' | // <VT>
862            '\u{000C}' | // <FF>
863            '\u{0020}' | // <SP>
864            '\u{00A0}' | // <NBSP>
865            '\u{FEFF}' | // <ZWNBSP>
866            '\u{1680}' | // Ogham Space Mark
867            '\u{2000}' | '\u{2001}' | '\u{2002}' | '\u{2003}' | '\u{2004}' |
868            '\u{2005}' | '\u{2006}' | '\u{2007}' | '\u{2008}' | '\u{2009}' |
869            '\u{200A}' | // Various space separators
870            '\u{202F}' | // Narrow No-Break Space
871            '\u{205F}' | // Medium Mathematical Space
872            '\u{3000}' // Ideographic Space
873        )
874    }
875
876    /// Helper function to check if a character is a `LineTerminator` character
877    fn is_line_terminator(ch: char) -> bool {
878        matches!(
879            ch,
880            '\u{000A}' | // <LF>
881            '\u{000D}' | // <CR>
882            '\u{2028}' | // <LS>
883            '\u{2029}' // <PS>
884        )
885    }
886
887    pub(crate) fn escape(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
888        let arg = args.get_or_undefined(0);
889
890        // 1. If S is not a String, throw a TypeError exception.
891        let Some(string) = arg.as_string() else {
892            return Err(JsNativeError::typ()
893                .with_message("RegExp.escape requires a string argument")
894                .into());
895        };
896
897        // 2. Let escaped be the empty String.
898        let mut escaped = CommonJsStringBuilder::new();
899
900        // 3. Let cpList be StringToCodePoints(S).
901        // 4. For each code point c of cpList, do
902        for (index, c) in string.code_points().enumerate() {
903            let code = c.as_u32();
904
905            // 4.a. If escaped is the empty String and c is matched by either DecimalDigit or AsciiLetter, then
906            if index == 0
907                && let CodePoint::Unicode(ch) = c
908                && (ch.is_ascii_digit() || ch.is_ascii_alphabetic())
909            {
910                // 4.a.ii-v. Escape using \xXX format
911                let escape_seq = format!("\\x{code:02x}");
912                escaped.push(escape_seq.as_str());
913                continue;
914            }
915
916            // 4.b. Else, set escaped to the string-concatenation of escaped and EncodeForRegExpEscape(c).
917            match c {
918                CodePoint::Unicode(ch) => {
919                    // EncodeForRegExpEscape step 1: SyntaxCharacter or U+002F (SOLIDUS)
920                    if matches!(
921                        ch,
922                        '^' | '$'
923                            | '\\'
924                            | '.'
925                            | '*'
926                            | '+'
927                            | '?'
928                            | '('
929                            | ')'
930                            | '['
931                            | ']'
932                            | '{'
933                            | '}'
934                            | '|'
935                            | '/'
936                    ) {
937                        escaped.push('\\');
938                        escaped.push(ch);
939                    }
940                    // Step 2: ControlEscape characters (Table 64)
941                    else if ch == '\x09' {
942                        escaped.push("\\t");
943                    } else if ch == '\x0A' {
944                        escaped.push("\\n");
945                    } else if ch == '\x0B' {
946                        escaped.push("\\v");
947                    } else if ch == '\x0C' {
948                        escaped.push("\\f");
949                    } else if ch == '\x0D' {
950                        escaped.push("\\r");
951                    }
952                    // Step 3-5: otherPunctuators or WhiteSpace or LineTerminator
953                    else if matches!(
954                        ch,
955                        ',' | '-'
956                            | '='
957                            | '<'
958                            | '>'
959                            | '#'
960                            | '&'
961                            | '!'
962                            | '%'
963                            | ':'
964                            | ';'
965                            | '@'
966                            | '~'
967                            | '\''
968                            | '`'
969                            | '"'
970                    ) || Self::is_whitespace(ch)
971                        || Self::is_line_terminator(ch)
972                    {
973                        let code = ch as u32;
974                        if code <= 0xFF {
975                            // Use \xXX format
976                            let escape_seq = format!("\\x{code:02x}");
977                            escaped.push(escape_seq.as_str());
978                        } else {
979                            // Use \uXXXX format
980                            let escape_seq = format!("\\u{code:04x}");
981                            escaped.push(escape_seq.as_str());
982                        }
983                    }
984                    // Step 6: All other Unicode characters
985                    else {
986                        escaped.push(ch);
987                    }
988                }
989                CodePoint::UnpairedSurrogate(surr) => {
990                    // Escape unpaired surrogates using \uXXXX format
991                    let escape_seq = format!("\\u{surr:04x}");
992                    escaped.push(escape_seq.as_str());
993                }
994            }
995        }
996
997        // 5. Return escaped.
998        Ok(JsValue::new(escaped.build()))
999    }
1000
1001    /// `RegExp.prototype.test( string )`
1002    ///
1003    /// The `test()` method executes a search for a match between a regular expression and a specified string.
1004    ///
1005    /// Returns `true` or `false`.
1006    ///
1007    /// More information:
1008    ///  - [ECMAScript reference][spec]
1009    ///  - [MDN documentation][mdn]
1010    ///
1011    /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.test
1012    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test
1013    pub(crate) fn test(
1014        this: &JsValue,
1015        args: &[JsValue],
1016        context: &mut Context,
1017    ) -> JsResult<JsValue> {
1018        // 1. Let R be the this value.
1019        // 2. If Type(R) is not Object, throw a TypeError exception.
1020        let this = this.as_object().ok_or_else(|| {
1021            JsNativeError::typ()
1022                .with_message("RegExp.prototype.test method called on incompatible value")
1023        })?;
1024
1025        // 3. Let string be ? ToString(S).
1026        let arg_str = args
1027            .first()
1028            .cloned()
1029            .unwrap_or_default()
1030            .to_string(context)?;
1031
1032        // 4. Let match be ? RegExpExec(R, string).
1033        // With the intrinsic `exec`, only whether it matches is needed, not the result array.
1034        let exec = this.get(js_string!("exec"), context)?;
1035        if let Some(rx) = Self::intrinsic_exec_target(&this, &exec) {
1036            let data = rx.borrow().data().clone();
1037            let matched = Self::builtin_exec_match(&rx.upcast(), &data, &arg_str, context)?;
1038            return Ok(JsValue::new(matched.is_some()));
1039        }
1040        let m = Self::abstract_exec_with(&this, &exec, arg_str, context)?;
1041
1042        // 5. If match is not null, return true; else return false.
1043        if m.is_some() {
1044            Ok(JsValue::new(true))
1045        } else {
1046            Ok(JsValue::new(false))
1047        }
1048    }
1049
1050    /// `RegExp.prototype.exec( string )`
1051    ///
1052    /// The `exec()` method executes a search for a match in a specified string.
1053    ///
1054    /// Returns a result array, or `null`.
1055    ///
1056    /// More information:
1057    ///  - [ECMAScript reference][spec]
1058    ///  - [MDN documentation][mdn]
1059    ///
1060    /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.exec
1061    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec
1062    pub(crate) fn exec(
1063        this: &JsValue,
1064        args: &[JsValue],
1065        context: &mut Context,
1066    ) -> JsResult<JsValue> {
1067        // 1. Let R be the this value.
1068        // 2. Perform ? RequireInternalSlot(R, [[RegExpMatcher]]).
1069        let this = this.as_object();
1070        let obj = this
1071            .and_then(|o| o.downcast::<RegExp>().ok())
1072            .ok_or_else(|| {
1073                JsNativeError::typ().with_message("RegExp.prototype.exec called with invalid value")
1074            })?;
1075
1076        // 3. Let S be ? ToString(string).
1077        let arg_str = args.get_or_undefined(0).to_string(context)?;
1078
1079        // 4. Return ? RegExpBuiltinExec(R, S).
1080        (Self::abstract_builtin_exec(obj, &arg_str, context)?)
1081            .map_or_else(|| Ok(JsValue::null()), |v| Ok(v.into()))
1082    }
1083
1084    /// `22.2.5.2.1 RegExpExec ( R, S )`
1085    ///
1086    /// More information:
1087    ///  - [ECMAScript reference][spec]
1088    ///
1089    /// [spec]: https://tc39.es/ecma262/#sec-regexpexec
1090    pub(crate) fn abstract_exec(
1091        this: &JsObject,
1092        input: JsString,
1093        context: &mut Context,
1094    ) -> JsResult<Option<JsObject>> {
1095        // 1. Assert: Type(R) is Object.
1096        // 2. Assert: Type(S) is String.
1097
1098        // 3. Let exec be ? Get(R, "exec").
1099        let exec = this.get(js_string!("exec"), context)?;
1100
1101        Self::abstract_exec_with(this, &exec, input, context)
1102    }
1103
1104    /// The `RegExp` that `exec` would run `RegExpBuiltinExec` on when `exec` is the intrinsic
1105    /// `RegExp.prototype.exec` and `this` is a `RegExp`, so callers can skip the call.
1106    fn intrinsic_exec_target(this: &JsObject, exec: &JsValue) -> Option<JsObject<RegExp>> {
1107        let exec = exec.as_object()?;
1108        let native = exec.downcast_ref::<NativeFunctionObject>()?;
1109        if !native.f.is_fn_ptr(Self::exec) {
1110            return None;
1111        }
1112        this.clone().downcast::<RegExp>().ok()
1113    }
1114
1115    /// Steps 4 to 6 of `RegExpExec`, with `exec` already read from `this`.
1116    fn abstract_exec_with(
1117        this: &JsObject,
1118        exec: &JsValue,
1119        input: JsString,
1120        context: &mut Context,
1121    ) -> JsResult<Option<JsObject>> {
1122        if let Some(rx) = Self::intrinsic_exec_target(this, exec) {
1123            return Self::abstract_builtin_exec(rx, &input, context);
1124        }
1125
1126        // 4. If IsCallable(exec) is true, then
1127        if let Some(exec) = exec.as_callable() {
1128            // a. Let result be ? Call(exec, R, « S »).
1129            let result = exec.call(&this.clone().into(), &[input.into()], context)?;
1130
1131            // b. If Type(result) is neither Object nor Null, throw a TypeError exception.
1132            if !result.is_object() && !result.is_null() {
1133                return Err(JsNativeError::typ()
1134                    .with_message("regexp exec returned neither object nor null")
1135                    .into());
1136            }
1137
1138            // c. Return result.
1139            return Ok(result.as_object());
1140        }
1141
1142        // 5. Perform ? RequireInternalSlot(R, [[RegExpMatcher]]).
1143        let Ok(this) = this.clone().downcast::<RegExp>() else {
1144            return Err(JsNativeError::typ()
1145                .with_message("RegExpExec called with invalid value")
1146                .into());
1147        };
1148
1149        // 6. Return ? RegExpBuiltinExec(R, S).
1150        Self::abstract_builtin_exec(this, &input, context)
1151    }
1152
1153    /// `22.2.7.2 RegExpBuiltinExec ( R, S )`
1154    ///
1155    /// More information:
1156    ///  - [ECMAScript reference][spec]
1157    ///
1158    /// [spec]: https://tc39.es/ecma262/#sec-regexpbuiltinexec
1159    pub(crate) fn abstract_builtin_exec(
1160        this: JsObject<RegExp>,
1161        input: &JsString,
1162        context: &mut Context,
1163    ) -> JsResult<Option<JsObject>> {
1164        let rx = this.borrow().data().clone();
1165        let this = this.upcast();
1166
1167        let Some((match_value, last_index)) = Self::builtin_exec_match(&this, &rx, input, context)?
1168        else {
1169            return Ok(None);
1170        };
1171
1172        // 6. If flags contains "d", let hasIndices be true; else let hasIndices be false.
1173        let has_indices = rx.flags.contains(RegExpFlags::HAS_INDICES);
1174
1175        // 14. Let e be r's endIndex value.
1176        // 15. If fullUnicode is true, set e to GetStringIndex(S, e).
1177        // NOTE: Step 15 is already taken care of by regress.
1178        let e = match_value.end();
1179
1180        Self::build_exec_result(&match_value, last_index, e, has_indices, input, context).map(Some)
1181    }
1182
1183    /// Steps 1 to 16 of `RegExpBuiltinExec`: runs the matcher from `lastIndex` and updates
1184    /// `lastIndex`, returning the match and its start index, or `None` where the result is null.
1185    /// `RegExp.prototype.test` needs no more than this.
1186    fn builtin_exec_match(
1187        this: &JsObject,
1188        rx: &RegExp,
1189        input: &JsString,
1190        context: &mut Context,
1191    ) -> JsResult<Option<(regress::Match, u64)>> {
1192        // 1. Let length be the length of S.
1193        let length = input.len() as u64;
1194
1195        // 2. Let lastIndex be ℝ(? ToLength(? Get(R, "lastIndex"))).
1196        let mut last_index = this
1197            .get(js_string!("lastIndex"), context)?
1198            .to_length(context)?;
1199
1200        // 3. Let flags be R.[[OriginalFlags]].
1201        let flags = rx.flags;
1202
1203        // 4. If flags contains "g", let global be true; else let global be false.
1204        let global = flags.contains(RegExpFlags::GLOBAL);
1205
1206        // 5. If flags contains "y", let sticky be true; else let sticky be false.
1207        let sticky = flags.contains(RegExpFlags::STICKY);
1208
1209        // 7. If global is false and sticky is false, set lastIndex to 0.
1210        if !global && !sticky {
1211            last_index = 0;
1212        }
1213
1214        // 8. Let matcher be R.[[RegExpMatcher]].
1215        let matcher = &rx.matcher;
1216
1217        // 9. If flags contains "u" or flags contains "v", let fullUnicode be true; else let fullUnicode be false.
1218        let full_unicode =
1219            flags.contains(RegExpFlags::UNICODE) || flags.contains(RegExpFlags::UNICODE_SETS);
1220
1221        // NOTE: The following steps are take care of by regress:
1222        //
1223        // SKIP: 10. Let matchSucceeded be false.
1224        // SKIP: 11. If fullUnicode is true, let input be StringToCodePoints(S). Otherwise, let input be a List whose elements are the code units that are the elements of S.
1225        // SKIP: 12. NOTE: Each element of input is considered to be a character.
1226        // SKIP: 13. Repeat, while matchSucceeded is false,
1227
1228        // 13.a. If lastIndex > length, then
1229        if last_index > length {
1230            // i. If global is true or sticky is true, then
1231            if global || sticky {
1232                // 1. Perform ? Set(R, "lastIndex", +0𝔽, true).
1233                this.set(js_string!("lastIndex"), 0, true, context)?;
1234            }
1235
1236            // ii. Return null.
1237            return Ok(None);
1238        }
1239
1240        // 13.b. Let inputIndex be the index into input of the character that was obtained from element lastIndex of S.
1241        // 13.c. Let r be matcher(input, inputIndex).
1242        let r: Option<regress::Match> = match (full_unicode, input.as_str().variant()) {
1243            (true | false, JsStrVariant::Latin1(bytes)) => {
1244                // TODO: Currently regress does not support latin1 encoding.
1245                // A short input is widened on the stack rather than in a new allocation.
1246                let mut short = ArrayVec::<u16, 256>::new();
1247                let long;
1248                let input: &[u16] = if bytes.len() <= short.capacity() {
1249                    short.extend(bytes.iter().map(|&byte| u16::from(byte)));
1250                    &short
1251                } else {
1252                    long = input.to_vec();
1253                    &long
1254                };
1255
1256                // NOTE: We can use the faster ucs2 variant since there will never be two byte unicode.
1257                matcher.find_from_ucs2(input, last_index as usize).next()
1258            }
1259            (true, JsStrVariant::Utf16(input)) => {
1260                matcher.find_from_utf16(input, last_index as usize).next()
1261            }
1262            (false, JsStrVariant::Utf16(input)) => {
1263                matcher.find_from_ucs2(input, last_index as usize).next()
1264            }
1265        };
1266
1267        let Some(match_value) = r else {
1268            // d. If r is failure, then
1269            //
1270            // NOTE: Merged the following steps (since we no longer have a loop):
1271            //       13.d.i. If sticky is true, then
1272            //       13.a.i. If global is true or sticky is true, then
1273            if global || sticky {
1274                // 1. Perform ? Set(R, "lastIndex", +0𝔽, true).
1275                this.set(js_string!("lastIndex"), 0, true, context)?;
1276            }
1277
1278            // MOVE: ii. Set lastIndex to AdvanceStringIndex(S, lastIndex, fullUnicode).
1279            // NOTE: Handled within the regress matches iterator, see below for last_index assignment.
1280
1281            // NOTE: Merged  and  steps:
1282            //       13.a.ii.  Return null.
1283            //       13.d.i.2. Return null.
1284            return Ok(None);
1285        };
1286
1287        // e. Else
1288        // SKIP: i. Assert: r is a MatchState.
1289        // SKIP: ii. Set matchSucceeded to true.
1290
1291        // NOTE: regress currently doesn't support the sticky flag so we have to emulate it.
1292        if sticky && match_value.start() != last_index as usize {
1293            // 1. Perform ? Set(R, "lastIndex", +0𝔽, true).
1294            this.set(js_string!("lastIndex"), 0, true, context)?;
1295
1296            // 2. Return null.
1297            return Ok(None);
1298        }
1299
1300        // 13.d.ii. Set lastIndex to AdvanceStringIndex(S, lastIndex, fullUnicode).
1301        // NOTE: Calculation of last_index is done in regress.
1302        last_index = match_value.start() as u64;
1303
1304        // 16. If global is true or sticky is true, then
1305        if global || sticky {
1306            // a. Perform ? Set(R, "lastIndex", 𝔽(e), true).
1307            this.set(js_string!("lastIndex"), match_value.end(), true, context)?;
1308        }
1309
1310        Ok(Some((match_value, last_index)))
1311    }
1312
1313    /// Steps 17 to 35 of `RegExpBuiltinExec`: the result array of a match. The `indices`
1314    /// arrays are only built when the regular expression has the `d` flag.
1315    fn build_exec_result(
1316        match_value: &regress::Match,
1317        last_index: u64,
1318        e: usize,
1319        has_indices: bool,
1320        input: &JsString,
1321        context: &mut Context,
1322    ) -> JsResult<JsObject> {
1323        // 17. Let n be the number of elements in r's captures List.
1324        let n = match_value.captures.len() as u64;
1325        // 18. Assert: n = R.[[RegExpRecord]].[[CapturingGroupsCount]].
1326        // 19. Assert: n < 232 - 1.
1327        debug_assert!(n < (1u64 << 32) - 1);
1328
1329        // 20. Let A be ! ArrayCreate(n + 1).
1330        // 21. Assert: The mathematical value of A's "length" property is n + 1.
1331        let a = Array::array_create(n + 1, None, context)?;
1332
1333        // 22. Perform ! CreateDataPropertyOrThrow(A, "index", 𝔽(lastIndex)).
1334        a.create_data_property_or_throw(js_string!("index"), last_index, context)?;
1335
1336        // 23. Perform ! CreateDataPropertyOrThrow(A, "input", S).
1337        a.create_data_property_or_throw(js_string!("input"), input.clone(), context)?;
1338
1339        // 24. Let match be the Match Record { [[StartIndex]]: lastIndex, [[EndIndex]]: e }.
1340        // Immediately convert it to an array according to 22.2.7.7 GetMatchIndexPair(S, match)
1341        // 1. Assert: match.[[StartIndex]] ≤ match.[[EndIndex]] ≤ the length of S.
1342        // 2. Return CreateArrayFromList(« 𝔽(match.[[StartIndex]]), 𝔽(match.[[EndIndex]]) »).
1343        let indices = if has_indices {
1344            let match_record = Array::create_array_from_list(
1345                [match_value.start().into(), match_value.end().into()],
1346                context,
1347            );
1348
1349            // 25. Let indices be a new empty List.
1350            let indices = Array::array_create(n + 1, None, context)?;
1351
1352            // 27. Append match to indices.
1353            indices.create_data_property_or_throw(0, match_record, context)?;
1354            Some(indices)
1355        } else {
1356            None
1357        };
1358
1359        // 28. Let matchedSubstr be GetMatchString(S, match).
1360        let matched_substr = input.get_expect((last_index as usize)..(e));
1361
1362        // 29. Perform ! CreateDataPropertyOrThrow(A, "0", matchedSubstr).
1363        a.create_data_property_or_throw(0, matched_substr, context)?;
1364
1365        let named_groups = match_value
1366            .named_groups()
1367            .collect::<Vec<(&str, Option<Range>)>>();
1368
1369        // Combines:
1370        // 26. Let groupNames be a new empty List.
1371        // 30. If R contains any GroupName, then
1372        // 31. Else,
1373        // 33. For each integer i such that 1 ≤ i ≤ n, in ascending order, do
1374        #[allow(clippy::if_not_else)]
1375        let (groups, group_names) = if !named_groups.clone().is_empty() {
1376            // a. Let groups be OrdinaryObjectCreate(null).
1377            let groups = JsObject::with_null_proto();
1378            let group_names = JsObject::with_null_proto();
1379
1380            // e. If the ith capture of R was defined with a GroupName, then
1381            // i. Let s be the CapturingGroupName of that GroupName.
1382            // ii. Perform ! CreateDataPropertyOrThrow(groups, s, capturedValue).
1383            // iii. Append s to groupNames.
1384            for (name, range) in named_groups {
1385                let name = js_string!(name);
1386                if let Some(range) = range {
1387                    let value = input.get_expect(range.clone());
1388
1389                    groups.create_data_property_or_throw(name.clone(), value, context)?;
1390
1391                    // 22.2.7.8 MakeMatchIndicesIndexPairArray ( S, indices, groupNames, hasGroups )
1392                    // a. Let matchIndices be indices[i].
1393                    // b. If matchIndices is not undefined, then
1394                    // i. Let matchIndexPair be GetMatchIndexPair(S, matchIndices).
1395                    // d. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(i)), matchIndexPair).
1396                    group_names.create_data_property_or_throw(
1397                        name.clone(),
1398                        Array::create_array_from_list(
1399                            [range.start.into(), range.end.into()],
1400                            context,
1401                        ),
1402                        context,
1403                    )?;
1404                } else {
1405                    groups.create_data_property_or_throw(
1406                        name.clone(),
1407                        JsValue::undefined(),
1408                        context,
1409                    )?;
1410
1411                    // 22.2.7.8 MakeMatchIndicesIndexPairArray ( S, indices, groupNames, hasGroups )
1412                    // c. Else,
1413                    // i. Let matchIndexPair be undefined.
1414                    // d. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(i)), matchIndexPair).
1415                    group_names.create_data_property_or_throw(
1416                        name,
1417                        JsValue::undefined(),
1418                        context,
1419                    )?;
1420                }
1421            }
1422
1423            (groups.into(), group_names.into())
1424        } else {
1425            // a. Let groups be undefined.
1426            (JsValue::undefined(), JsValue::undefined())
1427        };
1428
1429        // 22.2.7.8 MakeMatchIndicesIndexPairArray ( S, indices, groupNames, hasGroups )
1430        // 8. Perform ! CreateDataPropertyOrThrow(A, "groups", groups).
1431        if let Some(indices) = &indices {
1432            indices.create_data_property_or_throw(js_string!("groups"), group_names, context)?;
1433        }
1434
1435        // 32. Perform ! CreateDataPropertyOrThrow(A, "groups", groups).
1436        a.create_data_property_or_throw(js_string!("groups"), groups, context)?;
1437
1438        // 27. For each integer i such that i ≥ 1 and i ≤ n, in ascending order, do
1439        for i in 1..=n {
1440            // a. Let captureI be ith element of r's captures List.
1441            let capture = match_value.group(i as usize);
1442
1443            // b. If captureI is undefined, let capturedValue be undefined.
1444            // c. Else if fullUnicode is true, then
1445            // d. Else,
1446            let captured_value = capture.clone().map_or_else(JsValue::undefined, |range| {
1447                js_string!(input.get_expect(range)).into()
1448            });
1449
1450            // e. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(i)), capturedValue).
1451            a.create_data_property_or_throw(i, captured_value.clone(), context)?;
1452
1453            // 22.2.7.8 MakeMatchIndicesIndexPairArray ( S, indices, groupNames, hasGroups )
1454            if let Some(indices) = &indices {
1455                // b. If matchIndices is not undefined, then
1456                // i. Let matchIndexPair be GetMatchIndexPair(S, matchIndices).
1457                // c. Else,
1458                // i. Let matchIndexPair be undefined.
1459                let indices_range = capture.map_or_else(JsValue::undefined, |range| {
1460                    Array::create_array_from_list([range.start.into(), range.end.into()], context)
1461                        .into()
1462                });
1463
1464                // d. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(i)), matchIndexPair).
1465                indices.create_data_property_or_throw(i, indices_range, context)?;
1466            }
1467        }
1468
1469        // 34. If hasIndices is true, then
1470        // a. Let indicesArray be MakeMatchIndicesIndexPairArray(S, indices, groupNames, hasGroups).
1471        // b. Perform ! CreateDataPropertyOrThrow(A, "indices", indicesArray).
1472        if let Some(indices) = indices {
1473            a.create_data_property_or_throw(js_string!("indices"), indices, context)?;
1474        }
1475
1476        // 35. Return A.
1477        Ok(a)
1478    }
1479
1480    /// `RegExp.prototype[ @@match ]( string )`
1481    ///
1482    /// This method retrieves the matches when matching a string against a regular expression.
1483    ///
1484    /// More information:
1485    ///  - [ECMAScript reference][spec]
1486    ///  - [MDN documentation][mdn]
1487    ///
1488    /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@match
1489    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@match
1490    pub(crate) fn r#match(
1491        this: &JsValue,
1492        args: &[JsValue],
1493        context: &mut Context,
1494    ) -> JsResult<JsValue> {
1495        // 1. Let rx be the this value.
1496        // 2. If rx is not an Object, throw a TypeError exception.
1497        let Some(rx) = this.as_object() else {
1498            return Err(JsNativeError::typ()
1499                .with_message("RegExp.prototype.match method called on incompatible value")
1500                .into());
1501        };
1502
1503        // 3. Let S be ? ToString(string).
1504        let arg_str = args.get_or_undefined(0).to_string(context)?;
1505
1506        // 4. Let flags be ? ToString(? Get(rx, "flags")).
1507        let flags = rx.get(js_string!("flags"), context)?.to_string(context)?;
1508
1509        // 5. If flags does not contain "g", then
1510        if !flags.contains(b'g') {
1511            // a. Return ? RegExpExec(rx, S).
1512            return (Self::abstract_exec(&rx, arg_str, context)?)
1513                .map_or_else(|| Ok(JsValue::null()), |v| Ok(v.into()));
1514        }
1515
1516        // 6. Else,
1517
1518        // a. If flags contains "u" or flags contains "v", let fullUnicode be true. Otherwise, let fullUnicode be false.
1519        let full_unicode = flags.contains(b'u') || flags.contains(b'v');
1520
1521        // b. Perform ? Set(rx, "lastIndex", +0𝔽, true).
1522        rx.set(js_string!("lastIndex"), 0, true, context)?;
1523
1524        // c. Let A be ! ArrayCreate(0).
1525        let a = Array::array_create(0, None, context)?;
1526
1527        // d. Let n be 0.
1528        let mut n = 0;
1529
1530        // e. Repeat,
1531        loop {
1532            // i. Let result be ? RegExpExec(rx, S).
1533            let result = Self::abstract_exec(&rx, arg_str.clone(), context)?;
1534
1535            // ii. If result is null, then
1536            // iii. Else,
1537            if let Some(result) = result {
1538                // 1. Let matchStr be ? ToString(? Get(result, "0")).
1539                let match_str = result.get(0, context)?.to_string(context)?;
1540
1541                // 2. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), matchStr).
1542                a.create_data_property_or_throw(n, match_str.clone(), context)?;
1543
1544                // 3. If matchStr is the empty String, then
1545                if match_str.is_empty() {
1546                    // a. Let thisIndex be ℝ(? ToLength(? Get(rx, "lastIndex"))).
1547                    let this_index = rx
1548                        .get(js_string!("lastIndex"), context)?
1549                        .to_length(context)?;
1550
1551                    // b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode).
1552                    let next_index = advance_string_index(&arg_str, this_index, full_unicode);
1553
1554                    // c. Perform ? Set(rx, "lastIndex", 𝔽(nextIndex), true).
1555                    rx.set(
1556                        js_string!("lastIndex"),
1557                        JsValue::new(next_index),
1558                        true,
1559                        context,
1560                    )?;
1561                }
1562
1563                // 4. Set n to n + 1.
1564                n += 1;
1565            } else {
1566                // 1. If n = 0, return null.
1567                if n == 0 {
1568                    return Ok(JsValue::null());
1569                }
1570                // 2. Return A.
1571                return Ok(a.into());
1572            }
1573        }
1574    }
1575
1576    /// `RegExp.prototype.toString()`
1577    ///
1578    /// Return a string representing the regular expression.
1579    ///
1580    /// More information:
1581    ///  - [ECMAScript reference][spec]
1582    ///  - [MDN documentation][mdn]
1583    ///
1584    /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.tostring
1585    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/toString
1586    #[allow(clippy::wrong_self_convention)]
1587    pub(crate) fn to_string(
1588        this: &JsValue,
1589        _: &[JsValue],
1590        context: &mut Context,
1591    ) -> JsResult<JsValue> {
1592        // 1. Let R be the this value.
1593        // 2. If R is not an Object, throw a TypeError exception.
1594        let regexp = this.as_object().ok_or_else(|| {
1595            JsNativeError::typ()
1596                .with_message("RegExp.prototype.toString method called on incompatible value")
1597        })?;
1598
1599        // 3. Let pattern be ? ToString(? Get(R, "source")).
1600        let pattern = regexp
1601            .get(js_string!("source"), context)?
1602            .to_string(context)?;
1603
1604        // 4. Let flags be ? ToString(? Get(R, "flags")).
1605        let flags = regexp
1606            .get(js_string!("flags"), context)?
1607            .to_string(context)?;
1608
1609        // 5. Let result be the string-concatenation of "/", pattern, "/", and flags.
1610        // 6. Return result.
1611        Ok(js_string!(js_str!("/"), &pattern, js_str!("/"), &flags).into())
1612    }
1613
1614    /// `RegExp.prototype[ @@matchAll ]( string )`
1615    ///
1616    /// The `[@@matchAll]` method returns all matches of the regular expression against a string.
1617    ///
1618    /// More information:
1619    ///  - [ECMAScript reference][spec]
1620    ///  - [MDN documentation][mdn]
1621    ///
1622    /// [spec]: https://tc39.es/ecma262/#sec-regexp-prototype-matchall
1623    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@matchAll
1624    pub(crate) fn match_all(
1625        this: &JsValue,
1626        args: &[JsValue],
1627        context: &mut Context,
1628    ) -> JsResult<JsValue> {
1629        // 1. Let R be the this value.
1630        // 2. If Type(R) is not Object, throw a TypeError exception.
1631        let regexp = this.as_object().ok_or_else(|| {
1632            JsNativeError::typ()
1633                .with_message("RegExp.prototype.match_all method called on incompatible value")
1634        })?;
1635
1636        // 3. Let S be ? ToString(string).
1637        let arg_str = args.get_or_undefined(0).to_string(context)?;
1638
1639        // 4. Let C be ? SpeciesConstructor(R, %RegExp%).
1640        let c = regexp.species_constructor(StandardConstructors::regexp, context)?;
1641
1642        // 5. Let flags be ? ToString(? Get(R, "flags")).
1643        let flags = regexp
1644            .get(js_string!("flags"), context)?
1645            .to_string(context)?;
1646
1647        // 6. Let matcher be ? Construct(C, « R, flags »).
1648        let matcher = c.construct(&[this.clone(), flags.clone().into()], Some(&c), context)?;
1649
1650        // 7. Let lastIndex be ? ToLength(? Get(R, "lastIndex")).
1651        let last_index = regexp
1652            .get(js_string!("lastIndex"), context)?
1653            .to_length(context)?;
1654
1655        // 8. Perform ? Set(matcher, "lastIndex", lastIndex, true).
1656        matcher.set(js_string!("lastIndex"), last_index, true, context)?;
1657
1658        // 9. If flags contains "g", let global be true.
1659        // 10. Else, let global be false.
1660        let global = flags.contains(b'g');
1661
1662        // 11. If flags contains "u", let fullUnicode be true.
1663        // 12. Else, let fullUnicode be false.
1664        let unicode = flags.contains(b'u') || flags.contains(b'v');
1665
1666        // 13. Return ! CreateRegExpStringIterator(matcher, S, global, fullUnicode).
1667        Ok(RegExpStringIterator::create_regexp_string_iterator(
1668            matcher.clone(),
1669            arg_str,
1670            global,
1671            unicode,
1672            context,
1673        ))
1674    }
1675
1676    /// `RegExp.prototype [ @@replace ] ( string, replaceValue )`
1677    ///
1678    /// The [@@replace]() method replaces some or all matches of a this pattern in a string by a replacement,
1679    /// and returns the result of the replacement as a new string.
1680    /// The replacement can be a string or a function to be called for each match.
1681    ///
1682    /// More information:
1683    ///  - [ECMAScript reference][spec]
1684    ///  - [MDN documentation][mdn]
1685    ///
1686    /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@replace
1687    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@replace
1688    pub(crate) fn replace(
1689        this: &JsValue,
1690        args: &[JsValue],
1691        context: &mut Context,
1692    ) -> JsResult<JsValue> {
1693        // Helper enum.
1694        enum CallableOrString {
1695            FunctionalReplace(JsObject),
1696            ReplaceValue(JsString),
1697        }
1698
1699        // 1. Let rx be the this value.
1700        // 2. If rx is not an Object, throw a TypeError exception.
1701        let rx = this.as_object().ok_or_else(|| {
1702            JsNativeError::typ().with_message(
1703                "RegExp.prototype[Symbol.replace] method called on incompatible value",
1704            )
1705        })?;
1706
1707        // 3. Let S be ? ToString(string).
1708        let s = args.get_or_undefined(0).to_string(context)?;
1709
1710        // 4. Let lengthS be the length of S.
1711        let length_s = s.len();
1712
1713        let replace_value = args.get_or_undefined(1);
1714
1715        // 5. Let functionalReplace be IsCallable(replaceValue).
1716        let functional_replace = replace_value.as_callable();
1717
1718        // 6. If functionalReplace is false, then
1719        let replace_value = if let Some(callable) = functional_replace {
1720            CallableOrString::FunctionalReplace(callable)
1721        } else {
1722            // a. Set replaceValue to ? ToString(replaceValue).
1723            CallableOrString::ReplaceValue(replace_value.to_string(context)?)
1724        };
1725
1726        // 7. Let flags be ? ToString(? Get(rx, "flags")).
1727        let flags = rx.get(js_string!("flags"), context)?.to_string(context)?;
1728
1729        // 8. If flags contains "g", let global be true. Otherwise, let global be false.
1730        let global = flags.contains(b'g');
1731
1732        // 9. If global is true, then
1733        let full_unicode = if global {
1734            // a. If flags contains "u", let fullUnicode be true. Otherwise, let fullUnicode be false.
1735            let full_unicode = flags.contains(b'u');
1736
1737            // b. Perform ? Set(rx, "lastIndex", +0𝔽, true).
1738            rx.set(js_string!("lastIndex"), 0, true, context)?;
1739
1740            full_unicode
1741        } else {
1742            false
1743        };
1744
1745        // 10. Let results be a new empty List.
1746        let mut results = Vec::new();
1747
1748        // SKIPPED: 11. Let done be false.
1749        //
1750        // NOTE(HalidOdat): We don't keep track of `done`, we just break when done is true.
1751
1752        // 12. Repeat, while done is false,
1753        loop {
1754            // a. Let result be ? RegExpExec(rx, S).
1755            let result = Self::abstract_exec(&rx, s.clone(), context)?;
1756
1757            // b. If result is null, set done to true.
1758            let Some(result) = result else {
1759                // SKIPPED: 1. Set done to true.
1760                break;
1761            };
1762
1763            // c. Else,
1764            //  i. Append result to results.
1765            results.push(result.clone());
1766
1767            //  ii. If global is false, then
1768            if !global {
1769                // SKIPPED: 1. Set done to true.
1770                break;
1771            }
1772
1773            //  iii. Else,
1774            //    1. Let matchStr be ? ToString(? Get(result, "0")).
1775            let match_str = result.get(0, context)?.to_string(context)?;
1776
1777            //    2. If matchStr is the empty String, then
1778            if match_str.is_empty() {
1779                // a. Let thisIndex be ℝ(? ToLength(? Get(rx, "lastIndex"))).
1780                let this_index = rx
1781                    .get(js_string!("lastIndex"), context)?
1782                    .to_length(context)?;
1783
1784                // b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode).
1785                let next_index = advance_string_index(&s, this_index, full_unicode);
1786
1787                // c. Perform ? Set(rx, "lastIndex", 𝔽(nextIndex), true).
1788                rx.set(
1789                    js_string!("lastIndex"),
1790                    JsValue::new(next_index),
1791                    true,
1792                    context,
1793                )?;
1794            }
1795        }
1796
1797        // 16. If nextSourcePosition ≥ lengthS, return accumulatedResult.
1798        // 17. Return the string-concatenation of accumulatedResult and the substring of S from nextSourcePosition.
1799
1800        // 13. Let accumulatedResult be the empty String.
1801        let mut accumulated_result = vec![];
1802
1803        // 14. Let nextSourcePosition be 0.
1804        let mut next_source_position = 0;
1805
1806        // 15. For each element result of results, do
1807        for result in results {
1808            // a. Let resultLength be ? LengthOfArrayLike(result).
1809            let result_length = result.length_of_array_like(context)? as i64;
1810
1811            // b. Let nCaptures be max(resultLength - 1, 0).
1812            let n_captures = std::cmp::max(result_length - 1, 0);
1813
1814            // c. Let matched be ? ToString(? Get(result, "0")).
1815            let matched = result.get(0, context)?.to_string(context)?;
1816
1817            // d. Let matchLength be the length of matched.
1818            let match_length = matched.len();
1819
1820            // e. Let position be ? ToIntegerOrInfinity(? Get(result, "index")).
1821            let position = result
1822                .get(js_string!("index"), context)?
1823                .to_integer_or_infinity(context)?;
1824
1825            // f. Set position to the result of clamping position between 0 and lengthS.
1826            let position = position.clamp_finite(0, length_s as i64) as usize;
1827
1828            // g. Let captures be a new empty List.
1829            let mut captures = Vec::new();
1830
1831            // h. Let n be 1.
1832            // i. Repeat, while n ≤ nCaptures,
1833            for n in 1..=n_captures {
1834                // i. Let capN be ? Get(result, ! ToString(𝔽(n))).
1835                let mut cap_n = result.get(n, context)?;
1836
1837                // ii. If capN is not undefined, then
1838                if !cap_n.is_undefined() {
1839                    // 1. Set capN to ? ToString(capN).
1840                    cap_n = cap_n.to_string(context)?.into();
1841                }
1842
1843                // iii. Append capN to captures.
1844                captures.push(cap_n);
1845
1846                // iv. NOTE: When n = 1, the preceding step puts the first element into captures (at index 0).
1847                //     More generally, the nth capture (the characters captured by the nth set of capturing parentheses)
1848                //     is at captures[n - 1].
1849                //
1850                // v. Set n to n + 1.
1851            }
1852
1853            // j. Let namedCaptures be ? Get(result, "groups").
1854            let mut named_captures = result.get(js_string!("groups"), context)?;
1855
1856            let replacement = match replace_value {
1857                // k. If functionalReplace is true, then
1858                CallableOrString::FunctionalReplace(ref replace_value) => {
1859                    // i. Let replacerArgs be the list-concatenation of « matched », captures, and « 𝔽(position), S ».
1860                    let mut replacer_args = vec![JsValue::new(matched)];
1861                    replacer_args.extend(captures);
1862                    replacer_args.push(position.into());
1863                    replacer_args.push(s.clone().into());
1864
1865                    // ii. If namedCaptures is not undefined, then
1866                    if !named_captures.is_undefined() {
1867                        // 1. Append namedCaptures to replacerArgs.
1868                        replacer_args.push(named_captures);
1869                    }
1870
1871                    // iii. Let replValue be ? Call(replaceValue, undefined, replacerArgs).
1872                    let repl_value =
1873                        replace_value.call(&JsValue::undefined(), &replacer_args, context)?;
1874
1875                    // iv. Let replacement be ? ToString(replValue).
1876                    repl_value.to_string(context)?
1877                }
1878                // l. Else,
1879                CallableOrString::ReplaceValue(ref replace_value) => {
1880                    // i. If namedCaptures is not undefined, then
1881                    if !named_captures.is_undefined() {
1882                        // 1. Set namedCaptures to ? ToObject(namedCaptures).
1883                        named_captures = named_captures.to_object(context)?.into();
1884                    }
1885
1886                    // ii. Let replacement be ? GetSubstitution(matched, S, position, captures, namedCaptures, replaceValue).
1887                    string::get_substitution(
1888                        &matched,
1889                        &s,
1890                        position,
1891                        &captures,
1892                        &named_captures,
1893                        replace_value,
1894                        context,
1895                    )?
1896                }
1897            };
1898
1899            // m. If position ≥ nextSourcePosition, then
1900            if position >= next_source_position {
1901                // i. NOTE: position should not normally move backwards.
1902                //    If it does, it is an indication of an ill-behaving RegExp subclass or use of
1903                //    an access triggered side-effect to change the global flag or other characteristics of rx.
1904                //    In such cases, the corresponding substitution is ignored.
1905
1906                // ii. Set accumulatedResult to the string-concatenation of accumulatedResult, the substring of S from nextSourcePosition to position, and replacement.
1907                accumulated_result.extend(s.get_expect(next_source_position..position).iter());
1908                accumulated_result.extend(replacement.iter());
1909
1910                // iii. Set nextSourcePosition to position + matchLength.
1911                next_source_position = position + match_length;
1912            }
1913        }
1914
1915        // 16. If nextSourcePosition ≥ lengthS, return accumulatedResult.
1916        if next_source_position >= length_s {
1917            return Ok(js_string!(&accumulated_result[..]).into());
1918        }
1919
1920        // 17. Return the string-concatenation of accumulatedResult and the substring of S from nextSourcePosition.
1921        Ok(js_string!(
1922            &JsString::from(&accumulated_result[..]),
1923            &s.get_expect(next_source_position..)
1924        )
1925        .into())
1926    }
1927
1928    /// `RegExp.prototype[ @@search ]( string )`
1929    ///
1930    /// This method executes a search for a match between a this regular expression and a string.
1931    ///
1932    /// More information:
1933    ///  - [ECMAScript reference][spec]
1934    ///  - [MDN documentation][mdn]
1935    ///
1936    /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@search
1937    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@search
1938    pub(crate) fn search(
1939        this: &JsValue,
1940        args: &[JsValue],
1941        context: &mut Context,
1942    ) -> JsResult<JsValue> {
1943        // 1. Let rx be the this value.
1944        // 2. If Type(rx) is not Object, throw a TypeError exception.
1945        let rx = this.as_object().ok_or_else(|| {
1946            JsNativeError::typ()
1947                .with_message("RegExp.prototype[Symbol.search] method called on incompatible value")
1948        })?;
1949
1950        // 3. Let S be ? ToString(string).
1951        let arg_str = args.get_or_undefined(0).to_string(context)?;
1952
1953        // 4. Let previousLastIndex be ? Get(rx, "lastIndex").
1954        let previous_last_index = rx.get(js_string!("lastIndex"), context)?;
1955
1956        // 5. If SameValue(previousLastIndex, +0𝔽) is false, then
1957        if !JsValue::same_value(&previous_last_index, &JsValue::new(0)) {
1958            // a. Perform ? Set(rx, "lastIndex", +0𝔽, true).
1959            rx.set(js_string!("lastIndex"), 0, true, context)?;
1960        }
1961
1962        // 6. Let result be ? RegExpExec(rx, S).
1963        let result = Self::abstract_exec(&rx, arg_str, context)?;
1964
1965        // 7. Let currentLastIndex be ? Get(rx, "lastIndex").
1966        let current_last_index = rx.get(js_string!("lastIndex"), context)?;
1967
1968        // 8. If SameValue(currentLastIndex, previousLastIndex) is false, then
1969        if !JsValue::same_value(&current_last_index, &previous_last_index) {
1970            // a. Perform ? Set(rx, "lastIndex", previousLastIndex, true).
1971            rx.set(js_string!("lastIndex"), previous_last_index, true, context)?;
1972        }
1973
1974        // 9. If result is null, return -1𝔽.
1975        // 10. Return ? Get(result, "index").
1976        result.map_or_else(
1977            || Ok(JsValue::new(-1)),
1978            |result| result.get(js_string!("index"), context),
1979        )
1980    }
1981
1982    /// `RegExp.prototype [ @@split ] ( string, limit )`
1983    ///
1984    /// The [@@split]() method splits a String object into an array of strings by separating the string into substrings.
1985    ///
1986    /// More information:
1987    ///  - [ECMAScript reference][spec]
1988    ///  - [MDN documentation][mdn]
1989    ///
1990    /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@split
1991    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@split
1992    pub(crate) fn split(
1993        this: &JsValue,
1994        args: &[JsValue],
1995        context: &mut Context,
1996    ) -> JsResult<JsValue> {
1997        // 1. Let rx be the this value.
1998        // 2. If Type(rx) is not Object, throw a TypeError exception.
1999        let rx = this.as_object().ok_or_else(|| {
2000            JsNativeError::typ()
2001                .with_message("RegExp.prototype.split method called on incompatible value")
2002        })?;
2003
2004        // 3. Let S be ? ToString(string).
2005        let arg_str = args.get_or_undefined(0).to_string(context)?;
2006
2007        // 4. Let C be ? SpeciesConstructor(rx, %RegExp%).
2008        let constructor = rx.species_constructor(StandardConstructors::regexp, context)?;
2009
2010        // 5. Let flags be ? ToString(? Get(rx, "flags")).
2011        let flags = rx.get(js_string!("flags"), context)?.to_string(context)?;
2012
2013        // 6. If flags contains "u", let unicodeMatching be true.
2014        // 7. Else, let unicodeMatching be false.
2015        let unicode = flags.contains(b'u');
2016
2017        // 8. If flags contains "y", let newFlags be flags.
2018        // 9. Else, let newFlags be the string-concatenation of flags and "y".
2019        let new_flags = if flags.contains(b'y') {
2020            flags
2021        } else {
2022            js_string!(&flags, js_str!("y"))
2023        };
2024
2025        // 10. Let splitter be ? Construct(C, « rx, newFlags »).
2026        let splitter = constructor.construct(
2027            &[this.clone(), new_flags.into()],
2028            Some(&constructor),
2029            context,
2030        )?;
2031
2032        // 11. Let A be ! ArrayCreate(0).
2033        let a = Array::array_create(0, None, context)?;
2034
2035        // 12. Let lengthA be 0.
2036        let mut length_a = 0;
2037
2038        // 13. If limit is undefined, let lim be 2^32 - 1; else let lim be ℝ(? ToUint32(limit)).
2039        let limit = args.get_or_undefined(1);
2040        let lim = if limit.is_undefined() {
2041            u32::MAX
2042        } else {
2043            limit.to_u32(context)?
2044        };
2045
2046        // 14. If lim is 0, return A.
2047        if lim == 0 {
2048            return Ok(a.into());
2049        }
2050
2051        // 15. Let size be the length of S.
2052        let size = arg_str.len() as u64;
2053
2054        // 16. If size is 0, then
2055        if size == 0 {
2056            // a. Let z be ? RegExpExec(splitter, S).
2057            let result = Self::abstract_exec(&splitter, arg_str.clone(), context)?;
2058
2059            // b. If z is not null, return A.
2060            if result.is_some() {
2061                return Ok(a.into());
2062            }
2063
2064            // c. Perform ! CreateDataPropertyOrThrow(A, "0", S).
2065            a.create_data_property_or_throw(0, arg_str, context)?;
2066
2067            // d. Return A.
2068            return Ok(a.into());
2069        }
2070
2071        // With %RegExp% as C and the intrinsic `exec`, the splitter is a fresh RegExp that no user code
2072        // can reach, so a sticky match tried at each q is one forward search from q.
2073        let regexp = context.intrinsics().constructors().regexp();
2074        if JsObject::equals(&constructor, &regexp.constructor()) {
2075            let exec = regexp
2076                .prototype()
2077                .borrow()
2078                .properties()
2079                .get(&js_string!("exec").into())
2080                .and_then(|desc| desc.value().cloned());
2081            if let Some(splitter) =
2082                exec.and_then(|exec| Self::intrinsic_exec_target(&splitter, &exec))
2083            {
2084                return Self::split_by_search(&splitter, &arg_str, &a, lim, unicode, context);
2085            }
2086        }
2087
2088        // 17. Let p be 0.
2089        // 18. Let q be p.
2090        let mut p = 0;
2091        let mut q = p;
2092
2093        // 19. Repeat, while q < size,
2094        while q < size {
2095            // a. Perform ? Set(splitter, "lastIndex", 𝔽(q), true).
2096            splitter.set(js_string!("lastIndex"), JsValue::new(q), true, context)?;
2097
2098            // b. Let z be ? RegExpExec(splitter, S).
2099            let result = Self::abstract_exec(&splitter, arg_str.clone(), context)?;
2100
2101            // c. If z is null, set q to AdvanceStringIndex(S, q, unicodeMatching).
2102            // d. Else,
2103            if let Some(result) = result {
2104                // i. Let e be ℝ(? ToLength(? Get(splitter, "lastIndex"))).
2105                let mut e = splitter
2106                    .get(js_string!("lastIndex"), context)?
2107                    .to_length(context)?;
2108
2109                // ii. Set e to min(e, size).
2110                e = std::cmp::min(e, size);
2111
2112                // iii. If e = p, set q to AdvanceStringIndex(S, q, unicodeMatching).
2113                // iv. Else,
2114                if e == p {
2115                    q = advance_string_index(&arg_str, q, unicode);
2116                } else {
2117                    // 1. Let T be the substring of S from p to q.
2118                    let arg_str_substring = arg_str.get_expect(p as usize..q as usize);
2119
2120                    // 2. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T).
2121                    a.create_data_property_or_throw(length_a, arg_str_substring, context)?;
2122
2123                    // 3. Set lengthA to lengthA + 1.
2124                    length_a += 1;
2125
2126                    // 4. If lengthA = lim, return A.
2127                    if length_a == lim {
2128                        return Ok(a.into());
2129                    }
2130
2131                    // 5. Set p to e.
2132                    p = e;
2133
2134                    // 6. Let numberOfCaptures be ? LengthOfArrayLike(z).
2135                    let mut number_of_captures = result.length_of_array_like(context)? as isize;
2136
2137                    // 7. Set numberOfCaptures to max(numberOfCaptures - 1, 0).
2138                    number_of_captures = std::cmp::max(number_of_captures - 1, 0);
2139
2140                    // 8. Let i be 1.
2141                    // 9. Repeat, while i ≤ numberOfCaptures,
2142                    for i in 1..=number_of_captures {
2143                        // a. Let nextCapture be ? Get(z, ! ToString(𝔽(i))).
2144                        let next_capture = result.get(i, context)?;
2145
2146                        // b. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), nextCapture).
2147                        a.create_data_property_or_throw(length_a, next_capture, context)?;
2148
2149                        // d. Set lengthA to lengthA + 1.
2150                        length_a += 1;
2151
2152                        // e. If lengthA = lim, return A.
2153                        if length_a == lim {
2154                            return Ok(a.into());
2155                        }
2156                    }
2157
2158                    // 10. Set q to p.
2159                    q = p;
2160                }
2161            } else {
2162                q = advance_string_index(&arg_str, q, unicode);
2163            }
2164        }
2165
2166        // 20. Let T be the substring of S from p to size.
2167        let arg_str_substring = arg_str.get_expect(p as usize..size as usize);
2168
2169        // 21. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T).
2170        a.create_data_property_or_throw(length_a, arg_str_substring, context)?;
2171
2172        // 22. Return A.
2173        Ok(a.into())
2174    }
2175
2176    /// Steps 17 to 22 of `RegExp.prototype [ %Symbol.split% ]` for a splitter only the engine can
2177    /// observe: each piece is one forward search on the input widened once, where the spec loop
2178    /// runs a sticky match at every code unit.
2179    fn split_by_search(
2180        splitter: &JsObject<RegExp>,
2181        string: &JsString,
2182        array: &JsObject,
2183        lim: u32,
2184        unicode: bool,
2185        context: &mut Context,
2186    ) -> JsResult<JsValue> {
2187        let rx = splitter.borrow().data().clone();
2188        let full_unicode =
2189            rx.flags.contains(RegExpFlags::UNICODE) || rx.flags.contains(RegExpFlags::UNICODE_SETS);
2190        let widened;
2191        let (input, utf16): (&[u16], bool) = match string.as_str().variant() {
2192            JsStrVariant::Latin1(bytes) => {
2193                widened = bytes
2194                    .iter()
2195                    .map(|&byte| u16::from(byte))
2196                    .collect::<Vec<_>>();
2197                (&widened, false)
2198            }
2199            JsStrVariant::Utf16(units) => (units, full_unicode),
2200        };
2201        let size = input.len();
2202        let mut length_a = 0;
2203        let mut p = 0;
2204        let mut q = 0;
2205        while q < size {
2206            let found = if utf16 {
2207                rx.matcher.find_from_utf16(input, q).next()
2208            } else {
2209                rx.matcher.find_from_ucs2(input, q).next()
2210            };
2211            // The first q at which a sticky match succeeds, if any is below size.
2212            let Some(found) = found.filter(|found| found.start() < size) else {
2213                break;
2214            };
2215            let end = found.end().min(size);
2216            if end == p {
2217                q = advance_string_index(string, found.start() as u64, unicode) as usize;
2218                continue;
2219            }
2220            array.create_data_property_or_throw(
2221                length_a,
2222                string.get_expect(p..found.start()),
2223                context,
2224            )?;
2225            length_a += 1;
2226            if length_a == lim {
2227                return Ok(array.clone().into());
2228            }
2229            p = end;
2230            for capture in &found.captures {
2231                let value = capture
2232                    .clone()
2233                    .map_or_else(JsValue::undefined, |range| string.get_expect(range).into());
2234                array.create_data_property_or_throw(length_a, value, context)?;
2235                length_a += 1;
2236                if length_a == lim {
2237                    return Ok(array.clone().into());
2238                }
2239            }
2240            q = p;
2241        }
2242        array.create_data_property_or_throw(length_a, string.get_expect(p..size), context)?;
2243        Ok(array.clone().into())
2244    }
2245
2246    /// [`RegExp.prototype.compile ( pattern, flags )`][spec]
2247    ///
2248    /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.compile
2249    #[cfg(feature = "annex-b")]
2250    fn compile(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
2251        // 1. Let O be the this value.
2252        // 2. Perform ? RequireInternalSlot(O, [[RegExpMatcher]]).
2253
2254        let this = this
2255            .as_object()
2256            .filter(|o| o.is::<RegExp>())
2257            .ok_or_else(|| {
2258                JsNativeError::typ()
2259                    .with_message("`RegExp.prototype.compile` cannot be called for a non-object")
2260            })?;
2261        let pattern = args.get_or_undefined(0);
2262        let flags = args.get_or_undefined(1);
2263        // 3. If pattern is an Object and pattern has a [[RegExpMatcher]] internal slot, then
2264        let (pattern, flags) = if let Some((p, f)) = pattern.as_object().and_then(|o| {
2265            o.downcast_ref::<RegExp>()
2266                .map(|rx| (rx.original_source.clone(), rx.original_flags.clone()))
2267        }) {
2268            //     a. If flags is not undefined, throw a TypeError exception.
2269            if !flags.is_undefined() {
2270                return Err(JsNativeError::typ()
2271                    .with_message(
2272                        "`RegExp.prototype.compile` cannot be \
2273                called with both a RegExp initializer and new flags",
2274                    )
2275                    .into());
2276            }
2277            //     b. Let P be pattern.[[OriginalSource]].
2278            //     c. Let F be pattern.[[OriginalFlags]].
2279            (p.into(), f.into())
2280        } else {
2281            // 4. Else,
2282            //     a. Let P be pattern.
2283            //     b. Let F be flags.
2284            (pattern.clone(), flags.clone())
2285        };
2286
2287        let regexp = Self::compile_native_regexp(&pattern, &flags, context)?;
2288
2289        // 5. Return ? RegExpInitialize(O, P, F).
2290        {
2291            *this
2292                .downcast_mut::<RegExp>()
2293                .expect("already checked that the object was a RegExp") = regexp;
2294        }
2295
2296        this.set(js_string!("lastIndex"), 0, true, context)?;
2297
2298        Ok(this.into())
2299    }
2300}
2301
2302/// `22.2.5.2.3 AdvanceStringIndex ( S, index, unicode )`
2303///
2304/// More information:
2305///  - [ECMAScript reference][spec]
2306///
2307/// [spec]: https://tc39.es/ecma262/#sec-advancestringindex
2308fn advance_string_index(s: &JsString, index: u64, unicode: bool) -> u64 {
2309    // Regress only works with utf8, so this function differs from the spec.
2310
2311    // 1. Assert: index ≤ 2^53 - 1.
2312
2313    // 2. If unicode is false, return index + 1.
2314    if !unicode {
2315        return index + 1;
2316    }
2317
2318    // 3. Let length be the number of code units in S.
2319    let length = s.len() as u64;
2320
2321    // 4. If index + 1 ≥ length, return index + 1.
2322    if index + 1 > length {
2323        return index + 1;
2324    }
2325
2326    // 5. Let cp be ! CodePointAt(S, index).
2327    let code_point = s.code_point_at(index as usize);
2328
2329    index + code_point.code_unit_count() as u64
2330}