diff --git a/.jules/bolt.md b/.jules/bolt.md index 46c95870..2f0f1734 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -49,3 +49,7 @@ ## 2026-03-01 - [Optimize list concat with pre-allocation] **Learning:** Calling `clone()` on a list and then `extend()` with another list causes an unnecessary memory reallocation, making list concatenation inefficient for large lists. **Action:** Pre-calculate the combined length and use `Vec::with_capacity` followed by `extend()` from both iterators. This avoids reallocation and yields a performance improvement. + +## 2026-03-05 - [Optimize Unicode Text Casing Fast Paths] +**Learning:** When trying to avoid string allocations for `touppercase` and `tolowercase` if the string is already in the target case, using a simple check like `!text.chars().any(char::is_lowercase)` is flawed due to complex Unicode casing rules (e.g., modifier marks or Titlecase characters like `Dž`). These characters might not be lowercase, but they still change when uppercase is applied. +**Action:** Always verify that every character actually remains identical under the casing transformation. Use `.chars().all(|c| { let mut iter = c.to_uppercase(); iter.next() == Some(c) && iter.next().is_none() })` to safely identify if an allocation-free fast path can be taken. diff --git a/src/stdlib/helpers.rs b/src/stdlib/helpers.rs index 3dfe04a0..9c21a0e1 100644 --- a/src/stdlib/helpers.rs +++ b/src/stdlib/helpers.rs @@ -216,6 +216,26 @@ where Ok(Value::Text(op(&text).into())) } +/// Helper for unary text operations that can reuse the `Arc` to avoid allocations +/// when the string is unchanged. +/// +/// Handles argument count checking, type extraction, operation execution, +/// and result wrapping. +/// +/// # Arguments +/// +/// * `func_name` - Name of the function for error messages +/// * `args` - Arguments passed to the function +/// * `op` - The operation to perform on the text +pub fn unary_text_op_arc(func_name: &str, args: Vec, op: F) -> Result +where + F: Fn(Arc) -> Arc, +{ + check_arg_count(func_name, &args, 1)?; + let text = expect_text(&args[0])?; + Ok(Value::Text(op(text))) +} + /// Helper for binary text predicates ((String, String) -> bool) /// /// Handles argument count checking, type extraction, operation execution, diff --git a/src/stdlib/text.rs b/src/stdlib/text.rs index 6f166fd0..086a7c35 100644 --- a/src/stdlib/text.rs +++ b/src/stdlib/text.rs @@ -1,5 +1,6 @@ use super::helpers::{ binary_text_predicate, check_arg_count, expect_number, expect_text, unary_text_op, + unary_text_op_arc, }; use crate::interpreter::environment::Environment; use crate::interpreter::error::RuntimeError; @@ -90,11 +91,35 @@ fn parse_key_value_pairs( // which handles both text and lists pub fn native_touppercase(args: Vec) -> Result { - unary_text_op("touppercase", args, |text| text.to_uppercase()) + // Optimization: avoid string allocation if string is already uppercase + unary_text_op_arc("touppercase", args, |text| { + // fast path: check if it changes when converted to uppercase + let is_uppercase = text.chars().all(|c| { + let mut iter = c.to_uppercase(); + iter.next() == Some(c) && iter.next().is_none() + }); + if is_uppercase { + text + } else { + Arc::from(text.to_uppercase()) + } + }) } pub fn native_tolowercase(args: Vec) -> Result { - unary_text_op("tolowercase", args, |text| text.to_lowercase()) + // Optimization: avoid string allocation if string is already lowercase + unary_text_op_arc("tolowercase", args, |text| { + // fast path: check if it changes when converted to lowercase + let is_lowercase = text.chars().all(|c| { + let mut iter = c.to_lowercase(); + iter.next() == Some(c) && iter.next().is_none() + }); + if is_lowercase { + text + } else { + Arc::from(text.to_lowercase()) + } + }) } pub fn native_substring(args: Vec) -> Result { @@ -172,7 +197,15 @@ pub fn native_string_split(args: Vec) -> Result { } pub fn native_trim(args: Vec) -> Result { - unary_text_op("trim", args, |text| Arc::from(text.trim())) + // Optimization: avoid string allocation if string is already trimmed + unary_text_op_arc("trim", args, |text| { + let trimmed = text.trim(); + if trimmed.len() == text.len() { + text + } else { + Arc::from(trimmed) + } + }) } pub fn native_starts_with(args: Vec) -> Result {