Skip to main content

rusty_alto/
explicit.rs

1//! Dense explicit weighted tree automata and their builder.
2
3use crate::{
4    BottomUpTa, DetBottomUpTa, FxHashMap, FxHashSet, IndexedBottomUpTa, StateId, Symbol, TopDownTa,
5    traits::{CondensedTa, CondensedTopDownTa, StateUniverse, SymbolSet},
6};
7use fixedbitset::FixedBitSet;
8use smallvec::SmallVec;
9use std::hash::{BuildHasher, Hash, Hasher};
10use std::sync::OnceLock;
11use thiserror::Error;
12
13type Results = SmallVec<[StateId; 2]>;
14
15/// A fully materialized bottom-up tree automaton.
16///
17/// `Explicit` stores transition rules in lookup tables. It is the fastest
18/// representation when all rules are known ahead of time or after an implicit
19/// automaton has been materialized. Rules with arity 0, 1, and 2 use separate
20/// compact tables because those are the common hot paths.
21///
22/// Build values with [`ExplicitBuilder`]. Every transition rule has a weight;
23/// callers that do not have natural weights can use `1.0`.
24#[derive(Clone, Debug)]
25pub struct Explicit {
26    num_states: u32,
27    accepting: FixedBitSet,
28    rules: Vec<StoredRule>,
29    bottom_up_indexes: OnceLock<BottomUpIndexes>,
30    reachable_cache: OnceLock<FixedBitSet>,
31    result_index: OnceLock<Vec<Vec<usize>>>,
32    indexes: OnceLock<Indexes>,
33    condensed_cache: OnceLock<Vec<CondensedRule>>,
34}
35
36#[derive(Clone, Debug, Eq)]
37struct HigherKey(Symbol, Box<[StateId]>);
38
39impl PartialEq for HigherKey {
40    fn eq(&self, other: &Self) -> bool {
41        self.0 == other.0 && self.1 == other.1
42    }
43}
44
45impl Hash for HigherKey {
46    fn hash<H: Hasher>(&self, state: &mut H) {
47        self.0.hash(state);
48        self.1.hash(state);
49    }
50}
51
52#[derive(Clone, Debug)]
53struct StoredRule {
54    symbol: Symbol,
55    children: SmallVec<[StateId; 2]>,
56    result: StateId,
57    weight: f64,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq, Hash)]
61struct RuleKey {
62    symbol: Symbol,
63    children: SmallVec<[StateId; 2]>,
64    result: StateId,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq)]
68struct CondensedRule {
69    children: Box<[StateId]>,
70    symbols: SymbolSet,
71    result: StateId,
72}
73
74#[derive(Clone, Debug, Default)]
75struct BottomUpIndexes {
76    nullary: FxHashMap<Symbol, Results>,
77    unary: FxHashMap<(Symbol, StateId), Results>,
78    binary: FxHashMap<(Symbol, StateId, StateId), Results>,
79    higher: FxHashMap<HigherKey, Results>,
80}
81
82#[derive(Clone, Debug, Default)]
83struct Indexes {
84    by_child: FxHashMap<(Symbol, usize, StateId), Vec<usize>>,
85}
86
87/// Borrowed view of one transition rule in an [`Explicit`] automaton.
88///
89/// A rule means: when a node has `symbol` and its children have exactly
90/// `children`, the node may receive `result`.
91#[derive(Clone, Copy, Debug, PartialEq)]
92pub struct Rule<'a> {
93    /// Symbol on the tree node matched by this rule.
94    pub symbol: Symbol,
95    /// Required child-state tuple, in left-to-right child order.
96    pub children: &'a [StateId],
97    /// State assigned to the parent node when the rule applies.
98    pub result: StateId,
99    /// Weight assigned to this transition rule.
100    pub weight: f64,
101}
102
103/// Error returned when an explicit automaton cannot be built.
104#[derive(Clone, Debug, Error, PartialEq)]
105pub enum ExplicitBuildError {
106    /// The same transition was added more than once.
107    #[error("duplicate transition for symbol {symbol:?}, children {children:?}, result {result:?}")]
108    DuplicateTransition {
109        /// Symbol on the duplicated transition.
110        symbol: Symbol,
111        /// Child-state tuple on the duplicated transition.
112        children: Vec<StateId>,
113        /// Parent/result state on the duplicated transition.
114        result: StateId,
115    },
116}
117
118/// Builder for [`Explicit`] automata.
119///
120/// Allocate states with [`ExplicitBuilder::new_state`], add rules with
121/// [`ExplicitBuilder::add_rule`], mark accepting states with
122/// [`ExplicitBuilder::add_accepting`], then call [`ExplicitBuilder::build`].
123///
124/// The builder checks that every state in every rule was allocated by this
125/// builder. This catches many accidental mixups between automata early.
126#[derive(Clone, Debug, Default)]
127pub struct ExplicitBuilder {
128    next_state: u32,
129    accepting: Vec<StateId>,
130    rules: Vec<(Symbol, SmallVec<[StateId; 2]>, StateId, f64)>,
131}
132
133impl ExplicitBuilder {
134    /// Create an empty builder with no states and no rules.
135    pub fn new() -> Self {
136        Self::default()
137    }
138
139    /// Allocate and return a fresh state.
140    ///
141    /// States are assigned densely starting at `StateId(0)`.
142    pub fn new_state(&mut self) -> StateId {
143        assert_ne!(self.next_state, StateId::STUCK.0, "cannot allocate STUCK");
144        let id = StateId(self.next_state);
145        self.next_state += 1;
146        id
147    }
148
149    /// Mark a state as accepting.
150    ///
151    /// A tree is accepted when its root can be assigned one of the accepting
152    /// states. Passing a state not allocated by this builder panics.
153    pub fn add_accepting(&mut self, q: StateId) {
154        self.check_state(q);
155        self.accepting.push(q);
156    }
157
158    /// Add a bottom-up transition rule.
159    ///
160    /// `children` is the exact child-state tuple for the rule. An empty vector
161    /// creates a nullary rule, suitable for leaf symbols. Passing `STUCK` or a
162    /// state not allocated by this builder panics.
163    pub fn add_rule(&mut self, f: Symbol, children: Vec<StateId>, q: StateId) {
164        self.add_weighted_rule(f, children, q, 1.0);
165    }
166
167    /// Add a weighted bottom-up transition rule.
168    ///
169    /// `children` is the exact child-state tuple for the rule. An empty vector
170    /// creates a nullary rule, suitable for leaf symbols. Passing `STUCK` or a
171    /// state not allocated by this builder panics.
172    pub fn add_weighted_rule(
173        &mut self,
174        f: Symbol,
175        children: Vec<StateId>,
176        q: StateId,
177        weight: f64,
178    ) {
179        self.add_weighted_rule_inline(f, SmallVec::from_vec(children), q, weight);
180    }
181
182    /// Add a weighted rule from an inline child tuple.
183    ///
184    /// This avoids round-tripping through `Vec` in internal materializers that
185    /// already build child tuples in the same inline representation used by
186    /// [`Explicit`].
187    pub(crate) fn add_weighted_rule_inline(
188        &mut self,
189        f: Symbol,
190        children: SmallVec<[StateId; 2]>,
191        q: StateId,
192        weight: f64,
193    ) {
194        self.check_state(q);
195        for &child in &children {
196            self.check_state(child);
197        }
198        self.rules.push((f, children, q, weight));
199    }
200
201    /// Build the explicit automaton.
202    ///
203    /// Panics if duplicate transitions were added. Use [`Self::try_build`] to
204    /// receive a typed error instead.
205    pub fn build(self) -> Explicit {
206        self.try_build()
207            .expect("explicit automaton contains duplicate transitions")
208    }
209
210    /// Build the explicit automaton, rejecting duplicate transitions.
211    ///
212    /// Multiple rules with the same symbol and children but different result
213    /// states are preserved, making the automaton nondeterministic for that
214    /// query. The exact same `(symbol, children, result)` transition may not be
215    /// added twice, regardless of weight.
216    pub fn try_build(self) -> Result<Explicit, ExplicitBuildError> {
217        self.finish(true)
218    }
219
220    /// Build without checking for duplicate transitions.
221    ///
222    /// This is for internal algorithms that already enforce uniqueness while
223    /// generating rules. External parsers and callers should use [`Self::build`]
224    /// or [`Self::try_build`] so duplicates are rejected.
225    pub(crate) fn build_trusted(self) -> Explicit {
226        self.finish(false)
227            .expect("trusted explicit automaton build cannot fail")
228    }
229
230    fn finish(self, check_duplicates: bool) -> Result<Explicit, ExplicitBuildError> {
231        let mut accepting = FixedBitSet::with_capacity(self.next_state as usize);
232        for q in self.accepting {
233            accepting.set(q.index(), true);
234        }
235
236        let mut seen = FxHashSet::default();
237        let mut stored = Vec::with_capacity(self.rules.len());
238
239        for (symbol, children, result, weight) in self.rules {
240            if check_duplicates {
241                let key = RuleKey {
242                    symbol,
243                    children: children.clone(),
244                    result,
245                };
246                if !seen.insert(key) {
247                    return Err(ExplicitBuildError::DuplicateTransition {
248                        symbol,
249                        children: children.into_vec(),
250                        result,
251                    });
252                }
253            }
254            let rule = StoredRule {
255                symbol,
256                children,
257                result,
258                weight,
259            };
260            stored.push(rule);
261        }
262
263        Ok(Explicit {
264            num_states: self.next_state,
265            accepting,
266            rules: stored,
267            bottom_up_indexes: OnceLock::new(),
268            reachable_cache: OnceLock::new(),
269            result_index: OnceLock::new(),
270            indexes: OnceLock::new(),
271            condensed_cache: OnceLock::new(),
272        })
273    }
274
275    fn check_state(&self, q: StateId) {
276        assert!(
277            !q.is_stuck(),
278            "StateId::STUCK is not a valid explicit state"
279        );
280        assert!(
281            q.0 < self.next_state,
282            "state {:?} was not allocated by this builder",
283            q
284        );
285    }
286}
287
288impl Explicit {
289    /// Return the number of allocated states.
290    pub fn num_states(&self) -> u32 {
291        self.num_states
292    }
293
294    /// Return the number of transition rules in this automaton.
295    pub fn num_rules(&self) -> usize {
296        self.rules.len()
297    }
298
299    /// Return true if no tree can be accepted by this automaton.
300    ///
301    /// This computes reachable states from nullary rules and checks whether any
302    /// accepting state is reachable.
303    pub fn is_empty(&self) -> bool {
304        !self
305            .reachable_states()
306            .ones()
307            .any(|idx| self.accepting.contains(idx))
308    }
309
310    /// Compute states reachable from nullary rules by saturation.
311    ///
312    /// A state is reachable if some finite tree can receive that state at its
313    /// root. This is often useful for pruning or quick emptiness checks. The
314    /// result is cached after the first call because explicit automata are
315    /// immutable.
316    pub fn reachable_states(&self) -> FixedBitSet {
317        self.reachable_cache
318            .get_or_init(|| self.compute_reachable_states())
319            .clone()
320    }
321
322    fn compute_reachable_states(&self) -> FixedBitSet {
323        let mut reachable = FixedBitSet::with_capacity(self.num_states as usize);
324        let mut worklist = Vec::new();
325
326        let mut remaining: Vec<usize> = self.rules.iter().map(|r| r.children.len()).collect();
327        let mut mentions: FxHashMap<StateId, Vec<usize>> = FxHashMap::default();
328
329        for (idx, rule) in self.rules.iter().enumerate() {
330            if rule.children.is_empty()
331                && mark_reachable(&mut reachable, &mut worklist, rule.result)
332            {
333                continue;
334            }
335            let mut unique_children: SmallVec<[StateId; 4]> = SmallVec::new();
336            for &child in rule.children.iter() {
337                if !unique_children.contains(&child) {
338                    unique_children.push(child);
339                    mentions.entry(child).or_default().push(idx);
340                }
341            }
342        }
343
344        while let Some(q) = worklist.pop() {
345            let Some(dependents) = mentions.get(&q) else {
346                continue;
347            };
348            for &idx in dependents {
349                if remaining[idx] == 0 {
350                    continue;
351                }
352                let rule = &self.rules[idx];
353                let newly_satisfied = rule.children.iter().filter(|&&c| c == q).count();
354                remaining[idx] = remaining[idx].saturating_sub(newly_satisfied);
355                if remaining[idx] == 0 {
356                    mark_reachable(&mut reachable, &mut worklist, rule.result);
357                }
358            }
359        }
360
361        reachable
362    }
363
364    /// Iterate over all transition rules.
365    ///
366    /// The order is stable for a fixed automaton but should not be treated as a
367    /// semantic ordering.
368    pub fn rules(&self) -> impl Iterator<Item = Rule<'_>> {
369        self.rules.iter().map(|rule| Rule {
370            symbol: rule.symbol,
371            children: rule.children.as_slice(),
372            result: rule.result,
373            weight: rule.weight,
374        })
375    }
376
377    /// Iterate over rules with the given parent/result state.
378    pub fn rules_topdown(&self, parent: StateId) -> impl Iterator<Item = Rule<'_>> {
379        self.result_index()[parent.index()]
380            .iter()
381            .map(|&rule_idx| self.rule(rule_idx))
382    }
383
384    /// Return the transition rule at the given index.
385    ///
386    /// Provides O(1) indexed access to a borrowed view of a rule; the children
387    /// slice is not copied. The index must be less than [`Self::num_rules`];
388    /// passing an out-of-bounds index panics. Rule order matches the order
389    /// produced by [`Self::rules`].
390    pub fn rule(&self, rule_idx: usize) -> Rule<'_> {
391        let rule = &self.rules[rule_idx];
392        Rule {
393            symbol: rule.symbol,
394            children: rule.children.as_slice(),
395            result: rule.result,
396            weight: rule.weight,
397        }
398    }
399
400    pub(crate) fn rule_indexes_topdown(&self, parent: StateId) -> &[usize] {
401        &self.result_index()[parent.index()]
402    }
403
404    fn result_index(&self) -> &[Vec<usize>] {
405        self.result_index.get_or_init(|| {
406            let mut counts = vec![0usize; self.num_states as usize];
407            for rule in &self.rules {
408                counts[rule.result.index()] += 1;
409            }
410
411            let mut by_result = counts
412                .into_iter()
413                .map(Vec::with_capacity)
414                .collect::<Vec<_>>();
415            for (rule_idx, rule) in self.rules.iter().enumerate() {
416                by_result[rule.result.index()].push(rule_idx);
417            }
418            by_result
419        })
420    }
421
422    fn bottom_up_indexes(&self) -> &BottomUpIndexes {
423        self.bottom_up_indexes.get_or_init(|| {
424            let mut indexes = BottomUpIndexes::default();
425            for rule in &self.rules {
426                match rule.children.len() {
427                    0 => push_result(indexes.nullary.entry(rule.symbol).or_default(), rule.result),
428                    1 => push_result(
429                        indexes
430                            .unary
431                            .entry((rule.symbol, rule.children[0]))
432                            .or_default(),
433                        rule.result,
434                    ),
435                    2 => push_result(
436                        indexes
437                            .binary
438                            .entry((rule.symbol, rule.children[0], rule.children[1]))
439                            .or_default(),
440                        rule.result,
441                    ),
442                    _ => push_result(
443                        indexes
444                            .higher
445                            .entry(HigherKey(
446                                rule.symbol,
447                                rule.children.clone().into_vec().into_boxed_slice(),
448                            ))
449                            .or_default(),
450                        rule.result,
451                    ),
452                }
453            }
454            indexes
455        })
456    }
457
458    fn lookup_higher<'a>(
459        indexes: &'a BottomUpIndexes,
460        f: Symbol,
461        children: &[StateId],
462    ) -> Option<&'a Results> {
463        let mut hasher = indexes.higher.hasher().build_hasher();
464        f.hash(&mut hasher);
465        children.hash(&mut hasher);
466        let hash = hasher.finish();
467        indexes
468            .higher
469            .raw_entry()
470            .from_hash(hash, |k| k.0 == f && &*k.1 == children)
471            .map(|(_, v)| v)
472    }
473
474    fn indexes(&self) -> &Indexes {
475        self.indexes.get_or_init(|| {
476            let mut indexes = Indexes::default();
477            for (rule_idx, rule) in self.rules.iter().enumerate() {
478                for (position, &child) in rule.children.iter().enumerate() {
479                    indexes
480                        .by_child
481                        .entry((rule.symbol, position, child))
482                        .or_default()
483                        .push(rule_idx);
484                }
485            }
486            indexes
487        })
488    }
489
490    fn condensed_cache(&self) -> &[CondensedRule] {
491        self.condensed_cache.get_or_init(|| {
492            let mut groups: FxHashMap<(Vec<StateId>, StateId), SymbolSet> = FxHashMap::default();
493            for rule in &self.rules {
494                groups
495                    .entry((rule.children.to_vec(), rule.result))
496                    .or_default()
497                    .insert(rule.symbol);
498            }
499
500            let mut condensed: Vec<_> = groups
501                .into_iter()
502                .map(|((children, result), symbols)| CondensedRule {
503                    children: children.into_boxed_slice(),
504                    symbols,
505                    result,
506                })
507                .collect();
508            condensed.sort_by(|a, b| {
509                (&a.children, a.result, a.symbols.iter().collect::<Vec<_>>()).cmp(&(
510                    &b.children,
511                    b.result,
512                    b.symbols.iter().collect::<Vec<_>>(),
513                ))
514            });
515            condensed
516        })
517    }
518}
519
520impl BottomUpTa for Explicit {
521    type State = StateId;
522
523    fn step(&self, f: Symbol, children: &[StateId], out: &mut dyn FnMut(StateId)) {
524        let indexes = self.bottom_up_indexes();
525        let results = match children.len() {
526            0 => indexes.nullary.get(&f),
527            1 => indexes.unary.get(&(f, children[0])),
528            2 => indexes.binary.get(&(f, children[0], children[1])),
529            _ => Self::lookup_higher(indexes, f, children),
530        };
531        if let Some(results) = results {
532            for &q in results {
533                out(q);
534            }
535        }
536    }
537
538    fn is_accepting(&self, q: &StateId) -> bool {
539        !q.is_stuck() && self.accepting.contains(q.index())
540    }
541}
542
543impl DetBottomUpTa for Explicit {
544    fn step_det(&self, f: Symbol, children: &[StateId]) -> Option<StateId> {
545        let indexes = self.bottom_up_indexes();
546        let results = match children.len() {
547            0 => indexes.nullary.get(&f),
548            1 => indexes.unary.get(&(f, children[0])),
549            2 => indexes.binary.get(&(f, children[0], children[1])),
550            _ => Self::lookup_higher(indexes, f, children),
551        }?;
552        (results.len() == 1).then_some(results[0])
553    }
554}
555
556impl IndexedBottomUpTa for Explicit {
557    fn step_partial(
558        &self,
559        f: Symbol,
560        position: usize,
561        state_at_position: &StateId,
562        out: &mut dyn FnMut(&[StateId], StateId),
563    ) {
564        let Some(rule_indexes) = self
565            .indexes()
566            .by_child
567            .get(&(f, position, *state_at_position))
568        else {
569            return;
570        };
571
572        for &rule_idx in rule_indexes {
573            let rule = &self.rules[rule_idx];
574            out(&rule.children, rule.result);
575        }
576    }
577}
578
579impl TopDownTa for Explicit {
580    fn step_topdown(&self, parent: &StateId, out: &mut dyn FnMut(Symbol, &[StateId])) {
581        if parent.is_stuck() {
582            return;
583        }
584        let Some(rule_indexes) = self.result_index().get(parent.index()) else {
585            return;
586        };
587        for &rule_idx in rule_indexes {
588            let rule = &self.rules[rule_idx];
589            out(rule.symbol, &rule.children);
590        }
591    }
592
593    fn initial_states(&self, out: &mut dyn FnMut(StateId)) {
594        for idx in self.accepting.ones() {
595            out(StateId(idx as u32));
596        }
597    }
598}
599
600impl StateUniverse for Explicit {
601    fn all_states(&self, out: &mut dyn FnMut(StateId)) {
602        for idx in 0..self.num_states {
603            out(StateId(idx));
604        }
605    }
606}
607
608impl CondensedTa for Explicit {
609    fn condensed_rules(&self, out: &mut dyn FnMut(&[StateId], &SymbolSet, StateId)) {
610        for rule in self.condensed_cache() {
611            out(&rule.children, &rule.symbols, rule.result);
612        }
613    }
614
615    fn condensed_nullary_rules(&self, out: &mut dyn FnMut(&SymbolSet, StateId)) {
616        for rule in self.condensed_cache() {
617            if rule.children.is_empty() {
618                out(&rule.symbols, rule.result);
619            }
620        }
621    }
622
623    fn condensed_rules_by_child(
624        &self,
625        position: usize,
626        state: &StateId,
627        out: &mut dyn FnMut(&[StateId], &SymbolSet, StateId),
628    ) {
629        for rule in self.condensed_cache() {
630            if rule.children.get(position) == Some(state) {
631                out(&rule.children, &rule.symbols, rule.result);
632            }
633        }
634    }
635}
636
637impl CondensedTopDownTa for Explicit {
638    fn condensed_rules_by_parent(
639        &self,
640        parent: &StateId,
641        out: &mut dyn FnMut(&SymbolSet, &[StateId]),
642    ) {
643        for rule in self.condensed_cache() {
644            if &rule.result == parent {
645                out(&rule.symbols, &rule.children);
646            }
647        }
648    }
649
650    fn condensed_initial_states(&self, out: &mut dyn FnMut(StateId)) {
651        self.initial_states(out);
652    }
653}
654
655fn push_result(results: &mut Results, q: StateId) {
656    if !results.contains(&q) {
657        results.push(q);
658    }
659}
660
661fn mark_reachable(bits: &mut FixedBitSet, worklist: &mut Vec<StateId>, q: StateId) -> bool {
662    if bits.contains(q.index()) {
663        false
664    } else {
665        bits.set(q.index(), true);
666        worklist.push(q);
667        true
668    }
669}
670
671#[cfg(test)]
672mod tests {
673    use super::*;
674    use crate::BottomUpTa;
675    use std::collections::hash_map::DefaultHasher;
676
677    #[test]
678    fn add_rule_defaults_to_unit_weight() {
679        let mut b = ExplicitBuilder::new();
680        let q = b.new_state();
681        b.add_rule(Symbol(1), vec![], q);
682        let e = b.build();
683        let rule = e.rules().next().unwrap();
684        assert_eq!(rule.weight, 1.0);
685        let mut out = Vec::new();
686        e.step(Symbol(1), &[], &mut |q| out.push(q));
687        assert_eq!(out, vec![q]);
688    }
689
690    #[test]
691    fn add_weighted_rule_stores_weight() {
692        let mut b = ExplicitBuilder::new();
693        let q = b.new_state();
694        b.add_weighted_rule(Symbol(1), vec![], q, 0.25);
695        let e = b.build();
696        let rule = e.rules().next().unwrap();
697        assert_eq!(rule.weight, 0.25);
698    }
699
700    #[test]
701    fn add_weighted_rule_inline_preserves_child_tuple() {
702        let mut b = ExplicitBuilder::new();
703        let left = b.new_state();
704        let right = b.new_state();
705        let parent = b.new_state();
706        b.add_weighted_rule_inline(
707            Symbol(1),
708            SmallVec::from_slice(&[left, right]),
709            parent,
710            0.75,
711        );
712
713        let e = b.build();
714        let rule = e.rules().next().unwrap();
715        assert_eq!(rule.children, &[left, right]);
716        assert_eq!(rule.result, parent);
717        assert_eq!(rule.weight, 0.75);
718    }
719
720    #[test]
721    fn builder_rejects_duplicate_transition_with_same_weight() {
722        let mut b = ExplicitBuilder::new();
723        let q = b.new_state();
724        b.add_weighted_rule(Symbol(1), vec![], q, 0.5);
725        b.add_weighted_rule(Symbol(1), vec![], q, 0.5);
726        assert!(matches!(
727            b.try_build(),
728            Err(ExplicitBuildError::DuplicateTransition { .. })
729        ));
730    }
731
732    #[test]
733    fn builder_rejects_duplicate_transition_with_different_weight() {
734        let mut b = ExplicitBuilder::new();
735        let q = b.new_state();
736        b.add_weighted_rule(Symbol(1), vec![], q, 0.5);
737        b.add_weighted_rule(Symbol(1), vec![], q, 0.75);
738        assert!(matches!(
739            b.try_build(),
740            Err(ExplicitBuildError::DuplicateTransition { .. })
741        ));
742    }
743
744    #[test]
745    fn deterministic_matches_step_for_single_result() {
746        // When only one result state exists for a query, `step_det` must return
747        // it as `Some`, agreeing with `step`.
748        let mut b = ExplicitBuilder::new();
749        let q = b.new_state();
750        b.add_rule(Symbol(1), vec![], q);
751        let e = b.build();
752        assert_eq!(e.step_det(Symbol(1), &[]), Some(q));
753    }
754
755    #[test]
756    fn nondeterministic_step_det_returns_none() {
757        // If two rules share the same symbol and children but have different
758        // results, the automaton is nondeterministic for that query and
759        // `step_det` must return `None`.
760        let mut b = ExplicitBuilder::new();
761        let q0 = b.new_state();
762        let q1 = b.new_state();
763        b.add_rule(Symbol(1), vec![], q0);
764        b.add_rule(Symbol(1), vec![], q1);
765        let e = b.build();
766        assert_eq!(e.step_det(Symbol(1), &[]), None);
767    }
768
769    #[test]
770    fn reachable_saturates_rules() {
771        // Both states must be reachable once the leaf nullary rule fires and
772        // the binary rule's children are satisfied. `is_empty` returns false
773        // because the reachable set includes the accepting state.
774        let mut b = ExplicitBuilder::new();
775        let leaf = b.new_state();
776        let root = b.new_state();
777        b.add_rule(Symbol(0), vec![], leaf);
778        b.add_rule(Symbol(1), vec![leaf, leaf], root);
779        b.add_accepting(root);
780        let e = b.build();
781        let r = e.reachable_states();
782        assert!(r.contains(leaf.index()));
783        assert!(r.contains(root.index()));
784        assert!(!e.is_empty());
785    }
786
787    #[test]
788    fn higher_key_hash_matches_borrowed_tuple() {
789        // The `HigherKey` stored type must hash identically to the borrowed
790        // `(Symbol, &[StateId])` tuple used for allocation-free lookups.
791        // Divergence here would silently break higher-arity rule lookup.
792        let children = [StateId(1), StateId(2), StateId(3)];
793        let key = HigherKey(Symbol(7), Box::from(children));
794        let mut a = DefaultHasher::new();
795        key.hash(&mut a);
796        let mut b = DefaultHasher::new();
797        Symbol(7).hash(&mut b);
798        children.hash(&mut b);
799        assert_eq!(a.finish(), b.finish());
800    }
801
802    #[test]
803    fn higher_arity_lookup_works() {
804        // A ternary rule (arity 3, stored in the `higher` table) must be
805        // reachable via both `step` and `step_det` without allocation.
806        let mut b = ExplicitBuilder::new();
807        let q0 = b.new_state();
808        let q1 = b.new_state();
809        let q2 = b.new_state();
810        let q3 = b.new_state();
811        b.add_rule(Symbol(9), vec![q0, q1, q2], q3);
812        let e = b.build();
813        assert_eq!(e.step_det(Symbol(9), &[q0, q1, q2]), Some(q3));
814    }
815
816    #[test]
817    fn indexed_step_partial_finds_matching_binary_rules() {
818        // Given a known state at position 0, `step_partial` must return only
819        // the rules where that position actually holds that state — not the
820        // rule where position 0 holds a different state.
821        let mut b = ExplicitBuilder::new();
822        let left = b.new_state();
823        let right = b.new_state();
824        let root = b.new_state();
825        let other = b.new_state();
826        b.add_rule(Symbol(3), vec![left, right], root);
827        b.add_rule(Symbol(3), vec![other, right], other);
828        let e = b.build();
829
830        let mut found = Vec::new();
831        e.step_partial(Symbol(3), 0, &left, &mut |children, result| {
832            found.push((children.to_vec(), result));
833        });
834
835        assert_eq!(found, vec![(vec![left, right], root)]);
836    }
837
838    #[test]
839    fn indexed_step_partial_supports_higher_arity_rules() {
840        // `step_partial` must also index rules stored in the `higher` table
841        // (arity ≥ 3). Querying an interior position (1 of 3) must return the
842        // full child tuple and result.
843        let mut b = ExplicitBuilder::new();
844        let q0 = b.new_state();
845        let q1 = b.new_state();
846        let q2 = b.new_state();
847        let q3 = b.new_state();
848        b.add_rule(Symbol(9), vec![q0, q1, q2], q3);
849        let e = b.build();
850
851        let mut found = Vec::new();
852        e.step_partial(Symbol(9), 1, &q1, &mut |children, result| {
853            found.push((children.to_vec(), result));
854        });
855
856        assert_eq!(found, vec![(vec![q0, q1, q2], q3)]);
857    }
858
859    #[test]
860    fn topdown_enumerates_rules_by_parent() {
861        // `step_topdown` must enumerate every rule whose result is the queried
862        // parent state. `initial_states` must yield every accepting state.
863        let mut b = ExplicitBuilder::new();
864        let leaf = b.new_state();
865        let root = b.new_state();
866        b.add_rule(Symbol(0), vec![], leaf);
867        b.add_rule(Symbol(1), vec![leaf, leaf], root);
868        b.add_accepting(root);
869        let e = b.build();
870
871        let mut rules = Vec::new();
872        e.step_topdown(&root, &mut |symbol, children| {
873            rules.push((symbol, children.to_vec()));
874        });
875        let mut initials = Vec::new();
876        e.initial_states(&mut |q| initials.push(q));
877
878        assert_eq!(rules, vec![(Symbol(1), vec![leaf, leaf])]);
879        assert_eq!(initials, vec![root]);
880    }
881
882    #[test]
883    fn bottom_up_indexes_are_built_lazily() {
884        let mut b = ExplicitBuilder::new();
885        let leaf = b.new_state();
886        let root = b.new_state();
887        b.add_rule(Symbol(0), vec![], leaf);
888        b.add_rule(Symbol(1), vec![leaf], root);
889        b.add_accepting(root);
890        let e = b.build();
891
892        assert!(e.bottom_up_indexes.get().is_none());
893
894        let mut topdown_rules = Vec::new();
895        e.step_topdown(&root, &mut |symbol, children| {
896            topdown_rules.push((symbol, children.to_vec()));
897        });
898        assert_eq!(topdown_rules, vec![(Symbol(1), vec![leaf])]);
899        assert!(e.bottom_up_indexes.get().is_none());
900
901        let best = e.viterbi().unwrap();
902        assert_eq!(*best.arena().get_label(best.root()), Symbol(1));
903        assert!(e.bottom_up_indexes.get().is_none());
904
905        let mut leaves = Vec::new();
906        e.step(Symbol(0), &[], &mut |q| leaves.push(q));
907        assert_eq!(leaves, vec![leaf]);
908        assert!(e.bottom_up_indexes.get().is_some());
909    }
910
911    // Indexed access and iteration must yield identical Rule values in the same order.
912    #[test]
913    fn indexed_rule_access_matches_iteration() {
914        let mut b = ExplicitBuilder::new();
915        let q = b.new_state();
916        let r = b.new_state();
917        b.add_rule(Symbol(0), vec![], q);
918        b.add_rule(Symbol(1), vec![q], r);
919        b.add_accepting(r);
920        let a = b.build();
921
922        assert_eq!(a.num_rules(), 2);
923        for (i, rule) in a.rules().enumerate() {
924            assert_eq!(a.rule(i), rule);
925        }
926    }
927
928    #[test]
929    fn condensed_rules_groups_symbols_by_shape() {
930        // Two symbols with identical (children, result) should appear together
931        // in one condensed rule. A third symbol with a different children tuple
932        // must appear in a separate group. Every rule must be covered exactly once.
933        let mut b = ExplicitBuilder::new();
934        let q0 = b.new_state();
935        let q1 = b.new_state();
936        let qr = b.new_state();
937        // sym(0) and sym(1) both map (q0, q1) -> qr
938        b.add_rule(Symbol(0), vec![q0, q1], qr);
939        b.add_rule(Symbol(1), vec![q0, q1], qr);
940        // sym(2) maps (q1, q0) -> qr  (different children order)
941        b.add_rule(Symbol(2), vec![q1, q0], qr);
942        let e = b.build();
943
944        let mut groups: Vec<(Vec<StateId>, SymbolSet, StateId)> = Vec::new();
945        e.condensed_rules(&mut |children, sym_set, result| {
946            groups.push((children.to_vec(), sym_set.clone(), result));
947        });
948
949        // Find the group for (q0, q1) -> qr and verify both symbols are present.
950        let shared = groups
951            .iter()
952            .find(|(c, _, _)| c.as_slice() == [q0, q1])
953            .expect("group (q0,q1)->qr must exist");
954        assert!(shared.1.contains(Symbol(0)));
955        assert!(shared.1.contains(Symbol(1)));
956        assert_eq!(shared.2, qr);
957
958        // The (q1, q0) group must exist separately with only sym(2).
959        let solo = groups
960            .iter()
961            .find(|(c, _, _)| c.as_slice() == [q1, q0])
962            .expect("group (q1,q0)->qr must exist");
963        assert!(solo.1.contains(Symbol(2)));
964        assert_eq!(solo.1.len(), 1);
965    }
966}