From 1ee6d11a79936ff419d9f3914586b359785313ef Mon Sep 17 00:00:00 2001 From: reuben olinsky Date: Wed, 17 Jun 2026 12:20:40 +0200 Subject: [PATCH 1/2] build(deps): upgrade cached 0.59 -> 2.0.2 Migrate the `cached` dependency across its 0.x -> 1.x -> 2.x breaking changes (latest published 2.x is 2.0.2): - proc-macro path `cached::proc_macro::cached` -> `cached::macros::cached` - `size = N` -> `max_size = N` on every `#[cached]` - drop `result = true` (Result-returning cached fns now skip caching `Err` by default, preserving prior behavior) - `SizedCache` -> `LruCache`; the removed `with_size()` constructor becomes `LruCache::builder().max_size(64).build()` While here, take advantage of the macro's default key being built by cloning the args: switch the parser cached fns (arithmetic/word/tokenizer) to borrowed arguments with explicit `key`/`convert`, eliminating a redundant caller-side `.to_owned()` on every call (halves clones for those hot paths). Cascade `&str` through the prompt and parsing chains so no `#[allow]`s are needed. regex.rs: `build()` is now fallible but unwrap/expect/panic are all denied, so wrap the thread-local LRU cache in `Option` and build via `.build().ok()`, gracefully degrading to uncached regex compilation if construction ever fails. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 13 ++++++------- brush-core/Cargo.toml | 2 +- brush-core/src/expansion.rs | 18 ++++++------------ brush-core/src/prompt.rs | 8 ++++---- brush-core/src/regex.rs | 17 +++++++++++++---- brush-core/src/shell/parsing.rs | 15 ++++++++++----- brush-core/src/shell/prompts.rs | 6 ++++-- brush-parser/Cargo.toml | 2 +- brush-parser/src/arithmetic.rs | 8 ++++---- brush-parser/src/tokenizer.rs | 15 ++++++++++----- brush-parser/src/word.rs | 16 ++++++++++------ 11 files changed, 69 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 101784d7a..d6cd75f07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -738,15 +738,14 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cached" -version = "0.59.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b6f5d101f0f6322c8646a45b7c581a673e476329040d97565815c2461dd0c4" +checksum = "dc0df7748fe2f601e376916ab19e7bfc2c74461b8abe3bce2ce20036ad8de38f" dependencies = [ "ahash", "cached_proc_macro", "cached_proc_macro_types", "hashbrown 0.16.1", - "once_cell", "parking_lot", "thiserror 2.0.18", "web-time", @@ -754,9 +753,9 @@ dependencies = [ [[package]] name = "cached_proc_macro" -version = "0.27.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ebcf9c75f17a17d55d11afc98e46167d4790a263f428891b8705ab2f793eca3" +checksum = "66e734c52502e6cf54dce2ba07108906b04b8fe57f4f5e3ef7d58267b4abf060" dependencies = [ "darling 0.20.11", "proc-macro2", @@ -766,9 +765,9 @@ dependencies = [ [[package]] name = "cached_proc_macro_types" -version = "0.1.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" +checksum = "26cf465651fa6ad902a2d327ba60c3a6bc61c6a2f4ad70d091cf20dfda0074ef" [[package]] name = "calendrical_calculations" diff --git a/brush-core/Cargo.toml b/brush-core/Cargo.toml index d3f59d289..01b3c390c 100644 --- a/brush-core/Cargo.toml +++ b/brush-core/Cargo.toml @@ -26,7 +26,7 @@ async-recursion = "1.1.1" async-trait = "0.1.89" brush-parser = { version = "^0.4.0", path = "../brush-parser" } bon = "3.9.1" -cached = "0.59.0" +cached = "2.0.2" cfg-if = "1.0.4" chrono = "0.4.44" clap = { version = "4.6.0", features = ["derive", "wrap_help"] } diff --git a/brush-core/src/expansion.rs b/brush-core/src/expansion.rs index 3c89a2666..b344797b9 100644 --- a/brush-core/src/expansion.rs +++ b/brush-core/src/expansion.rs @@ -1467,7 +1467,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> { let mut transformed_fields = vec![]; for field in expanded_parameter.fields { let s = String::from(field); - let transformed = self.apply_transform_to(&op, s, came_from_undefined).await?; + let transformed = self.apply_transform_to(&op, &s, came_from_undefined).await?; transformed_fields.push(WordField::from(transformed)); } @@ -2000,7 +2000,7 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> { async fn apply_transform_to( &mut self, op: &ParameterTransformOp, - s: String, + s: &str, came_from_undefined: bool, ) -> Result { match op { @@ -2008,11 +2008,11 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> { prompt::expand_prompt(self.shell, self.params, s).await } brush_parser::word::ParameterTransformOp::CapitalizeInitial => { - Ok(to_initial_capitals(s.as_str())) + Ok(to_initial_capitals(s)) } brush_parser::word::ParameterTransformOp::ExpandEscapeSequences => { let (result, _) = escape::expand_backslash_escapes( - s.as_str(), + s, escape::EscapeExpansionMode::AnsiCQuotes, )?; Ok(String::from_utf8_lossy(result.as_slice()).into_owned()) @@ -2025,20 +2025,14 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> { } else { // TODO(expansion): This isn't right for arrays. // TODO(expansion): This doesn't honor 'separate_words' - Ok(escape::force_quote( - s.as_str(), - escape::QuoteMode::SingleQuote, - )) + Ok(escape::force_quote(s, escape::QuoteMode::SingleQuote)) } } brush_parser::word::ParameterTransformOp::Quoted => { if came_from_undefined { Ok(String::new()) } else { - Ok(escape::force_quote( - s.as_str(), - escape::QuoteMode::SingleQuote, - )) + Ok(escape::force_quote(s, escape::QuoteMode::SingleQuote)) } } brush_parser::word::ParameterTransformOp::ToLowerCase => Ok(s.to_lowercase()), diff --git a/brush-core/src/prompt.rs b/brush-core/src/prompt.rs index 919dad2ec..14ca0ae89 100644 --- a/brush-core/src/prompt.rs +++ b/brush-core/src/prompt.rs @@ -12,7 +12,7 @@ const VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH"); pub(crate) async fn expand_prompt( shell: &mut Shell, params: &ExecutionParameters, - spec: String, + spec: &str, ) -> Result { // Parse the prompt spec into its pieces. let prompt_pieces = parse_prompt(spec)?; @@ -58,11 +58,11 @@ pub(crate) async fn expand_prompt( Ok(formatted_prompt) } -#[cached::proc_macro::cached(size = 64, result = true)] +#[cached::macros::cached(max_size = 64, key = "String", convert = r#"{ spec.to_owned() }"#)] fn parse_prompt( - spec: String, + spec: &str, ) -> Result, brush_parser::WordParseError> { - brush_parser::prompt::parse(spec.as_str()) + brush_parser::prompt::parse(spec) } fn format_prompt_piece( diff --git a/brush-core/src/regex.rs b/brush-core/src/regex.rs index 1e9837251..9b07f830a 100644 --- a/brush-core/src/regex.rs +++ b/brush-core/src/regex.rs @@ -6,9 +6,15 @@ use std::cell::RefCell; use crate::error; use cached::Cached; +/// Cache mapping a (pattern, case-insensitive, multiline) key to a compiled regex. +type RegexCache = cached::LruCache<(String, bool, bool), fancy_regex::Regex>; + thread_local! { - static REGEX_CACHE: RefCell> = - RefCell::new(cached::SizedCache::with_size(64)); + // Wrapped in `Option` so that if cache construction ever fails we gracefully + // degrade to compiling regexes uncached rather than panicking. (With a fixed + // positive `max_size` this always succeeds, but `build()` is fallible.) + static REGEX_CACHE: RefCell> = + RefCell::new(cached::LruCache::builder().max_size(64).build().ok()); } /// Represents a piece of a regular expression. @@ -103,7 +109,8 @@ pub(crate) fn compile_regex( // Move regex_str into the key to avoid cloning on cache-hit path. let key = (regex_str, case_insensitive, multiline); - let cached_regex = REGEX_CACHE.with(|cache| cache.borrow_mut().cache_get(&key).cloned()); + let cached_regex = REGEX_CACHE + .with(|cache| cache.borrow_mut().as_mut().and_then(|c| c.cache_get(&key).cloned())); if let Some(re) = cached_regex { return Ok(re); } @@ -133,7 +140,9 @@ pub(crate) fn compile_regex( drop(regex_str); REGEX_CACHE.with(|cache| { - cache.borrow_mut().cache_set(key, re.clone()); + if let Some(c) = cache.borrow_mut().as_mut() { + c.cache_set(key, re.clone()); + } }); Ok(re) diff --git a/brush-core/src/shell/parsing.rs b/brush-core/src/shell/parsing.rs index b379a6b8c..963c64850 100644 --- a/brush-core/src/shell/parsing.rs +++ b/brush-core/src/shell/parsing.rs @@ -27,7 +27,8 @@ impl Shell { &self, s: S, ) -> Result { - parse_string_impl(s.into(), self.parser_options()) + let s: String = s.into(); + parse_string_impl(&s, &self.parser_options()) } /// Returns the options that should be used for parsing shell programs; reflects @@ -44,12 +45,16 @@ impl Shell { } } -#[cached::proc_macro::cached(size = 64, result = true)] +#[cached::macros::cached( + max_size = 64, + key = "(String, brush_parser::ParserOptions)", + convert = r#"{ (s.to_owned(), parser_options.to_owned()) }"# +)] fn parse_string_impl( - s: String, - parser_options: brush_parser::ParserOptions, + s: &str, + parser_options: &brush_parser::ParserOptions, ) -> Result { - let mut parser = create_parser(s.as_bytes(), &parser_options); + let mut parser = create_parser(s.as_bytes(), parser_options); tracing::debug!(target: trace_categories::PARSE, "Parsing string as program..."); parser.parse_program() diff --git a/brush-core/src/shell/prompts.rs b/brush-core/src/shell/prompts.rs index 4abe4d41d..84e01a8c9 100644 --- a/brush-core/src/shell/prompts.rs +++ b/brush-core/src/shell/prompts.rs @@ -55,9 +55,11 @@ impl Shell { let prev_last_result = self.last_exit_status(); let prev_last_pipeline_statuses = self.last_pipeline_statuses.clone(); - // Expand it. + // Expand it. We must own the spec here: it borrows `self` (via the + // returned `Cow`), and `expand_prompt` needs `&mut self`. + let prompt_spec = prompt_spec.into_owned(); let params = self.default_exec_params(); - let result = prompt::expand_prompt(self, ¶ms, prompt_spec.into_owned()).await; + let result = prompt::expand_prompt(self, ¶ms, &prompt_spec).await; // Restore the last exit status. self.last_pipeline_statuses = prev_last_pipeline_statuses; diff --git a/brush-parser/Cargo.toml b/brush-parser/Cargo.toml index 64ff7fcb9..262a79af9 100644 --- a/brush-parser/Cargo.toml +++ b/brush-parser/Cargo.toml @@ -27,7 +27,7 @@ winnow-parser = ["dep:winnow"] [dependencies] arbitrary = { version = "1.4.2", optional = true, features = ["derive"] } bon = "3.9.1" -cached = "0.59.0" +cached = "2.0.2" indenter = "0.3.4" miette = { version = "7.6.0", optional = true, default-features = false, features = [ "derive", diff --git a/brush-parser/src/arithmetic.rs b/brush-parser/src/arithmetic.rs index ffa0bd106..b0991ca32 100644 --- a/brush-parser/src/arithmetic.rs +++ b/brush-parser/src/arithmetic.rs @@ -9,13 +9,13 @@ use crate::error; /// /// * `input` - The arithmetic expression to parse, in string form. pub fn parse(input: &str) -> Result { - cacheable_parse(input.to_owned()) + cacheable_parse(input) } -#[cached::proc_macro::cached(size = 64, result = true)] -fn cacheable_parse(input: String) -> Result { +#[cached::macros::cached(max_size = 64, key = "String", convert = r#"{ input.to_owned() }"#)] +fn cacheable_parse(input: &str) -> Result { tracing::debug!(target: "arithmetic", "parsing arithmetic expression: '{input}'"); - arithmetic::full_expression(input.as_str()) + arithmetic::full_expression(input) .map_err(|e| error::WordParseError::ArithmeticExpression(e.into())) } diff --git a/brush-parser/src/tokenizer.rs b/brush-parser/src/tokenizer.rs index ceee9e364..76fdfa615 100644 --- a/brush-parser/src/tokenizer.rs +++ b/brush-parser/src/tokenizer.rs @@ -491,15 +491,20 @@ pub fn tokenize_str_with_options( input: &str, options: &TokenizerOptions, ) -> Result, TokenizerError> { - uncached_tokenize_string(input.to_owned(), options.to_owned()) + uncached_tokenize_string(input, options) } -#[cached::proc_macro::cached(name = "TOKENIZE_CACHE", size = 64, result = true)] +#[cached::macros::cached( + name = "TOKENIZE_CACHE", + max_size = 64, + key = "(String, TokenizerOptions)", + convert = r#"{ (input.to_owned(), options.to_owned()) }"# +)] fn uncached_tokenize_string( - input: String, - options: TokenizerOptions, + input: &str, + options: &TokenizerOptions, ) -> Result, TokenizerError> { - uncached_tokenize_str(input.as_str(), &options) + uncached_tokenize_str(input, options) } /// Break the given input shell script string into tokens, returning the tokens. diff --git a/brush-parser/src/word.rs b/brush-parser/src/word.rs index 6413cb4a4..99c4195f6 100644 --- a/brush-parser/src/word.rs +++ b/brush-parser/src/word.rs @@ -528,18 +528,22 @@ pub fn parse( word: &str, options: &ParserOptions, ) -> Result, error::WordParseError> { - cacheable_parse(word.to_owned(), options.to_owned()) + cacheable_parse(word, options) } -#[cached::proc_macro::cached(size = 64, result = true)] +#[cached::macros::cached( + max_size = 64, + key = "(String, ParserOptions)", + convert = r#"{ (word.to_owned(), options.to_owned()) }"# +)] fn cacheable_parse( - word: String, - options: ParserOptions, + word: &str, + options: &ParserOptions, ) -> Result, error::WordParseError> { tracing::debug!(target: "expansion", "Parsing word '{}'", word); - let pieces = expansion_parser::unexpanded_word(word.as_str(), &options) - .map_err(|err| error::WordParseError::Word(word.clone(), err.into()))?; + let pieces = expansion_parser::unexpanded_word(word, options) + .map_err(|err| error::WordParseError::Word(word.to_owned(), err.into()))?; tracing::debug!(target: "expansion", "Parsed word '{}' => {{{:?}}}", word, pieces); From 9f39c074fc90ff92f7000a1fdfdd0fc01ff4d3d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:29:00 +0000 Subject: [PATCH 2/2] style: apply cargo fmt to brush-core --- brush-core/src/expansion.rs | 10 +++++----- brush-core/src/regex.rs | 8 ++++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/brush-core/src/expansion.rs b/brush-core/src/expansion.rs index b344797b9..a25639ee6 100644 --- a/brush-core/src/expansion.rs +++ b/brush-core/src/expansion.rs @@ -1467,7 +1467,9 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> { let mut transformed_fields = vec![]; for field in expanded_parameter.fields { let s = String::from(field); - let transformed = self.apply_transform_to(&op, &s, came_from_undefined).await?; + let transformed = self + .apply_transform_to(&op, &s, came_from_undefined) + .await?; transformed_fields.push(WordField::from(transformed)); } @@ -2011,10 +2013,8 @@ impl<'a, SE: extensions::ShellExtensions> WordExpander<'a, SE> { Ok(to_initial_capitals(s)) } brush_parser::word::ParameterTransformOp::ExpandEscapeSequences => { - let (result, _) = escape::expand_backslash_escapes( - s, - escape::EscapeExpansionMode::AnsiCQuotes, - )?; + let (result, _) = + escape::expand_backslash_escapes(s, escape::EscapeExpansionMode::AnsiCQuotes)?; Ok(String::from_utf8_lossy(result.as_slice()).into_owned()) } brush_parser::word::ParameterTransformOp::PossiblyQuoteWithArraysExpanded { diff --git a/brush-core/src/regex.rs b/brush-core/src/regex.rs index 9b07f830a..3feffdb61 100644 --- a/brush-core/src/regex.rs +++ b/brush-core/src/regex.rs @@ -109,8 +109,12 @@ pub(crate) fn compile_regex( // Move regex_str into the key to avoid cloning on cache-hit path. let key = (regex_str, case_insensitive, multiline); - let cached_regex = REGEX_CACHE - .with(|cache| cache.borrow_mut().as_mut().and_then(|c| c.cache_get(&key).cloned())); + let cached_regex = REGEX_CACHE.with(|cache| { + cache + .borrow_mut() + .as_mut() + .and_then(|c| c.cache_get(&key).cloned()) + }); if let Some(re) = cached_regex { return Ok(re); }