Skip to main content

rusty_alto/
application.rs

1//! Stable, owned presentation types for desktop and web frontends.
2//!
3//! The core algorithms deliberately expose compact IDs and borrowed trees.
4//! Frontends usually need the inverse trade-off: resolved names and owned
5//! values that can safely cross worker-thread and event-loop boundaries.
6
7use crate::{
8    AstarHeuristic, AstarOptions, BottomUpTa, EvaluatedAlgebraValue, Explicit, Irtg, IrtgError,
9    MaterializationStrategy, StateId, Symbol, TreeValue,
10};
11use packed_term_arena::{
12    parser::parse_tree,
13    tree::{Tree, TreeArena},
14};
15
16/// A read-only summary of an explicit automaton.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct AutomatonSummary {
19    /// Number of transition rules.
20    pub rule_count: usize,
21    /// Number of allocated states.
22    pub state_count: u32,
23    /// Largest child count of any rule.
24    pub maximum_rank: usize,
25    /// Whether the accepted language is empty.
26    pub is_empty: bool,
27}
28
29/// Cardinality of an explicit automaton's derivation-tree language.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31pub enum LanguageCardinality {
32    /// The language contains exactly this many derivation trees.
33    Finite(usize),
34    /// A productive cycle makes the language infinite.
35    Infinite,
36    /// The finite count exceeds the platform's `usize` range.
37    TooLarge,
38}
39
40/// Metadata about one interpretation.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct InterpretationInfo {
43    /// Interpretation name used in grammar and corpus files.
44    pub name: String,
45    /// Declared Alto algebra class name.
46    pub class_name: String,
47    /// Whether this interpretation can constrain parsing input.
48    pub input_capable: bool,
49    /// Whether this interpretation can require evaluation to produce a value.
50    pub non_null_filter_capable: bool,
51}
52
53/// One resolved automaton rule suitable for tables and serialization.
54#[derive(Clone, Debug, PartialEq)]
55pub struct ResolvedRule {
56    /// Human-readable parent state.
57    pub parent: String,
58    /// Whether the parent state is accepting.
59    pub parent_is_final: bool,
60    /// Human-readable rule symbol.
61    pub symbol: String,
62    /// Human-readable child states in order.
63    pub children: Vec<String>,
64    /// Rule weight.
65    pub weight: f64,
66    /// Homomorphic image term for each interpretation.
67    pub interpretation_terms: Vec<(String, String)>,
68}
69
70/// An owned interpretation result.
71#[derive(Debug)]
72pub enum RenderedValue {
73    /// A scalar or otherwise non-tree textual value.
74    Text(String),
75    /// A structured tree value retaining its arena.
76    Tree(TreeValue),
77}
78
79/// One named result of interpreting a derivation.
80#[derive(Debug)]
81pub struct RenderedInterpretation {
82    /// Interpretation name.
83    pub name: String,
84    /// Evaluated value.
85    pub value: RenderedValue,
86}
87
88/// One named, evaluated interpretation retaining visualization and codec dispatch.
89#[derive(Debug)]
90pub struct EvaluatedInterpretation {
91    /// Interpretation name.
92    pub name: String,
93    /// Evaluated algebra value.
94    pub value: EvaluatedAlgebraValue,
95}
96
97/// Frontend-friendly parsing strategy without borrowed heuristic tables.
98#[derive(Clone, Copy, Debug, PartialEq)]
99pub enum ParseStrategy {
100    /// Parent-driven condensed intersection.
101    TopDownCondensed,
102    /// Child-indexed condensed intersection.
103    IndexedCondensed,
104    /// Generic A* with the zero heuristic.
105    AstarZero {
106        /// Stop once the highest-ranked complete derivation is found.
107        stop_at_first_goal: bool,
108        /// Optional merit threshold.
109        beam: Option<f64>,
110    },
111}
112
113impl ParseStrategy {
114    /// Convert this owned frontend choice into the core strategy type.
115    pub fn materialization_strategy(&self) -> MaterializationStrategy<'static> {
116        match *self {
117            Self::TopDownCondensed => MaterializationStrategy::TopDownCondensed,
118            Self::IndexedCondensed => MaterializationStrategy::IndexedCondensed,
119            Self::AstarZero {
120                stop_at_first_goal,
121                beam,
122            } => MaterializationStrategy::Astar {
123                heuristic: AstarHeuristic::Zero,
124                options: AstarOptions {
125                    stop_at_first_goal,
126                    beam,
127                },
128            },
129        }
130    }
131}
132
133impl Explicit {
134    /// Return an owned summary without exposing internal indexes.
135    pub fn application_summary(&self) -> AutomatonSummary {
136        AutomatonSummary {
137            rule_count: self.rules().count(),
138            state_count: self.num_states(),
139            maximum_rank: self
140                .rules()
141                .map(|rule| rule.children.len())
142                .max()
143                .unwrap_or(0),
144            is_empty: self.is_empty(),
145        }
146    }
147
148    /// Return the number of accepted derivation trees without enumerating them.
149    pub fn language_cardinality(&self) -> LanguageCardinality {
150        let state_count = self.num_states() as usize;
151        let rules = self.rules().collect::<Vec<_>>();
152        let mut productive = vec![false; state_count];
153        let mut changed = true;
154        while changed {
155            changed = false;
156            for rule in &rules {
157                if !productive[rule.result.index()]
158                    && rule.children.iter().all(|child| productive[child.index()])
159                {
160                    productive[rule.result.index()] = true;
161                    changed = true;
162                }
163            }
164        }
165
166        let mut relevant = vec![false; state_count];
167        let mut stack = (0..state_count)
168            .map(|index| StateId(index as u32))
169            .filter(|state| self.is_accepting(state) && productive[state.index()])
170            .collect::<Vec<_>>();
171        while let Some(state) = stack.pop() {
172            if std::mem::replace(&mut relevant[state.index()], true) {
173                continue;
174            }
175            for rule in rules.iter().filter(|rule| rule.result == state) {
176                if rule.children.iter().all(|child| productive[child.index()]) {
177                    stack.extend(rule.children.iter().copied());
178                }
179            }
180        }
181
182        fn has_cycle(
183            state: StateId,
184            rules: &[crate::Rule<'_>],
185            productive: &[bool],
186            relevant: &[bool],
187            colors: &mut [u8],
188        ) -> bool {
189            colors[state.index()] = 1;
190            for child in rules
191                .iter()
192                .filter(|rule| {
193                    rule.result == state
194                        && rule.children.iter().all(|child| productive[child.index()])
195                })
196                .flat_map(|rule| rule.children.iter().copied())
197                .filter(|child| relevant[child.index()])
198            {
199                if colors[child.index()] == 1
200                    || (colors[child.index()] == 0
201                        && has_cycle(child, rules, productive, relevant, colors))
202                {
203                    return true;
204                }
205            }
206            colors[state.index()] = 2;
207            false
208        }
209
210        let mut colors = vec![0; state_count];
211        for index in 0..state_count {
212            if relevant[index]
213                && colors[index] == 0
214                && has_cycle(
215                    StateId(index as u32),
216                    &rules,
217                    &productive,
218                    &relevant,
219                    &mut colors,
220                )
221            {
222                return LanguageCardinality::Infinite;
223            }
224        }
225
226        fn count_state(
227            state: StateId,
228            rules: &[crate::Rule<'_>],
229            productive: &[bool],
230            memo: &mut [Option<Option<usize>>],
231        ) -> Option<usize> {
232            if let Some(count) = memo[state.index()] {
233                return count;
234            }
235            let mut total = 0usize;
236            for rule in rules.iter().filter(|rule| {
237                rule.result == state && rule.children.iter().all(|child| productive[child.index()])
238            }) {
239                let mut combinations = 1usize;
240                for &child in rule.children {
241                    combinations =
242                        combinations.checked_mul(count_state(child, rules, productive, memo)?)?;
243                }
244                total = total.checked_add(combinations)?;
245            }
246            memo[state.index()] = Some(Some(total));
247            Some(total)
248        }
249
250        let mut memo = vec![None; state_count];
251        let mut total = 0usize;
252        for index in 0..state_count {
253            let state = StateId(index as u32);
254            if self.is_accepting(&state) && productive[index] {
255                let Some(count) = count_state(state, &rules, &productive, &mut memo) else {
256                    return LanguageCardinality::TooLarge;
257                };
258                let Some(next) = total.checked_add(count) else {
259                    return LanguageCardinality::TooLarge;
260                };
261                total = next;
262            }
263        }
264        LanguageCardinality::Finite(total)
265    }
266
267    /// Resolve rules with caller-supplied state and symbol naming.
268    pub fn resolve_rules(
269        &self,
270        mut state_name: impl FnMut(StateId) -> String,
271        mut symbol_name: impl FnMut(Symbol) -> String,
272    ) -> Vec<ResolvedRule> {
273        self.rules()
274            .map(|rule| ResolvedRule {
275                parent: state_name(rule.result),
276                parent_is_final: self.is_accepting(&rule.result),
277                symbol: symbol_name(rule.symbol),
278                children: rule.children.iter().copied().map(&mut state_name).collect(),
279                weight: rule.weight,
280                interpretation_terms: Vec::new(),
281            })
282            .collect()
283    }
284
285    /// Resolve a single rule by index with caller-supplied naming.
286    pub fn resolved_rule(
287        &self,
288        index: usize,
289        mut state_name: impl FnMut(StateId) -> String,
290        mut symbol_name: impl FnMut(Symbol) -> String,
291    ) -> ResolvedRule {
292        let rule = self.rule(index);
293        ResolvedRule {
294            parent: state_name(rule.result),
295            parent_is_final: self.is_accepting(&rule.result),
296            symbol: symbol_name(rule.symbol),
297            children: rule.children.iter().copied().map(&mut state_name).collect(),
298            weight: rule.weight,
299            interpretation_terms: Vec::new(),
300        }
301    }
302}
303
304impl Irtg {
305    /// Return sorted interpretation metadata for deterministic presentation.
306    pub fn interpretation_info(&self) -> Vec<InterpretationInfo> {
307        let mut values: Vec<_> = self
308            .interpretations()
309            .map(|interpretation| InterpretationInfo {
310                name: interpretation.name().to_owned(),
311                class_name: interpretation.class_name().to_owned(),
312                input_capable: interpretation.is_inputable(),
313                non_null_filter_capable: interpretation.supports_non_null_filter(),
314            })
315            .collect();
316        values.sort_by(|a, b| a.name.cmp(&b.name));
317        values
318    }
319
320    /// Resolve grammar rules, including each interpretation's homomorphic term.
321    pub fn resolved_grammar_rules(&self) -> Vec<ResolvedRule> {
322        let mut interpretations: Vec<_> = self.interpretations().collect();
323        interpretations.sort_by_key(|interpretation| interpretation.name());
324
325        self.grammar()
326            .rules()
327            .map(|rule| {
328                let interpretation_terms = interpretations
329                    .iter()
330                    .map(|interpretation| {
331                        let text = interpretation
332                            .homomorphism()
333                            .get(rule.symbol)
334                            .map(|root| {
335                                format_hom_term(
336                                    interpretation.homomorphism().arena(),
337                                    root,
338                                    interpretation.algebra_signature(),
339                                )
340                            })
341                            .unwrap_or_default();
342                        (interpretation.name().to_owned(), text)
343                    })
344                    .collect();
345
346                ResolvedRule {
347                    parent: self.states().resolve(rule.result).clone(),
348                    parent_is_final: self.grammar().is_accepting(&rule.result),
349                    symbol: self.grammar_signature().resolve(rule.symbol).to_owned(),
350                    children: rule
351                        .children
352                        .iter()
353                        .map(|&state| self.states().resolve(state).clone())
354                        .collect(),
355                    weight: rule.weight,
356                    interpretation_terms,
357                }
358            })
359            .collect()
360    }
361
362    /// Number of grammar rules.
363    pub fn num_grammar_rules(&self) -> usize {
364        self.grammar().num_rules()
365    }
366
367    /// Resolve a single grammar rule by index, including interpretation terms.
368    ///
369    /// Intended for on-demand, per-row resolution (e.g. a virtualized table).
370    /// To resolve every rule, prefer [`Self::resolved_grammar_rules`], which
371    /// sorts interpretations once instead of per call.
372    pub fn resolved_grammar_rule(&self, index: usize) -> ResolvedRule {
373        let mut interpretations: Vec<_> = self.interpretations().collect();
374        interpretations.sort_by_key(|interpretation| interpretation.name());
375
376        let rule = self.grammar().rule(index);
377        let interpretation_terms = interpretations
378            .iter()
379            .map(|interpretation| {
380                let text = interpretation
381                    .homomorphism()
382                    .get(rule.symbol)
383                    .map(|root| {
384                        format_hom_term(
385                            interpretation.homomorphism().arena(),
386                            root,
387                            interpretation.algebra_signature(),
388                        )
389                    })
390                    .unwrap_or_default();
391                (interpretation.name().to_owned(), text)
392            })
393            .collect();
394
395        ResolvedRule {
396            parent: self.states().resolve(rule.result).clone(),
397            parent_is_final: self.grammar().is_accepting(&rule.result),
398            symbol: self.grammar_signature().resolve(rule.symbol).to_owned(),
399            children: rule
400                .children
401                .iter()
402                .map(|&state| self.states().resolve(state).clone())
403                .collect(),
404            weight: rule.weight,
405            interpretation_terms,
406        }
407    }
408
409    /// Resolve a derivation tree to an owned string-labelled tree.
410    pub fn resolve_derivation(&self, arena: &TreeArena<Symbol>, root: Tree) -> TreeValue {
411        let (arena, root) = self.grammar_signature().resolve_tree(arena, root);
412        TreeValue::new(arena, root)
413    }
414
415    /// Evaluate all interpretations and retain tree structure where available.
416    pub fn render_derivation(
417        &self,
418        arena: &TreeArena<Symbol>,
419        root: Tree,
420    ) -> Result<Vec<RenderedInterpretation>, IrtgError> {
421        let mut interpretations: Vec<_> = self.interpretations().collect();
422        interpretations.sort_by_key(|interpretation| interpretation.name());
423
424        interpretations
425            .into_iter()
426            .map(|interpretation| {
427                let text = interpretation.interpret_to_string(arena, root)?;
428                let value = if interpretation.is_tree_valued() {
429                    let mut tree_arena = TreeArena::new();
430                    match parse_tree(&mut tree_arena, &text) {
431                        Ok(tree_root) => RenderedValue::Tree(TreeValue::new(tree_arena, tree_root)),
432                        Err(_) => RenderedValue::Text(text),
433                    }
434                } else {
435                    RenderedValue::Text(text)
436                };
437                Ok(RenderedInterpretation {
438                    name: interpretation.name().to_owned(),
439                    value,
440                })
441            })
442            .collect()
443    }
444
445    /// Evaluate every interpretation without flattening its public value type.
446    pub fn evaluate_derivation(
447        &self,
448        arena: &TreeArena<Symbol>,
449        root: Tree,
450    ) -> Result<Vec<EvaluatedInterpretation>, IrtgError> {
451        let mut interpretations: Vec<_> = self.interpretations().collect();
452        interpretations.sort_by_key(|interpretation| interpretation.name());
453        interpretations
454            .into_iter()
455            .map(|interpretation| {
456                Ok(EvaluatedInterpretation {
457                    name: interpretation.name().to_owned(),
458                    value: interpretation.evaluate_derivation(arena, root)?,
459                })
460            })
461            .collect()
462    }
463}
464
465fn format_hom_term(
466    arena: &TreeArena<crate::HomLabel>,
467    node: Tree,
468    signature: &crate::Signature,
469) -> String {
470    let label = match arena.get_label(node) {
471        crate::HomLabel::Symbol(symbol) => signature.resolve(*symbol).to_owned(),
472        crate::HomLabel::Var(index) => format!("?{}", index + 1),
473    };
474    let children = arena.get_children(node);
475    if children.is_empty() {
476        label
477    } else {
478        let children = children
479            .iter()
480            .map(|&child| format_hom_term(arena, child, signature))
481            .collect::<Vec<_>>()
482            .join(", ");
483        format!("{label}({children})")
484    }
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use crate::parse_irtg;
491
492    const GRAMMAR: &str = r#"
493interpretation string: de.up.ling.irtg.algebra.StringAlgebra
494interpretation tree: de.up.ling.irtg.algebra.TreeWithAritiesAlgebra
495
496S! -> r(NP, VP) [0.7]
497  [string] *(?1, ?2)
498  [tree] S_2(?1, ?2)
499
500NP -> john [1.0]
501  [string] john
502  [tree] John_0
503
504VP -> sleeps [1.0]
505  [string] sleeps
506  [tree] sleeps_0
507"#;
508
509    #[test]
510    fn resolves_single_explicit_rule() {
511        let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
512        let g = irtg.grammar();
513        let all = g.resolve_rules(
514            |s| format!("q{}", s.index()),
515            |sym| irtg.grammar_signature().resolve(sym).to_owned(),
516        );
517        for (i, expected) in all.iter().enumerate() {
518            let one = g.resolved_rule(
519                i,
520                |s| format!("q{}", s.index()),
521                |sym| irtg.grammar_signature().resolve(sym).to_owned(),
522            );
523            assert_eq!(&one, expected);
524        }
525    }
526
527    #[test]
528    fn resolves_application_records() {
529        let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
530        let summary = irtg.grammar().application_summary();
531        assert_eq!(summary.rule_count, 3);
532        assert_eq!(summary.maximum_rank, 2);
533        let rules = irtg.resolved_grammar_rules();
534        assert_eq!(rules[0].parent, "S");
535        assert!(rules[0].parent_is_final);
536        assert_eq!(rules[0].interpretation_terms[0].0, "string");
537    }
538
539    #[test]
540    fn strategy_conversion_is_stable() {
541        assert!(matches!(
542            ParseStrategy::IndexedCondensed.materialization_strategy(),
543            MaterializationStrategy::IndexedCondensed
544        ));
545        let strategy = ParseStrategy::AstarZero {
546            stop_at_first_goal: true,
547            beam: Some(0.2),
548        }
549        .materialization_strategy();
550        assert!(matches!(strategy, MaterializationStrategy::Astar { .. }));
551    }
552
553    #[test]
554    fn resolves_single_grammar_rule() {
555        let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
556        let all = irtg.resolved_grammar_rules();
557        assert_eq!(irtg.num_grammar_rules(), all.len());
558        for (i, expected) in all.iter().enumerate() {
559            assert_eq!(&irtg.resolved_grammar_rule(i), expected);
560        }
561    }
562
563    #[test]
564    fn preserves_tree_results() {
565        let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
566        let string = irtg
567            .interpretation_ref("string")
568            .unwrap()
569            .parse_object_erased("john sleeps")
570            .unwrap();
571        let chart = irtg
572            .parse([irtg
573                .interpretation_ref("string")
574                .unwrap()
575                .input_erased(string)])
576            .unwrap();
577        assert!(chart.state_names.iter().any(|name| name == "NP[0-1]"));
578        assert!(chart.state_names.iter().any(|name| name == "VP[1-2]"));
579        assert!(chart.state_names.iter().any(|name| name == "S[0-2]"));
580        let best = chart.automaton.viterbi().unwrap();
581        let rendered = irtg.render_derivation(best.arena(), best.root()).unwrap();
582        assert!(matches!(rendered[0].value, RenderedValue::Text(_)));
583        assert!(matches!(rendered[1].value, RenderedValue::Tree(_)));
584
585        let evaluated = irtg.evaluate_derivation(best.arena(), best.root()).unwrap();
586        assert!(matches!(
587            evaluated[0].value.visual(),
588            crate::VisualRepresentation::Text(text) if text == "john sleeps"
589        ));
590        assert!(matches!(
591            evaluated[1].value.visual(),
592            crate::VisualRepresentation::Tree(_)
593        ));
594        assert_eq!(evaluated[0].value.codecs()[0].name, "string");
595        assert_eq!(evaluated[0].value.encode("STRING").unwrap(), "john sleeps");
596        assert_eq!(evaluated[1].value.codecs()[0].name, "display");
597    }
598
599    #[test]
600    fn evaluates_feature_structures_without_flattening_them() {
601        let irtg = parse_irtg(
602            br#"
603            interpretation ft: de.up.ling.irtg.algebra.FeatureStructureAlgebra
604            S! -> value
605              [ft] "[left: #x [case: nom], right: #x]"
606            "# as &[u8],
607        )
608        .unwrap();
609        let best = irtg.grammar().viterbi().unwrap();
610        let evaluated = irtg.evaluate_derivation(best.arena(), best.root()).unwrap();
611        assert!(matches!(
612            evaluated[0].value.visual(),
613            crate::VisualRepresentation::FeatureStructure(_)
614        ));
615        assert_eq!(evaluated[0].value.codecs()[0].name, "display");
616        assert!(
617            evaluated[0]
618                .value
619                .encode("display")
620                .unwrap()
621                .contains("case")
622        );
623    }
624
625    #[test]
626    fn reports_opt_in_non_null_filter_capability() {
627        let irtg = parse_irtg(
628            br#"
629            interpretation ft: de.up.ling.irtg.algebra.FeatureStructureAlgebra
630            interpretation string: de.up.ling.irtg.algebra.StringAlgebra
631            S! -> value
632              [ft] "[case: nom]"
633              [string] word
634            "# as &[u8],
635        )
636        .unwrap();
637        let info = irtg.interpretation_info();
638        let ft = info.iter().find(|item| item.name == "ft").unwrap();
639        let string = info.iter().find(|item| item.name == "string").unwrap();
640        assert!(ft.non_null_filter_capable);
641        assert!(!string.non_null_filter_capable);
642        assert!(
643            irtg.interpretation_ref("ft")
644                .unwrap()
645                .supports_non_null_filter()
646        );
647        assert!(
648            !irtg
649                .interpretation_ref("string")
650                .unwrap()
651                .supports_non_null_filter()
652        );
653        assert!(matches!(
654            irtg.filter_non_null(irtg.grammar(), "string"),
655            Err(crate::IrtgError::NonNullFilterUnsupported { .. })
656        ));
657    }
658
659    #[test]
660    fn reports_finite_and_infinite_language_cardinality() {
661        let finite = parse_irtg(GRAMMAR.as_bytes()).unwrap();
662        assert_eq!(
663            finite.grammar().language_cardinality(),
664            LanguageCardinality::Finite(1)
665        );
666
667        let recursive = parse_irtg(
668            br#"
669            interpretation string: de.up.ling.irtg.algebra.StringAlgebra
670
671            S! -> wrap(S)
672              [string] *(x, ?1)
673            S -> leaf
674              [string] x
675            "# as &[u8],
676        )
677        .unwrap();
678        assert_eq!(
679            recursive.grammar().language_cardinality(),
680            LanguageCardinality::Infinite
681        );
682    }
683}