Skip to main content

rusty_alto/
traits.rs

1//! Core oracle traits for bottom-up, top-down, indexed, and condensed automata.
2
3use crate::Symbol;
4use smallvec::SmallVec;
5use std::hash::Hash;
6
7/// A bottom-up tree automaton queried as an oracle.
8///
9/// Implement this trait when you want the library to run or combine your
10/// automaton. The method [`BottomUpTa::step`] receives a node symbol and the
11/// states already assigned to the node's children. It reports every possible
12/// state for the parent by calling the callback.
13///
14/// Implementations may be explicit table lookups, like [`crate::Explicit`], or
15/// implicit computations, such as a type checker or derivative construction.
16/// `step` should behave like a pure function: the same symbol and child states
17/// should produce the same parent states, without duplicates.
18pub trait BottomUpTa {
19    /// State type carried by the automaton.
20    ///
21    /// Rich implicit automata can use application-level states here. Wrap them
22    /// in [`crate::Memo`] when a dense [`crate::StateId`] representation is
23    /// needed. Decomposition automata used for IRTG chart construction should
24    /// also implement [`std::fmt::Display`] for this type. The display form is
25    /// exposed as one structured component of
26    /// [`crate::ParseChart::state_parts`], so it should be concise,
27    /// deterministic, meaningful to users, and free of unstable internal IDs.
28    type State: Clone + Eq + Hash;
29
30    /// Report all possible parent states for `f(children...)`.
31    ///
32    /// Call `out(q)` once for each valid result state `q`. If no rule applies,
33    /// do not call `out`. The order is not specified, but duplicate states
34    /// should not be emitted.
35    fn step(&self, f: Symbol, children: &[Self::State], out: &mut dyn FnMut(Self::State));
36
37    /// Return whether `q` is an accepting state.
38    ///
39    /// A tree is accepted when the root receives at least one accepting state.
40    fn is_accepting(&self, q: &Self::State) -> bool;
41}
42
43impl<A: BottomUpTa + ?Sized> BottomUpTa for &A {
44    type State = A::State;
45
46    fn step(&self, f: Symbol, children: &[Self::State], out: &mut dyn FnMut(Self::State)) {
47        (**self).step(f, children, out);
48    }
49
50    fn is_accepting(&self, q: &Self::State) -> bool {
51        (**self).is_accepting(q)
52    }
53}
54
55/// Faster interface for deterministic bottom-up automata.
56///
57/// Deterministic automata have at most one parent state for each symbol and
58/// child-state tuple. Implementing this trait lets [`crate::run_det`] avoid
59/// allocating state sets and avoid callback overhead.
60pub trait DetBottomUpTa: BottomUpTa {
61    /// Return the unique result state, or `None` if no transition exists.
62    ///
63    /// This method must agree with [`BottomUpTa::step`]: if it returns
64    /// `Some(q)`, then `step` should emit exactly `q`; if it returns `None`,
65    /// then `step` should emit no states.
66    fn step_det(&self, f: Symbol, children: &[Self::State]) -> Option<Self::State>;
67
68    /// Group key for symbols that share a transition function.
69    ///
70    /// Any two symbols returning the same `det_group` value must yield the same
71    /// [`step_det`](DetBottomUpTa::step_det) result for every child tuple. The
72    /// default puts each symbol in its own group (`f.0`), i.e. no sharing.
73    /// Condensed automata override this so a caller can compute one transition
74    /// per group and reuse it for every symbol in the group (e.g. `InvHom`, whose
75    /// transition depends only on the image term shared by a whole symbol set).
76    fn det_group(&self, f: Symbol) -> u32 {
77        f.0
78    }
79}
80
81impl<A: DetBottomUpTa + ?Sized> DetBottomUpTa for &A {
82    fn step_det(&self, f: Symbol, children: &[Self::State]) -> Option<Self::State> {
83        (**self).step_det(f, children)
84    }
85
86    fn det_group(&self, f: Symbol) -> u32 {
87        (**self).det_group(f)
88    }
89}
90
91/// Finite enumeration of an automaton's state space.
92///
93/// Most oracle-style automata only need to answer local transition queries.
94/// Some complete algorithms, such as condensed inverse homomorphism for a bare
95/// variable image, also need to enumerate every possible inner state. Keep this
96/// as a separate refinement so infinite or very large implicit automata can
97/// still implement [`BottomUpTa`] without promising finite enumeration.
98pub trait StateUniverse: BottomUpTa {
99    /// Report every state in the finite universe exactly once.
100    fn all_states(&self, out: &mut dyn FnMut(Self::State));
101}
102
103impl<A: StateUniverse + ?Sized> StateUniverse for &A {
104    fn all_states(&self, out: &mut dyn FnMut(Self::State)) {
105        (**self).all_states(out);
106    }
107}
108
109/// Indexed bottom-up rule enumeration for sibling-finder-style joins.
110///
111/// [`BottomUpTa::step`] answers a complete transition query. This refinement
112/// answers a partial query: given a symbol, child position, and state at that
113/// position, enumerate the full rules that match. Product and parsing
114/// algorithms can use this to join compatible rules without enumerating child
115/// tuples that never occur in either component.
116pub trait IndexedBottomUpTa: BottomUpTa {
117    /// Report every rule `f(children...) -> q` where `children[position]`
118    /// equals `state_at_position`.
119    ///
120    /// `children` is borrowed from the implementation and is valid only for
121    /// the callback. Implementations must not emit duplicate rules.
122    #[allow(clippy::type_complexity)]
123    fn step_partial(
124        &self,
125        f: Symbol,
126        position: usize,
127        state_at_position: &Self::State,
128        out: &mut dyn FnMut(&[Self::State], Self::State),
129    );
130}
131
132impl<A: IndexedBottomUpTa + ?Sized> IndexedBottomUpTa for &A {
133    fn step_partial(
134        &self,
135        f: Symbol,
136        position: usize,
137        state_at_position: &Self::State,
138        out: &mut dyn FnMut(&[Self::State], Self::State),
139    ) {
140        (**self).step_partial(f, position, state_at_position, out);
141    }
142}
143
144/// Optional top-down view of a bottom-up automaton.
145///
146/// Not every automaton can enumerate rules by parent state. Implement this
147/// refinement when algorithms need to ask: given a parent state, which symbols
148/// and child-state tuples can produce it?
149pub trait TopDownTa: BottomUpTa {
150    /// Report every rule `f(children...) -> parent`.
151    ///
152    /// `children` is borrowed from the implementation and is valid only for
153    /// the callback.
154    fn step_topdown(&self, parent: &Self::State, out: &mut dyn FnMut(Symbol, &[Self::State]));
155
156    /// Report the initial states of the top-down view.
157    ///
158    /// For bottom-up automata these are exactly the accepting states.
159    fn initial_states(&self, out: &mut dyn FnMut(Self::State));
160}
161
162impl<A: TopDownTa + ?Sized> TopDownTa for &A {
163    fn step_topdown(&self, parent: &Self::State, out: &mut dyn FnMut(Symbol, &[Self::State])) {
164        (**self).step_topdown(parent, out);
165    }
166
167    fn initial_states(&self, out: &mut dyn FnMut(Self::State)) {
168        (**self).initial_states(out);
169    }
170}
171
172/// Compact sorted set of symbols, used by [`CondensedTa`] to group rules.
173///
174/// Internally a sorted, deduplicated inline vector. Lookup is O(log n) via
175/// binary search. Most label sets are tiny (1–4 symbols).
176#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
177pub struct SymbolSet(SmallVec<[Symbol; 4]>);
178
179impl SymbolSet {
180    /// Create an empty set.
181    #[inline]
182    pub fn new() -> Self {
183        Self::default()
184    }
185
186    /// Insert a symbol, maintaining sorted and deduplicated order.
187    #[inline]
188    pub fn insert(&mut self, s: Symbol) {
189        match self.0.binary_search(&s) {
190            Ok(_) => {}
191            Err(idx) => self.0.insert(idx, s),
192        }
193    }
194
195    /// Return whether the set contains `s`.
196    #[inline]
197    pub fn contains(&self, s: Symbol) -> bool {
198        self.0.binary_search(&s).is_ok()
199    }
200
201    /// Iterate over symbols in sorted order.
202    #[inline]
203    pub fn iter(&self) -> impl Iterator<Item = Symbol> + '_ {
204        self.0.iter().copied()
205    }
206
207    /// Return the number of symbols.
208    #[inline]
209    pub fn len(&self) -> usize {
210        self.0.len()
211    }
212
213    /// Return whether the set is empty.
214    #[inline]
215    pub fn is_empty(&self) -> bool {
216        self.0.is_empty()
217    }
218
219    /// Remove all symbols from the set.
220    #[inline]
221    pub fn clear(&mut self) {
222        self.0.clear();
223    }
224}
225
226impl FromIterator<Symbol> for SymbolSet {
227    fn from_iter<I: IntoIterator<Item = Symbol>>(iter: I) -> Self {
228        let mut set = Self::new();
229        for s in iter {
230            set.insert(s);
231        }
232        set
233    }
234}
235
236/// Refinement of [`BottomUpTa`] that enumerates rules grouped by transition shape.
237///
238/// For each distinct `(children, result)` pair that occurs in any rule, the
239/// automaton collects all symbols `f` such that `f(children) -> result` into a
240/// [`SymbolSet`] and emits them together.
241///
242/// The main use case is efficient materialization when multiple source symbols
243/// share the same homomorphic image: rather than invoking the term evaluator
244/// once per symbol, a single evaluation covers the entire label set.
245pub trait CondensedTa: BottomUpTa {
246    /// Enumerate every distinct `(children, result)` pair, reporting the set of
247    /// symbols that have that shape.
248    ///
249    /// Each shape is emitted exactly once. `children` is borrowed from the
250    /// implementation; the slice is only valid for the duration of the callback.
251    #[allow(clippy::type_complexity)]
252    fn condensed_rules(&self, out: &mut dyn FnMut(&[Self::State], &SymbolSet, Self::State));
253
254    /// Enumerate condensed nullary rules.
255    ///
256    /// The default implementation filters [`CondensedTa::condensed_rules`].
257    /// Implementations on hot paths should override this when nullary rules can
258    /// be produced or indexed more cheaply than the full condensed relation.
259    fn condensed_nullary_rules(&self, out: &mut dyn FnMut(&SymbolSet, Self::State)) {
260        self.condensed_rules(&mut |children, symbols, result| {
261            if children.is_empty() {
262                out(symbols, result);
263            }
264        });
265    }
266
267    /// Enumerate condensed rules whose child at `position` is `state`.
268    ///
269    /// The default implementation filters [`CondensedTa::condensed_rules`].
270    /// Implementations on hot paths should override this to avoid materializing
271    /// or scanning unrelated condensed rules.
272    #[allow(clippy::type_complexity)]
273    fn condensed_rules_by_child(
274        &self,
275        position: usize,
276        state: &Self::State,
277        out: &mut dyn FnMut(&[Self::State], &SymbolSet, Self::State),
278    ) {
279        self.condensed_rules(&mut |children, symbols, result| {
280            if children.get(position) == Some(state) {
281                out(children, symbols, result);
282            }
283        });
284    }
285}
286
287/// Optional parent-indexed view of a condensed automaton.
288///
289/// This is the condensed analogue of [`TopDownTa`]: given a parent state,
290/// enumerate all outgoing rule shapes, grouping all compatible symbols in a
291/// [`SymbolSet`]. Implementations should stream results and avoid materializing
292/// the whole condensed relation.
293pub trait CondensedTopDownTa: BottomUpTa {
294    /// Report every condensed rule `symbols(children...) -> parent`.
295    ///
296    /// `children` and `symbols` are borrowed from the implementation and are
297    /// valid only for the duration of the callback.
298    fn condensed_rules_by_parent(
299        &self,
300        parent: &Self::State,
301        out: &mut dyn FnMut(&SymbolSet, &[Self::State]),
302    );
303
304    /// Report initial states of the top-down view.
305    fn condensed_initial_states(&self, out: &mut dyn FnMut(Self::State));
306}
307
308impl<A: CondensedTopDownTa + ?Sized> CondensedTopDownTa for &A {
309    fn condensed_rules_by_parent(
310        &self,
311        parent: &Self::State,
312        out: &mut dyn FnMut(&SymbolSet, &[Self::State]),
313    ) {
314        (**self).condensed_rules_by_parent(parent, out);
315    }
316
317    fn condensed_initial_states(&self, out: &mut dyn FnMut(Self::State)) {
318        (**self).condensed_initial_states(out);
319    }
320}