Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
20 changes: 20 additions & 0 deletions src/stdlib/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,26 @@ where
Ok(Value::Text(op(&text).into()))
}

/// Helper for unary text operations that can reuse the `Arc<str>` 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<F>(func_name: &str, args: Vec<Value>, op: F) -> Result<Value, RuntimeError>
where
F: Fn(Arc<str>) -> Arc<str>,
{
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,
Expand Down
39 changes: 36 additions & 3 deletions src/stdlib/text.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -90,11 +91,35 @@ fn parse_key_value_pairs(
// which handles both text and lists

pub fn native_touppercase(args: Vec<Value>) -> Result<Value, RuntimeError> {
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<Value>) -> Result<Value, RuntimeError> {
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<Value>) -> Result<Value, RuntimeError> {
Expand Down Expand Up @@ -172,7 +197,15 @@ pub fn native_string_split(args: Vec<Value>) -> Result<Value, RuntimeError> {
}

pub fn native_trim(args: Vec<Value>) -> Result<Value, RuntimeError> {
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<Value>) -> Result<Value, RuntimeError> {
Expand Down