diff --git a/src/errors.rs b/src/errors.rs index 22d497d9..572d8924 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -33,11 +33,13 @@ pub enum Error { // Runtime errors D1001NumberOfOutRange(f64), D1002NegatingNonNumeric(usize, String), + D1004ZeroLengthMatch(usize), D1009MultipleKeys(usize, String), D2014RangeOutOfBounds(usize, isize), D3001StringNotFinite(usize), D3010EmptyPattern(usize), D3011NegativeLimit(usize), + D3012InvalidReplacementType(usize), D3020NegativeLimit(usize), D3030NonNumericCast(usize, String), D3050SecondArguement(String), @@ -122,11 +124,13 @@ impl Error { // Runtime errors Error::D1001NumberOfOutRange(..) => "D1001", Error::D1002NegatingNonNumeric(..) => "D1002", + Error::D1004ZeroLengthMatch(..) => "D1004", Error::D1009MultipleKeys(..) => "D1009", Error::D2014RangeOutOfBounds(..) => "D2014", Error::D3001StringNotFinite(..) => "D3001", Error::D3010EmptyPattern(..) => "D3010", Error::D3011NegativeLimit(..) => "D3011", + Error::D3012InvalidReplacementType(..) => "D3012", Error::D3020NegativeLimit(..) => "D3020", Error::D3030NonNumericCast(..) => "D3030", Error::D3050SecondArguement(..) => "D3050", @@ -228,6 +232,8 @@ impl fmt::Display for Error { D1001NumberOfOutRange(ref n) => write!(f, "Number out of range: {}", n), D1002NegatingNonNumeric(ref p, ref v) => write!(f, "{}: Cannot negate a non-numeric value `{}`", p, v), + D1004ZeroLengthMatch(ref p) => + write!(f, "{}: Regular expression matches zero length string", p), D1009MultipleKeys(ref p, ref k) => write!(f, "{}: Multiple key definitions evaluate to same key: {}", p, k), D2014RangeOutOfBounds(ref p, ref s) => @@ -238,6 +244,7 @@ impl fmt::Display for Error { write!(f, "{}: Second argument of replace function cannot be an empty string", p), D3011NegativeLimit(ref p) => write!(f, "{}: Fourth argument of replace function must evaluate to a positive number", p), + D3012InvalidReplacementType(ref p) => write!(f, "{}: Attempted to replace a matched string with a non-string value", p), D3020NegativeLimit(ref p) => write!(f, "{}: Third argument of split function must evaluate to a positive number", p), D3030NonNumericCast(ref p, ref n) => diff --git a/src/evaluator.rs b/src/evaluator.rs index b15c93a4..a479e2fb 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1192,7 +1192,7 @@ impl<'a> Evaluator<'a> { evaluated_args.push(arg); } - let mut result = self.apply_function( + let result = self.apply_function( proc.char_index, input, evaluated_proc, @@ -1200,6 +1200,18 @@ impl<'a> Evaluator<'a> { frame, )?; + let result = self.trampoline_evaluate_value(result, input, frame)?; + + Ok(result) + } + + /// Iteratively evaluate a function until a non-function value is returned. + fn trampoline_evaluate_value( + &self, + mut result: &'a Value<'a>, + input: &'a Value<'a>, + frame: &Frame<'a>, + ) -> Result<&'a Value<'a>> { // Trampoline loop for tail-call optimization // TODO: This loop needs help while let Value::Lambda { diff --git a/src/evaluator/functions.rs b/src/evaluator/functions.rs index 2fdbb803..4d62e041 100644 --- a/src/evaluator/functions.rs +++ b/src/evaluator/functions.rs @@ -2,6 +2,7 @@ use base64::Engine; use chrono::{TimeZone, Utc}; use hashbrown::{DefaultHashBuilder, HashMap}; use rand::Rng; +use regress::{Range, Regex}; use std::borrow::{Borrow, Cow}; use std::collections::HashSet; use std::time::{SystemTime, UNIX_EPOCH}; @@ -97,6 +98,11 @@ impl<'a, 'e> FunctionContext<'a, 'e> { self.evaluator .apply_function(self.char_index, self.input, proc, args, &self.frame) } + + pub fn trampoline_evaluate_value(&self, value: &'a Value<'a>) -> Result<&'a Value<'a>> { + self.evaluator + .trampoline_evaluate_value(value, self.input, &self.frame) + } } /// Extend the given values with value. @@ -224,7 +230,8 @@ pub fn fn_map<'a>( args.push(arr); } - let mapped = context.evaluate_function(func, &args)?; + let mapped = context.trampoline_evaluate_value(context.evaluate_function(func, &args)?)?; + if !mapped.is_undefined() { result.push(mapped); } @@ -624,12 +631,29 @@ pub fn fn_contains<'a>( } assert_arg!(str_value.is_string(), context, 1); - assert_arg!(token_value.is_string(), context, 2); let str_value = str_value.as_str(); - let token_value = token_value.as_str(); - Ok(Value::bool(str_value.contains(&token_value.to_string()))) + // Check if token_value is a regex or string + let contains_result = match token_value { + Value::Regex(ref regex_literal) => { + let regex = regex_literal.get_regex(); + regex.find_iter(&str_value).next().is_some() + } + Value::String(_) => { + let token_value = token_value.as_str(); + str_value.contains(&token_value.to_string()) + } + _ => { + return Err(Error::T0410ArgumentNotValid( + context.char_index, + 2, + context.name.to_string(), + )); + } + }; + + Ok(Value::bool(contains_result)) } pub fn fn_replace<'a>( @@ -650,12 +674,8 @@ pub fn fn_replace<'a>( } assert_arg!(str_value.is_string(), context, 1); - assert_arg!(pattern_value.is_string(), context, 2); - assert_arg!(replacement_value.is_string(), context, 3); let str_value = str_value.as_str(); - let pattern_value = pattern_value.as_str(); - let replacement_value = replacement_value.as_str(); let limit_value = if limit_value.is_undefined() { None } else { @@ -663,20 +683,211 @@ pub fn fn_replace<'a>( if limit_value.as_isize().is_negative() { return Err(Error::D3011NegativeLimit(context.char_index)); } - Some(limit_value.as_isize()) + Some(limit_value.as_isize() as usize) }; - let replaced_string = if let Some(limit) = limit_value { - str_value.replacen( - &pattern_value.to_string(), - &replacement_value, - limit as usize, - ) - } else { - str_value.replace(&pattern_value.to_string(), &replacement_value) + // Check if pattern_value is a Regex or String and handle appropriately + let regex = match pattern_value { + Value::Regex(ref regex_literal) => regex_literal.get_regex(), + Value::String(ref pattern_str) => { + assert_arg!(replacement_value.is_string(), context, 3); + let replacement_str = replacement_value.as_str(); + + let replaced_string = if let Some(limit) = limit_value { + str_value.replacen(&pattern_str.to_string(), &replacement_str, limit) + } else { + str_value.replace(&pattern_str.to_string(), &replacement_str) + }; + + return Ok(Value::string(context.arena, &replaced_string)); + } + _ => bad_arg!(context, 2), }; - Ok(Value::string(context.arena, &replaced_string)) + let mut result = String::new(); + let mut last_end = 0; + + for (replacements, m) in regex.find_iter(&str_value).enumerate() { + if m.range().is_empty() { + return Err(Error::D1004ZeroLengthMatch(context.char_index)); + } + + if let Some(limit) = limit_value { + if replacements >= limit { + break; + } + } + + result.push_str(&str_value[last_end..m.start()]); + + let match_str = &str_value[m.start()..m.end()]; + + // Process replacement based on the replacement_value type + let replacement_text = match replacement_value { + Value::NativeFn { func, .. } => { + let match_list = evaluate_match(context.arena, regex, match_str, None); + + let func_result = func(context.clone(), &[match_list])?; + + if let Value::String(ref s) = func_result { + s.as_str().to_string() + } else { + return Err(Error::D3012InvalidReplacementType(context.char_index)); + } + } + + func @ Value::Lambda { .. } => { + let match_list = evaluate_match(context.arena, regex, match_str, None); + + let args = &[match_list]; + + let func_result = + context.trampoline_evaluate_value(context.evaluate_function(func, args)?)?; + + match func_result { + Value::String(ref s) => s.as_str().to_string(), + _ => return Err(Error::D3012InvalidReplacementType(context.char_index)), + } + } + + Value::String(replacement_str) => { + evaluate_replacement_string(replacement_str.as_str(), &str_value, &m) + } + + _ => bad_arg!(context, 3), + }; + + result.push_str(&replacement_text); + last_end = m.end(); + } + + result.push_str(&str_value[last_end..]); + + Ok(Value::string(context.arena, &result)) +} + +/// Parse and evaluate a replacement string. +/// +/// Parsing the string is context-dependent because of an odd jsonata behavior: +/// - if $NM is a valid match group number, it is replaced with the match. +/// - if $NM is not valid, it is replaced with the match for $M followed by a literal 'N'. +/// +/// This is why the `Match` object is needed. +/// +/// # Parameters +/// - `replacement_str`: The replacement string to parse and evaluate. +/// - `str_value`: The complete original string, the first argument to `$replace`. +/// - `m`: The `Match` object for the current match which is being replaced. +fn evaluate_replacement_string( + replacement_str: &str, + str_value: &str, + m: ®ress::Match, +) -> String { + #[derive(Debug)] + enum S { + Literal, + Dollar, + Group(u32), + End, + } + + let mut state = S::Literal; + let mut acc = String::new(); + + let groups: Vec> = m.groups().collect(); + let mut chars = replacement_str.chars(); + + loop { + let c = chars.next(); + match (&state, c) { + (S::Literal, Some('$')) => { + state = S::Dollar; + } + (S::Literal, Some(c)) => { + acc.push(c); + } + (S::Dollar, Some('$')) => { + acc.push('$'); + state = S::Literal; + } + + // Start parsing a group number + (S::Dollar, Some(c)) if c.is_numeric() => { + let digit = c + .to_digit(10) + .expect("numeric char failed to parse as digit"); + state = S::Group(digit); + } + + // `$` followed by something other than a group number + // (including end of string) is treated as a literal `$` + (S::Dollar, c) => { + acc.push('$'); + c.inspect(|c| acc.push(*c)); + state = S::Literal; + } + + // Still parsing a group number + (S::Group(so_far), Some(c)) if c.is_numeric() => { + let digit = c + .to_digit(10) + .expect("numeric char failed to parse as digit"); + + let next = so_far * 10 + digit; + let groups_len = groups.len() as u32; + + // A bizarre behavior of the jsonata reference implementation is that in $NM if NM is not a + // valid group number, it will use $N and treat M as a literal. This is not documented behavior and + // feels like a bug, but our test cases cover it in several ways. + if next >= groups_len { + if let Some(match_range) = groups.get(*so_far as usize).and_then(|x| x.as_ref()) + { + str_value + .get(match_range.start..match_range.end) + .inspect(|s| acc.push_str(s)); + } else { + // The capture group did not match. + } + + acc.push(c); + + state = S::Literal + } else { + state = S::Group(next); + } + } + + // The group number is complete, so we can now process it + (S::Group(index), c) => { + if let Some(match_range) = groups.get(*index as usize).and_then(|x| x.as_ref()) { + str_value + .get(match_range.start..match_range.end) + .inspect(|s| acc.push_str(s)); + } else { + // The capture group did not match. + } + + if let Some(c) = c { + if c == '$' { + state = S::Dollar; + } else { + acc.push(c); + state = S::Literal; + } + } else { + state = S::End; + } + } + (S::Literal, None) => { + state = S::End; + } + + (S::End, _) => { + break; + } + } + } + acc } pub fn fn_split<'a>( @@ -692,36 +903,91 @@ pub fn fn_split<'a>( } assert_arg!(str_value.is_string(), context, 1); - assert_arg!(separator_value.is_string(), context, 2); let str_value = str_value.as_str(); - let separator_value = separator_value.as_str(); - let limit_value = if limit_value.is_undefined() { + let separator_is_regex = match separator_value { + Value::Regex(_) => true, + Value::String(_) => false, + _ => { + return Err(Error::T0410ArgumentNotValid( + context.char_index, + 2, + context.name.to_string(), + )); + } + }; + + // Handle optional limit + let limit = if limit_value.is_undefined() { None } else { - assert_arg!(limit_value.is_number(), context, 4); - if limit_value.as_isize().is_negative() { + assert_arg!(limit_value.is_number(), context, 3); + if limit_value.as_f64() < 0.0 { return Err(Error::D3020NegativeLimit(context.char_index)); } - Some(limit_value.as_isize()) + Some(limit_value.as_f64() as usize) }; - let substrings: Vec<&str> = if let Some(limit) = limit_value { - str_value - .split(&separator_value.to_string()) - .take(limit as usize) - .collect() - } else { - str_value.split(&separator_value.to_string()).collect() - }; + let substrings: Vec = if separator_is_regex { + // Regex-based split using find_iter to find matches + let regex = match separator_value { + Value::Regex(ref regex_literal) => regex_literal.get_regex(), + _ => unreachable!(), + }; - let substrings_count = substrings.len(); + let mut results = Vec::new(); + let mut last_end = 0; + let effective_limit = limit.unwrap_or(usize::MAX); - let result = Value::array_with_capacity(context.arena, substrings_count, ArrayFlags::empty()); - for (index, substring) in substrings.into_iter().enumerate() { - if substring.is_empty() && (index == 0 || index == substrings_count - 1) { - continue; + for m in regex.find_iter(&str_value) { + if results.len() >= effective_limit { + break; + } + + if m.start() > last_end { + let substring = str_value[last_end..m.start()].to_string(); + results.push(substring); + } + + last_end = m.end(); + } + + if results.len() < effective_limit { + let remaining = str_value[last_end..].to_string(); + results.push(remaining); } + results + } else { + // Convert separator_value to &str + let separator_str = separator_value.as_str().to_string(); + let separator_str = separator_str.as_str(); + if separator_str.is_empty() { + // Split into individual characters, collecting directly into a Vec + if let Some(limit) = limit { + str_value + .chars() + .take(limit) + .map(|c| c.to_string()) + .collect() + } else { + str_value.chars().map(|c| c.to_string()).collect() + } + } else if let Some(limit) = limit { + str_value + .split(separator_str) + .take(limit) + .map(|s| s.to_string()) + .collect() + } else { + str_value + .split(separator_str) + .map(|s| s.to_string()) + .collect() + } + }; + + let result = Value::array_with_capacity(context.arena, substrings.len(), ArrayFlags::empty()); + for substring in &substrings { result.push(Value::string(context.arena, substring)); } Ok(result) @@ -1770,58 +2036,89 @@ pub fn fn_match<'a>( _ => return Err(Error::D3010EmptyPattern(context.char_index)), }; - let limit = args - .get(2) - .and_then(|val| { - if val.is_number() { - Some(val.as_f64() as usize) - } else { - None - } - }) - .unwrap_or(usize::MAX); + let limit = args.get(2).and_then(|val| { + if val.is_number() { + Some(val.as_f64() as usize) + } else { + None + } + }); + + Ok(evaluate_match( + context.arena, + regex_literal.get_regex(), + &value_to_validate.as_str(), + limit, + )) +} - let key_match = BumpString::from_str_in("match", context.arena); - let key_index = BumpString::from_str_in("index", context.arena); - let key_groups = BumpString::from_str_in("groups", context.arena); +/// An inner helper which evaluates the `$match` function. +/// +/// The return value is a Value::Array which looks like: +/// +/// [ +/// { +/// "match": "ab", +/// "index": 0, +/// "groups": ["b"] +/// }, +/// { +/// "match": "abb", +/// "index": 2, +/// "groups": ["bb"] +/// }, +/// { +/// "match": "abb", +/// "index": 5, +/// "groups": ["bb" ] +/// } +/// ] +fn evaluate_match<'a>( + arena: &'a Bump, + regex: &Regex, + input_str: &str, + limit: Option, +) -> &'a Value<'a> { + let limit = limit.unwrap_or(usize::MAX); + + let key_match = BumpString::from_str_in("match", arena); + let key_index = BumpString::from_str_in("index", arena); + let key_groups = BumpString::from_str_in("groups", arena); let mut matches: bumpalo::collections::Vec<&Value<'a>> = - bumpalo::collections::Vec::new_in(context.arena); + bumpalo::collections::Vec::new_in(arena); - for (i, m) in regex_literal - .get_regex() - .find_iter(&value_to_validate.as_str()) - .enumerate() - { + for (i, m) in regex.find_iter(input_str).enumerate() { if i >= limit { break; } - let matched_text = &value_to_validate.as_str()[m.start()..m.end()]; - let match_str = context - .arena - .alloc(Value::string(context.arena, matched_text)); + let matched_text = &input_str[m.start()..m.end()]; + let match_str = arena.alloc(Value::string(arena, matched_text)); + + let index_val = arena.alloc(Value::number(arena, i as f64)); + + // Extract capture groups as values + let capture_groups = m + .groups() + .filter_map(|group| group.map(|range| &input_str[range.start..range.end])) + .map(|s| BumpString::from_str_in(s, arena)) + .map(|s| &*arena.alloc(Value::String(s))) + // Skip the first group which is the entire match + .skip(1); - let index_val = context - .arena - .alloc(Value::number(context.arena, m.start() as f64)); + let group_vec = BumpVec::from_iter_in(capture_groups, arena); - let group_vec: bumpalo::collections::Vec<&Value<'a>> = - bumpalo::collections::Vec::new_in(context.arena); - let groups_val = context - .arena - .alloc(Value::Array(group_vec, ArrayFlags::empty())); + let groups_val = arena.alloc(Value::Array(group_vec, ArrayFlags::empty())); let mut match_obj: HashMap, DefaultHashBuilder, &Bump> = - HashMap::with_capacity_and_hasher_in(3, DefaultHashBuilder::default(), context.arena); + HashMap::with_capacity_and_hasher_in(3, DefaultHashBuilder::default(), arena); match_obj.insert(key_match.clone(), match_str); match_obj.insert(key_index.clone(), index_val); match_obj.insert(key_groups.clone(), groups_val); - matches.push(context.arena.alloc(Value::Object(match_obj))); + matches.push(arena.alloc(Value::Object(match_obj))); } - Ok(context - .arena - .alloc(Value::Array(matches, ArrayFlags::empty()))) + arena.alloc(Value::Array(matches, ArrayFlags::empty())) } diff --git a/tests/testsuite.rs b/tests/testsuite.rs index 293fb5b1..f8126c51 100644 --- a/tests/testsuite.rs +++ b/tests/testsuite.rs @@ -117,17 +117,17 @@ fn test_case(resource: &str) { ); } else { eprintln!("RESULT: {}", result); - assert_eq!(result, expected_result); + assert_eq!(expected_result, result); } } Err(error) => { eprintln!("ERROR: {}", error); - let code = if !case["error"].is_undefined() { + let expected_code = if !case["error"].is_undefined() { &case["error"]["code"] } else { &case["code"] }; - assert_eq!(*code, error.code()); + assert_eq!(*expected_code, error.code()); } } } diff --git a/tests/testsuite/skip/regex/case000.json b/tests/testsuite/groups/regex/case000.json similarity index 100% rename from tests/testsuite/skip/regex/case000.json rename to tests/testsuite/groups/regex/case000.json diff --git a/tests/testsuite/skip/regex/case001.json b/tests/testsuite/groups/regex/case001.json similarity index 100% rename from tests/testsuite/skip/regex/case001.json rename to tests/testsuite/groups/regex/case001.json diff --git a/tests/testsuite/skip/regex/case002.json b/tests/testsuite/groups/regex/case002.json similarity index 100% rename from tests/testsuite/skip/regex/case002.json rename to tests/testsuite/groups/regex/case002.json diff --git a/tests/testsuite/skip/regex/case003.json b/tests/testsuite/groups/regex/case003.json similarity index 100% rename from tests/testsuite/skip/regex/case003.json rename to tests/testsuite/groups/regex/case003.json diff --git a/tests/testsuite/skip/regex/case004.json b/tests/testsuite/groups/regex/case004.json similarity index 100% rename from tests/testsuite/skip/regex/case004.json rename to tests/testsuite/groups/regex/case004.json diff --git a/tests/testsuite/skip/regex/case005.json b/tests/testsuite/groups/regex/case005.json similarity index 100% rename from tests/testsuite/skip/regex/case005.json rename to tests/testsuite/groups/regex/case005.json diff --git a/tests/testsuite/skip/regex/case006.json b/tests/testsuite/groups/regex/case006.json similarity index 100% rename from tests/testsuite/skip/regex/case006.json rename to tests/testsuite/groups/regex/case006.json diff --git a/tests/testsuite/skip/regex/case007.json b/tests/testsuite/groups/regex/case007.json similarity index 100% rename from tests/testsuite/skip/regex/case007.json rename to tests/testsuite/groups/regex/case007.json diff --git a/tests/testsuite/skip/regex/case008.json b/tests/testsuite/groups/regex/case008.json similarity index 100% rename from tests/testsuite/skip/regex/case008.json rename to tests/testsuite/groups/regex/case008.json diff --git a/tests/testsuite/skip/regex/case009.json b/tests/testsuite/groups/regex/case009.json similarity index 100% rename from tests/testsuite/skip/regex/case009.json rename to tests/testsuite/groups/regex/case009.json diff --git a/tests/testsuite/skip/regex/case010.json b/tests/testsuite/groups/regex/case010.json similarity index 100% rename from tests/testsuite/skip/regex/case010.json rename to tests/testsuite/groups/regex/case010.json diff --git a/tests/testsuite/skip/regex/case011.json b/tests/testsuite/groups/regex/case011.json similarity index 100% rename from tests/testsuite/skip/regex/case011.json rename to tests/testsuite/groups/regex/case011.json diff --git a/tests/testsuite/skip/regex/case012.json b/tests/testsuite/groups/regex/case012.json similarity index 100% rename from tests/testsuite/skip/regex/case012.json rename to tests/testsuite/groups/regex/case012.json diff --git a/tests/testsuite/skip/regex/case013.json b/tests/testsuite/groups/regex/case013.json similarity index 100% rename from tests/testsuite/skip/regex/case013.json rename to tests/testsuite/groups/regex/case013.json diff --git a/tests/testsuite/skip/regex/case014.json b/tests/testsuite/groups/regex/case014.json similarity index 100% rename from tests/testsuite/skip/regex/case014.json rename to tests/testsuite/groups/regex/case014.json diff --git a/tests/testsuite/skip/regex/case015.json b/tests/testsuite/groups/regex/case015.json similarity index 100% rename from tests/testsuite/skip/regex/case015.json rename to tests/testsuite/groups/regex/case015.json diff --git a/tests/testsuite/skip/regex/case016.json b/tests/testsuite/groups/regex/case016.json similarity index 100% rename from tests/testsuite/skip/regex/case016.json rename to tests/testsuite/groups/regex/case016.json diff --git a/tests/testsuite/skip/regex/case017.json b/tests/testsuite/groups/regex/case017.json similarity index 100% rename from tests/testsuite/skip/regex/case017.json rename to tests/testsuite/groups/regex/case017.json diff --git a/tests/testsuite/skip/regex/case018.json b/tests/testsuite/groups/regex/case018.json similarity index 100% rename from tests/testsuite/skip/regex/case018.json rename to tests/testsuite/groups/regex/case018.json diff --git a/tests/testsuite/skip/regex/case019.json b/tests/testsuite/groups/regex/case019.json similarity index 100% rename from tests/testsuite/skip/regex/case019.json rename to tests/testsuite/groups/regex/case019.json diff --git a/tests/testsuite/skip/regex/case020.json b/tests/testsuite/groups/regex/case020.json similarity index 100% rename from tests/testsuite/skip/regex/case020.json rename to tests/testsuite/groups/regex/case020.json diff --git a/tests/testsuite/skip/regex/case021.json b/tests/testsuite/groups/regex/case021.json similarity index 100% rename from tests/testsuite/skip/regex/case021.json rename to tests/testsuite/groups/regex/case021.json diff --git a/tests/testsuite/skip/regex/case022.json b/tests/testsuite/groups/regex/case022.json similarity index 100% rename from tests/testsuite/skip/regex/case022.json rename to tests/testsuite/groups/regex/case022.json diff --git a/tests/testsuite/skip/regex/case023.json b/tests/testsuite/groups/regex/case023.json similarity index 100% rename from tests/testsuite/skip/regex/case023.json rename to tests/testsuite/groups/regex/case023.json diff --git a/tests/testsuite/skip/regex/case024.json b/tests/testsuite/groups/regex/case024.json similarity index 100% rename from tests/testsuite/skip/regex/case024.json rename to tests/testsuite/groups/regex/case024.json diff --git a/tests/testsuite/skip/regex/case025.json b/tests/testsuite/groups/regex/case025.json similarity index 100% rename from tests/testsuite/skip/regex/case025.json rename to tests/testsuite/groups/regex/case025.json diff --git a/tests/testsuite/skip/regex/case026.json b/tests/testsuite/groups/regex/case026.json similarity index 100% rename from tests/testsuite/skip/regex/case026.json rename to tests/testsuite/groups/regex/case026.json diff --git a/tests/testsuite/skip/regex/case027.json b/tests/testsuite/groups/regex/case027.json similarity index 100% rename from tests/testsuite/skip/regex/case027.json rename to tests/testsuite/groups/regex/case027.json diff --git a/tests/testsuite/skip/regex/case028.json b/tests/testsuite/groups/regex/case028.json similarity index 100% rename from tests/testsuite/skip/regex/case028.json rename to tests/testsuite/groups/regex/case028.json diff --git a/tests/testsuite/skip/regex/case029.json b/tests/testsuite/groups/regex/case029.json similarity index 100% rename from tests/testsuite/skip/regex/case029.json rename to tests/testsuite/groups/regex/case029.json diff --git a/tests/testsuite/skip/regex/case030.json b/tests/testsuite/groups/regex/case030.json similarity index 100% rename from tests/testsuite/skip/regex/case030.json rename to tests/testsuite/groups/regex/case030.json diff --git a/tests/testsuite/skip/regex/case031.json b/tests/testsuite/groups/regex/case031.json similarity index 100% rename from tests/testsuite/skip/regex/case031.json rename to tests/testsuite/groups/regex/case031.json diff --git a/tests/testsuite/skip/regex/case032.json b/tests/testsuite/groups/regex/case032.json similarity index 100% rename from tests/testsuite/skip/regex/case032.json rename to tests/testsuite/groups/regex/case032.json diff --git a/tests/testsuite/skip/regex/case033.json b/tests/testsuite/groups/regex/case033.json similarity index 100% rename from tests/testsuite/skip/regex/case033.json rename to tests/testsuite/groups/regex/case033.json diff --git a/tests/testsuite/skip/regex/case034.json b/tests/testsuite/groups/regex/case034.json similarity index 100% rename from tests/testsuite/skip/regex/case034.json rename to tests/testsuite/groups/regex/case034.json diff --git a/tests/testsuite/skip/regex/case035.json b/tests/testsuite/groups/regex/case035.json similarity index 100% rename from tests/testsuite/skip/regex/case035.json rename to tests/testsuite/groups/regex/case035.json diff --git a/tests/testsuite/skip/regex/case036.json b/tests/testsuite/groups/regex/case036.json similarity index 100% rename from tests/testsuite/skip/regex/case036.json rename to tests/testsuite/groups/regex/case036.json diff --git a/tests/testsuite/skip/regex/case038.json b/tests/testsuite/groups/regex/case038.json similarity index 100% rename from tests/testsuite/skip/regex/case038.json rename to tests/testsuite/groups/regex/case038.json