1use once_cell::sync::OnceCell;
2use serde::de::{Deserialize, Deserializer};
3use serde::ser::{Serialize, Serializer};
4use std::error::Error;
5use std::{
6 collections::HashMap,
7 sync::{Arc, Weak},
8};
9
10#[derive(Debug)]
16pub struct Regex {
17 regex_str: String,
18 regex: Arc<OnceCell<regex_impl::Regex>>,
19 #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
20 engines: Option<fancy_regex::EngineCache>,
21}
22
23#[derive(Debug, Default)]
25pub(crate) struct RegexCache {
26 patterns: HashMap<String, Weak<OnceCell<regex_impl::Regex>>>,
27 #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
28 engines: fancy_regex::EngineCache,
29}
30
31impl RegexCache {
32 pub(crate) fn share(&mut self, regex: &mut Regex) {
33 #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
34 {
35 regex.engines = Some(self.engines.clone());
36 }
37 if let Some(cached) = self.patterns.get(®ex.regex_str).and_then(Weak::upgrade) {
38 regex.regex = cached;
39 } else {
40 self.patterns
41 .insert(regex.regex_str.clone(), Arc::downgrade(®ex.regex));
42 }
43 }
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct Region {
49 region: regex_impl::Region,
50}
51
52pub(crate) type SearchScratch = regex_impl::SearchScratch;
53
54impl Regex {
55 #[cfg(all(feature = "owner-census", not(feature = "regex-onig")))]
56 pub(crate) fn visit_engines<'a>(
57 &'a self,
58 visit: impl FnMut(fancy_regex::owner_census::Engine<'a>),
59 ) {
60 if let Some(compiled) = self.regex.get() {
61 compiled.visit_engines(visit);
62 }
63 }
64
65 #[cfg(feature = "regex-census")]
66 pub(crate) fn compiled_owner(&self) -> Option<usize> {
67 self.regex.get().map(|_| Arc::as_ptr(&self.regex) as usize)
68 }
69
70 pub fn new(regex_str: String) -> Self {
75 Self {
76 regex_str,
77 regex: Arc::new(OnceCell::new()),
78 #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
79 engines: None,
80 }
81 }
82
83 pub fn try_compile(regex_str: &str) -> Option<Box<dyn Error + Send + Sync + 'static>> {
85 regex_impl::Regex::new(regex_str).err()
86 }
87
88 pub fn regex_str(&self) -> &str {
90 &self.regex_str
91 }
92
93 pub fn is_match(&self, text: &str) -> bool {
95 self.regex().is_match(text)
96 }
97
98 pub fn search(
106 &self,
107 text: &str,
108 begin: usize,
109 end: usize,
110 region: Option<&mut Region>,
111 ) -> bool {
112 self.regex()
113 .search(text, begin, end, region.map(|r| &mut r.region))
114 }
115
116 pub(crate) fn search_with_scratch(
117 &self,
118 text: &str,
119 begin: usize,
120 end: usize,
121 region: &mut Region,
122 scratch: &mut SearchScratch,
123 ) -> bool {
124 self.regex()
125 .search_with_scratch(text, begin, end, Some(&mut region.region), scratch)
126 }
127
128 pub(crate) fn possible_with_ascii_bytes(&self, present: u128) -> bool {
129 #[cfg(not(feature = "regex-onig"))]
130 {
131 self.regex().required_ascii_bytes() & !present == 0
132 }
133 #[cfg(feature = "regex-onig")]
134 {
135 let _ = present;
136 true
137 }
138 }
139
140 fn regex(&self) -> ®ex_impl::Regex {
141 self.regex.get_or_init(|| {
142 #[cfg(feature = "syntax-timing")]
143 let _timing =
144 super::syntax_timing::Initializer::new(super::syntax_timing::Phase::OuterRegex);
145 #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
146 if let Some(cache) = &self.engines {
147 return regex_impl::Regex::new_cached(&self.regex_str, cache)
148 .expect("regex string should be pre-tested");
149 }
150 regex_impl::Regex::new(&self.regex_str).expect("regex string should be pre-tested")
151 })
152 }
153}
154
155impl Clone for Regex {
156 fn clone(&self) -> Self {
157 Regex {
158 regex_str: self.regex_str.clone(),
159 regex: Arc::clone(&self.regex),
160 #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
161 engines: self.engines.clone(),
162 }
163 }
164}
165
166impl PartialEq for Regex {
167 fn eq(&self, other: &Regex) -> bool {
168 self.regex_str == other.regex_str
169 }
170}
171
172impl Eq for Regex {}
173
174impl Serialize for Regex {
175 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
176 where
177 S: Serializer,
178 {
179 serializer.serialize_str(&self.regex_str)
180 }
181}
182
183impl<'de> Deserialize<'de> for Regex {
184 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185 where
186 D: Deserializer<'de>,
187 {
188 let regex_str = String::deserialize(deserializer)?;
189 Ok(Regex::new(regex_str))
190 }
191}
192
193impl Region {
194 pub(crate) fn owned_heap_bytes(&self) -> Option<usize> {
195 regex_impl::region_owned_heap_bytes(&self.region)
196 }
197
198 pub fn new() -> Self {
199 Self {
200 region: regex_impl::new_region(),
201 }
202 }
203
204 pub fn pos(&self, index: usize) -> Option<(usize, usize)> {
209 self.region.pos(index)
210 }
211}
212
213impl Default for Region {
214 fn default() -> Self {
215 Self::new()
216 }
217}
218
219#[cfg(feature = "regex-onig")]
220mod regex_impl {
221 pub use onig::Region;
222 use onig::{MatchParam, RegexOptions, SearchOptions, Syntax};
223 use std::error::Error;
224
225 #[derive(Default)]
226 pub(crate) struct SearchScratch;
227
228 #[derive(Debug)]
229 pub struct Regex {
230 regex: onig::Regex,
231 }
232
233 pub fn new_region() -> Region {
234 Region::with_capacity(8)
235 }
236
237 pub(super) fn region_owned_heap_bytes(_: &Region) -> Option<usize> {
238 None
239 }
240
241 impl Regex {
242 pub fn new(regex_str: &str) -> Result<Regex, Box<dyn Error + Send + Sync + 'static>> {
243 let result = onig::Regex::with_options(
244 regex_str,
245 RegexOptions::REGEX_OPTION_CAPTURE_GROUP,
246 Syntax::default(),
247 );
248 match result {
249 Ok(regex) => Ok(Regex { regex }),
250 Err(error) => Err(Box::new(error)),
251 }
252 }
253
254 pub(super) fn search_with_scratch(
255 &self,
256 text: &str,
257 begin: usize,
258 end: usize,
259 region: Option<&mut Region>,
260 _: &mut SearchScratch,
261 ) -> bool {
262 self.search(text, begin, end, region)
263 }
264
265 pub fn is_match(&self, text: &str) -> bool {
266 self.regex
267 .match_with_options(text, 0, SearchOptions::SEARCH_OPTION_NONE, None)
268 .is_some()
269 }
270
271 pub fn search(
272 &self,
273 text: &str,
274 begin: usize,
275 end: usize,
276 region: Option<&mut Region>,
277 ) -> bool {
278 let matched = self.regex.search_with_param(
279 text,
280 begin,
281 end,
282 SearchOptions::SEARCH_OPTION_NONE,
283 region,
284 MatchParam::default(),
285 );
286
287 matches!(matched, Ok(Some(_)))
291 }
292 }
293}
294
295#[cfg(all(feature = "regex-fancy", not(feature = "regex-onig")))]
297mod regex_impl {
298 use std::error::Error;
299
300 #[derive(Debug)]
301 pub struct Regex {
302 regex: fancy_regex::Regex,
303 }
304
305 pub(crate) use fancy_regex::SearchScratch;
306
307 #[derive(Clone, Debug, Eq, PartialEq)]
308 pub struct Region {
309 positions: Vec<Option<(usize, usize)>>,
310 }
311
312 pub fn new_region() -> Region {
313 Region {
314 positions: Vec::with_capacity(8),
315 }
316 }
317
318 pub(super) fn region_owned_heap_bytes(region: &Region) -> Option<usize> {
319 region
320 .positions
321 .capacity()
322 .checked_mul(std::mem::size_of::<Option<(usize, usize)>>())
323 }
324
325 impl Regex {
326 pub fn new(regex_str: &str) -> Result<Regex, Box<dyn Error + Send + Sync + 'static>> {
327 Self::build(
328 regex_str,
329 #[cfg(feature = "shared-engines")]
330 None,
331 )
332 }
333
334 #[cfg(feature = "shared-engines")]
335 pub(super) fn new_cached(
336 regex_str: &str,
337 cache: &fancy_regex::EngineCache,
338 ) -> Result<Regex, Box<dyn Error + Send + Sync + 'static>> {
339 Self::build(regex_str, Some(cache))
340 }
341
342 fn build(
343 regex_str: &str,
344 #[cfg(feature = "shared-engines")] cache: Option<&fancy_regex::EngineCache>,
345 ) -> Result<Regex, Box<dyn Error + Send + Sync + 'static>> {
346 let mut builder = fancy_regex::RegexBuilder::new(regex_str);
347 builder.oniguruma_mode(true);
348 #[cfg(feature = "shared-engines")]
349 let result = match cache {
350 Some(cache) => builder.build_with_cache(cache),
351 None => builder.build(),
352 };
353 #[cfg(not(feature = "shared-engines"))]
354 let result = builder.build();
355 match result {
356 Ok(regex) => Ok(Regex { regex }),
357 Err(error) => Err(Box::new(error)),
358 }
359 }
360
361 #[cfg(feature = "owner-census")]
362 pub(super) fn visit_engines<'a>(
363 &'a self,
364 visit: impl FnMut(fancy_regex::owner_census::Engine<'a>),
365 ) {
366 self.regex.visit_engines(visit);
367 }
368
369 pub fn is_match(&self, text: &str) -> bool {
370 self.regex.is_match(text).unwrap_or(false)
372 }
373
374 pub fn search(
375 &self,
376 text: &str,
377 begin: usize,
378 end: usize,
379 region: Option<&mut Region>,
380 ) -> bool {
381 self.search_with_scratch(text, begin, end, region, &mut SearchScratch::default())
382 }
383
384 pub(super) fn search_with_scratch(
385 &self,
386 text: &str,
387 begin: usize,
388 end: usize,
389 region: Option<&mut Region>,
390 scratch: &mut SearchScratch,
391 ) -> bool {
392 if let Ok(Some(captures)) =
396 self.regex
397 .captures_from_pos_with_scratch(&text[..end], begin, scratch)
398 {
399 if let Some(region) = region {
400 region.init_from_captures(&captures);
401 }
402 true
403 } else {
404 false
405 }
406 }
407
408 pub(super) fn required_ascii_bytes(&self) -> u128 {
409 self.regex.required_ascii_bytes()
410 }
411 }
412
413 impl Region {
414 fn init_from_captures(&mut self, captures: &fancy_regex::Captures) {
415 self.positions.clear();
416 for i in 0..captures.len() {
417 let pos = captures.get(i).map(|m| (m.start(), m.end()));
418 self.positions.push(pos);
419 }
420 }
421
422 pub fn pos(&self, i: usize) -> Option<(usize, usize)> {
423 if i < self.positions.len() {
424 self.positions[i]
425 } else {
426 None
427 }
428 }
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn owned_heap_tracks_capture_growth_and_private_clones() {
438 let regex = Regex::new("(a)".repeat(16));
439 let text = "a".repeat(16);
440 let mut region = Region::new();
441 let empty_bytes = region.owned_heap_bytes();
442 assert!(regex.search(&text, 0, text.len(), Some(&mut region)));
443 let captured_bytes = region.owned_heap_bytes();
444 let cloned = region.clone();
445 for capture in 0..=16 {
446 assert_eq!(cloned.pos(capture), region.pos(capture));
447 }
448 assert_eq!(cloned.pos(16), Some((15, 16)));
449 if cfg!(feature = "regex-onig") {
450 assert_eq!(captured_bytes, None);
451 assert_eq!(cloned.owned_heap_bytes(), None);
452 } else {
453 assert!(captured_bytes.unwrap() > empty_bytes.unwrap());
454 let minimum = 17 * std::mem::size_of::<Option<(usize, usize)>>();
455 assert!(cloned.owned_heap_bytes().unwrap() >= minimum);
456 }
457 assert!(Regex::new("(b)".into()).search("b", 0, 1, Some(&mut region)));
458 assert_eq!(region.pos(1), Some((0, 1)));
459 assert_eq!(region.pos(16), None);
460 assert_eq!(cloned.pos(16), Some((15, 16)));
461 assert_eq!(region.owned_heap_bytes(), captured_bytes);
462 }
463
464 #[test]
465 fn caches_compiled_regex() {
466 let regex = Regex::new(String::from(r"\w+"));
467
468 assert!(regex.regex.get().is_none());
469 assert!(regex.is_match("test"));
470 assert!(regex.regex.get().is_some());
471 }
472
473 #[test]
474 fn serde_as_string() {
475 let pattern: Regex = serde_json::from_str("\"just a string\"").unwrap();
476 assert_eq!(pattern.regex_str(), "just a string");
477 let back_to_str = serde_json::to_string(&pattern).unwrap();
478 assert_eq!(back_to_str, "\"just a string\"");
479 }
480
481 #[cfg(all(feature = "shared-engines", not(feature = "regex-onig")))]
482 #[test]
483 fn only_static_rules_receive_engine_cache_admission() {
484 use crate::parsing::syntax_definition::{MatchOperation, MatchPattern};
485
486 let mut closing = MatchPattern {
487 has_captures: true,
488 regex: Regex::new(r"^\1$".into()),
489 scope: vec![],
490 captures: None,
491 operation: MatchOperation::None,
492 with_prototype: None,
493 };
494 let mut cache = RegexCache::default();
495 cache.share(&mut closing.regex);
496 assert!(closing.regex.engines.is_some());
497 assert!(closing.regex.clone().engines.is_some());
498 let restored: Regex =
499 serde_json::from_str(&serde_json::to_string(&closing.regex).unwrap()).unwrap();
500 assert!(restored.engines.is_none());
501 let opener = Regex::new(r"<<(\w+)".into());
502 let mut region = Region::new();
503 assert!(opener.search("<<END", 0, 5, Some(&mut region)));
504 let derived = closing.regex_with_refs(®ion, "<<END");
505 assert!(derived.engines.is_none());
506 assert!(derived.is_match("END"));
507 assert!(!derived.is_match("OTHER"));
508 assert!(opener.engines.is_none());
509 }
510
511 #[test]
512 fn shared_compilation_keeps_captures_private_and_releases_its_owner() {
513 let text = r"(?<=prefix:)([a-z]+)([0-9]+)";
514 let mut first = Regex::new(text.into());
515 let mut second = Regex::new(text.into());
516 let independent = Regex::new(text.into());
517 let mut cache = RegexCache::default();
518 cache.share(&mut first);
519 cache.share(&mut second);
520 assert!(Arc::ptr_eq(&first.regex, &second.regex));
521 assert!(!Arc::ptr_eq(&first.regex, &independent.regex));
522 let owner = Arc::downgrade(&first.regex);
523
524 let mut a = Region::new();
525 let mut b = Region::new();
526 assert!(first.search("prefix:alpha12", 0, 14, Some(&mut a)));
527 assert!(second.search("prefix:beta345", 0, 14, Some(&mut b)));
528 assert_eq!(a.pos(1), Some((7, 12)));
529 assert_eq!(b.pos(1), Some((7, 11)));
530 assert_eq!(a.pos(2), Some((12, 14)));
531 assert_eq!(b.pos(2), Some((11, 14)));
532 assert!(second.regex.get().is_some());
533 let cloned = first.clone();
534 assert!(Arc::ptr_eq(&first.regex, &cloned.regex));
535 std::thread::scope(|threads| {
536 for regex in [&first, &second, &cloned] {
537 threads.spawn(move || {
538 let mut region = Region::new();
539 for _ in 0..100 {
540 assert!(regex.search("prefix:alpha12", 0, 14, Some(&mut region)));
541 assert_eq!(region.pos(1), Some((7, 12)));
542 assert!(!regex.search("not a match", 0, 11, Some(&mut region)));
543 }
544 });
545 }
546 });
547 drop((first, second, cloned));
548 assert!(owner.upgrade().is_none());
549 assert_eq!(cache.patterns.len(), 1);
550 let mut replacement = Regex::new(text.into());
551 cache.share(&mut replacement);
552 assert!(Arc::ptr_eq(
553 &cache.patterns[text].upgrade().unwrap(),
554 &replacement.regex,
555 ));
556 assert!(replacement.regex.get().is_none());
557 assert!(replacement.search("prefix:gamma67", 0, 14, Some(&mut a)));
558 assert_eq!(a.pos(1), Some((7, 12)));
559 assert_eq!(a.pos(2), Some((12, 14)));
560 }
561
562 #[cfg(feature = "yaml-load")]
563 #[test]
564 fn grammar_interning_preserves_rule_actions_clone_and_serialization() {
565 use crate::parsing::syntax_definition::Pattern;
566 use crate::parsing::{
567 ParseState, Scope, ScopeStackOp, SyntaxDefinition, SyntaxSet, SyntaxSetBuilder,
568 };
569
570 let definition = SyntaxDefinition::load_from_str(
571 "name: Shared\nscope: source.shared\ncontexts:\n main:\n - match: 'a'\n scope: keyword.first\n push: second\n second:\n - match: 'a'\n scope: string.second\n pop: true\n",
572 true, None,
573 ).unwrap();
574 let mut builder = SyntaxSetBuilder::new();
575 builder.add(definition);
576 let set = builder.build();
577 let syntax = &set.syntaxes()[0];
578 let first = set.get_context(&syntax.context_ids()["main"]).unwrap();
579 let second = set.get_context(&syntax.context_ids()["second"]).unwrap();
580 let (Pattern::Match(a), Pattern::Match(b)) = (&first.patterns[0], &second.patterns[0])
581 else {
582 panic!("expected match rules")
583 };
584 assert!(Arc::ptr_eq(&a.regex.regex, &b.regex.regex));
585 let expected = ParseState::new(syntax).parse_line("aa\n", &set).unwrap();
586 assert!(expected.contains(&(0, ScopeStackOp::Push(Scope::new("keyword.first").unwrap()))));
587 assert!(expected.contains(&(1, ScopeStackOp::Push(Scope::new("string.second").unwrap()))));
588 let restored: SyntaxSet = crate::dumps::from_binary(&crate::dumps::dump_binary(&set));
589 for set in [set.clone(), restored] {
590 let answer = ParseState::new(&set.syntaxes()[0])
591 .parse_line("aa\n", &set)
592 .unwrap();
593 assert_eq!(answer, expected);
594 }
595 }
596}