From 7195f7904a45ad7f4b0ec28a1369d17ed09d58e5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 09:59:42 +0000 Subject: [PATCH 1/4] Refactor pattern stdlib to use helper functions Co-authored-by: logbie <1138960+logbie@users.noreply.github.com> --- src/interpreter/mod.rs | 6 +- src/stdlib/helpers.rs | 29 ++++++ src/stdlib/pattern.rs | 192 +++++-------------------------------- src/stdlib/pattern_test.rs | 9 +- 4 files changed, 63 insertions(+), 173 deletions(-) diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 46c92281..7f8a2d14 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -6659,7 +6659,8 @@ impl Interpreter { .await?; let args = vec![text_val, pattern_val, replacement_val]; // Note: text, pattern, then replacement - crate::stdlib::pattern::native_pattern_replace(args, *_line, *_column) + crate::stdlib::pattern::native_pattern_replace(args) + .map_err(|e| RuntimeError::with_kind(e.message, *_line, *_column, e.kind)) } Expression::PatternSplit { @@ -6672,7 +6673,8 @@ impl Interpreter { let pattern_val = self.evaluate_expression(pattern, Rc::clone(&env)).await?; let args = vec![text_val, pattern_val]; - crate::stdlib::pattern::native_pattern_split(args, *_line, *_column) + crate::stdlib::pattern::native_pattern_split(args) + .map_err(|e| RuntimeError::with_kind(e.message, *_line, *_column, e.kind)) } Expression::StringSplit { text, diff --git a/src/stdlib/helpers.rs b/src/stdlib/helpers.rs index 50709836..e5a929b4 100644 --- a/src/stdlib/helpers.rs +++ b/src/stdlib/helpers.rs @@ -434,3 +434,32 @@ pub fn expect_datetime(value: &Value) -> Result, Runti )), } } + +/// Extracts a CompiledPattern value from a WFL Value, returning it as a reference-counted CompiledPattern. +/// +/// Returns an `Rc` clone (incrementing the reference count) if the value +/// is a Pattern variant. +/// +/// # Arguments +/// +/// * `value` - The WFL Value to extract from +/// +/// # Returns +/// +/// Returns an `Rc` clone (incrementing the reference count) if the value +/// is a Pattern variant. +/// +/// # Errors +/// +/// Returns `RuntimeError` if the value is not a Pattern, with an error message +/// indicating the expected type and the actual type received. +pub fn expect_pattern(value: &Value) -> Result, RuntimeError> { + match value { + Value::Pattern(p) => Ok(Rc::clone(p)), + _ => Err(RuntimeError::new( + format!("Expected a pattern, got {}", value.type_name()), + 0, + 0, + )), + } +} diff --git a/src/stdlib/pattern.rs b/src/stdlib/pattern.rs index 57ce9fd9..24ec9c10 100644 --- a/src/stdlib/pattern.rs +++ b/src/stdlib/pattern.rs @@ -1,3 +1,4 @@ +use super::helpers::{check_arg_count, expect_pattern, expect_text}; use crate::interpreter::environment::Environment; use crate::interpreter::error::RuntimeError; use crate::interpreter::value::Value; @@ -28,74 +29,24 @@ pub fn register(env: &mut Environment) { /// Native function: pattern_matches(text, pattern) -> boolean /// Tests if text matches the given compiled pattern pub fn pattern_matches_native(args: Vec) -> Result { - if args.len() != 2 { - return Err(RuntimeError::new( - "pattern_matches requires exactly 2 arguments (text, pattern)".to_string(), - 0, - 0, - )); - } - - let text_str = match &args[0] { - Value::Text(s) => s.as_ref(), - _ => { - return Err(RuntimeError::new( - "First argument to pattern_matches must be text".to_string(), - 0, - 0, - )); - } - }; + check_arg_count("pattern_matches", &args, 2)?; - let compiled_pattern = match &args[1] { - Value::Pattern(p) => p, - _ => { - return Err(RuntimeError::new( - "Second argument to pattern_matches must be a compiled pattern".to_string(), - 0, - 0, - )); - } - }; + let text_str = expect_text(&args[0])?; + let compiled_pattern = expect_pattern(&args[1])?; - let matches = compiled_pattern.matches(text_str); + let matches = compiled_pattern.matches(text_str.as_ref()); Ok(Value::Bool(matches)) } /// Native function: pattern_find(text, pattern) -> object or null /// Finds the first match of pattern in text pub fn pattern_find_native(args: Vec) -> Result { - if args.len() != 2 { - return Err(RuntimeError::new( - "pattern_find requires exactly 2 arguments (text, pattern)".to_string(), - 0, - 0, - )); - } + check_arg_count("pattern_find", &args, 2)?; - let text_str = match &args[0] { - Value::Text(s) => s.as_ref(), - _ => { - return Err(RuntimeError::new( - "First argument to pattern_find must be text".to_string(), - 0, - 0, - )); - } - }; + let text_str = expect_text(&args[0])?; + let compiled_pattern = expect_pattern(&args[1])?; - let compiled_pattern = match &args[1] { - Value::Pattern(p) => p, - _ => { - return Err(RuntimeError::new( - "Second argument to pattern_find must be a compiled pattern".to_string(), - 0, - 0, - )); - } - }; - - match compiled_pattern.find(text_str) { + match compiled_pattern.find(text_str.as_ref()) { Some(match_result) => { let mut result_map = HashMap::new(); result_map.insert( @@ -129,37 +80,12 @@ pub fn pattern_find_native(args: Vec) -> Result { /// Native function: pattern_find_all(text, pattern) -> list /// Finds all matches of pattern in text pub fn pattern_find_all_native(args: Vec) -> Result { - if args.len() != 2 { - return Err(RuntimeError::new( - "pattern_find_all requires exactly 2 arguments (text, pattern)".to_string(), - 0, - 0, - )); - } - - let text_str = match &args[0] { - Value::Text(s) => s.as_ref(), - _ => { - return Err(RuntimeError::new( - "First argument to pattern_find_all must be text".to_string(), - 0, - 0, - )); - } - }; + check_arg_count("pattern_find_all", &args, 2)?; - let compiled_pattern = match &args[1] { - Value::Pattern(p) => p, - _ => { - return Err(RuntimeError::new( - "Second argument to pattern_find_all must be a compiled pattern".to_string(), - 0, - 0, - )); - } - }; + let text_str = expect_text(&args[0])?; + let compiled_pattern = expect_pattern(&args[1])?; - let matches = compiled_pattern.find_all(text_str); + let matches = compiled_pattern.find_all(text_str.as_ref()); let mut result_list = Vec::new(); for match_result in matches { @@ -193,98 +119,30 @@ pub fn pattern_find_all_native(args: Vec) -> Result } /// Native function for pattern replacement (called by interpreter) -pub fn native_pattern_replace( - args: Vec, - line: usize, - column: usize, -) -> Result { - if args.len() != 3 { - return Err(RuntimeError::new( - "pattern_replace requires exactly 3 arguments".to_string(), - line, - column, - )); - } - - let text = match &args[0] { - Value::Text(t) => t.as_ref(), - _ => { - return Err(RuntimeError::new( - "First argument must be text".to_string(), - line, - column, - )); - } - }; +pub fn native_pattern_replace(args: Vec) -> Result { + check_arg_count("pattern_replace", &args, 3)?; - let _pattern = match &args[1] { - Value::Pattern(p) => p.as_ref(), - _ => { - return Err(RuntimeError::new( - "Second argument must be a pattern".to_string(), - line, - column, - )); - } - }; - - let _replacement = match &args[2] { - Value::Text(t) => t.as_ref(), - _ => { - return Err(RuntimeError::new( - "Third argument must be text".to_string(), - line, - column, - )); - } - }; + let text = expect_text(&args[0])?; + let _pattern = expect_pattern(&args[1])?; + let _replacement = expect_text(&args[2])?; // TODO: Update to use new pattern system for replacement - Ok(Value::Text(Arc::from(text))) + Ok(Value::Text(text)) } /// Native function for pattern splitting (called by interpreter) -pub fn native_pattern_split( - args: Vec, - line: usize, - column: usize, -) -> Result { - if args.len() != 2 { - return Err(RuntimeError::new( - "pattern_split requires exactly 2 arguments".to_string(), - line, - column, - )); - } +pub fn native_pattern_split(args: Vec) -> Result { + check_arg_count("pattern_split", &args, 2)?; - let text = match &args[0] { - Value::Text(t) => t.as_ref(), - _ => { - return Err(RuntimeError::new( - "First argument must be text".to_string(), - line, - column, - )); - } - }; - - let pattern = match &args[1] { - Value::Pattern(p) => p, - _ => { - return Err(RuntimeError::new( - "Second argument must be a pattern".to_string(), - line, - column, - )); - } - }; + let text = expect_text(&args[0])?; + let pattern = expect_pattern(&args[1])?; // Find all matches of the pattern in the text - let matches = pattern.find_all(text); + let matches = pattern.find_all(text.as_ref()); // If no matches, return the entire text as a single element if matches.is_empty() { - let parts = vec![Value::Text(Arc::from(text))]; + let parts = vec![Value::Text(Arc::clone(&text))]; return Ok(Value::List(Rc::new(RefCell::new(parts)))); } diff --git a/src/stdlib/pattern_test.rs b/src/stdlib/pattern_test.rs index a7a4a9a2..ca05614a 100644 --- a/src/stdlib/pattern_test.rs +++ b/src/stdlib/pattern_test.rs @@ -21,7 +21,7 @@ mod tests { result .unwrap_err() .to_string() - .contains("exactly 2 arguments") + .contains("expects 2 arguments") ); } @@ -33,7 +33,7 @@ mod tests { result .unwrap_err() .to_string() - .contains("exactly 2 arguments") + .contains("expects 2 arguments") ); } @@ -45,7 +45,7 @@ mod tests { result .unwrap_err() .to_string() - .contains("exactly 2 arguments") + .contains("expects 2 arguments") ); } @@ -57,6 +57,7 @@ mod tests { ]; let result = pattern_matches_native(args); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("First argument")); + // Updated error message from expect_text helper + assert!(result.unwrap_err().to_string().contains("Expected text")); } } From 9bcf5107d53d23c81b70fc4835240e10ebd0d76e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:33:28 +0000 Subject: [PATCH 2/4] Implement pattern replacement and update docs Co-authored-by: logbie <1138960+logbie@users.noreply.github.com> --- src/stdlib/helpers.rs | 11 +++++++++++ src/stdlib/pattern.rs | 29 +++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/stdlib/helpers.rs b/src/stdlib/helpers.rs index e5a929b4..11ec42a4 100644 --- a/src/stdlib/helpers.rs +++ b/src/stdlib/helpers.rs @@ -453,6 +453,17 @@ pub fn expect_datetime(value: &Value) -> Result, Runti /// /// Returns `RuntimeError` if the value is not a Pattern, with an error message /// indicating the expected type and the actual type received. +/// +/// # Examples +/// +/// ```ignore +/// pub fn pattern_matches_native(args: Vec) -> Result { +/// check_arg_count("pattern_matches", &args, 2)?; +/// let compiled_pattern = expect_pattern(&args[1])?; +/// // Use compiled_pattern.matches(...) +/// Ok(Value::Bool(true)) +/// } +/// ``` pub fn expect_pattern(value: &Value) -> Result, RuntimeError> { match value { Value::Pattern(p) => Ok(Rc::clone(p)), diff --git a/src/stdlib/pattern.rs b/src/stdlib/pattern.rs index 24ec9c10..cbb78f83 100644 --- a/src/stdlib/pattern.rs +++ b/src/stdlib/pattern.rs @@ -123,11 +123,32 @@ pub fn native_pattern_replace(args: Vec) -> Result { check_arg_count("pattern_replace", &args, 3)?; let text = expect_text(&args[0])?; - let _pattern = expect_pattern(&args[1])?; - let _replacement = expect_text(&args[2])?; + let pattern = expect_pattern(&args[1])?; + let replacement = expect_text(&args[2])?; + + let text_str = text.as_ref(); + let matches = pattern.find_all(text_str); + + // If no matches, return original text + if matches.is_empty() { + return Ok(Value::Text(text)); + } + + let mut result = String::with_capacity(text_str.len()); + let mut last_end = 0; + + for m in matches { + // Append text between last match and current match + result.push_str(&text_str[last_end..m.start]); + // Append replacement + result.push_str(replacement.as_ref()); + last_end = m.end; + } + + // Append remaining text + result.push_str(&text_str[last_end..]); - // TODO: Update to use new pattern system for replacement - Ok(Value::Text(text)) + Ok(Value::Text(Arc::from(result))) } /// Native function for pattern splitting (called by interpreter) From f8ed226f1d7292eaf38c94c12efe832e103cb0b2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:13:22 +0000 Subject: [PATCH 3/4] Fix UTF-8 slicing in pattern replace Co-authored-by: logbie <1138960+logbie@users.noreply.github.com> --- src/stdlib/pattern.rs | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/src/stdlib/pattern.rs b/src/stdlib/pattern.rs index cbb78f83..82f3549a 100644 --- a/src/stdlib/pattern.rs +++ b/src/stdlib/pattern.rs @@ -134,19 +134,53 @@ pub fn native_pattern_replace(args: Vec) -> Result { return Ok(Value::Text(text)); } + // Build character-to-byte index mapping + // This is needed because match indices are character offsets, but string slicing uses byte offsets + let char_to_byte: Vec = text_str + .char_indices() + .map(|(byte_idx, _)| byte_idx) + .collect(); + let mut char_to_byte = char_to_byte; + char_to_byte.push(text_str.len()); // Add final byte position + let mut result = String::with_capacity(text_str.len()); - let mut last_end = 0; + let mut last_end_char = 0; for m in matches { + // Convert character indices to byte indices + // Safe access: if index >= len, use length of string (byte offset) + let start_byte = if m.start < char_to_byte.len() { + char_to_byte[m.start] + } else { + text_str.len() + }; + + let last_end_byte = if last_end_char < char_to_byte.len() { + char_to_byte[last_end_char] + } else { + text_str.len() + }; + // Append text between last match and current match - result.push_str(&text_str[last_end..m.start]); + if start_byte > last_end_byte { + result.push_str(&text_str[last_end_byte..start_byte]); + } + // Append replacement result.push_str(replacement.as_ref()); - last_end = m.end; + last_end_char = m.end; } // Append remaining text - result.push_str(&text_str[last_end..]); + let last_end_byte = if last_end_char < char_to_byte.len() { + char_to_byte[last_end_char] + } else { + text_str.len() + }; + + if last_end_byte < text_str.len() { + result.push_str(&text_str[last_end_byte..]); + } Ok(Value::Text(Arc::from(result))) } From 1a48ac968308048492758b1238eca46c9162ef44 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:30:57 +0000 Subject: [PATCH 4/4] Fix UTF-8 slicing in pattern replace Co-authored-by: logbie <1138960+logbie@users.noreply.github.com>