diff --git a/crates/executor-core/src/sdk/stdlib/regex.rs b/crates/executor-core/src/sdk/stdlib/regex.rs index 7ba5557..3fc87a3 100644 --- a/crates/executor-core/src/sdk/stdlib/regex.rs +++ b/crates/executor-core/src/sdk/stdlib/regex.rs @@ -1,13 +1,29 @@ //! `regex::` — non-backtracking regular expressions (Rust `regex` crate). //! -//! Patterns compile per call. No cache: premature for v1.1.0, and the -//! `regex` crate's linear-time guarantees keep per-call cost bounded. -//! Catastrophic patterns are rejected at compile time by the crate -//! itself; no extra defense needed. +//! F-P-014: compiled patterns are cached in a thread-local LRU so a +//! tight script loop like `regex::is_match("\\d+", x)` doesn't recompile +//! every iteration. Compile dominates `is_match` on short strings; +//! caching shrinks per-call cost to a HashMap probe + `Arc::clone`. +use std::cell::RefCell; +use std::num::NonZeroUsize; +use std::sync::Arc; + +use lru::LruCache; use regex::Regex; use rhai::{Array, Dynamic, Engine as RhaiEngine, EvalAltResult, Module}; +/// Per-thread cap on compiled regex patterns. 128 is well above what +/// any sensible script uses but bounded enough to keep memory in +/// check on a fleet of long-running threads. +const REGEX_CACHE_SIZE: usize = 128; + +thread_local! { + static REGEX_CACHE: RefCell>> = RefCell::new( + LruCache::new(NonZeroUsize::new(REGEX_CACHE_SIZE).expect("non-zero")) + ); +} + pub fn register(engine: &mut RhaiEngine) { let mut module = Module::new(); register_is_match(&mut module); @@ -20,8 +36,18 @@ pub fn register(engine: &mut RhaiEngine) { engine.register_static_module("regex", module.into()); } -fn compile(pattern: &str) -> Result> { - Regex::new(pattern).map_err(|e| format!("invalid regex: {e}").into()) +fn compile(pattern: &str) -> Result, Box> { + REGEX_CACHE.with(|cell| { + if let Some(cached) = cell.borrow_mut().get(pattern) { + return Ok(cached.clone()); + } + let re = Arc::new( + Regex::new(pattern) + .map_err(|e| -> Box { format!("invalid regex: {e}").into() })?, + ); + cell.borrow_mut().put(pattern.to_string(), re.clone()); + Ok(re) + }) } fn register_is_match(module: &mut Module) {