use crate::{PersistentVector, Value};
use cljrs_gc::{GcPtr, GcVisitor, MarkVisitor, Trace};
use std::borrow::Cow;
use std::fmt;
use std::sync::{Mutex, OnceLock};
#[cfg(feature = "regex-full")]
use regex as engine;
#[cfg(all(feature = "small-regex", not(feature = "regex-full")))]
use regex_lite as engine;
#[cfg(not(any(feature = "regex-full", feature = "small-regex")))]
compile_error!(
"cljrs-value needs a regex engine: keep the default `regex-full` feature, \
or enable `small-regex` to use regex-lite instead."
);
#[derive(Debug, Clone)]
pub struct Pattern {
re: engine::Regex,
anchored: OnceLock<Option<engine::Regex>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatternError(String);
impl fmt::Display for PatternError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for PatternError {}
impl Pattern {
pub fn new(pattern: &str) -> Result<Pattern, PatternError> {
engine::Regex::new(pattern)
.map(|re| Pattern {
re,
anchored: OnceLock::new(),
})
.map_err(|e| PatternError(e.to_string()))
}
pub fn as_str(&self) -> &str {
self.re.as_str()
}
pub fn captures<'h>(&self, haystack: &'h str) -> Option<Captures<'h>> {
self.re.captures(haystack).map(Captures)
}
pub fn captures_full<'h>(&self, haystack: &'h str) -> Option<Captures<'h>> {
match self.anchored() {
Some(re) => re.captures(haystack).map(Captures),
None => self
.captures(haystack)
.filter(|cap| cap.start() == 0 && cap.end() == haystack.len()),
}
}
fn anchored(&self) -> Option<&engine::Regex> {
self.anchored
.get_or_init(|| engine::Regex::new(&format!(r"\A(?:{})\z", self.as_str())).ok())
.as_ref()
}
pub fn captures_at<'h>(&self, haystack: &'h str, start: usize) -> Option<Captures<'h>> {
self.re.captures_at(haystack, start).map(Captures)
}
pub fn replace<'h>(&self, haystack: &'h str, replacement: &str) -> Cow<'h, str> {
self.re.replace(haystack, replacement)
}
pub fn replace_all<'h>(&self, haystack: &'h str, replacement: &str) -> Cow<'h, str> {
self.re.replace_all(haystack, replacement)
}
pub fn split<'h>(&self, haystack: &'h str) -> impl Iterator<Item = &'h str> {
self.re.split(haystack)
}
pub fn splitn<'h>(&self, haystack: &'h str, limit: usize) -> impl Iterator<Item = &'h str> {
self.re.splitn(haystack, limit)
}
}
impl fmt::Display for Pattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl Trace for Pattern {
fn trace(&self, _: &mut MarkVisitor) {}
}
#[derive(Debug)]
pub struct Captures<'h>(engine::Captures<'h>);
impl<'h> Captures<'h> {
pub fn full(&self) -> &'h str {
self.whole().as_str()
}
pub fn start(&self) -> usize {
self.whole().start()
}
pub fn end(&self) -> usize {
self.whole().end()
}
pub fn group_count(&self) -> usize {
self.0.len()
}
pub fn groups(&self) -> impl Iterator<Item = Option<&'h str>> + '_ {
self.0.iter().map(|g| g.map(|m| m.as_str()))
}
fn whole(&self) -> engine::Match<'h> {
self.0
.get(0)
.expect("group 0 always participates in a successful match")
}
}
#[derive(Debug, Clone)]
pub enum MatchPhase {
New,
Matching(usize),
Complete,
}
#[derive(Debug, Clone)]
struct MatcherState {
phase: MatchPhase,
last_match: Option<MatchResult>,
}
#[derive(Debug)]
pub struct Matcher {
pub pattern: GcPtr<Pattern>,
haystack: GcPtr<String>,
state: Mutex<MatcherState>,
match_all: bool,
}
#[derive(Debug, Clone)]
pub struct MatchResult {
pub full: String,
pub groups: Vec<Option<String>>,
}
impl Clone for Matcher {
fn clone(&self) -> Matcher {
let state = self.state.lock().unwrap().clone();
Matcher {
pattern: self.pattern.clone(),
haystack: self.haystack.clone(),
state: Mutex::new(state.clone()),
match_all: self.match_all,
}
}
}
impl Trace for Matcher {
fn trace(&self, visitor: &mut MarkVisitor) {
visitor.visit(&self.pattern);
visitor.visit(&self.haystack);
}
}
impl Matcher {
pub fn new(pattern: Pattern, source: String, match_all: bool) -> Self {
Self::from_ptr(GcPtr::new(pattern), source, match_all)
}
pub fn from_ptr(pattern: GcPtr<Pattern>, source: String, match_all: bool) -> Self {
Self {
pattern,
haystack: GcPtr::new(source),
state: Mutex::new(MatcherState {
phase: MatchPhase::New,
last_match: None,
}),
match_all,
}
}
pub fn next(&self) -> MatchPhase {
let mut state = self.state.lock().unwrap();
let pattern = self.pattern.get();
let haystack = self.haystack.get();
match state.phase {
MatchPhase::New => {
let cap = if self.match_all {
pattern.captures_full(haystack)
} else {
pattern.captures(haystack)
};
*state = Self::step(cap, haystack);
}
MatchPhase::Matching(n) => {
let cap = if self.match_all || n > haystack.len() {
None
} else {
pattern.captures_at(haystack, n)
};
*state = Self::step(cap, haystack);
}
MatchPhase::Complete => {}
}
state.phase.clone()
}
fn step(cap: Option<Captures<'_>>, haystack: &str) -> MatcherState {
match cap {
Some(cap) => MatcherState {
phase: MatchPhase::Matching(resume_from(&cap, haystack)),
last_match: Some(MatchResult::new(&cap)),
},
None => MatcherState {
phase: MatchPhase::Complete,
last_match: None,
},
}
}
pub fn capture(&self) -> Option<MatchResult> {
let state = self.state.lock().unwrap();
state.last_match.clone()
}
pub fn phase(&self) -> MatchPhase {
self.state.lock().unwrap().phase.clone()
}
}
fn resume_from(cap: &Captures<'_>, haystack: &str) -> usize {
let end = cap.end();
if cap.start() != end {
return end;
}
match haystack[end..].chars().next() {
Some(c) => end + c.len_utf8(),
None => end + 1,
}
}
impl MatchResult {
pub fn new(cap: &Captures<'_>) -> Self {
Self {
full: cap.full().to_string(),
groups: cap.groups().map(|g| g.map(|e| e.to_string())).collect(),
}
}
pub fn to_value(&self) -> Value {
if self.groups.len() == 1 || self.groups.iter().skip(1).all(|g| g.is_none()) {
Value::Str(GcPtr::new(self.full.to_string()))
} else {
let groups: Vec<Value> = self
.groups
.iter()
.map(|g| match g {
Some(m) => Value::Str(GcPtr::new(m.to_string())),
None => Value::Nil,
})
.collect();
Value::Vector(GcPtr::new(PersistentVector::from_iter(groups)))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pattern_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Pattern>();
}
#[test]
fn captures_expose_groups_in_order() {
let p = Pattern::new(r"(\d+)-(\d+)").unwrap();
let cap = p.captures("x 12-345 y").unwrap();
assert_eq!(cap.full(), "12-345");
assert_eq!(cap.group_count(), 3);
assert_eq!(cap.end(), 8);
assert_eq!(
cap.groups().collect::<Vec<_>>(),
vec![Some("12-345"), Some("12"), Some("345")]
);
}
#[test]
fn non_participating_group_is_none() {
let p = Pattern::new(r"(a)|(b)").unwrap();
let cap = p.captures("b").unwrap();
assert_eq!(
cap.groups().collect::<Vec<_>>(),
vec![Some("b"), None, Some("b")]
);
}
#[test]
fn captures_at_resumes_after_a_match() {
let p = Pattern::new(r"\d+").unwrap();
let cap = p.captures_at("a1 b22", 2).unwrap();
assert_eq!(cap.full(), "22");
}
#[test]
fn invalid_pattern_reports_the_engine_message() {
let err = Pattern::new(r"(").unwrap_err();
assert!(!err.to_string().is_empty());
}
#[test]
fn split_replace_and_display() {
let p = Pattern::new(r",\s*").unwrap();
assert_eq!(p.split("a, b,c").collect::<Vec<_>>(), vec!["a", "b", "c"]);
assert_eq!(p.splitn("a, b,c", 2).collect::<Vec<_>>(), vec!["a", "b,c"]);
assert_eq!(p.replace("a, b,c", "|"), "a|b,c");
assert_eq!(p.replace_all("a, b,c", "|"), "a|b|c");
assert_eq!(p.as_str(), r",\s*");
assert_eq!(p.to_string(), r",\s*");
}
fn full_match(pattern: &str, haystack: &str) -> Option<MatchResult> {
let m = Matcher::new(Pattern::new(pattern).unwrap(), haystack.to_string(), true);
m.next();
m.capture()
}
#[test]
fn match_all_requires_the_whole_haystack() {
for (pattern, haystack) in [
(r"\d+", "42"),
(r"\d+", "424"),
(r"\d+", "4"),
(r"a+", "aaa"),
(r".*", "hello"),
] {
assert_eq!(
full_match(pattern, haystack).map(|c| c.full),
Some(haystack.to_string()),
"{pattern} should match all of {haystack}"
);
}
let cap = full_match(r"(\d+)-(\d+)", "12-345").unwrap();
assert_eq!(cap.full, "12-345");
assert_eq!(
cap.groups,
vec![Some("12-345".into()), Some("12".into()), Some("345".into())]
);
assert!(full_match(r"(a)(b)", "abc").is_none());
assert!(full_match(r"(a)", "ab").is_none());
assert!(full_match(r"\d+", "42x").is_none());
assert!(full_match(r"(a)(b)", "xab").is_none());
assert!(full_match(r"\d+", "x42").is_none());
assert!(full_match(r"\d+", "abc").is_none());
}
#[test]
fn match_all_beats_leftmost_first_preference() {
assert_eq!(full_match(r"a|ab", "ab").map(|c| c.full), Some("ab".into()));
assert_eq!(
full_match(r"(a|ab)(c|bc)", "abc").map(|c| c.full),
Some("abc".into())
);
assert_eq!(
full_match(r".*?", "hello").map(|c| c.full),
Some("hello".into())
);
assert_eq!(
full_match(r"(\w+?)(\d*)", "ab12").map(|c| c.groups),
Some(vec![
Some("ab12".into()),
Some("ab".into()),
Some("12".into())
])
);
let p = Pattern::new(r"a|ab").unwrap();
assert_eq!(p.captures("ab").unwrap().full(), "a");
}
#[test]
fn match_all_preserves_groups_and_inline_flags() {
let cap = full_match(r"(?i)(a)(b)", "AB").unwrap();
assert_eq!(cap.full, "AB");
assert_eq!(
cap.groups,
vec![Some("AB".into()), Some("A".into()), Some("B".into())]
);
assert_eq!(full_match(r"", "").map(|c| c.full), Some(String::new()));
assert!(full_match(r"", "a").is_none());
}
#[test]
fn match_all_yields_at_most_one_match() {
let m = Matcher::new(Pattern::new(r"a*").unwrap(), "aaa".to_string(), true);
assert!(matches!(m.next(), MatchPhase::Matching(3)));
assert_eq!(m.capture().unwrap().full, "aaa");
assert!(matches!(m.next(), MatchPhase::Complete));
assert!(m.capture().is_none());
assert!(matches!(m.next(), MatchPhase::Complete));
}
#[test]
fn match_all_completes_when_the_match_is_partial() {
let m = Matcher::new(Pattern::new(r"(a)").unwrap(), "ab".to_string(), true);
assert!(matches!(m.next(), MatchPhase::Complete));
assert!(m.capture().is_none());
let mut steps = 0;
while let MatchPhase::New | MatchPhase::Matching(_) = m.next() {
steps += 1;
assert!(steps < 10, "matcher never reached a terminal state");
}
}
fn drain(pattern: &str, haystack: &str) -> Vec<String> {
let m = Matcher::new(Pattern::new(pattern).unwrap(), haystack.to_string(), false);
let mut found = Vec::new();
while let MatchPhase::Matching(_) = m.next() {
found.push(m.capture().unwrap().full);
assert!(
found.len() < 16,
"matcher never reached Complete: {found:?}"
);
}
assert!(matches!(m.phase(), MatchPhase::Complete));
found
}
#[test]
fn zero_width_matches_advance_and_terminate() {
assert_eq!(drain(r"a*", "aaa"), vec!["aaa", ""]);
assert_eq!(drain(r"a*", "bab"), vec!["", "a", "", ""]);
assert_eq!(drain(r"", "ab"), vec!["", "", ""]);
assert_eq!(drain(r"", ""), vec![""]);
assert_eq!(drain(r"x*", "ab"), vec!["", "", ""]);
}
#[test]
fn zero_width_advance_respects_utf8_boundaries() {
assert_eq!(drain(r"x*", "é"), vec!["", ""]);
assert_eq!(drain(r"x*", "日本"), vec!["", "", ""]);
assert_eq!(drain(r"é*", "éé"), vec!["éé", ""]);
}
#[test]
fn matcher_walks_every_match_then_completes() {
let p = Pattern::new(r"\d+").unwrap();
let m = Matcher::new(p, "a1 b22 c333".to_string(), false);
let mut found = Vec::new();
while let MatchPhase::Matching(_) = m.next() {
found.push(m.capture().unwrap().full);
}
assert_eq!(found, vec!["1", "22", "333"]);
assert!(matches!(m.phase(), MatchPhase::Complete));
assert!(m.capture().is_none());
}
}