tsrun 0.1.23

A TypeScript interpreter designed for embedding in applications
Documentation
//! RegExp built-in methods

use crate::error::JsError;
use crate::gc::Gc;
use crate::interpreter::Interpreter;
use crate::platform::CompiledRegex;
use crate::prelude::{Rc, String, ToString, Vec};
use crate::value::{ExoticObject, Guarded, JsObject, JsString, JsValue, PropertyKey};

/// Initialize RegExp.prototype with test and exec methods
pub fn init_regexp_prototype(interp: &mut Interpreter) {
    let proto = interp.regexp_prototype.clone();

    interp.register_method(&proto, "test", regexp_test, 1);
    interp.register_method(&proto, "exec", regexp_exec, 1);
}

/// Create RegExp constructor
pub fn create_regexp_constructor(interp: &mut Interpreter) -> Gc<JsObject> {
    let constructor = interp.create_native_function("RegExp", regexp_constructor, 2);

    // Set constructor.prototype = RegExp.prototype
    let proto_key = PropertyKey::String(interp.intern("prototype"));
    constructor
        .borrow_mut()
        .set_property(proto_key, JsValue::Object(interp.regexp_prototype.clone()));

    // Set RegExp.prototype.constructor = RegExp
    let constructor_key = PropertyKey::String(interp.intern("constructor"));
    interp
        .regexp_prototype
        .borrow_mut()
        .set_property(constructor_key, JsValue::Object(constructor.clone()));

    // Add Symbol.species getter
    interp.register_species_getter(&constructor);

    constructor
}

pub fn regexp_constructor(
    interp: &mut Interpreter,
    _this: JsValue,
    args: &[JsValue],
) -> Result<Guarded, JsError> {
    let empty = interp.intern("");
    let pattern_arg = args
        .first()
        .cloned()
        .unwrap_or(JsValue::String(empty.clone()));
    let pattern = interp.to_js_string(&pattern_arg).to_string();
    let flags_arg = args.get(1).cloned().unwrap_or(JsValue::String(empty));
    let flags = interp.to_js_string(&flags_arg).to_string();

    // Compile the regex to validate it (also caches for later use)
    let compiled = interp.compile_regexp(&pattern, &flags)?;

    // Pre-intern all property keys
    let source_key = PropertyKey::String(interp.intern("source"));
    let flags_key = PropertyKey::String(interp.intern("flags"));
    let global_key = PropertyKey::String(interp.intern("global"));
    let ignore_case_key = PropertyKey::String(interp.intern("ignoreCase"));
    let multiline_key = PropertyKey::String(interp.intern("multiline"));
    let dot_all_key = PropertyKey::String(interp.intern("dotAll"));
    let unicode_key = PropertyKey::String(interp.intern("unicode"));
    let sticky_key = PropertyKey::String(interp.intern("sticky"));
    let last_index_key = PropertyKey::String(interp.intern("lastIndex"));

    let guard = interp.heap.create_guard();
    let regexp_obj = interp.create_object(&guard);
    {
        let mut obj = regexp_obj.borrow_mut();
        obj.exotic = ExoticObject::RegExp {
            pattern: pattern.clone(),
            flags: flags.clone(),
            compiled: Some(compiled),
        };
        obj.prototype = Some(interp.regexp_prototype.clone());
        obj.set_property(source_key, JsValue::String(JsString::from(pattern)));
        obj.set_property(flags_key, JsValue::String(JsString::from(flags.clone())));
        obj.set_property(global_key, JsValue::Boolean(flags.contains('g')));
        obj.set_property(ignore_case_key, JsValue::Boolean(flags.contains('i')));
        obj.set_property(multiline_key, JsValue::Boolean(flags.contains('m')));
        obj.set_property(dot_all_key, JsValue::Boolean(flags.contains('s')));
        obj.set_property(unicode_key, JsValue::Boolean(flags.contains('u')));
        obj.set_property(sticky_key, JsValue::Boolean(flags.contains('y')));
        // Initialize lastIndex to 0
        obj.set_property(last_index_key, JsValue::Number(0.0));
    }
    Ok(Guarded::with_guard(JsValue::Object(regexp_obj), guard))
}

/// Get pattern and flags from a RegExp object
pub fn get_regexp_data(this: &JsValue) -> Result<(String, String), JsError> {
    let JsValue::Object(obj) = this else {
        return Err(JsError::type_error("this is not a RegExp"));
    };
    let obj_ref = obj.borrow();
    if let ExoticObject::RegExp {
        ref pattern,
        ref flags,
        ..
    } = obj_ref.exotic
    {
        Ok((pattern.clone(), flags.clone()))
    } else {
        Err(JsError::type_error("this is not a RegExp"))
    }
}

/// Get the compiled regex from a RegExp object, compiling if needed
pub fn get_compiled_regexp(
    interp: &Interpreter,
    obj: &Gc<JsObject>,
) -> Result<Rc<dyn CompiledRegex>, JsError> {
    let obj_ref = obj.borrow();
    if let ExoticObject::RegExp {
        ref pattern,
        ref flags,
        ref compiled,
        ..
    } = obj_ref.exotic
    {
        if let Some(compiled) = compiled {
            return Ok(compiled.clone());
        }
        // Not cached - compile now
        let pattern = pattern.clone();
        let flags = flags.clone();
        drop(obj_ref);
        interp.compile_regexp(&pattern, &flags)
    } else {
        Err(JsError::type_error("this is not a RegExp"))
    }
}

pub fn regexp_test(
    interp: &mut Interpreter,
    this: JsValue,
    args: &[JsValue],
) -> Result<Guarded, JsError> {
    let JsValue::Object(ref obj) = this else {
        return Err(JsError::type_error("this is not a RegExp"));
    };

    let re = get_compiled_regexp(interp, obj)?;

    // Use ToString abstract operation (calls object's toString if needed)
    let input_arg = args.first().cloned().unwrap_or(JsValue::Undefined);
    let input = interp.coerce_to_string(&input_arg)?.to_string();

    let is_match = re
        .is_match(&input)
        .map_err(|e| JsError::syntax_error(e, 0, 0))?;
    Ok(Guarded::unguarded(JsValue::Boolean(is_match)))
}

pub fn regexp_exec(
    interp: &mut Interpreter,
    this: JsValue,
    args: &[JsValue],
) -> Result<Guarded, JsError> {
    let JsValue::Object(ref obj) = this else {
        return Err(JsError::type_error("this is not a RegExp"));
    };

    let (_, flags) = get_regexp_data(&this)?;
    let re = get_compiled_regexp(interp, obj)?;

    // Use ToString abstract operation (calls object's toString if needed)
    let input_arg = args.first().cloned().unwrap_or(JsValue::Undefined);
    let input = interp.coerce_to_string(&input_arg)?.to_string();

    let is_global = flags.contains('g');
    let is_sticky = flags.contains('y');

    // Pre-intern property keys
    let last_index_key = PropertyKey::String(interp.intern("lastIndex"));
    let index_key = PropertyKey::String(interp.intern("index"));
    let input_key = PropertyKey::String(interp.intern("input"));

    // Get lastIndex for global/sticky regexes
    let last_index = if is_global || is_sticky {
        let li = obj
            .borrow()
            .get_property(&last_index_key)
            .unwrap_or(JsValue::Number(0.0));
        match li {
            JsValue::Number(n) => n as usize,
            _ => 0,
        }
    } else {
        0
    };

    // Check if lastIndex is past end of string
    if last_index > input.len() {
        if is_global || is_sticky {
            obj.borrow_mut()
                .set_property(last_index_key, JsValue::Number(0.0));
        }
        return Ok(Guarded::unguarded(JsValue::Null));
    }

    // Use the provider's find method which handles start position
    let match_result = re
        .find(&input, last_index)
        .map_err(|e| JsError::syntax_error(e, 0, 0))?;

    match match_result {
        Some(regex_match) => {
            // Build result array from captures
            let mut result = Vec::new();
            for capture in &regex_match.captures {
                match capture {
                    Some((start, end)) => {
                        let s = input.get(*start..*end).unwrap_or("");
                        result.push(JsValue::String(JsString::from(s)));
                    }
                    None => result.push(JsValue::Undefined),
                }
            }

            let guard = interp.heap.create_guard();
            let arr = interp.create_array_from(&guard, result);

            // Set index property (match start position)
            arr.borrow_mut()
                .set_property(index_key, JsValue::Number(regex_match.start as f64));
            arr.borrow_mut()
                .set_property(input_key, JsValue::String(JsString::from(input.clone())));

            // Update lastIndex for global/sticky regexes
            if is_global || is_sticky {
                obj.borrow_mut()
                    .set_property(last_index_key, JsValue::Number(regex_match.end as f64));
            }

            Ok(Guarded::with_guard(JsValue::Object(arr), guard))
        }
        None => {
            // Reset lastIndex to 0 on no match for global/sticky
            if is_global || is_sticky {
                obj.borrow_mut()
                    .set_property(last_index_key, JsValue::Number(0.0));
            }
            Ok(Guarded::unguarded(JsValue::Null))
        }
    }
}