From 2cc6436248100c8274bc60cb1a67cdcc9a189206 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:57:43 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20text=20opera?= =?UTF-8?q?tions=20to=20avoid=20allocations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced `unary_text_op_arc` helper to allow returning the original `Arc` reference for `touppercase`, `tolowercase`, and `trim` if the string does not need modification. This avoids unnecessary `String` heap allocations when the string is already in its expected target state (e.g., already uppercase, lowercase, or trimmed). Co-authored-by: logbie <1138960+logbie@users.noreply.github.com> --- .jules/bolt.md | 4 ++++ patch_text.rs | 31 +++++++++++++++++++++++++++++++ src/stdlib/helpers.rs | 20 ++++++++++++++++++++ src/stdlib/text.rs | 39 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 patch_text.rs 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/patch_text.rs b/patch_text.rs new file mode 100644 index 00000000..63e47783 --- /dev/null +++ b/patch_text.rs @@ -0,0 +1,31 @@ +pub fn native_touppercase(args: Vec) -> Result { + // 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 { + // 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()) + } + }) +} 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 { From 5b8d74c53fdf2025245eaaf6ef844a75257ae10d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:50:04 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20text=20opera?= =?UTF-8?q?tions=20to=20avoid=20allocations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced `unary_text_op_arc` helper to allow returning the original `Arc` reference for `touppercase`, `tolowercase`, and `trim` if the string does not need modification. This avoids unnecessary `String` heap allocations when the string is already in its expected target state (e.g., already uppercase, lowercase, or trimmed). Co-authored-by: logbie <1138960+logbie@users.noreply.github.com> --- patch_text.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 patch_text.rs diff --git a/patch_text.rs b/patch_text.rs deleted file mode 100644 index 63e47783..00000000 --- a/patch_text.rs +++ /dev/null @@ -1,31 +0,0 @@ -pub fn native_touppercase(args: Vec) -> Result { - // 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 { - // 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()) - } - }) -}