Skip to main content

rexact_core/
language.rs

1//! The user-facing handle: a compiled language with matching, minimal
2//! automaton and fingerprint.
3
4use crate::alphabet::Alphabet;
5use crate::dfa::{Dfa, MinDfa};
6use crate::limits::{Budget, Limits};
7use crate::lower::Lower;
8use crate::term::{TermId, TermStore};
9use rexact_syntax::error::{ErrorCode, RexactError};
10use rexact_syntax::Pattern;
11
12/// Full: the pattern must match the entire input. Search: any substring may
13/// match (FR-A11).
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Mode {
16    /// Whole-input match.
17    Full,
18    /// Match anywhere.
19    Search,
20}
21
22/// A pattern compiled to a term and a lazily built automaton.
23pub struct Language {
24    store: TermStore,
25    dfa: Dfa,
26    root: TermId,
27    alphabet: Alphabet,
28    limits: Limits,
29}
30
31fn limit(code: ErrorCode) -> RexactError {
32    RexactError::new(code)
33}
34
35impl Language {
36    /// Compile a parsed pattern. `alphabet` defaults per dialect.
37    pub fn compile(
38        p: &Pattern,
39        alphabet: Option<Alphabet>,
40        mode: Mode,
41        limits: &Limits,
42    ) -> Result<Language, RexactError> {
43        let alphabet = alphabet.unwrap_or_else(|| Alphabet::default_for(p.dialect, p.flags));
44        let mut store = TermStore::new(alphabet.universe());
45        let root = lower_into(&mut store, p, alphabet, mode, limits)?;
46        Self::from_parts(store, root, alphabet, limits)
47    }
48
49    /// Wrap an existing term (used by the analyses and by tests).
50    pub fn from_parts(
51        mut store: TermStore,
52        root: TermId,
53        alphabet: Alphabet,
54        limits: &Limits,
55    ) -> Result<Language, RexactError> {
56        let mut budget = Budget::new(limits.clone());
57        let dfa = Dfa::new(&mut store, root, alphabet.name(), &mut budget).map_err(limit)?;
58        Ok(Language {
59            store,
60            dfa,
61            root,
62            alphabet,
63            limits: limits.clone(),
64        })
65    }
66
67    /// The alphabet mode, reported with every result.
68    pub fn alphabet(&self) -> Alphabet {
69        self.alphabet
70    }
71    /// The root term.
72    pub fn root(&self) -> TermId {
73        self.root
74    }
75    /// The term store (read-only).
76    pub fn store(&self) -> &TermStore {
77        &self.store
78    }
79
80    /// Full-match test on text, encoded per the alphabet mode.
81    pub fn matches(&mut self, input: &str) -> Result<bool, RexactError> {
82        let units = self.alphabet.encode(input);
83        self.matches_units(&units)
84    }
85
86    /// Full-match test on symbols.
87    pub fn matches_units(&mut self, input: &[u32]) -> Result<bool, RexactError> {
88        let mut budget = Budget::new(self.limits.clone());
89        self.dfa
90            .matches(&mut self.store, input, &mut budget)
91            .map_err(limit)
92    }
93
94    /// Minimal complete automaton in canonical form.
95    pub fn minimal(&mut self) -> Result<MinDfa, RexactError> {
96        let mut budget = Budget::new(self.limits.clone());
97        self.dfa
98            .minimize(&mut self.store, &mut budget)
99            .map_err(limit)
100    }
101
102    /// Language fingerprint: equal for equal languages in the same alphabet.
103    pub fn fingerprint(&mut self) -> Result<String, RexactError> {
104        Ok(self.minimal()?.fingerprint())
105    }
106
107    /// (states built so far, symbol classes).
108    pub fn stats(&self) -> (usize, usize) {
109        (self.dfa.num_states(), self.dfa.num_classes())
110    }
111}
112
113/// Fingerprint of a term that lives in a caller-owned store. Equal results
114/// mean equal languages over `alphabet` (the canonical form is the authority;
115/// see [`MinDfa::canonical_form`]).
116pub fn fingerprint_term(
117    store: &mut TermStore,
118    root: TermId,
119    alphabet: Alphabet,
120    limits: &Limits,
121) -> Result<String, RexactError> {
122    let mut budget = Budget::new(limits.clone());
123    let mut dfa = Dfa::new(store, root, alphabet.name(), &mut budget).map_err(limit)?;
124    Ok(dfa
125        .minimize(store, &mut budget)
126        .map_err(limit)?
127        .fingerprint())
128}
129
130/// Lower a pattern into an existing store, so several patterns can be analysed
131/// together (equivalence, comparison). `store` must have been created with
132/// `alphabet.universe()`.
133pub fn lower_into(
134    store: &mut TermStore,
135    p: &Pattern,
136    alphabet: Alphabet,
137    mode: Mode,
138    limits: &Limits,
139) -> Result<TermId, RexactError> {
140    let mut budget = Budget::new(limits.clone());
141    budget
142        .check_length(p.source.chars().count() as u64)
143        .map_err(limit)?;
144    let (term, _) = {
145        let mut l = Lower {
146            store,
147            alphabet,
148            dialect: p.dialect,
149            chars: p.source.chars().collect(),
150            budget: &mut budget,
151        };
152        l.pattern(p)?
153    };
154    Ok(match mode {
155        Mode::Full => term,
156        Mode::Search => {
157            let any = store.any_symbol();
158            let all = store.star(any);
159            store.concat_all(&[all, term, all])
160        }
161    })
162}
163
164/// Minimal automaton of a term that lives in a caller-owned store.
165pub fn minimal_of_term(
166    store: &mut TermStore,
167    root: TermId,
168    alphabet: Alphabet,
169    limits: &Limits,
170) -> Result<crate::dfa::MinDfa, RexactError> {
171    let mut budget = Budget::new(limits.clone());
172    let mut dfa = Dfa::new(store, root, alphabet.name(), &mut budget).map_err(limit)?;
173    dfa.minimize(store, &mut budget).map_err(limit)
174}
175
176/// A pattern resolved for backtracking analysis, with the store its
177/// zero-width terms live in.
178pub struct Resolved {
179    /// The tree.
180    pub root: crate::resolved::RNode,
181    /// Store holding assertion terms.
182    pub store: TermStore,
183    /// Alphabet mode in force.
184    pub alphabet: Alphabet,
185}
186
187/// Resolve `p` for analysis (does not build any automaton).
188pub fn resolve(
189    p: &Pattern,
190    alphabet: Option<Alphabet>,
191    limits: &Limits,
192) -> Result<Resolved, RexactError> {
193    let alphabet = alphabet.unwrap_or_else(|| Alphabet::default_for(p.dialect, p.flags));
194    let mut store = TermStore::new(alphabet.universe());
195    let mut budget = Budget::new(limits.clone());
196    budget
197        .check_length(p.source.chars().count() as u64)
198        .map_err(limit)?;
199    let root = {
200        let mut l = Lower {
201            store: &mut store,
202            alphabet,
203            dialect: p.dialect,
204            chars: p.source.chars().collect(),
205            budget: &mut budget,
206        };
207        l.resolve_pattern(p)?
208    };
209    Ok(Resolved {
210        root,
211        store,
212        alphabet,
213    })
214}