Skip to main content

rexact_core/
dfa.rs

1//! Derivative automaton, matching, minimisation and canonical form
2//! (FR-B03..B05).
3//!
4//! Contract: [`Dfa`] explores derivatives of a start term lazily and caches
5//! every transition, so matching costs O(input length) after the states it
6//! needs exist (FR-B05). [`Dfa::minimize`] returns the unique minimal complete
7//! automaton over the coarsest symbol-class partition, numbered canonically,
8//! so two terms have equal [`MinDfa::canonical_form`] exactly when their
9//! languages are equal (FR-B04).
10//!
11//! Invariants: state 0 is the start; the automaton is complete (the dead state
12//! is the `Empty` term); classes partition the universe and are ordered by
13//! smallest member. Failure modes: every state creation is charged to the
14//! [`Budget`]; a breach returns its [`ErrorCode`] and leaves the automaton
15//! consistent.
16
17use crate::charset::CharSet;
18use crate::limits::Budget;
19use crate::sha256::sha256_hex;
20use crate::term::{TermId, TermStore, NO_CTX};
21use rexact_syntax::error::ErrorCode;
22use std::collections::HashMap;
23
24const UNSET: u32 = u32::MAX;
25/// Rough bytes charged per interned term, for the memory limit.
26const TERM_BYTES: u64 = 96;
27
28/// Lazily built deterministic automaton over symbol classes.
29pub struct Dfa {
30    classes: Vec<CharSet>,
31    /// (lo, hi, class) sorted by `lo`, for symbol-to-class lookup.
32    lookup: Vec<(u32, u32, u32)>,
33    states: Vec<TermId>,
34    /// Context of each state (matters only for context-dependent terms).
35    ctxs: Vec<u32>,
36    index: HashMap<(TermId, u32), u32>,
37    trans: Vec<u32>,
38    accepting: Vec<bool>,
39    seen_terms: usize,
40    alphabet: &'static str,
41}
42
43impl Dfa {
44    /// Start an automaton at `start`. The symbol classes are those of `start`.
45    pub fn new(
46        store: &mut TermStore,
47        start: TermId,
48        alphabet: &'static str,
49        budget: &mut Budget,
50    ) -> Result<Dfa, ErrorCode> {
51        let classes = (*store.partition(start)).clone();
52        let mut lookup: Vec<(u32, u32, u32)> = Vec::new();
53        for (i, c) in classes.iter().enumerate() {
54            for &(lo, hi) in c.ranges() {
55                lookup.push((lo, hi, i as u32));
56            }
57        }
58        lookup.sort_unstable();
59        let mut d = Dfa {
60            classes,
61            lookup,
62            states: Vec::new(),
63            ctxs: Vec::new(),
64            index: HashMap::new(),
65            trans: Vec::new(),
66            accepting: Vec::new(),
67            seen_terms: store.len(),
68            alphabet,
69        };
70        let ctx = if store.ctx_dep(start) {
71            store.ctx_start()
72        } else {
73            NO_CTX
74        };
75        d.intern_state(store, start, ctx, budget)?;
76        Ok(d)
77    }
78
79    /// Number of states created so far.
80    pub fn num_states(&self) -> usize {
81        self.states.len()
82    }
83    /// Number of symbol classes.
84    pub fn num_classes(&self) -> usize {
85        self.classes.len()
86    }
87
88    fn intern_state(
89        &mut self,
90        store: &TermStore,
91        t: TermId,
92        ctx: u32,
93        budget: &mut Budget,
94    ) -> Result<u32, ErrorCode> {
95        // Only context-dependent terms care what came before.
96        let ctx = if store.ctx_dep(t) { ctx } else { NO_CTX };
97        if let Some(&s) = self.index.get(&(t, ctx)) {
98            return Ok(s);
99        }
100        budget.add_states(1)?;
101        budget.add_memory(4 * self.classes.len() as u64 + TERM_BYTES)?;
102        budget.check_time()?;
103        let id = self.states.len() as u32;
104        self.states.push(t);
105        self.ctxs.push(ctx);
106        self.index.insert((t, ctx), id);
107        self.trans
108            .extend(std::iter::repeat(UNSET).take(self.classes.len()));
109        self.accepting.push(store.accepting(t, ctx));
110        Ok(id)
111    }
112
113    /// Class index of a symbol, or `None` when outside the universe.
114    pub fn class_of(&self, c: u32) -> Option<usize> {
115        let i = self.lookup.partition_point(|&(lo, _, _)| lo <= c);
116        let &(_, hi, k) = self.lookup.get(i.checked_sub(1)?)?;
117        (c <= hi).then_some(k as usize)
118    }
119
120    /// Transition from `s` on `class`, computed once and cached.
121    pub fn step(
122        &mut self,
123        store: &mut TermStore,
124        s: u32,
125        class: usize,
126        budget: &mut Budget,
127    ) -> Result<u32, ErrorCode> {
128        let slot = s as usize * self.classes.len() + class;
129        if self.trans[slot] != UNSET {
130            return Ok(self.trans[slot]);
131        }
132        let rep = self.classes[class].smallest().unwrap_or(0);
133        let (term, ctx) = (self.states[s as usize], self.ctxs[s as usize]);
134        let d = store.deriv_in(term, rep, ctx);
135        let next_ctx = if store.ctx_dep(d) {
136            store.advance_ctx(ctx, rep)
137        } else {
138            NO_CTX
139        };
140        let grown = store.len().saturating_sub(self.seen_terms) as u64;
141        self.seen_terms = store.len();
142        budget.add_memory(grown * TERM_BYTES)?;
143        let t = self.intern_state(store, d, next_ctx, budget)?;
144        self.trans[slot] = t;
145        Ok(t)
146    }
147
148    /// Full-match test: does the whole input belong to the language?
149    pub fn matches(
150        &mut self,
151        store: &mut TermStore,
152        input: &[u32],
153        budget: &mut Budget,
154    ) -> Result<bool, ErrorCode> {
155        let mut s = 0u32;
156        for &c in input {
157            let Some(k) = self.class_of(c) else {
158                return Ok(false);
159            };
160            s = self.step(store, s, k, budget)?;
161            if self.states[s as usize] == store.empty() {
162                return Ok(false);
163            }
164        }
165        Ok(self.accepting[s as usize])
166    }
167
168    /// Build every reachable state and transition.
169    pub fn explore(&mut self, store: &mut TermStore, budget: &mut Budget) -> Result<(), ErrorCode> {
170        let mut i = 0;
171        while i < self.states.len() {
172            for c in 0..self.classes.len() {
173                self.step(store, i as u32, c, budget)?;
174            }
175            i += 1;
176        }
177        Ok(())
178    }
179
180    /// Minimal complete automaton in canonical numbering.
181    pub fn minimize(
182        &mut self,
183        store: &mut TermStore,
184        budget: &mut Budget,
185    ) -> Result<MinDfa, ErrorCode> {
186        self.explore(store, budget)?;
187        let (n, k) = (self.states.len(), self.classes.len());
188
189        // Hopcroft partition refinement: O(n k log n), so long chains such as
190        // a{0,9999} (n = 10,000) stay fast where Moore refinement is quadratic.
191        let (block, count) = hopcroft(n, k, &self.trans, &self.accepting, budget)?;
192
193        // Quotient automaton: one row per block.
194        let mut rep = vec![usize::MAX; count];
195        for (i, &b) in block.iter().enumerate() {
196            if rep[b as usize] == usize::MAX {
197                rep[b as usize] = i;
198            }
199        }
200        let q = |b: usize, c: usize| block[self.trans[rep[b] * k + c] as usize];
201
202        // Merge symbol classes with identical columns (coarsest partition).
203        let mut groups: Vec<Vec<usize>> = Vec::new();
204        let mut by_col: HashMap<Vec<u32>, usize> = HashMap::new();
205        for c in 0..k {
206            let col: Vec<u32> = (0..count).map(|b| q(b, c)).collect();
207            let g = *by_col.entry(col).or_insert_with(|| {
208                groups.push(Vec::new());
209                groups.len() - 1
210            });
211            groups[g].push(c);
212        }
213        let merged: Vec<CharSet> = groups
214            .iter()
215            .map(|g| {
216                g.iter()
217                    .fold(CharSet::empty(), |acc, &c| acc.union(&self.classes[c]))
218            })
219            .collect();
220        let k2 = groups.len();
221
222        // Canonical numbering: breadth-first from the start, merged classes in order.
223        let start = block[0] as usize;
224        let mut canon = vec![u32::MAX; count];
225        let mut order = vec![start];
226        canon[start] = 0;
227        let mut i = 0;
228        while i < order.len() {
229            for g in &groups {
230                let t = q(order[i], g[0]) as usize;
231                if canon[t] == u32::MAX {
232                    canon[t] = order.len() as u32;
233                    order.push(t);
234                }
235            }
236            i += 1;
237        }
238        let mut trans = Vec::with_capacity(order.len() * k2);
239        let mut accepting = Vec::with_capacity(order.len());
240        for &b in &order {
241            for g in &groups {
242                trans.push(canon[q(b, g[0]) as usize]);
243            }
244            accepting.push(self.accepting[rep[b]]);
245        }
246        Ok(MinDfa {
247            alphabet: self.alphabet,
248            classes: merged,
249            trans,
250            accepting,
251        })
252    }
253}
254
255/// Minimal complete automaton with canonical numbering.
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct MinDfa {
258    alphabet: &'static str,
259    classes: Vec<CharSet>,
260    trans: Vec<u32>,
261    accepting: Vec<bool>,
262}
263
264impl MinDfa {
265    /// Number of states.
266    pub fn num_states(&self) -> usize {
267        self.accepting.len()
268    }
269    /// Number of symbol classes after merging indistinguishable ones.
270    pub fn num_classes(&self) -> usize {
271        self.classes.len()
272    }
273    /// Symbol classes, ordered by smallest member.
274    pub fn classes(&self) -> &[CharSet] {
275        &self.classes
276    }
277    /// Is state `s` accepting?
278    pub fn is_accepting(&self, s: usize) -> bool {
279        self.accepting[s]
280    }
281    /// Transition table lookup.
282    pub fn next(&self, s: usize, class: usize) -> usize {
283        self.trans[s * self.classes.len() + class] as usize
284    }
285
286    /// Full-match test on the minimal automaton.
287    pub fn accepts(&self, input: &[u32]) -> bool {
288        let mut s = 0usize;
289        for &c in input {
290            match self.classes.iter().position(|k| k.contains(c)) {
291                Some(k) => s = self.next(s, k),
292                None => return false,
293            }
294        }
295        self.accepting[s]
296    }
297
298    /// Exact textual form; equal strings iff equal languages (same alphabet).
299    pub fn canonical_form(&self) -> String {
300        let mut out = format!(
301            "rexact-dfa v1\nalphabet {}\nclasses {}\n",
302            self.alphabet,
303            self.classes.len()
304        );
305        for c in &self.classes {
306            let parts: Vec<String> = c
307                .ranges()
308                .iter()
309                .map(|&(l, h)| format!("{l:X}-{h:X}"))
310                .collect();
311            out.push_str(&parts.join(","));
312            out.push('\n');
313        }
314        out.push_str(&format!("states {}\n", self.accepting.len()));
315        out.push_str(
316            &self
317                .accepting
318                .iter()
319                .map(|&a| if a { '1' } else { '0' })
320                .collect::<String>(),
321        );
322        out.push('\n');
323        let k = self.classes.len();
324        for s in 0..self.accepting.len() {
325            let row: Vec<String> = (0..k).map(|c| self.trans[s * k + c].to_string()).collect();
326            out.push_str(&row.join(" "));
327            out.push('\n');
328        }
329        out
330    }
331
332    /// SHA-256 of the canonical form: the language fingerprint (FR-B04). The
333    /// canonical form is the authority; the hash is its compact name.
334    pub fn fingerprint(&self) -> String {
335        sha256_hex(self.canonical_form().as_bytes())
336    }
337}
338
339/// Hopcroft's partition refinement. Returns the block of every state and the
340/// number of blocks (ids are dense, `0..count`).
341fn hopcroft(
342    n: usize,
343    k: usize,
344    trans: &[u32],
345    accepting: &[bool],
346    budget: &mut Budget,
347) -> Result<(Vec<u32>, usize), ErrorCode> {
348    // Inverse transitions per class in CSR form: predecessors of t on class c
349    // are list[c*n + start[c*(n+1)+t] .. start[c*(n+1)+t+1]].
350    let mut start = vec![0u32; k * (n + 1)];
351    let mut list = vec![0u32; k * n];
352    for c in 0..k {
353        let base = c * (n + 1);
354        for s in 0..n {
355            start[base + trans[s * k + c] as usize + 1] += 1;
356        }
357        for t in 0..n {
358            start[base + t + 1] += start[base + t];
359        }
360        let mut fill: Vec<u32> = start[base..base + n].to_vec();
361        for s in 0..n {
362            let t = trans[s * k + c] as usize;
363            list[c * n + fill[t] as usize] = s as u32;
364            fill[t] += 1;
365        }
366    }
367
368    let mut block_of = vec![0u32; n];
369    let mut members: Vec<Vec<u32>> = Vec::new();
370    let (mut fin, mut non) = (Vec::new(), Vec::new());
371    for (s, &is_final) in accepting.iter().enumerate() {
372        if is_final {
373            fin.push(s as u32);
374        } else {
375            non.push(s as u32);
376        }
377    }
378    for g in [non, fin] {
379        if !g.is_empty() {
380            let id = members.len() as u32;
381            for &s in &g {
382                block_of[s as usize] = id;
383            }
384            members.push(g);
385        }
386    }
387    let mut in_work = vec![false; members.len()];
388    let mut inx: Vec<Vec<u32>> = vec![Vec::new(); members.len()];
389    let mut work: Vec<u32> = Vec::new();
390    if members.len() == 2 {
391        let small = if members[0].len() <= members[1].len() {
392            0
393        } else {
394            1
395        };
396        work.push(small);
397        in_work[small as usize] = true;
398    }
399
400    let mut mark = vec![0u32; n];
401    let mut stamp = 0u32;
402    let mut touched: Vec<u32> = Vec::new();
403    while let Some(a) = work.pop() {
404        in_work[a as usize] = false;
405        budget.check_time()?;
406        let splitter = members[a as usize].clone();
407        for c in 0..k {
408            stamp += 1;
409            touched.clear();
410            let base = c * (n + 1);
411            for &t in &splitter {
412                let lo = start[base + t as usize] as usize;
413                let hi = start[base + t as usize + 1] as usize;
414                for &s in &list[c * n + lo..c * n + hi] {
415                    if mark[s as usize] != stamp {
416                        mark[s as usize] = stamp;
417                        let b = block_of[s as usize];
418                        if inx[b as usize].is_empty() {
419                            touched.push(b);
420                        }
421                        inx[b as usize].push(s);
422                    }
423                }
424            }
425            for &b in &touched {
426                let moved = std::mem::take(&mut inx[b as usize]);
427                if moved.len() == members[b as usize].len() {
428                    continue; // the whole block is inside X: no split
429                }
430                let nb = members.len() as u32;
431                members[b as usize].retain(|&s| mark[s as usize] != stamp);
432                for &s in &moved {
433                    block_of[s as usize] = nb;
434                }
435                members.push(moved);
436                in_work.push(false);
437                inx.push(Vec::new());
438                if in_work[b as usize] {
439                    in_work[nb as usize] = true;
440                    work.push(nb);
441                } else {
442                    let small = if members[b as usize].len() <= members[nb as usize].len() {
443                        b
444                    } else {
445                        nb
446                    };
447                    in_work[small as usize] = true;
448                    work.push(small);
449                }
450            }
451        }
452    }
453    Ok((block_of, members.len()))
454}