Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
40 changes: 40 additions & 0 deletions src/stdlib/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,3 +434,43 @@ pub fn expect_datetime(value: &Value) -> Result<Rc<chrono::NaiveDateTime>, Runti
)),
}
}

/// Extracts a CompiledPattern value from a WFL Value, returning it as a reference-counted CompiledPattern.
///
/// Returns an `Rc<CompiledPattern>` clone (incrementing the reference count) if the value
/// is a Pattern variant.
///
/// # Arguments
///
/// * `value` - The WFL Value to extract from
///
/// # Returns
///
/// Returns an `Rc<CompiledPattern>` 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.
Comment on lines +438 to +455

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment repeats the same “Returns an Rc<CompiledPattern> clone …” text in both the summary section and # Returns. Consider deduplicating to keep the docs concise (e.g., mention the Rc::clone behavior once, in # Returns).

Copilot uses AI. Check for mistakes.
///
/// # Examples
///
/// ```ignore
/// pub fn pattern_matches_native(args: Vec<Value>) -> Result<Value, RuntimeError> {
/// 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<Rc<crate::pattern::CompiledPattern>, RuntimeError> {
match value {
Value::Pattern(p) => Ok(Rc::clone(p)),
_ => Err(RuntimeError::new(
format!("Expected a pattern, got {}", value.type_name()),

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect_text uses the phrasing "Expected text, got ...", while expect_pattern uses "Expected a pattern, got ...". For consistency (and easier test/assert matching across helpers), consider standardizing the phrasing, e.g. "Expected pattern, got ..." (or update the other helpers to include the article).

Suggested change
format!("Expected a pattern, got {}", value.type_name()),
format!("Expected pattern, got {}", value.type_name()),

Copilot uses AI. Check for mistakes.
0,
0,
)),
}
}
Comment on lines +467 to +476

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new error message uses a different phrasing than other expect_* helpers (e.g., expect_text is "Expected text, got ..."). For consistency (and to simplify tests that match on message fragments), consider changing this to "Expected pattern, got {}" to align with the existing helper style.

Copilot uses AI. Check for mistakes.
239 changes: 76 additions & 163 deletions src/stdlib/pattern.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Value>) -> Result<Value, RuntimeError> {
if args.len() != 2 {
return Err(RuntimeError::new(
"pattern_matches requires exactly 2 arguments (text, pattern)".to_string(),
0,
0,
));
}
check_arg_count("pattern_matches", &args, 2)?;

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,
));
}
};
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_matches must be a compiled pattern".to_string(),
0,
0,
));
}
};

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<Value>) -> Result<Value, RuntimeError> {
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 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,
));
}
};
let text_str = expect_text(&args[0])?;
let compiled_pattern = expect_pattern(&args[1])?;

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(
Expand Down Expand Up @@ -129,37 +80,12 @@ pub fn pattern_find_native(args: Vec<Value>) -> Result<Value, RuntimeError> {
/// Native function: pattern_find_all(text, pattern) -> list
/// Finds all matches of pattern in text
pub fn pattern_find_all_native(args: Vec<Value>) -> Result<Value, RuntimeError> {
if args.len() != 2 {
return Err(RuntimeError::new(
"pattern_find_all requires exactly 2 arguments (text, pattern)".to_string(),
0,
0,
));
}
check_arg_count("pattern_find_all", &args, 2)?;

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,
));
}
};

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 {
Expand Down Expand Up @@ -193,98 +119,85 @@ pub fn pattern_find_all_native(args: Vec<Value>) -> Result<Value, RuntimeError>
}

/// Native function for pattern replacement (called by interpreter)
pub fn native_pattern_replace(
args: Vec<Value>,
line: usize,
column: usize,
) -> Result<Value, RuntimeError> {
if args.len() != 3 {
return Err(RuntimeError::new(
"pattern_replace requires exactly 3 arguments".to_string(),
line,
column,
));
pub fn native_pattern_replace(args: Vec<Value>) -> Result<Value, RuntimeError> {
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 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));
}
Comment on lines +122 to 135

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR changes native_pattern_replace from the prior placeholder behavior (it previously returned the original text) to a real replacement implementation. I don’t see any new/updated tests in this diff that exercise replacement correctness (basic replacement, multiple matches, leading/trailing matches, and Unicode text where char/byte indices differ). Adding targeted tests would help prevent regressions in the new indexing/slicing logic.

Copilot uses AI. Check for mistakes.

let text = match &args[0] {
Value::Text(t) => t.as_ref(),
_ => {
return Err(RuntimeError::new(
"First argument must be text".to_string(),
line,
column,
));
}
};
// 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<usize> = text_str
.char_indices()
.map(|(byte_idx, _)| byte_idx)
.collect();
let mut char_to_byte = char_to_byte;
Comment on lines +139 to +143

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces an unnecessary shadowing assignment (let mut char_to_byte = char_to_byte;). Prefer declaring it mutable in the initial binding to reduce churn and make the intent clearer.

Suggested change
let char_to_byte: Vec<usize> = text_str
.char_indices()
.map(|(byte_idx, _)| byte_idx)
.collect();
let mut char_to_byte = char_to_byte;
let mut char_to_byte: Vec<usize> = text_str
.char_indices()
.map(|(byte_idx, _)| byte_idx)
.collect();

Copilot uses AI. Check for mistakes.
char_to_byte.push(text_str.len()); // Add final byte position

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 mut result = String::with_capacity(text_str.len());
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()
};

let _replacement = match &args[2] {
Value::Text(t) => t.as_ref(),
_ => {
return Err(RuntimeError::new(
"Third argument must be text".to_string(),
line,
column,
));
// Append text between last match and current match
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_char = m.end;
}

// Append remaining text
let last_end_byte = if last_end_char < char_to_byte.len() {
char_to_byte[last_end_char]
} else {
text_str.len()
};

// TODO: Update to use new pattern system for replacement
Ok(Value::Text(Arc::from(text)))
if last_end_byte < text_str.len() {
result.push_str(&text_str[last_end_byte..]);
}

Ok(Value::Text(Arc::from(result)))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// Native function for pattern splitting (called by interpreter)
pub fn native_pattern_split(
args: Vec<Value>,
line: usize,
column: usize,
) -> Result<Value, RuntimeError> {
if args.len() != 2 {
return Err(RuntimeError::new(
"pattern_split requires exactly 2 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_split(args: Vec<Value>) -> Result<Value, RuntimeError> {
check_arg_count("pattern_split", &args, 2)?;

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))));
}

Expand Down
9 changes: 5 additions & 4 deletions src/stdlib/pattern_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ mod tests {
result
.unwrap_err()
.to_string()
.contains("exactly 2 arguments")
.contains("expects 2 arguments")
);
}

Expand All @@ -33,7 +33,7 @@ mod tests {
result
.unwrap_err()
.to_string()
.contains("exactly 2 arguments")
.contains("expects 2 arguments")
);
}

Expand All @@ -45,7 +45,7 @@ mod tests {
result
.unwrap_err()
.to_string()
.contains("exactly 2 arguments")
.contains("expects 2 arguments")
);
}

Expand All @@ -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"));
}
}
Loading