diff --git a/.jules/bolt.md b/.jules/bolt.md index 99f4cf5..3644ef5 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -2,3 +2,7 @@ **Learning:** In tight parsing loops in Rust (e.g., `normalize_keywords` and `remove_numeric_separators`), using `.chars().collect::>()` introduces severe performance overhead due to O(N) memory allocation per operation. Similarly, repeatedly allocating new `String` instances for word extraction and `.to_ascii_lowercase()` within loops causes significant heap traffic. **Action:** When performing string transformations or extractions in high-frequency functions, prefer slicing directly with `&str`, checking bytes directly (e.g. `value.as_bytes()`) where possible (such as when scanning for single-byte ASCII characters), and using non-allocating comparison methods like `eq_ignore_ascii_case()` instead of allocating a lowercase string. Always avoid `.chars().collect::>()` when a streaming iterator (`chars()` or `as_bytes()`) suffices. +## 2026-08-12 - [Avoid String allocation during initial parsing segment split] +**Learning:** In `split_segments`, constructing `Segment` previously performed `text.to_owned()` for every split segment. This caused unnecessary memory allocation, as the parsed segment string could just borrow from the original input `&str`. + +**Action:** Update parsing intermediate structs (like `Segment`) to carry string slices (`&'a str`) representing chunks of the input string rather than owning `String`s when they are only used briefly to route segments to transformation parsers. diff --git a/compiler/rockql-parser/src/lib.rs b/compiler/rockql-parser/src/lib.rs index 04e1d44..1ddfaa9 100644 --- a/compiler/rockql-parser/src/lib.rs +++ b/compiler/rockql-parser/src/lib.rs @@ -28,8 +28,8 @@ impl Display for Diagnostic { } #[derive(Debug)] -struct Segment { - text: String, +struct Segment<'a> { + text: &'a str, span: Span, } @@ -47,7 +47,7 @@ pub fn parse(source: &str) -> Result> { let mut diagnostics = Vec::new(); for segment in segments { - match parse_transform(&segment.text, segment.span) { + match parse_transform(segment.text, segment.span) { Ok(transform) => transforms.push(SpannedTransform::new(segment.span, transform)), Err(diagnostic) => diagnostics.push(diagnostic), } @@ -64,7 +64,7 @@ pub fn format_source(source: &str) -> Result> { parse(source).map(|query| format!("{query}\n")) } -fn split_segments(source: &str) -> Vec { +fn split_segments(source: &str) -> Vec> { let mut segments = Vec::new(); for (line_index, line) in source.lines().enumerate() { @@ -88,7 +88,7 @@ fn split_segments(source: &str) -> Vec { segments } -fn push_segment(segments: &mut Vec, raw: &str, line: usize, byte_start: usize) { +fn push_segment<'a>(segments: &mut Vec>, raw: &'a str, line: usize, byte_start: usize) { let text = raw.trim(); if text.is_empty() { return; @@ -96,7 +96,7 @@ fn push_segment(segments: &mut Vec, raw: &str, line: usize, byte_start: let leading_bytes = raw.find(text).unwrap_or(0); segments.push(Segment { - text: text.to_owned(), + text, span: Span::new(line, byte_start + leading_bytes + 1), }); }