polars_utils/
regex_cache.rs1use std::cell::RefCell;
2use std::sync::Arc;
3
4use regex::bytes::Regex as BytesRegex;
5use regex::{Regex, RegexBuilder};
6
7use crate::cache::LruCache;
8
9fn get_size_limit() -> Option<usize> {
10 Some(
11 std::env::var("POLARS_REGEX_SIZE_LIMIT")
12 .ok()
13 .filter(|l| !l.is_empty())?
14 .parse()
15 .expect("invalid POLARS_REGEX_SIZE_LIMIT"),
16 )
17}
18
19pub struct RegexCache {
26 cache: LruCache<String, Regex>,
27 bytes_cache: LruCache<usize, (Arc<BytesRegex>, BytesRegex)>,
28 size_limit: Option<usize>,
29}
30
31impl RegexCache {
32 fn new() -> Self {
33 Self {
34 cache: LruCache::with_capacity(32),
35 bytes_cache: LruCache::with_capacity(32),
36 size_limit: get_size_limit(),
37 }
38 }
39
40 pub fn compile(&mut self, re: &str) -> Result<&Regex, regex::Error> {
41 let size_limit = &mut self.size_limit;
42 let r = self.cache.try_get_or_insert_with(re, |re| {
43 build_within_size_limit(size_limit, |limit| {
44 let mut builder = RegexBuilder::new(re);
45 if let Some(bytes) = limit {
46 builder.size_limit(bytes);
47 }
48 builder.build()
49 })
50 });
51 Ok(&*r?)
52 }
53
54 pub fn get_or_insert_bytes(&mut self, re: &Arc<BytesRegex>) -> &BytesRegex {
56 &self
58 .bytes_cache
59 .get_or_insert_with(&(Arc::as_ptr(re) as usize), |_| {
60 (re.clone(), re.as_ref().clone())
61 })
62 .1
63 }
64}
65
66fn build_within_size_limit<R>(
69 size_limit: &mut Option<usize>,
70 build: impl Fn(Option<usize>) -> Result<R, regex::Error>,
71) -> Result<R, regex::Error> {
72 loop {
73 match build(*size_limit) {
74 err @ Err(regex::Error::CompiledTooBig(_)) => {
75 let new_size_limit = get_size_limit();
76 if new_size_limit != *size_limit {
77 *size_limit = new_size_limit;
78 continue; }
80 break err;
81 },
82 r => break r,
83 }
84 }
85}
86
87thread_local! {
88 static LOCAL_REGEX_CACHE: RefCell<RegexCache> = RefCell::new(RegexCache::new());
89}
90
91pub fn compile_regex(re: &str) -> Result<Regex, regex::Error> {
92 LOCAL_REGEX_CACHE.with_borrow_mut(|cache| cache.compile(re).cloned())
93}
94
95pub fn with_regex_cache<R, F: FnOnce(&mut RegexCache) -> R>(f: F) -> R {
96 LOCAL_REGEX_CACHE.with_borrow_mut(f)
97}
98
99#[macro_export]
100macro_rules! cached_regex {
101 () => {};
102
103 ($vis:vis static $name:ident = $regex:expr; $($rest:tt)*) => {
104 #[allow(clippy::disallowed_methods)]
105 $vis static $name: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| regex::Regex::new($regex).unwrap());
106 $crate::regex_cache::cached_regex!($($rest)*);
107 };
108}
109pub use cached_regex;
110
111#[cfg(test)]
112mod tests {
113 use regex::bytes::RegexBuilder as BytesRegexBuilder;
114
115 use super::*;
116
117 #[test]
118 fn bytes_cache_preserves_builder_options() {
119 let mut cache = RegexCache::new();
120 let sensitive = Arc::new(BytesRegexBuilder::new("abc").build().unwrap());
121 let insensitive = Arc::new(
122 BytesRegexBuilder::new("abc")
123 .case_insensitive(true)
124 .build()
125 .unwrap(),
126 );
127
128 for _ in 0..2 {
129 assert!(!cache.get_or_insert_bytes(&sensitive).is_match(b"ABC"));
130 assert!(cache.get_or_insert_bytes(&insensitive).is_match(b"ABC"));
131 }
132 }
133
134 #[test]
135 fn bytes_cache_reuses_cloned_arc() {
136 let mut cache = RegexCache::new();
137 let re = Arc::new(BytesRegexBuilder::new("abc").build().unwrap());
138 let cloned = re.clone();
139 let cached = std::ptr::from_ref(cache.get_or_insert_bytes(&re));
140
141 assert_eq!(
142 cached,
143 std::ptr::from_ref(cache.get_or_insert_bytes(&cloned))
144 );
145 assert_ne!(cached, Arc::as_ptr(&re));
146 }
147
148 #[test]
149 fn bytes_cache_retains_arc_until_eviction() {
150 let mut cache = RegexCache::new();
151 let regex = BytesRegexBuilder::new("abc").build().unwrap();
152 let re = Arc::new(regex.clone());
153 let weak = Arc::downgrade(&re);
154 cache.get_or_insert_bytes(&re);
155 drop(re);
156 assert!(weak.upgrade().is_some());
157
158 for _ in 0..32 {
159 let re = Arc::new(regex.clone());
160 cache.get_or_insert_bytes(&re);
161 }
162 assert!(weak.upgrade().is_none());
163 }
164}