Skip to main content

rusty_alto/
irtg.rs

1//! Interpreted regular tree grammars.
2
3use crate::{
4    APPEND_SYMBOL, Algebra, BinarizedTagTreeDecompositionAutomaton, Binarizing, BottomUpTa,
5    CodecMetadata, CondensedTa, DisplayCodec, Explicit, ExplicitBuildError, ExplicitBuilder,
6    FeatureStructure, FeatureStructureAlgebra, FxHashMap, FxHashSet, Homomorphism,
7    HomomorphismError, IndexedCondensedIntersectionStats, Interner, InvHom, MinHeuristic,
8    ObligatoryLeafTables, OutputCodec, OutputCodecError, OutputCodecRegistry, OutsideHeuristic,
9    ParseControl, ScoredZeroHeuristic, Signature, SignatureError, SpaceJoinCodec, StateId,
10    StateUniverse, StringAlgebra, Symbol, TagStringAlgebra, TagStringDecompositionAutomaton,
11    TagStringValue, TagTreeAlgebra, TagTreeDecompositionAutomaton, TopDownTa, TreeAlgebra,
12    UniversalSxHeuristic, VisualRepresentation, ViterbiTree, WeightScorer, ZeroHeuristic,
13    alto_ast::{AstHomTerm, AstIrtg, AstState, LexError, Tok, lex},
14    alto_grammar,
15    astar::{
16        AstarOptions, AstarStats, PreparedAstarGrammar, astar_one_best_with_stats,
17        astar_string_one_best_with_stats, astar_string_one_best_with_stats_prepared,
18        materialize_astar_intersection_with_pairs_controlled,
19        materialize_astar_string_intersection_with_controlled,
20    },
21    materialize::{
22        materialize_indexed_condensed_intersection_with_pairs_controlled,
23        materialize_topdown_condensed_intersection,
24        materialize_topdown_condensed_intersection_with_pairs_controlled,
25    },
26};
27use lalrpop_util::ParseError;
28use packed_term_arena::tree::{Tree, TreeArena};
29use std::{
30    any::Any,
31    fmt,
32    hash::Hash,
33    io::Read,
34    marker::PhantomData,
35    sync::{Arc, Mutex},
36};
37use thiserror::Error;
38
39fn product_state_names<S: Clone + Eq + Hash + fmt::Display>(
40    left_names: &[String],
41    right_states: &Interner<S>,
42    pairs: &[(StateId, StateId)],
43) -> Vec<String> {
44    pairs
45        .iter()
46        .map(|&(left, right)| {
47            let left = left_names
48                .get(left.index())
49                .cloned()
50                .unwrap_or_else(|| format!("q{}", left.0));
51            format!("{left} × {}", right_states.resolve(right))
52        })
53        .collect()
54}
55
56fn product_state_parts<S: Clone + Eq + Hash + fmt::Display>(
57    left_parts: &[Vec<String>],
58    right_states: &Interner<S>,
59    pairs: &[(StateId, StateId)],
60) -> Vec<Vec<String>> {
61    pairs
62        .iter()
63        .map(|&(left, right)| {
64            let mut parts = left_parts
65                .get(left.index())
66                .cloned()
67                .unwrap_or_else(|| vec![format!("q{}", left.0)]);
68            parts.push(right_states.resolve(right).to_string());
69            parts
70        })
71        .collect()
72}
73
74fn string_product_state_names(
75    left_names: &[String],
76    right_states: &Interner<crate::algebras::Span>,
77    pairs: &[(StateId, StateId)],
78) -> Vec<String> {
79    pairs
80        .iter()
81        .map(|&(left, right)| {
82            let left = left_names
83                .get(left.index())
84                .cloned()
85                .unwrap_or_else(|| format!("q{}", left.0));
86            format!("{left}{}", right_states.resolve(right))
87        })
88        .collect()
89}
90
91fn filter_feature_chart(
92    chart: &Explicit,
93    algebra: &FeatureStructureAlgebra,
94    homomorphism: &Homomorphism,
95    control: &ParseControl,
96) -> Result<NonNullFilteredChart, IrtgError> {
97    control.check().map_err(|_| IrtgError::Cancelled)?;
98    let filter = InvHom::new(algebra.filter(), homomorphism);
99    let rules: Vec<_> = chart
100        .rules()
101        .map(|rule| {
102            (
103                rule.symbol,
104                rule.children.to_vec(),
105                rule.result,
106                rule.weight,
107            )
108        })
109        .collect();
110    let mut builder = ExplicitBuilder::new();
111    let mut pair_ids = FxHashMap::<(StateId, FeatureStructure), StateId>::default();
112    let mut value_ids = FxHashMap::<FeatureStructure, usize>::default();
113    let mut state_origins = Vec::<(StateId, usize)>::new();
114    let mut variants = vec![Vec::<(FeatureStructure, StateId)>::new(); chart.num_states() as usize];
115    let mut emitted = FxHashSet::<(Symbol, Vec<StateId>, StateId)>::default();
116
117    let intern = |left: StateId,
118                  value: FeatureStructure,
119                  builder: &mut ExplicitBuilder,
120                  pair_ids: &mut FxHashMap<(StateId, FeatureStructure), StateId>,
121                  value_ids: &mut FxHashMap<FeatureStructure, usize>,
122                  state_origins: &mut Vec<(StateId, usize)>,
123                  variants: &mut [Vec<(FeatureStructure, StateId)>]| {
124        if let Some(&id) = pair_ids.get(&(left, value.clone())) {
125            return (id, false);
126        }
127        let next_value_id = value_ids.len();
128        let value_id = *value_ids.entry(value.clone()).or_insert(next_value_id);
129        let id = builder.new_state();
130        if chart.is_accepting(&left) {
131            builder.add_accepting(id);
132        }
133        pair_ids.insert((left, value.clone()), id);
134        state_origins.push((left, value_id));
135        variants[left.index()].push((value, id));
136        (id, true)
137    };
138
139    let mut changed = true;
140    while changed {
141        control.check().map_err(|_| IrtgError::Cancelled)?;
142        changed = false;
143        for (symbol, children, result, weight) in &rules {
144            control.check().map_err(|_| IrtgError::Cancelled)?;
145            if children.is_empty() {
146                filter.step(*symbol, &[], &mut |value| {
147                    if control.is_cancelled() {
148                        return;
149                    }
150                    let (parent, is_new) = intern(
151                        *result,
152                        value,
153                        &mut builder,
154                        &mut pair_ids,
155                        &mut value_ids,
156                        &mut state_origins,
157                        &mut variants,
158                    );
159                    changed |= is_new;
160                    if emitted.insert((*symbol, Vec::new(), parent)) {
161                        builder.add_weighted_rule(*symbol, Vec::new(), parent, *weight);
162                    }
163                });
164                continue;
165            }
166            if children
167                .iter()
168                .any(|child| variants[child.index()].is_empty())
169            {
170                continue;
171            }
172            let pool_storage: Vec<_> = children
173                .iter()
174                .map(|child| variants[child.index()].clone())
175                .collect();
176            let pools: Vec<_> = pool_storage.iter().map(Vec::as_slice).collect();
177            crate::run::cartesian_product(&pools, |combination| {
178                if control.is_cancelled() {
179                    return;
180                }
181                let child_values: Vec<_> =
182                    combination.iter().map(|(value, _)| value.clone()).collect();
183                let child_ids: Vec<_> = combination.iter().map(|(_, id)| *id).collect();
184                filter.step(*symbol, &child_values, &mut |value| {
185                    if control.is_cancelled() {
186                        return;
187                    }
188                    let (parent, is_new) = intern(
189                        *result,
190                        value,
191                        &mut builder,
192                        &mut pair_ids,
193                        &mut value_ids,
194                        &mut state_origins,
195                        &mut variants,
196                    );
197                    changed |= is_new;
198                    if emitted.insert((*symbol, child_ids.clone(), parent)) {
199                        builder.add_weighted_rule(*symbol, child_ids.clone(), parent, *weight);
200                    }
201                });
202            });
203            control.check().map_err(|_| IrtgError::Cancelled)?;
204        }
205    }
206    Ok(NonNullFilteredChart {
207        automaton: builder.build(),
208        state_origins,
209    })
210}
211
212/// A chart filtered to terms whose interpretation evaluates successfully.
213#[derive(Debug)]
214pub struct NonNullFilteredChart {
215    /// Filtered automaton.
216    pub automaton: Explicit,
217    /// For each filtered state, its source-chart state and dense filter-state ID.
218    pub state_origins: Vec<(StateId, usize)>,
219}
220
221const STRING_ALGEBRA: &str = "de.up.ling.irtg.algebra.StringAlgebra";
222const TAG_STRING_ALGEBRA: &str = "de.up.ling.irtg.algebra.TagStringAlgebra";
223const TAG_TREE_ALGEBRA: &str = "de.up.ling.irtg.algebra.TagTreeAlgebra";
224const TAG_TREE_WITH_ARITIES_ALGEBRA: &str = "de.up.ling.irtg.algebra.TagTreeWithAritiesAlgebra";
225const BINARIZING_TAG_TREE_ALGEBRA: &str = "de.up.ling.irtg.algebra.BinarizingTagTreeAlgebra";
226const BINARIZING_TAG_TREE_WITH_ARITIES_ALGEBRA: &str =
227    "de.up.ling.irtg.algebra.BinarizingTagTreeWithAritiesAlgebra";
228const FEATURE_STRUCTURE_ALGEBRA: &str = "de.up.ling.irtg.algebra.FeatureStructureAlgebra";
229const TREE_WITH_ARITIES_ALGEBRA: &str = "de.up.ling.irtg.algebra.TreeWithAritiesAlgebra";
230const BINARIZING_TREE_WITH_ARITIES_ALGEBRA: &str =
231    "de.up.ling.irtg.algebra.BinarizingTreeWithAritiesAlgebra";
232
233// ---------------------------------------------------------------------------
234// Materialization strategy
235// ---------------------------------------------------------------------------
236
237/// Which algorithm to use when materializing an intersection.
238///
239/// The strategies recognize the same derivations but explore the product
240/// automaton differently:
241///
242/// - [`TopDownCondensed`](Self::TopDownCondensed) is the recommended default
243///   when a complete chart is needed.
244/// - [`IndexedCondensed`](Self::IndexedCondensed) is primarily useful for
245///   algorithm comparison, statistics, or workloads where benchmarking shows
246///   it is advantageous. It can generate very large candidate sets for some
247///   ambiguous or discontinuous decompositions.
248/// - [`Astar`](Self::Astar) is intended for weight-directed search and can stop
249///   early when only the best derivation is needed.
250///
251/// Strategy choice depends on the resulting IRTG, its decomposition automata,
252/// and representative inputs—not on the grammar's source filename or codec.
253/// See the
254/// [parsing-algorithm guide](https://github.com/coli-saar/rusty-alto/wiki/Parsing-Algorithms)
255/// for a user-facing comparison.
256pub enum MaterializationStrategy<'h> {
257    /// Top-down condensed intersection.
258    ///
259    /// This is the default used by [`Irtg::parse`]. It starts from accepting
260    /// product states and follows compatible condensed rules downward,
261    /// producing a complete explicit parse chart.
262    ///
263    /// Prefer this as the general chart-building default unless measurements
264    /// on the intended workload support another choice.
265    TopDownCondensed,
266    /// Bottom-up indexed condensed intersection.
267    ///
268    /// This grows reachable product states through partial-child indexes and
269    /// returns detailed intersection statistics. It also produces a complete
270    /// chart; it is not an early-exit one-best parser.
271    ///
272    /// Candidate generation can become substantially more expensive than the
273    /// top-down strategy for some workloads, including some TAG-derived IRTGs.
274    /// This is a property of the automata and inputs, not of a `.tag` filename.
275    IndexedCondensed,
276    /// A* intersection with a configurable heuristic.
277    ///
278    /// **Precondition**: all grammar rule weights must be ≤ 1 (probability
279    /// weights).  If any weight exceeds 1 the strategy is rejected at
280    /// parse time with [`IrtgError::AstarWeightPrecondition`].
281    ///
282    /// Use this when weight-directed search or early one-best termination is
283    /// more important than constructing the complete parse chart. Heuristic
284    /// compatibility depends on the input algebra; SX-family heuristics are
285    /// specific to compatible string interpretations.
286    Astar {
287        /// Heuristic to guide the A* search.
288        heuristic: AstarHeuristic<'h>,
289        /// Options for the A* materializer.
290        options: AstarOptions,
291    },
292}
293
294/// Heuristic choice for [`MaterializationStrategy::Astar`].
295pub enum AstarHeuristic<'h> {
296    /// Uninformed heuristic (always returns 1.0).  A* degenerates to Knuth
297    /// order and is exact.
298    Zero,
299    /// Grammar-only outside-weight heuristic.  Algebra-agnostic and
300    /// sentence-independent; compute once with [`OutsideHeuristic::from_grammar`].
301    Outside(&'h OutsideHeuristic),
302    /// Universal SX heuristic table built for `n_max`; `n` is the length of the
303    /// current sentence.  The table is admissible for any sentence with `n ≤ n_max`.
304    /// Build once with [`UniversalSxHeuristic::new`] and reuse across all sentences.
305    UniversalSx {
306        /// Precomputed universal SX table.
307        table: &'h UniversalSxHeuristic,
308        /// Current sentence length.
309        n: usize,
310    },
311    /// Universal SX combined with the obligatory-leaf F filter via `min`. SX
312    /// bounds outside *weight*; F prunes spans whose context cannot supply an
313    /// obligatory leaf. Both are admissible, so the combination stays exact.
314    /// `oblig` is grammar-only (build once); the per-input terminal supply is
315    /// derived from the sentence at call time.
316    UniversalSxF {
317        /// Precomputed universal SX table.
318        table: &'h UniversalSxHeuristic,
319        /// Grammar-level obligatory-leaf tables.
320        oblig: &'h ObligatoryLeafTables,
321        /// Current sentence length.
322        n: usize,
323    },
324}
325
326/// An interpreted regular tree grammar.
327#[derive(Debug)]
328pub struct Irtg {
329    grammar: Explicit,
330    states: Interner<String>,
331    grammar_signature: Signature,
332    interpretations: FxHashMap<String, Interpretation>,
333}
334
335impl Irtg {
336    /// Return the explicit grammar automaton.
337    pub fn grammar(&self) -> &Explicit {
338        &self.grammar
339    }
340
341    /// Return the grammar signature.
342    pub fn grammar_signature(&self) -> &Signature {
343        &self.grammar_signature
344    }
345
346    /// Return the grammar state-name interner.
347    pub fn states(&self) -> &Interner<String> {
348        &self.states
349    }
350
351    /// Return the names of interpretations backed by [`StringAlgebra`].
352    pub fn string_interpretation_names(&self) -> Vec<&str> {
353        let mut names: Vec<_> = self
354            .interpretations
355            .values()
356            .filter(|interpretation| interpretation.kind == InterpretationKind::String)
357            .map(|interpretation| interpretation.name.as_str())
358            .collect();
359        names.sort_unstable();
360        names
361    }
362
363    /// Return the names of all interpretations that can be used as parse inputs.
364    pub fn input_interpretation_names(&self) -> Vec<&str> {
365        let mut names: Vec<_> = self
366            .interpretations
367            .values()
368            .filter(|interpretation| interpretation.is_inputable())
369            .map(|interpretation| interpretation.name.as_str())
370            .collect();
371        names.sort_unstable();
372        names
373    }
374
375    /// Filter a parse chart to derivations whose feature-structure interpretation
376    /// evaluates successfully.
377    pub fn filter_non_null(
378        &self,
379        chart: &Explicit,
380        interpretation_name: &str,
381    ) -> Result<Explicit, IrtgError> {
382        Ok(self
383            .filter_non_null_with_state_origins(chart, interpretation_name)?
384            .automaton)
385    }
386
387    /// Filter a chart while retaining source-state provenance for display.
388    pub fn filter_non_null_with_state_origins(
389        &self,
390        chart: &Explicit,
391        interpretation_name: &str,
392    ) -> Result<NonNullFilteredChart, IrtgError> {
393        self.filter_non_null_with_state_origins_controlled(
394            chart,
395            interpretation_name,
396            &ParseControl::new(),
397        )
398    }
399
400    /// Filter a chart while allowing another thread to request cancellation.
401    ///
402    /// This is the cancellable counterpart of
403    /// [`Self::filter_non_null_with_state_origins`]. Clones of `control` may be
404    /// held by another thread. If cancellation is observed, the method returns
405    /// [`IrtgError::Cancelled`] and discards the partial filtered chart.
406    ///
407    /// Cancellation is cooperative and is checked at safe points during the
408    /// feature-structure fixpoint computation.
409    pub fn filter_non_null_with_state_origins_controlled(
410        &self,
411        chart: &Explicit,
412        interpretation_name: &str,
413        control: &ParseControl,
414    ) -> Result<NonNullFilteredChart, IrtgError> {
415        let interpretation = self
416            .interpretations
417            .get(interpretation_name)
418            .ok_or_else(|| IrtgError::UnknownInterpretation(interpretation_name.to_owned()))?;
419        if !interpretation.supports_non_null_filter() {
420            return Err(IrtgError::NonNullFilterUnsupported {
421                interpretation: interpretation_name.to_owned(),
422                class_name: interpretation.class_name.clone(),
423            });
424        }
425        let algebra = interpretation.algebra.lock().unwrap();
426        let algebra = algebra
427            .as_ref()
428            .downcast_ref::<FeatureStructureAlgebra>()
429            .expect("non-null filter capability must match its algebra");
430        filter_feature_chart(chart, algebra, &interpretation.homomorphism, control)
431    }
432
433    /// Iterate over all interpretations (in unspecified order).
434    pub fn interpretations(&self) -> impl Iterator<Item = &Interpretation> {
435        self.interpretations.values()
436    }
437
438    /// Return a reference to a named interpretation, or `None` if absent.
439    pub fn interpretation_ref(&self, name: &str) -> Option<&Interpretation> {
440        self.interpretations.get(name)
441    }
442
443    /// Return a typed handle for a named interpretation.
444    pub fn interpretation<A>(&self, name: &str) -> Result<TypedInterpretation<'_, A>, IrtgError>
445    where
446        A: Algebra + 'static,
447    {
448        let interpretation = self
449            .interpretations
450            .get(name)
451            .ok_or_else(|| IrtgError::UnknownInterpretation(name.to_owned()))?;
452        if interpretation.algebra.lock().unwrap().as_ref().is::<A>() {
453            Ok(TypedInterpretation {
454                interpretation,
455                _algebra: PhantomData,
456            })
457        } else {
458            Err(IrtgError::WrongAlgebraType {
459                interpretation: name.to_owned(),
460                requested: std::any::type_name::<A>(),
461                actual: interpretation.class_name.clone(),
462            })
463        }
464    }
465
466    /// Parse with one or more interpretation inputs using the default strategy
467    /// ([`MaterializationStrategy::TopDownCondensed`]).
468    pub fn parse<'a>(
469        &self,
470        inputs: impl IntoIterator<Item = ParseInput<'a>>,
471    ) -> Result<ParseChart, IrtgError> {
472        self.parse_with(inputs, &MaterializationStrategy::TopDownCondensed)
473    }
474
475    /// Parse with one or more interpretation inputs, selecting the
476    /// materialization algorithm via `strategy`.
477    ///
478    /// For [`MaterializationStrategy::Astar`] the precondition is that all
479    /// grammar rule weights are ≤ 1.  If that is violated the method returns
480    /// [`IrtgError::AstarWeightPrecondition`] immediately.
481    pub fn parse_with<'a>(
482        &self,
483        inputs: impl IntoIterator<Item = ParseInput<'a>>,
484        strategy: &MaterializationStrategy<'_>,
485    ) -> Result<ParseChart, IrtgError> {
486        self.parse_with_control(inputs, strategy, &ParseControl::new())
487    }
488
489    /// Parse with a strategy and a cooperative cancellation control.
490    ///
491    /// The control may be cloned and canceled from another thread. On
492    /// cancellation this method returns [`IrtgError::Cancelled`] and discards
493    /// any partial chart. Cancellation is checked at safe points and therefore
494    /// does not forcibly terminate the parsing thread.
495    ///
496    /// Use [`Self::parse`] or [`Self::parse_with`] when cancellation is not
497    /// needed.
498    pub fn parse_with_control<'a>(
499        &self,
500        inputs: impl IntoIterator<Item = ParseInput<'a>>,
501        strategy: &MaterializationStrategy<'_>,
502        control: &ParseControl,
503    ) -> Result<ParseChart, IrtgError> {
504        control.check().map_err(|_| IrtgError::Cancelled)?;
505        // Guard: A* requires probability weights (≤ 1).
506        if matches!(strategy, MaterializationStrategy::Astar { .. }) {
507            self.check_astar_weight_precondition()?;
508        }
509
510        let mut chart = self.grammar.clone();
511        let mut state_names = (0..chart.num_states())
512            .map(|state| self.states.resolve(StateId(state)).clone())
513            .collect::<Vec<_>>();
514        let mut state_parts = state_names
515            .iter()
516            .cloned()
517            .map(|name| vec![name])
518            .collect::<Vec<_>>();
519        let mut stats = Vec::new();
520
521        for input in inputs {
522            control.check().map_err(|_| IrtgError::Cancelled)?;
523            let interpretation = input.interpretation;
524            match interpretation.kind {
525                InterpretationKind::String => {
526                    let value = *input.value.downcast::<Vec<Symbol>>().map_err(|_| {
527                        IrtgError::WrongInputType {
528                            interpretation: interpretation.name.clone(),
529                        }
530                    })?;
531                    let decomp = interpretation.decompose_string(value)?;
532                    let invhom = InvHom::new(decomp, &interpretation.homomorphism);
533                    let next_chart = match strategy {
534                        MaterializationStrategy::TopDownCondensed => {
535                            let (c, right_states, pairs, stat) =
536                                materialize_topdown_condensed_intersection_with_pairs_controlled(
537                                    &chart, &invhom, control,
538                                )
539                                .map_err(|_| IrtgError::Cancelled)?;
540                            state_names =
541                                string_product_state_names(&state_names, &right_states, &pairs);
542                            state_parts = product_state_parts(&state_parts, &right_states, &pairs);
543                            stats.push(stat);
544                            c
545                        }
546                        MaterializationStrategy::IndexedCondensed => {
547                            let (c, right_states, pairs, stat) =
548                                materialize_indexed_condensed_intersection_with_pairs_controlled(
549                                    &chart, &invhom, control,
550                                )
551                                .map_err(|_| IrtgError::Cancelled)?;
552                            state_names =
553                                string_product_state_names(&state_names, &right_states, &pairs);
554                            state_parts = product_state_parts(&state_parts, &right_states, &pairs);
555                            stats.push(stat);
556                            c
557                        }
558                        MaterializationStrategy::Astar {
559                            heuristic,
560                            options: _,
561                        } => {
562                            // We need owned options — clone by rebuilding (AstarOptions is not Clone).
563                            // Instead, call materialize_astar_intersection with a fresh options value.
564                            // We route via a helper to avoid duplicating logic.
565                            let (c, right_states, pairs) = self
566                                .run_astar_chart(&chart, &invhom, heuristic, strategy, control)?;
567                            state_names =
568                                string_product_state_names(&state_names, &right_states, &pairs);
569                            state_parts = product_state_parts(&state_parts, &right_states, &pairs);
570                            // A* produces no IndexedCondensedIntersectionStats; we push nothing.
571                            c
572                        }
573                    };
574                    chart = next_chart;
575                }
576                InterpretationKind::TagString => {
577                    let value =
578                        *input
579                            .value
580                            .downcast::<TagStringValue<Symbol>>()
581                            .map_err(|_| IrtgError::WrongInputType {
582                                interpretation: interpretation.name.clone(),
583                            })?;
584                    let decomp = interpretation.decompose_tag_string(value)?;
585                    let (next_chart, next_names, next_parts, stat) = self.run_generic_chart(
586                        &chart,
587                        &state_names,
588                        &state_parts,
589                        decomp,
590                        &interpretation.homomorphism,
591                        strategy,
592                        &interpretation.name,
593                        control,
594                    )?;
595                    if let Some(stat) = stat {
596                        stats.push(stat);
597                    }
598                    chart = next_chart;
599                    state_names = next_names;
600                    state_parts = next_parts;
601                }
602                InterpretationKind::TagTree => {
603                    let value =
604                        *input
605                            .value
606                            .downcast::<Tree>()
607                            .map_err(|_| IrtgError::WrongInputType {
608                                interpretation: interpretation.name.clone(),
609                            })?;
610                    let decomp = interpretation.decompose_tag_tree(value)?;
611                    let (next_chart, next_names, next_parts, stat) = self.run_generic_chart(
612                        &chart,
613                        &state_names,
614                        &state_parts,
615                        decomp,
616                        &interpretation.homomorphism,
617                        strategy,
618                        &interpretation.name,
619                        control,
620                    )?;
621                    if let Some(stat) = stat {
622                        stats.push(stat);
623                    }
624                    chart = next_chart;
625                    state_names = next_names;
626                    state_parts = next_parts;
627                }
628                InterpretationKind::BinarizedTagTree => {
629                    let value =
630                        *input
631                            .value
632                            .downcast::<Tree>()
633                            .map_err(|_| IrtgError::WrongInputType {
634                                interpretation: interpretation.name.clone(),
635                            })?;
636                    let decomp = interpretation.decompose_binarized_tag_tree(value)?;
637                    let (next_chart, next_names, next_parts, stat) = self.run_generic_chart(
638                        &chart,
639                        &state_names,
640                        &state_parts,
641                        decomp,
642                        &interpretation.homomorphism,
643                        strategy,
644                        &interpretation.name,
645                        control,
646                    )?;
647                    if let Some(stat) = stat {
648                        stats.push(stat);
649                    }
650                    chart = next_chart;
651                    state_names = next_names;
652                    state_parts = next_parts;
653                }
654                InterpretationKind::OutputOnly => {
655                    return Err(IrtgError::NotInputable {
656                        interpretation: interpretation.name.clone(),
657                    });
658                }
659            }
660        }
661
662        Ok(ParseChart {
663            automaton: chart,
664            state_names,
665            state_parts,
666            stats,
667        })
668    }
669
670    /// Return the single best parse tree (Viterbi tree) for the given inputs
671    /// and strategy.
672    ///
673    /// For [`MaterializationStrategy::Astar`] this calls [`crate::astar_one_best`]
674    /// directly without building a full chart.  For chart-based strategies it
675    /// falls back to [`Irtg::parse_with`] and calls `.automaton.viterbi()`.
676    pub fn best_with<'a>(
677        &self,
678        inputs: impl IntoIterator<Item = ParseInput<'a>>,
679        strategy: &MaterializationStrategy<'_>,
680    ) -> Result<Option<ViterbiTree>, IrtgError> {
681        self.best_with_scorer(inputs, strategy, &crate::ProbabilityScorer)
682    }
683
684    /// Return the single best parse tree using `scorer` for weight products.
685    pub fn best_with_scorer<'a, S: WeightScorer>(
686        &self,
687        inputs: impl IntoIterator<Item = ParseInput<'a>>,
688        strategy: &MaterializationStrategy<'_>,
689        scorer: &S,
690    ) -> Result<Option<ViterbiTree>, IrtgError> {
691        self.best_with_scorer_and_stats(inputs, strategy, scorer)
692            .map(|(tree, _)| tree)
693    }
694
695    /// Return the single best parse tree using `scorer`, plus A* stats when available.
696    pub fn best_with_scorer_and_stats<'a, S: WeightScorer>(
697        &self,
698        inputs: impl IntoIterator<Item = ParseInput<'a>>,
699        strategy: &MaterializationStrategy<'_>,
700        scorer: &S,
701    ) -> Result<(Option<ViterbiTree>, Option<AstarStats>), IrtgError> {
702        self.best_with_scorer_and_stats_impl(inputs, strategy, scorer, None)
703    }
704
705    /// Like [`Self::best_with_scorer_and_stats`], reusing grammar-only A*
706    /// indexes for the common single-string-interpretation path.
707    pub fn best_with_scorer_and_stats_prepared<'a, S: WeightScorer>(
708        &self,
709        inputs: impl IntoIterator<Item = ParseInput<'a>>,
710        strategy: &MaterializationStrategy<'_>,
711        scorer: &S,
712        prepared: &PreparedAstarGrammar,
713    ) -> Result<(Option<ViterbiTree>, Option<AstarStats>), IrtgError> {
714        self.best_with_scorer_and_stats_impl(inputs, strategy, scorer, Some(prepared))
715    }
716
717    fn best_with_scorer_and_stats_impl<'a, S: WeightScorer>(
718        &self,
719        inputs: impl IntoIterator<Item = ParseInput<'a>>,
720        strategy: &MaterializationStrategy<'_>,
721        scorer: &S,
722        prepared: Option<&PreparedAstarGrammar>,
723    ) -> Result<(Option<ViterbiTree>, Option<AstarStats>), IrtgError> {
724        // For A* we can short-circuit and avoid building the chart.
725        if let MaterializationStrategy::Astar { heuristic, .. } = strategy {
726            // Guard
727            self.check_astar_weight_precondition()?;
728
729            // We need to iterate inputs and run astar_one_best on the last
730            // grammar (after intersecting all inputs in order).  For simplicity
731            // we run parse_with for all-but-last inputs (if any) and then do
732            // astar_one_best for the final one.  In practice IRTGs usually have
733            // a single string interpretation per parse call, so this is fast.
734            let inputs: Vec<_> = inputs.into_iter().collect();
735            if inputs.is_empty() {
736                return Ok((self.grammar.viterbi_with(scorer), None));
737            }
738
739            // Split: all-but-last via parse_with(TopDownCondensed), then last via astar_one_best.
740            let (last, rest) = inputs.split_last().unwrap();
741            // Build intermediate chart from all-but-last inputs (if any).
742            if !rest.is_empty() {
743                // Collect rest as owned inputs — but ParseInput<'a> is not Clone.
744                // We cannot split a Vec<ParseInput<'_>> this way because ParseInput doesn't
745                // implement Clone.  Instead we re-collect from the iterator below.
746                // Since we already consumed the iterator into a Vec, we need to handle
747                // the rest of the inputs with their Box<dyn Any> values.
748                // The simplest approach: call parse_with on rest using TopDownCondensed.
749                // We cannot re-use ParseInput because it owns value; instead we call
750                // materialize_topdown_condensed_intersection directly.
751                //
752                // NOTE: this path is only reached when there are 2+ inputs and the
753                // caller uses Astar.  The typical single-interpretation use case avoids it.
754                return self.best_with_multi(inputs, heuristic, strategy, scorer);
755            }
756
757            // Run astar_one_best for the last (only) input.
758            let interpretation = last.interpretation;
759            match interpretation.kind {
760                InterpretationKind::String => {
761                    let value = last
762                        .value
763                        .downcast_ref::<Vec<Symbol>>()
764                        .ok_or_else(|| IrtgError::WrongInputType {
765                            interpretation: interpretation.name.clone(),
766                        })?
767                        .clone();
768                    let decomp = interpretation.decompose_string(value)?;
769                    let invhom = InvHom::new(decomp, &interpretation.homomorphism);
770                    let (tree, stats) = self.run_astar_one_best_with_scorer(
771                        &self.grammar,
772                        &invhom,
773                        heuristic,
774                        scorer,
775                        prepared,
776                    );
777                    Ok((tree, Some(stats)))
778                }
779                InterpretationKind::TagString => {
780                    let value = last
781                        .value
782                        .downcast_ref::<TagStringValue<Symbol>>()
783                        .ok_or_else(|| IrtgError::WrongInputType {
784                            interpretation: interpretation.name.clone(),
785                        })?
786                        .clone();
787                    let decomp = interpretation.decompose_tag_string(value)?;
788                    let (tree, stats) = self.run_generic_one_best(
789                        &self.grammar,
790                        decomp,
791                        &interpretation.homomorphism,
792                        heuristic,
793                        scorer,
794                        &interpretation.name,
795                    )?;
796                    Ok((tree, Some(stats)))
797                }
798                InterpretationKind::TagTree => {
799                    let value = *last.value.downcast_ref::<Tree>().ok_or_else(|| {
800                        IrtgError::WrongInputType {
801                            interpretation: interpretation.name.clone(),
802                        }
803                    })?;
804                    let decomp = interpretation.decompose_tag_tree(value)?;
805                    let (tree, stats) = self.run_generic_one_best(
806                        &self.grammar,
807                        decomp,
808                        &interpretation.homomorphism,
809                        heuristic,
810                        scorer,
811                        &interpretation.name,
812                    )?;
813                    Ok((tree, Some(stats)))
814                }
815                InterpretationKind::BinarizedTagTree => {
816                    let value = *last.value.downcast_ref::<Tree>().ok_or_else(|| {
817                        IrtgError::WrongInputType {
818                            interpretation: interpretation.name.clone(),
819                        }
820                    })?;
821                    let decomp = interpretation.decompose_binarized_tag_tree(value)?;
822                    let (tree, stats) = self.run_generic_one_best(
823                        &self.grammar,
824                        decomp,
825                        &interpretation.homomorphism,
826                        heuristic,
827                        scorer,
828                        &interpretation.name,
829                    )?;
830                    Ok((tree, Some(stats)))
831                }
832                InterpretationKind::OutputOnly => Err(IrtgError::NotInputable {
833                    interpretation: interpretation.name.clone(),
834                }),
835            }
836        } else {
837            // Chart strategies: build the chart and run Viterbi on it.
838            let chart = self.parse_with(inputs, strategy)?;
839            Ok((chart.automaton.viterbi_with(scorer), None))
840        }
841    }
842
843    // -----------------------------------------------------------------------
844    // Private helpers
845    // -----------------------------------------------------------------------
846
847    #[allow(clippy::too_many_arguments)]
848    fn run_generic_chart<R>(
849        &self,
850        chart: &Explicit,
851        left_state_names: &[String],
852        left_state_parts: &[Vec<String>],
853        decomp: R,
854        homomorphism: &Homomorphism,
855        strategy: &MaterializationStrategy<'_>,
856        interpretation: &str,
857        control: &ParseControl,
858    ) -> Result<
859        (
860            Explicit,
861            Vec<String>,
862            Vec<Vec<String>>,
863            Option<IndexedCondensedIntersectionStats>,
864        ),
865        IrtgError,
866    >
867    where
868        R: CondensedTa + StateUniverse + TopDownTa,
869        R::State: Clone + Eq + Hash + fmt::Display,
870    {
871        let invhom = InvHom::new(decomp, homomorphism);
872        match strategy {
873            MaterializationStrategy::TopDownCondensed => {
874                let (chart, right_states, pairs, stats) =
875                    materialize_topdown_condensed_intersection_with_pairs_controlled(
876                        chart, &invhom, control,
877                    )
878                    .map_err(|_| IrtgError::Cancelled)?;
879                let names = product_state_names(left_state_names, &right_states, &pairs);
880                let parts = product_state_parts(left_state_parts, &right_states, &pairs);
881                Ok((chart, names, parts, Some(stats)))
882            }
883            MaterializationStrategy::IndexedCondensed => {
884                let (chart, right_states, pairs, stats) =
885                    materialize_indexed_condensed_intersection_with_pairs_controlled(
886                        chart, &invhom, control,
887                    )
888                    .map_err(|_| IrtgError::Cancelled)?;
889                let names = product_state_names(left_state_names, &right_states, &pairs);
890                let parts = product_state_parts(left_state_parts, &right_states, &pairs);
891                Ok((chart, names, parts, Some(stats)))
892            }
893            MaterializationStrategy::Astar { heuristic, options } => {
894                let options = AstarOptions {
895                    stop_at_first_goal: options.stop_at_first_goal,
896                    beam: options.beam,
897                };
898                let (chart, right_states, pairs) = match heuristic {
899                    AstarHeuristic::Zero => {
900                        let heuristic = ZeroHeuristic;
901                        let (chart, states, pairs, _) =
902                            materialize_astar_intersection_with_pairs_controlled(
903                                chart,
904                                &invhom,
905                                &heuristic,
906                                options,
907                                &crate::ProbabilityScorer,
908                                control,
909                            );
910                        control.check().map_err(|_| IrtgError::Cancelled)?;
911                        (chart, states, pairs)
912                    }
913                    AstarHeuristic::Outside(heuristic) => {
914                        let (chart, states, pairs, _) =
915                            materialize_astar_intersection_with_pairs_controlled(
916                                chart,
917                                &invhom,
918                                *heuristic,
919                                options,
920                                &crate::ProbabilityScorer,
921                                control,
922                            );
923                        control.check().map_err(|_| IrtgError::Cancelled)?;
924                        (chart, states, pairs)
925                    }
926                    AstarHeuristic::UniversalSx { .. } | AstarHeuristic::UniversalSxF { .. } => {
927                        return Err(IrtgError::IncompatibleHeuristic {
928                            interpretation: interpretation.to_owned(),
929                            message: "SX and SXF heuristics require a StringAlgebra interpretation"
930                                .to_owned(),
931                        });
932                    }
933                };
934                let names = product_state_names(left_state_names, &right_states, &pairs);
935                let parts = product_state_parts(left_state_parts, &right_states, &pairs);
936                Ok((chart, names, parts, None))
937            }
938        }
939    }
940
941    fn run_generic_one_best<R, S>(
942        &self,
943        chart: &Explicit,
944        decomp: R,
945        homomorphism: &Homomorphism,
946        heuristic: &AstarHeuristic<'_>,
947        scorer: &S,
948        interpretation: &str,
949    ) -> Result<(Option<ViterbiTree>, AstarStats), IrtgError>
950    where
951        R: CondensedTa + StateUniverse,
952        R::State: Clone + Eq + Hash,
953        S: WeightScorer,
954    {
955        let invhom = InvHom::new(decomp, homomorphism);
956        match heuristic {
957            AstarHeuristic::Zero => {
958                let heuristic = ScoredZeroHeuristic::new(scorer);
959                Ok(astar_one_best_with_stats(
960                    chart, &invhom, &heuristic, scorer,
961                ))
962            }
963            AstarHeuristic::Outside(heuristic) => Ok(astar_one_best_with_stats(
964                chart, &invhom, *heuristic, scorer,
965            )),
966            AstarHeuristic::UniversalSx { .. } | AstarHeuristic::UniversalSxF { .. } => {
967                Err(IrtgError::IncompatibleHeuristic {
968                    interpretation: interpretation.to_owned(),
969                    message: "SX and SXF heuristics require a StringAlgebra interpretation"
970                        .to_owned(),
971                })
972            }
973        }
974    }
975
976    /// Check the A* weight precondition: all grammar rule weights must be ≤ 1.
977    fn check_astar_weight_precondition(&self) -> Result<(), IrtgError> {
978        for rule in self.grammar.rules() {
979            if rule.weight > 1.0 {
980                // `Signature::resolve` panics if the symbol is absent; use
981                // a bounds check first since all interned rules should be present.
982                let symbol = if (rule.symbol.0 as usize) < self.grammar_signature.len() {
983                    self.grammar_signature.resolve(rule.symbol).to_owned()
984                } else {
985                    format!("{:?}", rule.symbol)
986                };
987                return Err(IrtgError::AstarWeightPrecondition {
988                    weight: rule.weight,
989                    symbol,
990                });
991            }
992        }
993        Ok(())
994    }
995
996    /// Run A* chart materializer for a single `InvHom<StringDecompositionAutomaton>`.
997    fn run_astar_chart(
998        &self,
999        chart: &Explicit,
1000        invhom: &InvHom<'_, crate::algebras::StringDecompositionAutomaton>,
1001        heuristic: &AstarHeuristic<'_>,
1002        strategy: &MaterializationStrategy<'_>,
1003        control: &ParseControl,
1004    ) -> Result<
1005        (
1006            Explicit,
1007            Interner<crate::algebras::Span>,
1008            Vec<(StateId, StateId)>,
1009        ),
1010        IrtgError,
1011    > {
1012        let options = match strategy {
1013            MaterializationStrategy::Astar { options, .. } => AstarOptions {
1014                stop_at_first_goal: options.stop_at_first_goal,
1015                beam: options.beam,
1016            },
1017            _ => unreachable!(),
1018        };
1019        let (new_chart, right_states, pairs, _) = match heuristic {
1020            AstarHeuristic::Zero => {
1021                let h = ZeroHeuristic;
1022                materialize_astar_string_intersection_with_controlled(
1023                    chart,
1024                    invhom,
1025                    &h,
1026                    options,
1027                    &crate::ProbabilityScorer,
1028                    control,
1029                )
1030            }
1031            AstarHeuristic::Outside(h) => materialize_astar_string_intersection_with_controlled(
1032                chart,
1033                invhom,
1034                *h,
1035                options,
1036                &crate::ProbabilityScorer,
1037                control,
1038            ),
1039            AstarHeuristic::UniversalSx { table, n } => {
1040                let h = table.for_sentence(*n);
1041                materialize_astar_string_intersection_with_controlled(
1042                    chart,
1043                    invhom,
1044                    &h,
1045                    options,
1046                    &crate::ProbabilityScorer,
1047                    control,
1048                )
1049            }
1050            AstarHeuristic::UniversalSxF { table, oblig, n } => {
1051                let sx = table.for_sentence(*n);
1052                let f = oblig.for_sentence(invhom.inner().sentence(), &crate::ProbabilityScorer);
1053                let h = MinHeuristic::new(sx, f);
1054                materialize_astar_string_intersection_with_controlled(
1055                    chart,
1056                    invhom,
1057                    &h,
1058                    options,
1059                    &crate::ProbabilityScorer,
1060                    control,
1061                )
1062            }
1063        };
1064        control.check().map_err(|_| IrtgError::Cancelled)?;
1065        Ok((new_chart, right_states, pairs))
1066    }
1067
1068    /// Run `astar_one_best` for a single `InvHom<StringDecompositionAutomaton>`.
1069    fn run_astar_one_best_with_scorer<S: WeightScorer>(
1070        &self,
1071        chart: &Explicit,
1072        invhom: &InvHom<'_, crate::algebras::StringDecompositionAutomaton>,
1073        heuristic: &AstarHeuristic<'_>,
1074        scorer: &S,
1075        prepared: Option<&PreparedAstarGrammar>,
1076    ) -> (Option<ViterbiTree>, AstarStats) {
1077        macro_rules! run {
1078            ($h:expr) => {
1079                if let Some(prepared) = prepared {
1080                    astar_string_one_best_with_stats_prepared(chart, prepared, invhom, $h, scorer)
1081                } else {
1082                    astar_string_one_best_with_stats(chart, invhom, $h, scorer)
1083                }
1084            };
1085        }
1086        match heuristic {
1087            AstarHeuristic::Zero => {
1088                let h = ScoredZeroHeuristic::new(scorer);
1089                run!(&h)
1090            }
1091            AstarHeuristic::Outside(h) => run!(*h),
1092            AstarHeuristic::UniversalSx { table, n } => {
1093                let h = table.for_sentence(*n);
1094                run!(&h)
1095            }
1096            AstarHeuristic::UniversalSxF { table, oblig, n } => {
1097                let sx = table.for_sentence(*n);
1098                let f = oblig.for_sentence(invhom.inner().sentence(), scorer);
1099                let h = MinHeuristic::new(sx, f);
1100                run!(&h)
1101            }
1102        }
1103    }
1104
1105    /// Fallback for `best_with` when there are 2+ inputs with A* strategy.
1106    /// Runs all inputs using TopDownCondensed, then A* on the last one.
1107    fn best_with_multi<'a, S: WeightScorer>(
1108        &self,
1109        inputs: Vec<ParseInput<'a>>,
1110        heuristic: &AstarHeuristic<'_>,
1111        _strategy: &MaterializationStrategy<'_>,
1112        scorer: &S,
1113    ) -> Result<(Option<ViterbiTree>, Option<AstarStats>), IrtgError> {
1114        // We can't clone ParseInput, so we process them one by one.
1115        let n = inputs.len();
1116        let mut chart = self.grammar.clone();
1117
1118        for (i, input) in inputs.into_iter().enumerate() {
1119            let interpretation = input.interpretation;
1120            let is_last = i == n - 1;
1121            match interpretation.kind {
1122                InterpretationKind::String => {
1123                    let value = *input.value.downcast::<Vec<Symbol>>().map_err(|_| {
1124                        IrtgError::WrongInputType {
1125                            interpretation: interpretation.name.clone(),
1126                        }
1127                    })?;
1128                    let decomp = interpretation.decompose_string(value)?;
1129                    let invhom = InvHom::new(decomp, &interpretation.homomorphism);
1130                    if is_last {
1131                        let (tree, stats) = self.run_astar_one_best_with_scorer(
1132                            &chart, &invhom, heuristic, scorer, None,
1133                        );
1134                        return Ok((tree, Some(stats)));
1135                    } else {
1136                        let (next, _, _) =
1137                            materialize_topdown_condensed_intersection(&chart, &invhom);
1138                        chart = next;
1139                    }
1140                }
1141                InterpretationKind::TagString => {
1142                    let value =
1143                        *input
1144                            .value
1145                            .downcast::<TagStringValue<Symbol>>()
1146                            .map_err(|_| IrtgError::WrongInputType {
1147                                interpretation: interpretation.name.clone(),
1148                            })?;
1149                    let decomp = interpretation.decompose_tag_string(value)?;
1150                    if is_last {
1151                        let (tree, stats) = self.run_generic_one_best(
1152                            &chart,
1153                            decomp,
1154                            &interpretation.homomorphism,
1155                            heuristic,
1156                            scorer,
1157                            &interpretation.name,
1158                        )?;
1159                        return Ok((tree, Some(stats)));
1160                    }
1161                    let invhom = InvHom::new(decomp, &interpretation.homomorphism);
1162                    chart = materialize_topdown_condensed_intersection(&chart, &invhom).0;
1163                }
1164                InterpretationKind::TagTree => {
1165                    let value =
1166                        *input
1167                            .value
1168                            .downcast::<Tree>()
1169                            .map_err(|_| IrtgError::WrongInputType {
1170                                interpretation: interpretation.name.clone(),
1171                            })?;
1172                    let decomp = interpretation.decompose_tag_tree(value)?;
1173                    if is_last {
1174                        let (tree, stats) = self.run_generic_one_best(
1175                            &chart,
1176                            decomp,
1177                            &interpretation.homomorphism,
1178                            heuristic,
1179                            scorer,
1180                            &interpretation.name,
1181                        )?;
1182                        return Ok((tree, Some(stats)));
1183                    }
1184                    let invhom = InvHom::new(decomp, &interpretation.homomorphism);
1185                    chart = materialize_topdown_condensed_intersection(&chart, &invhom).0;
1186                }
1187                InterpretationKind::BinarizedTagTree => {
1188                    let value =
1189                        *input
1190                            .value
1191                            .downcast::<Tree>()
1192                            .map_err(|_| IrtgError::WrongInputType {
1193                                interpretation: interpretation.name.clone(),
1194                            })?;
1195                    let decomp = interpretation.decompose_binarized_tag_tree(value)?;
1196                    if is_last {
1197                        let (tree, stats) = self.run_generic_one_best(
1198                            &chart,
1199                            decomp,
1200                            &interpretation.homomorphism,
1201                            heuristic,
1202                            scorer,
1203                            &interpretation.name,
1204                        )?;
1205                        return Ok((tree, Some(stats)));
1206                    }
1207                    let invhom = InvHom::new(decomp, &interpretation.homomorphism);
1208                    chart = materialize_topdown_condensed_intersection(&chart, &invhom).0;
1209                }
1210                InterpretationKind::OutputOnly => {
1211                    return Err(IrtgError::NotInputable {
1212                        interpretation: interpretation.name.clone(),
1213                    });
1214                }
1215            }
1216        }
1217
1218        Ok((chart.viterbi_with(scorer), None))
1219    }
1220}
1221
1222/// Object-safe bridge that interprets a derivation tree through an interpretation's
1223/// homomorphism and algebra, then renders the resulting value with a fixed output codec.
1224///
1225/// One concrete `Box<dyn DerivationRenderer>` is bound per algebra when the IRTG is built,
1226/// so [`Interpretation`] can render values without the caller knowing concrete algebra types.
1227trait DerivationRenderer: fmt::Debug + Send + Sync {
1228    fn render(
1229        &self,
1230        algebra: &dyn Any,
1231        homomorphism: &Homomorphism,
1232        arena: &TreeArena<Symbol>,
1233        root: Tree,
1234        name: &str,
1235    ) -> Result<String, IrtgError>;
1236
1237    fn evaluate(
1238        &self,
1239        algebra: &dyn Any,
1240        homomorphism: &Homomorphism,
1241        arena: &TreeArena<Symbol>,
1242        root: Tree,
1243        name: &str,
1244    ) -> Result<EvaluatedAlgebraValue, IrtgError>;
1245}
1246
1247/// [`DerivationRenderer`] for a concrete algebra `A` paired with codec `C`.
1248struct TypedDerivationCodec<A, C> {
1249    codec: C,
1250    output_codecs: Arc<OutputCodecRegistry>,
1251    _algebra: PhantomData<fn() -> A>,
1252}
1253
1254impl<A, C> TypedDerivationCodec<A, C> {
1255    fn new(codec: C) -> Self {
1256        Self {
1257            codec,
1258            output_codecs: Arc::new(OutputCodecRegistry::standard()),
1259            _algebra: PhantomData,
1260        }
1261    }
1262}
1263
1264// Manual `Debug` so the bound is only on `C` (the derive would also require `A: Debug`).
1265impl<A, C: fmt::Debug> fmt::Debug for TypedDerivationCodec<A, C> {
1266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1267        f.debug_struct("TypedDerivationCodec")
1268            .field("codec", &self.codec)
1269            .finish()
1270    }
1271}
1272
1273impl<A, C> DerivationRenderer for TypedDerivationCodec<A, C>
1274where
1275    A: Algebra + 'static,
1276    A: Send,
1277    A::Value: Send + Sync + 'static,
1278    C: OutputCodec<A::Value, Output = String> + fmt::Debug + Send + Sync,
1279{
1280    fn render(
1281        &self,
1282        algebra: &dyn Any,
1283        homomorphism: &Homomorphism,
1284        arena: &TreeArena<Symbol>,
1285        root: Tree,
1286        name: &str,
1287    ) -> Result<String, IrtgError> {
1288        let algebra = algebra
1289            .downcast_ref::<A>()
1290            .ok_or_else(|| IrtgError::WrongInputType {
1291                interpretation: name.to_owned(),
1292            })?;
1293        // Map the derivation tree through the homomorphism, then evaluate in the algebra.
1294        let mut image = TreeArena::new();
1295        let image_root = homomorphism.apply(arena, root, &mut image)?;
1296        let value =
1297            algebra
1298                .evaluate_term(&image, image_root)
1299                .ok_or_else(|| IrtgError::ObjectParse {
1300                    interpretation: name.to_owned(),
1301                    message: "derivation tree did not evaluate in the algebra".to_owned(),
1302                })?;
1303        Ok(self.codec.encode(&value))
1304    }
1305
1306    fn evaluate(
1307        &self,
1308        algebra: &dyn Any,
1309        homomorphism: &Homomorphism,
1310        arena: &TreeArena<Symbol>,
1311        root: Tree,
1312        name: &str,
1313    ) -> Result<EvaluatedAlgebraValue, IrtgError> {
1314        let algebra = algebra
1315            .downcast_ref::<A>()
1316            .ok_or_else(|| IrtgError::WrongInputType {
1317                interpretation: name.to_owned(),
1318            })?;
1319        let mut image = TreeArena::new();
1320        let image_root = homomorphism.apply(arena, root, &mut image)?;
1321        let value =
1322            algebra
1323                .evaluate_term(&image, image_root)
1324                .ok_or_else(|| IrtgError::ObjectParse {
1325                    interpretation: name.to_owned(),
1326                    message: "derivation tree did not evaluate in the algebra".to_owned(),
1327                })?;
1328        let visual = algebra.visualize(&value);
1329        Ok(EvaluatedAlgebraValue {
1330            inner: Box::new(TypedEvaluatedAlgebraValue {
1331                value,
1332                visual,
1333                output_codecs: self.output_codecs.clone(),
1334            }),
1335        })
1336    }
1337}
1338
1339trait ErasedEvaluatedAlgebraValue: fmt::Debug + Send + Sync {
1340    fn visual(&self) -> &VisualRepresentation;
1341    fn codecs(&self) -> Vec<CodecMetadata>;
1342    fn encode(&self, codec_name: &str) -> Result<String, OutputCodecError>;
1343}
1344
1345struct TypedEvaluatedAlgebraValue<V> {
1346    value: V,
1347    visual: VisualRepresentation,
1348    output_codecs: Arc<OutputCodecRegistry>,
1349}
1350
1351impl<V> fmt::Debug for TypedEvaluatedAlgebraValue<V> {
1352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1353        f.debug_struct("TypedEvaluatedAlgebraValue")
1354            .field("visual", &self.visual)
1355            .finish_non_exhaustive()
1356    }
1357}
1358
1359impl<V: Send + Sync + 'static> ErasedEvaluatedAlgebraValue for TypedEvaluatedAlgebraValue<V> {
1360    fn visual(&self) -> &VisualRepresentation {
1361        &self.visual
1362    }
1363
1364    fn codecs(&self) -> Vec<CodecMetadata> {
1365        self.output_codecs
1366            .codecs_for::<V>()
1367            .iter()
1368            .map(|codec| *codec.metadata())
1369            .collect()
1370    }
1371
1372    fn encode(&self, codec_name: &str) -> Result<String, OutputCodecError> {
1373        Ok(self
1374            .output_codecs
1375            .codec_for_name::<V>(codec_name)?
1376            .encode(&self.value))
1377    }
1378}
1379
1380/// An evaluated algebra value whose concrete Rust type remains available to codec dispatch.
1381#[derive(Debug)]
1382pub struct EvaluatedAlgebraValue {
1383    inner: Box<dyn ErasedEvaluatedAlgebraValue>,
1384}
1385
1386impl EvaluatedAlgebraValue {
1387    /// Return the algebra's preferred GUI-neutral visualization.
1388    pub fn visual(&self) -> &VisualRepresentation {
1389        self.inner.visual()
1390    }
1391
1392    /// List textual codecs registered for this value's exact public type.
1393    pub fn codecs(&self) -> Vec<CodecMetadata> {
1394        self.inner.codecs()
1395    }
1396
1397    /// Encode the value with a registered textual codec.
1398    pub fn encode(&self, codec_name: &str) -> Result<String, OutputCodecError> {
1399        self.inner.encode(codec_name)
1400    }
1401}
1402
1403/// A named interpretation of an IRTG.
1404#[derive(Debug)]
1405pub struct Interpretation {
1406    name: String,
1407    class_name: String,
1408    kind: InterpretationKind,
1409    algebra: Mutex<Box<dyn Any + Send>>,
1410    algebra_signature: Signature,
1411    homomorphism: Homomorphism,
1412    renderer: Box<dyn DerivationRenderer>,
1413}
1414
1415impl Interpretation {
1416    fn decompose_string(
1417        &self,
1418        value: Vec<Symbol>,
1419    ) -> Result<crate::StringDecompositionAutomaton, IrtgError> {
1420        let algebra = self.algebra.lock().unwrap();
1421        let algebra = algebra
1422            .as_ref()
1423            .downcast_ref::<StringAlgebra>()
1424            .ok_or_else(|| IrtgError::WrongInputType {
1425                interpretation: self.name.clone(),
1426            })?;
1427        Ok(algebra.decompose(value))
1428    }
1429
1430    fn decompose_tag_string(
1431        &self,
1432        value: TagStringValue<Symbol>,
1433    ) -> Result<TagStringDecompositionAutomaton, IrtgError> {
1434        let algebra = self.algebra.lock().unwrap();
1435        let algebra = algebra
1436            .as_ref()
1437            .downcast_ref::<TagStringAlgebra>()
1438            .ok_or_else(|| IrtgError::WrongInputType {
1439                interpretation: self.name.clone(),
1440            })?;
1441        algebra
1442            .decompose(value)
1443            .ok_or_else(|| IrtgError::ObjectParse {
1444                interpretation: self.name.clone(),
1445                message: "TAG decomposition inputs must be contiguous strings".to_owned(),
1446            })
1447    }
1448
1449    fn decompose_tag_tree(&self, value: Tree) -> Result<TagTreeDecompositionAutomaton, IrtgError> {
1450        let algebra = self.algebra.lock().unwrap();
1451        let algebra = algebra
1452            .as_ref()
1453            .downcast_ref::<TagTreeAlgebra>()
1454            .ok_or_else(|| IrtgError::WrongInputType {
1455                interpretation: self.name.clone(),
1456            })?;
1457        Ok(algebra.decompose(value))
1458    }
1459
1460    fn decompose_binarized_tag_tree(
1461        &self,
1462        value: Tree,
1463    ) -> Result<BinarizedTagTreeDecompositionAutomaton, IrtgError> {
1464        let algebra = self.algebra.lock().unwrap();
1465        let algebra = algebra
1466            .as_ref()
1467            .downcast_ref::<Binarizing<TagTreeAlgebra>>()
1468            .ok_or_else(|| IrtgError::WrongInputType {
1469                interpretation: self.name.clone(),
1470            })?;
1471        let append = algebra
1472            .append_symbol()
1473            .ok_or_else(|| IrtgError::ObjectParse {
1474                interpretation: self.name.clone(),
1475                message: "binarizing TAG tree algebra has no append symbol".to_owned(),
1476            })?;
1477        Ok(BinarizedTagTreeDecompositionAutomaton::new(
1478            algebra.inner().decompose_binarized(value),
1479            append,
1480        ))
1481    }
1482
1483    /// Return the interpretation name.
1484    pub fn name(&self) -> &str {
1485        &self.name
1486    }
1487
1488    /// Return the declared Alto algebra class name.
1489    pub fn class_name(&self) -> &str {
1490        &self.class_name
1491    }
1492
1493    /// Return the algebra signature.
1494    pub fn algebra_signature(&self) -> &Signature {
1495        &self.algebra_signature
1496    }
1497
1498    /// Return the homomorphism.
1499    pub fn homomorphism(&self) -> &Homomorphism {
1500        &self.homomorphism
1501    }
1502
1503    /// Return whether this interpretation can serve as a parse input (its algebra is decomposable).
1504    /// Output-only interpretations (e.g. tree algebras) return `false`.
1505    pub fn is_inputable(&self) -> bool {
1506        !matches!(self.kind, InterpretationKind::OutputOnly)
1507    }
1508
1509    /// Return whether this interpretation can constrain parsing to defined values.
1510    pub fn supports_non_null_filter(&self) -> bool {
1511        self.algebra
1512            .lock()
1513            .unwrap()
1514            .as_ref()
1515            .is::<FeatureStructureAlgebra>()
1516    }
1517
1518    /// Return whether interpreted values use the tree textual representation.
1519    pub fn is_tree_valued(&self) -> bool {
1520        matches!(
1521            self.kind,
1522            InterpretationKind::TagTree | InterpretationKind::BinarizedTagTree
1523        ) || (self.kind == InterpretationKind::OutputOnly
1524            && self.class_name != FEATURE_STRUCTURE_ALGEBRA)
1525    }
1526
1527    /// Parse a textual object with this interpretation's algebra, returning a type-erased
1528    /// value suitable for [`input_erased`](Self::input_erased).
1529    pub fn parse_object_erased(&self, input: &str) -> Result<Box<dyn Any + Send>, IrtgError> {
1530        match self.kind {
1531            InterpretationKind::String => {
1532                let mut algebra = self.algebra.lock().unwrap();
1533                let algebra = algebra
1534                    .as_mut()
1535                    .downcast_mut::<StringAlgebra>()
1536                    .ok_or_else(|| IrtgError::WrongInputType {
1537                        interpretation: self.name.clone(),
1538                    })?;
1539                let value = algebra
1540                    .parse_object(input)
1541                    .map_err(|err| IrtgError::ObjectParse {
1542                        interpretation: self.name.clone(),
1543                        message: err.to_string(),
1544                    })?;
1545                Ok(Box::new(value))
1546            }
1547            InterpretationKind::TagString => {
1548                let mut algebra = self.algebra.lock().unwrap();
1549                let algebra = algebra
1550                    .as_mut()
1551                    .downcast_mut::<TagStringAlgebra>()
1552                    .ok_or_else(|| IrtgError::WrongInputType {
1553                        interpretation: self.name.clone(),
1554                    })?;
1555                let value = algebra
1556                    .parse_object(input)
1557                    .map_err(|err| IrtgError::ObjectParse {
1558                        interpretation: self.name.clone(),
1559                        message: err.to_string(),
1560                    })?;
1561                Ok(Box::new(value))
1562            }
1563            InterpretationKind::TagTree => {
1564                let mut algebra = self.algebra.lock().unwrap();
1565                let algebra = algebra
1566                    .as_mut()
1567                    .downcast_mut::<TagTreeAlgebra>()
1568                    .ok_or_else(|| IrtgError::WrongInputType {
1569                        interpretation: self.name.clone(),
1570                    })?;
1571                let value = algebra
1572                    .parse_object(input)
1573                    .map_err(|err| IrtgError::ObjectParse {
1574                        interpretation: self.name.clone(),
1575                        message: err.to_string(),
1576                    })?;
1577                Ok(Box::new(value))
1578            }
1579            InterpretationKind::BinarizedTagTree => {
1580                let mut algebra = self.algebra.lock().unwrap();
1581                let algebra = algebra
1582                    .as_mut()
1583                    .downcast_mut::<Binarizing<TagTreeAlgebra>>()
1584                    .ok_or_else(|| IrtgError::WrongInputType {
1585                        interpretation: self.name.clone(),
1586                    })?;
1587                let value = algebra
1588                    .parse_object(input)
1589                    .map_err(|err| IrtgError::ObjectParse {
1590                        interpretation: self.name.clone(),
1591                        message: err.to_string(),
1592                    })?;
1593                Ok(Box::new(value))
1594            }
1595            InterpretationKind::OutputOnly => Err(IrtgError::ObjectParse {
1596                interpretation: self.name.clone(),
1597                message: "interpretation is output-only and cannot be parsed as a parse input"
1598                    .to_owned(),
1599            }),
1600        }
1601    }
1602
1603    /// Package a type-erased value (from [`parse_object_erased`](Self::parse_object_erased))
1604    /// as a [`ParseInput`] for [`Irtg::parse`].
1605    pub fn input_erased(&self, value: Box<dyn Any + Send>) -> ParseInput<'_> {
1606        ParseInput {
1607            interpretation: self,
1608            value,
1609        }
1610    }
1611
1612    /// Interpret a derivation tree (over grammar symbols) through this interpretation and
1613    /// render the resulting algebra value as text using the interpretation's output codec.
1614    pub fn interpret_to_string(
1615        &self,
1616        arena: &TreeArena<Symbol>,
1617        root: Tree,
1618    ) -> Result<String, IrtgError> {
1619        let algebra = self.algebra.lock().unwrap();
1620        self.renderer
1621            .render(&**algebra, &self.homomorphism, arena, root, &self.name)
1622    }
1623
1624    /// Evaluate a derivation while retaining its typed value for visualization and codecs.
1625    pub fn evaluate_derivation(
1626        &self,
1627        arena: &TreeArena<Symbol>,
1628        root: Tree,
1629    ) -> Result<EvaluatedAlgebraValue, IrtgError> {
1630        let algebra = self.algebra.lock().unwrap();
1631        self.renderer
1632            .evaluate(&**algebra, &self.homomorphism, arena, root, &self.name)
1633    }
1634}
1635
1636#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1637enum InterpretationKind {
1638    /// A [`StringAlgebra`] interpretation: can be a parse input (decomposable).
1639    String,
1640    /// A decomposable TAG string interpretation.
1641    TagString,
1642    /// A decomposable TAG derived-tree interpretation.
1643    TagTree,
1644    /// A decomposable binarized TAG derived-tree interpretation.
1645    BinarizedTagTree,
1646    /// An output-only interpretation (e.g. a tree algebra): values can be produced from a
1647    /// derivation tree, but the algebra cannot be decomposed for use as a parse input.
1648    OutputOnly,
1649}
1650
1651/// Typed access to an interpretation.
1652pub struct TypedInterpretation<'i, A> {
1653    interpretation: &'i Interpretation,
1654    _algebra: PhantomData<A>,
1655}
1656
1657impl<'i, A> TypedInterpretation<'i, A>
1658where
1659    A: Algebra + 'static,
1660    A::InternalValue: Send + 'static,
1661    A::ParseError: fmt::Display,
1662{
1663    /// Return the interpretation name.
1664    pub fn name(&self) -> &str {
1665        self.interpretation.name()
1666    }
1667
1668    /// Return the interpretation's algebra signature.
1669    pub fn algebra_signature(&self) -> &Signature {
1670        self.interpretation.algebra_signature()
1671    }
1672
1673    /// Return the interpretation's homomorphism.
1674    pub fn homomorphism(&self) -> &Homomorphism {
1675        self.interpretation.homomorphism()
1676    }
1677
1678    /// Parse a textual object using the interpretation's algebra (to its internal value).
1679    pub fn parse_object(&self, input: &str) -> Result<A::InternalValue, IrtgError> {
1680        let mut algebra = self.interpretation.algebra.lock().unwrap();
1681        let algebra =
1682            algebra
1683                .as_mut()
1684                .downcast_mut::<A>()
1685                .ok_or_else(|| IrtgError::WrongInputType {
1686                    interpretation: self.interpretation.name.clone(),
1687                })?;
1688        algebra
1689            .parse_object(input)
1690            .map_err(|err| IrtgError::ObjectParse {
1691                interpretation: self.interpretation.name.clone(),
1692                message: err.to_string(),
1693            })
1694    }
1695
1696    /// Package a typed internal value as an input for [`Irtg::parse`].
1697    pub fn input(&self, value: A::InternalValue) -> ParseInput<'i> {
1698        ParseInput {
1699            interpretation: self.interpretation,
1700            value: Box::new(value),
1701        }
1702    }
1703
1704    /// Interpret a derivation tree and return the typed external algebra value.
1705    pub fn interpret_derivation(
1706        &self,
1707        arena: &TreeArena<Symbol>,
1708        root: Tree,
1709    ) -> Result<A::Value, IrtgError> {
1710        let algebra = self.interpretation.algebra.lock().unwrap();
1711        let algebra =
1712            algebra
1713                .as_ref()
1714                .downcast_ref::<A>()
1715                .ok_or_else(|| IrtgError::WrongInputType {
1716                    interpretation: self.interpretation.name.clone(),
1717                })?;
1718        let mut image = TreeArena::new();
1719        let image_root = self
1720            .interpretation
1721            .homomorphism
1722            .apply(arena, root, &mut image)?;
1723        algebra
1724            .evaluate_term(&image, image_root)
1725            .ok_or_else(|| IrtgError::ObjectParse {
1726                interpretation: self.interpretation.name.clone(),
1727                message: "derivation tree did not evaluate in the algebra".to_owned(),
1728            })
1729    }
1730}
1731
1732/// Type-erased parse input created by a typed interpretation handle.
1733pub struct ParseInput<'i> {
1734    interpretation: &'i Interpretation,
1735    value: Box<dyn Any + Send>,
1736}
1737
1738/// The parse chart returned by [`Irtg::parse`].
1739#[derive(Debug)]
1740pub struct ParseChart {
1741    /// Explicit grammar chart after all input constraints were applied.
1742    pub automaton: Explicit,
1743    /// Human-readable state labels in dense state-ID order.
1744    pub state_names: Vec<String>,
1745    /// State-label components in dense state-ID order.
1746    ///
1747    /// The first component is the grammar state. Each subsequent component is
1748    /// the state contributed by one decomposition automaton, in intersection
1749    /// order. Frontends can use these components for styling or inspection
1750    /// without parsing [`Self::state_names`].
1751    ///
1752    /// A decomposition automaton makes its states meaningful here by giving
1753    /// its `State` type a concise, deterministic [`std::fmt::Display`]
1754    /// implementation. Display output should identify the state to a user,
1755    /// avoid debug-only details and unstable IDs, and be unambiguous within
1756    /// that automaton. For example, string and TAG-string decomposition states
1757    /// display as `[0-2]` and `[0-2, 3-5]`.
1758    pub state_parts: Vec<Vec<String>>,
1759    /// Per-intersection materialization statistics.
1760    pub stats: Vec<IndexedCondensedIntersectionStats>,
1761}
1762
1763/// Errors returned by IRTG parsing, construction, and parsing.
1764#[derive(Debug, Error)]
1765pub enum IrtgError {
1766    /// The caller requested cooperative cancellation.
1767    ///
1768    /// No partial parse chart or filtered chart is returned. Because
1769    /// cancellation is cooperative, the operation may finish its current
1770    /// indivisible automaton or algebra callback before returning this error.
1771    #[error("parsing cancelled")]
1772    Cancelled,
1773    /// Input bytes were not valid UTF-8.
1774    #[error("input is not valid UTF-8: {0}")]
1775    Utf8(#[from] std::string::FromUtf8Error),
1776    /// Reading failed.
1777    #[error("failed to read IRTG: {0}")]
1778    Io(#[from] std::io::Error),
1779    /// Syntax error.
1780    #[error("{0}")]
1781    Syntax(String),
1782    /// A signature rejected a symbol.
1783    #[error("{0}")]
1784    Signature(#[from] SignatureError),
1785    /// A homomorphism rejected an image.
1786    #[error("{0}")]
1787    Homomorphism(#[from] HomomorphismError),
1788    /// The grammar automaton could not be built.
1789    #[error("{0}")]
1790    Automaton(#[from] ExplicitBuildError),
1791    /// A named interpretation was not found.
1792    #[error("unknown interpretation {0:?}")]
1793    UnknownInterpretation(String),
1794    /// A requested interpretation has a different concrete algebra type.
1795    #[error("interpretation {interpretation:?} has algebra {actual}, not {requested}")]
1796    WrongAlgebraType {
1797        /// Interpretation name.
1798        interpretation: String,
1799        /// Requested Rust type.
1800        requested: &'static str,
1801        /// Actual Alto class name.
1802        actual: String,
1803    },
1804    /// A parse input value has the wrong concrete value type.
1805    #[error("wrong input value type for interpretation {interpretation:?}")]
1806    WrongInputType {
1807        /// Interpretation name.
1808        interpretation: String,
1809    },
1810    /// An output-only interpretation was used as a parse input.
1811    #[error("interpretation {interpretation:?} is output-only and cannot be a parse input")]
1812    NotInputable {
1813        /// Interpretation name.
1814        interpretation: String,
1815    },
1816    /// An interpretation's algebra does not provide an all-defined-values filter.
1817    #[error(
1818        "interpretation {interpretation:?} with algebra {class_name} does not support non-null filtering"
1819    )]
1820    NonNullFilterUnsupported {
1821        /// Interpretation name.
1822        interpretation: String,
1823        /// Alto algebra class name.
1824        class_name: String,
1825    },
1826    /// A parse strategy selected a heuristic that is not defined for this algebra.
1827    #[error("heuristic is incompatible with interpretation {interpretation:?}: {message}")]
1828    IncompatibleHeuristic {
1829        /// Interpretation name.
1830        interpretation: String,
1831        /// Human-readable incompatibility.
1832        message: String,
1833    },
1834    /// The algebra could not parse an object.
1835    #[error("failed to parse object for interpretation {interpretation:?}: {message}")]
1836    ObjectParse {
1837        /// Interpretation name.
1838        interpretation: String,
1839        /// Parser error.
1840        message: String,
1841    },
1842    /// The declared algebra is not implemented yet.
1843    #[error("unsupported algebra {class_name} for interpretation {interpretation:?}")]
1844    UnsupportedAlgebra {
1845        /// Interpretation name.
1846        interpretation: String,
1847        /// Alto class name.
1848        class_name: String,
1849    },
1850    /// A* strategy requires all grammar rule weights ≤ 1 (probability weights).
1851    #[error(
1852        "A* requires all grammar rule weights ≤ 1, but found weight {weight} for rule {symbol:?}"
1853    )]
1854    AstarWeightPrecondition {
1855        /// The offending weight.
1856        weight: f64,
1857        /// The grammar symbol name of the offending rule.
1858        symbol: String,
1859    },
1860}
1861
1862/// Parse an Alto-format IRTG from UTF-8 bytes.
1863pub fn parse_irtg<R: Read>(mut reader: R) -> Result<Irtg, IrtgError> {
1864    let mut bytes = Vec::new();
1865    reader.read_to_end(&mut bytes)?;
1866    let input = String::from_utf8(bytes)?;
1867    let tokens = lex(&input).map_err(irtg_lex_error)?;
1868    let ast = alto_grammar::IrtgParser::new()
1869        .parse(tokens.into_iter().map(Ok))
1870        .map_err(irtg_parse_error)?;
1871    build_irtg(ast)
1872}
1873
1874pub(crate) fn build_irtg(ast: AstIrtg) -> Result<Irtg, IrtgError> {
1875    let mut builder = ExplicitBuilder::new();
1876    let mut states = Interner::new();
1877    let mut state_ids = FxHashMap::default();
1878    let mut grammar_signature = Signature::new();
1879    let mut homs = FxHashMap::default();
1880    let mut algebra_signatures = FxHashMap::default();
1881    let interpretation_algebras = ast
1882        .interpretations
1883        .iter()
1884        .map(|decl| (decl.name.clone(), decl.algebra.clone()))
1885        .collect::<FxHashMap<_, _>>();
1886
1887    for decl in &ast.interpretations {
1888        homs.insert(decl.name.clone(), Homomorphism::new());
1889        algebra_signatures.insert(decl.name.clone(), Signature::new());
1890    }
1891
1892    for rule in ast.rules {
1893        let parent = state_id(&mut builder, &mut states, &mut state_ids, &rule.auto.parent);
1894        if rule.auto.parent.is_final {
1895            builder.add_accepting(parent);
1896        }
1897        let child_ids: Vec<_> = rule
1898            .auto
1899            .children
1900            .iter()
1901            .map(|child| {
1902                let id = state_id(&mut builder, &mut states, &mut state_ids, child);
1903                if child.is_final {
1904                    builder.add_accepting(id);
1905                }
1906                id
1907            })
1908            .collect();
1909        let symbol = grammar_signature.intern(rule.auto.symbol.clone(), child_ids.len())?;
1910        builder.add_weighted_rule(symbol, child_ids, parent, rule.auto.weight.unwrap_or(1.0));
1911
1912        for hom_rule in rule.homs {
1913            let Some(hom) = homs.get_mut(&hom_rule.interpretation) else {
1914                return Err(IrtgError::UnknownInterpretation(hom_rule.interpretation));
1915            };
1916            let signature = algebra_signatures
1917                .get_mut(&hom_rule.interpretation)
1918                .expect("hom and signature maps are initialized together");
1919            let normalized;
1920            let term = if interpretation_algebras
1921                .get(&hom_rule.interpretation)
1922                .map(String::as_str)
1923                == Some(FEATURE_STRUCTURE_ALGEBRA)
1924            {
1925                normalized = normalize_legacy_feature_term(&hom_rule.term);
1926                &normalized
1927            } else {
1928                &hom_rule.term
1929            };
1930            let term = lower_hom_term(term, hom, signature)?;
1931            hom.add(symbol, rule.auto.children.len(), term)?;
1932        }
1933    }
1934
1935    let mut interpretations = FxHashMap::default();
1936    for decl in ast.interpretations {
1937        let (kind, algebra, algebra_signature, renderer): (
1938            InterpretationKind,
1939            Box<dyn Any + Send>,
1940            Signature,
1941            Box<dyn DerivationRenderer>,
1942        ) = if decl.algebra == STRING_ALGEBRA {
1943            let signature = algebra_signatures.remove(&decl.name).unwrap_or_default();
1944            let algebra = StringAlgebra::with_signature(signature.clone());
1945            let renderer = Box::new(TypedDerivationCodec::<StringAlgebra, SpaceJoinCodec>::new(
1946                SpaceJoinCodec,
1947            ));
1948            (
1949                InterpretationKind::String,
1950                Box::new(algebra),
1951                signature,
1952                renderer,
1953            )
1954        } else if decl.algebra == TAG_STRING_ALGEBRA {
1955            let signature = algebra_signatures.remove(&decl.name).unwrap_or_default();
1956            let algebra = TagStringAlgebra::with_signature(signature.clone());
1957            let renderer = Box::new(TypedDerivationCodec::<TagStringAlgebra, DisplayCodec>::new(
1958                DisplayCodec,
1959            ));
1960            (
1961                InterpretationKind::TagString,
1962                Box::new(algebra),
1963                signature,
1964                renderer,
1965            )
1966        } else if decl.algebra == TAG_TREE_ALGEBRA || decl.algebra == TAG_TREE_WITH_ARITIES_ALGEBRA
1967        {
1968            let signature = algebra_signatures.remove(&decl.name).unwrap_or_default();
1969            let algebra = if decl.algebra == TAG_TREE_WITH_ARITIES_ALGEBRA {
1970                TagTreeAlgebra::with_arities(signature.clone())
1971            } else {
1972                TagTreeAlgebra::tree(signature.clone())
1973            };
1974            let renderer = Box::new(TypedDerivationCodec::<TagTreeAlgebra, DisplayCodec>::new(
1975                DisplayCodec,
1976            ));
1977            (
1978                InterpretationKind::TagTree,
1979                Box::new(algebra),
1980                signature,
1981                renderer,
1982            )
1983        } else if decl.algebra == BINARIZING_TAG_TREE_ALGEBRA
1984            || decl.algebra == BINARIZING_TAG_TREE_WITH_ARITIES_ALGEBRA
1985        {
1986            let mut signature = algebra_signatures.remove(&decl.name).unwrap_or_default();
1987            let append = Some(signature.intern(APPEND_SYMBOL.to_owned(), 2)?);
1988            let inner = if decl.algebra == BINARIZING_TAG_TREE_WITH_ARITIES_ALGEBRA {
1989                TagTreeAlgebra::with_arities(signature.clone())
1990            } else {
1991                TagTreeAlgebra::tree(signature.clone())
1992            };
1993            let algebra = Binarizing::new(inner, append);
1994            let renderer = Box::new(TypedDerivationCodec::<
1995                Binarizing<TagTreeAlgebra>,
1996                DisplayCodec,
1997            >::new(DisplayCodec));
1998            (
1999                InterpretationKind::BinarizedTagTree,
2000                Box::new(algebra),
2001                signature,
2002                renderer,
2003            )
2004        } else if decl.algebra == FEATURE_STRUCTURE_ALGEBRA {
2005            let signature = algebra_signatures.remove(&decl.name).unwrap_or_default();
2006            let algebra = FeatureStructureAlgebra::with_signature(signature.clone());
2007            let renderer = Box::new(
2008                TypedDerivationCodec::<FeatureStructureAlgebra, DisplayCodec>::new(DisplayCodec),
2009            );
2010            (
2011                InterpretationKind::OutputOnly,
2012                Box::new(algebra),
2013                signature,
2014                renderer,
2015            )
2016        } else if decl.algebra == TREE_WITH_ARITIES_ALGEBRA {
2017            let signature = algebra_signatures.remove(&decl.name).unwrap_or_default();
2018            let algebra = TreeAlgebra::with_arities(signature.clone());
2019            let renderer = Box::new(TypedDerivationCodec::<TreeAlgebra, DisplayCodec>::new(
2020                DisplayCodec,
2021            ));
2022            (
2023                InterpretationKind::OutputOnly,
2024                Box::new(algebra),
2025                signature,
2026                renderer,
2027            )
2028        } else if decl.algebra == BINARIZING_TREE_WITH_ARITIES_ALGEBRA {
2029            let signature = algebra_signatures.remove(&decl.name).unwrap_or_default();
2030            let append = signature.get(APPEND_SYMBOL);
2031            let algebra = Binarizing::new(TreeAlgebra::with_arities(signature.clone()), append);
2032            let renderer = Box::new(
2033                TypedDerivationCodec::<Binarizing<TreeAlgebra>, DisplayCodec>::new(DisplayCodec),
2034            );
2035            (
2036                InterpretationKind::OutputOnly,
2037                Box::new(algebra),
2038                signature,
2039                renderer,
2040            )
2041        } else {
2042            return Err(IrtgError::UnsupportedAlgebra {
2043                interpretation: decl.name.clone(),
2044                class_name: decl.algebra.clone(),
2045            });
2046        };
2047        let homomorphism = homs.remove(&decl.name).unwrap_or_else(Homomorphism::new);
2048        interpretations.insert(
2049            decl.name.clone(),
2050            Interpretation {
2051                name: decl.name,
2052                class_name: decl.algebra,
2053                kind,
2054                algebra: Mutex::new(algebra),
2055                algebra_signature,
2056                homomorphism,
2057                renderer,
2058            },
2059        );
2060    }
2061
2062    Ok(Irtg {
2063        grammar: builder.try_build()?,
2064        states,
2065        grammar_signature,
2066        interpretations,
2067    })
2068}
2069
2070fn state_id(
2071    builder: &mut ExplicitBuilder,
2072    states: &mut Interner<String>,
2073    state_ids: &mut FxHashMap<String, StateId>,
2074    state: &AstState,
2075) -> StateId {
2076    if let Some(&id) = state_ids.get(&state.name) {
2077        return id;
2078    }
2079    let id = builder.new_state();
2080    let interned = states.intern(state.name.clone());
2081    debug_assert_eq!(id, interned);
2082    state_ids.insert(state.name.clone(), id);
2083    id
2084}
2085
2086fn normalize_legacy_feature_term(term: &AstHomTerm) -> AstHomTerm {
2087    match term {
2088        AstHomTerm::Variable(variable) => AstHomTerm::Variable(*variable),
2089        AstHomTerm::Symbol(label, children) => {
2090            let children = children.iter().map(normalize_legacy_feature_term).collect();
2091            if let Some(specification) = label.strip_prefix("emba_") {
2092                let mut attributes = specification.split('_');
2093                if let (Some(top), Some(bottom)) = (attributes.next(), attributes.next()) {
2094                    return AstHomTerm::Symbol(format!("remap_root={top},foot={bottom}"), children);
2095                }
2096            }
2097            AstHomTerm::Symbol(label.clone(), children)
2098        }
2099    }
2100}
2101
2102fn lower_hom_term(
2103    term: &AstHomTerm,
2104    hom: &mut Homomorphism,
2105    signature: &mut Signature,
2106) -> Result<Tree, IrtgError> {
2107    match term {
2108        AstHomTerm::Variable(variable) => {
2109            if *variable == 0 {
2110                return Err(IrtgError::Syntax(
2111                    "Alto homomorphism variables are one-based; ?0 is invalid".to_owned(),
2112                ));
2113            }
2114            Ok(hom.add_var(variable - 1))
2115        }
2116        AstHomTerm::Symbol(name, children) => {
2117            let children = children
2118                .iter()
2119                .map(|child| lower_hom_term(child, hom, signature))
2120                .collect::<Result<Vec<_>, _>>()?;
2121            let symbol = signature.intern(name.clone(), children.len())?;
2122            Ok(hom.add_symbol(symbol, children))
2123        }
2124    }
2125}
2126
2127fn irtg_lex_error(err: LexError) -> IrtgError {
2128    IrtgError::Syntax(err.to_string())
2129}
2130
2131fn irtg_parse_error(err: ParseError<usize, Tok, String>) -> IrtgError {
2132    IrtgError::Syntax(match err {
2133        ParseError::InvalidToken { location } => format!("invalid token at byte {location}"),
2134        ParseError::UnrecognizedEof { location, expected } => {
2135            format!(
2136                "unexpected EOF at byte {location}; expected {}",
2137                expected.join(", ")
2138            )
2139        }
2140        ParseError::UnrecognizedToken { token, expected } => format!(
2141            "unexpected token {:?} at byte {}; expected {}",
2142            token.1,
2143            token.0,
2144            expected.join(", ")
2145        ),
2146        ParseError::ExtraToken { token } => {
2147            format!("unexpected extra token {:?} at byte {}", token.1, token.0)
2148        }
2149        ParseError::User { error } => error,
2150    })
2151}
2152
2153#[cfg(test)]
2154mod tests {
2155    use super::*;
2156
2157    #[test]
2158    fn parses_tiny_string_irtg_and_accepts_compatible_input() {
2159        let irtg = parse_irtg(
2160            br#"
2161            interpretation english: de.up.ling.irtg.algebra.StringAlgebra
2162
2163            S! -> r(NP,VP) [1.0]
2164              [english] *(?1,?2)
2165
2166            NP -> john_rule
2167              [english] john
2168
2169            VP -> watches_rule
2170              [english] watches
2171            "# as &[u8],
2172        )
2173        .unwrap();
2174
2175        let english = irtg.interpretation::<StringAlgebra>("english").unwrap();
2176        let value = english.parse_object("john watches").unwrap();
2177        let chart = irtg.parse([english.input(value)]).unwrap();
2178        assert!(!chart.automaton.is_empty());
2179
2180        let bad = english.parse_object("john sleeps").unwrap();
2181        let chart = irtg.parse([english.input(bad)]).unwrap();
2182        assert!(chart.automaton.is_empty());
2183    }
2184
2185    #[test]
2186    fn typed_interpretation_returns_tree_value_without_rendering() {
2187        let irtg = parse_irtg(
2188            br#"
2189            interpretation string: de.up.ling.irtg.algebra.StringAlgebra
2190            interpretation tree: de.up.ling.irtg.algebra.TreeWithAritiesAlgebra
2191
2192            S! -> r(A,B)
2193              [string] *(?1,?2)
2194              [tree] S_2(?1,?2)
2195
2196            A -> a
2197              [string] a
2198              [tree] NN_1(a_0)
2199
2200            B -> b
2201              [string] b
2202              [tree] VB_1(b_0)
2203            "# as &[u8],
2204        )
2205        .unwrap();
2206
2207        let string = irtg.interpretation::<StringAlgebra>("string").unwrap();
2208        let input = string.parse_object("a b").unwrap();
2209        let best = irtg
2210            .best_with(
2211                [string.input(input)],
2212                &MaterializationStrategy::TopDownCondensed,
2213            )
2214            .unwrap()
2215            .unwrap();
2216        let tree = irtg.interpretation::<TreeAlgebra>("tree").unwrap();
2217        let value = tree
2218            .interpret_derivation(best.arena(), best.root())
2219            .unwrap();
2220        assert_eq!(value.to_string(), "S(NN(a), VB(b))");
2221        assert_eq!(value.arena().get_label(value.root()), "S");
2222    }
2223
2224    #[test]
2225    fn parses_multi_interpretation_irtg_and_enforces_both_inputs() {
2226        let irtg = parse_irtg(
2227            br#"
2228            interpretation english: de.up.ling.irtg.algebra.StringAlgebra
2229            interpretation german: de.up.ling.irtg.algebra.StringAlgebra
2230
2231            S! -> r(A,B)
2232              [english] *(?1,?2)
2233              [german] *(?1,?2)
2234
2235            A -> a
2236              [english] john
2237              [german] hans
2238
2239            B -> b
2240              [english] watches
2241              [german] sieht
2242            "# as &[u8],
2243        )
2244        .unwrap();
2245
2246        let english = irtg.interpretation::<StringAlgebra>("english").unwrap();
2247        let german = irtg.interpretation::<StringAlgebra>("german").unwrap();
2248        let english_value = english.parse_object("john watches").unwrap();
2249        let german_value = german.parse_object("hans sieht").unwrap();
2250        let chart = irtg
2251            .parse([english.input(english_value), german.input(german_value)])
2252            .unwrap();
2253        assert!(!chart.automaton.is_empty());
2254
2255        let english_value = english.parse_object("john watches").unwrap();
2256        let german_value = german.parse_object("hans schaut").unwrap();
2257        let chart = irtg
2258            .parse([english.input(english_value), german.input(german_value)])
2259            .unwrap();
2260        assert!(chart.automaton.is_empty());
2261    }
2262
2263    #[test]
2264    fn reads_actual_alto_format_cfg_fixture() {
2265        let irtg = parse_irtg(include_bytes!("../benchdata/irtg/cfg.irtg").as_slice()).unwrap();
2266        let interpretation = irtg.interpretation::<StringAlgebra>("i").unwrap();
2267        let value = interpretation
2268            .parse_object("john watches the woman")
2269            .unwrap();
2270        let chart = irtg.parse([interpretation.input(value)]).unwrap();
2271        assert!(!chart.automaton.is_empty());
2272        assert_eq!(irtg.grammar().rules().count(), 12);
2273    }
2274
2275    #[test]
2276    fn parses_features_comments_quoted_names_and_scientific_weights() {
2277        let irtg = parse_irtg(
2278            br#"
2279            interpretation 'surface': de.up.ling.irtg.algebra.StringAlgebra
2280            feature constructor: SomeFeature(A, B)
2281            /* block comment */
2282            'S root'! -> 'r root'('A one') [3.3921302578018993E-4]
2283              [surface] wrap(?1) // line comment
2284
2285            'A one' -> leaf()
2286              [surface] 'hello world'
2287            "# as &[u8],
2288        )
2289        .unwrap();
2290
2291        assert_eq!(irtg.grammar().rules().count(), 2);
2292        let parent = irtg.states().get(&"S root".to_owned()).unwrap();
2293        let symbol = irtg.grammar_signature().get("r root").unwrap();
2294        let rule = irtg
2295            .grammar()
2296            .rules()
2297            .find(|rule| rule.symbol == symbol)
2298            .unwrap();
2299        assert_eq!(rule.result, parent);
2300        assert!((rule.weight - 3.3921302578018993E-4).abs() < 1e-12);
2301        let surface = irtg.interpretation::<StringAlgebra>("surface").unwrap();
2302        assert!(surface.algebra_signature().get("wrap").is_some());
2303        assert!(surface.algebra_signature().get("hello world").is_some());
2304    }
2305
2306    #[test]
2307    fn rejects_unknown_hom_interpretation() {
2308        let err = parse_irtg(
2309            br#"
2310            interpretation i: de.up.ling.irtg.algebra.StringAlgebra
2311            S! -> r
2312              [missing] x
2313            "# as &[u8],
2314        )
2315        .unwrap_err();
2316        assert!(matches!(err, IrtgError::UnknownInterpretation(name) if name == "missing"));
2317    }
2318
2319    #[test]
2320    fn parse_irtg_rejects_invalid_utf8_reader() {
2321        let err = parse_irtg(&b"\xff"[..]).unwrap_err();
2322        assert!(matches!(err, IrtgError::Utf8(_)));
2323    }
2324
2325    #[test]
2326    fn rejects_zero_variable() {
2327        let err = parse_irtg(
2328            br#"
2329            interpretation i: de.up.ling.irtg.algebra.StringAlgebra
2330            S! -> r(A)
2331              [i] ?0
2332            "# as &[u8],
2333        )
2334        .unwrap_err();
2335        assert!(matches!(err, IrtgError::Syntax(message) if message.contains("?0")));
2336    }
2337
2338    #[test]
2339    fn rejects_duplicate_grammar_transitions() {
2340        let err = parse_irtg(
2341            br#"
2342            interpretation i: de.up.ling.irtg.algebra.StringAlgebra
2343            S! -> r [1.0]
2344            S! -> r [2.0]
2345            "# as &[u8],
2346        )
2347        .unwrap_err();
2348        assert!(matches!(err, IrtgError::Automaton(_)));
2349    }
2350
2351    // -----------------------------------------------------------------------
2352    // Phase E: MaterializationStrategy tests
2353    // -----------------------------------------------------------------------
2354
2355    fn tiny_irtg_bytes() -> &'static [u8] {
2356        br#"
2357        interpretation english: de.up.ling.irtg.algebra.StringAlgebra
2358
2359        S! -> r(NP,VP) [1.0]
2360          [english] *(?1,?2)
2361
2362        NP -> john_rule [1.0]
2363          [english] john
2364
2365        VP -> watches_rule [1.0]
2366          [english] watches
2367        "#
2368    }
2369
2370    #[test]
2371    fn parse_with_top_down_condensed_matches_parse() {
2372        let irtg = parse_irtg(tiny_irtg_bytes()).unwrap();
2373        let english = irtg.interpretation::<StringAlgebra>("english").unwrap();
2374
2375        let value = english.parse_object("john watches").unwrap();
2376        let chart_default = irtg.parse([english.input(value.clone())]).unwrap();
2377
2378        let chart_explicit = irtg
2379            .parse_with(
2380                [english.input(value)],
2381                &MaterializationStrategy::TopDownCondensed,
2382            )
2383            .unwrap();
2384
2385        // Both strategies should agree on emptiness.
2386        assert_eq!(
2387            chart_default.automaton.is_empty(),
2388            chart_explicit.automaton.is_empty()
2389        );
2390        assert!(!chart_explicit.automaton.is_empty());
2391    }
2392
2393    #[test]
2394    fn parse_with_indexed_condensed_works() {
2395        let irtg = parse_irtg(tiny_irtg_bytes()).unwrap();
2396        let english = irtg.interpretation::<StringAlgebra>("english").unwrap();
2397
2398        let value = english.parse_object("john watches").unwrap();
2399        let chart = irtg
2400            .parse_with(
2401                [english.input(value)],
2402                &MaterializationStrategy::IndexedCondensed,
2403            )
2404            .unwrap();
2405
2406        assert!(!chart.automaton.is_empty());
2407
2408        // Rejection case.
2409        let bad = english.parse_object("john sleeps").unwrap();
2410        let chart = irtg
2411            .parse_with(
2412                [english.input(bad)],
2413                &MaterializationStrategy::IndexedCondensed,
2414            )
2415            .unwrap();
2416        assert!(chart.automaton.is_empty());
2417    }
2418
2419    #[test]
2420    fn parse_with_astar_zero_returns_non_empty_chart_for_known_sentence() {
2421        let irtg = parse_irtg(tiny_irtg_bytes()).unwrap();
2422        let english = irtg.interpretation::<StringAlgebra>("english").unwrap();
2423
2424        let value = english.parse_object("john watches").unwrap();
2425        let chart = irtg
2426            .parse_with(
2427                [english.input(value)],
2428                &MaterializationStrategy::Astar {
2429                    heuristic: AstarHeuristic::Zero,
2430                    options: AstarOptions {
2431                        stop_at_first_goal: true,
2432                        beam: None,
2433                    },
2434                },
2435            )
2436            .unwrap();
2437
2438        assert!(!chart.automaton.is_empty());
2439    }
2440
2441    #[test]
2442    fn string_chart_state_names_are_consistent_across_strategies() {
2443        let irtg = parse_irtg(tiny_irtg_bytes()).unwrap();
2444        let english = irtg.interpretation::<StringAlgebra>("english").unwrap();
2445        let strategies = [
2446            MaterializationStrategy::TopDownCondensed,
2447            MaterializationStrategy::IndexedCondensed,
2448            MaterializationStrategy::Astar {
2449                heuristic: AstarHeuristic::Zero,
2450                options: AstarOptions {
2451                    stop_at_first_goal: false,
2452                    beam: None,
2453                },
2454            },
2455        ];
2456
2457        for strategy in strategies {
2458            let value = english.parse_object("john watches").unwrap();
2459            let chart = irtg.parse_with([english.input(value)], &strategy).unwrap();
2460            assert!(chart.state_names.iter().any(|name| name == "NP[0-1]"));
2461            assert!(chart.state_names.iter().any(|name| name == "VP[1-2]"));
2462            assert!(chart.state_names.iter().any(|name| name == "S[0-2]"));
2463            assert!(chart.state_names.iter().all(|name| !name.starts_with('q')));
2464        }
2465    }
2466
2467    #[test]
2468    fn astar_strategy_rejects_grammar_with_weight_greater_than_one() {
2469        let irtg = parse_irtg(
2470            br#"
2471            interpretation i: de.up.ling.irtg.algebra.StringAlgebra
2472
2473            S! -> r [5.0]
2474              [i] hello
2475
2476            "# as &[u8],
2477        )
2478        .unwrap();
2479
2480        let interp = irtg.interpretation::<StringAlgebra>("i").unwrap();
2481        let value = interp.parse_object("hello").unwrap();
2482
2483        let err = irtg
2484            .parse_with(
2485                [interp.input(value)],
2486                &MaterializationStrategy::Astar {
2487                    heuristic: AstarHeuristic::Zero,
2488                    options: AstarOptions {
2489                        stop_at_first_goal: true,
2490                        beam: None,
2491                    },
2492                },
2493            )
2494            .unwrap_err();
2495
2496        assert!(
2497            matches!(err, IrtgError::AstarWeightPrecondition { weight, .. } if weight > 1.0),
2498            "expected AstarWeightPrecondition, got {:?}",
2499            err
2500        );
2501    }
2502
2503    #[test]
2504    fn astar_weight_precondition_error_contains_weight_and_symbol() {
2505        let irtg = parse_irtg(
2506            br#"
2507            interpretation i: de.up.ling.irtg.algebra.StringAlgebra
2508
2509            S! -> my_rule [3.5]
2510              [i] hello
2511            "# as &[u8],
2512        )
2513        .unwrap();
2514
2515        let interp = irtg.interpretation::<StringAlgebra>("i").unwrap();
2516        let value = interp.parse_object("hello").unwrap();
2517
2518        let err = irtg
2519            .parse_with(
2520                [interp.input(value)],
2521                &MaterializationStrategy::Astar {
2522                    heuristic: AstarHeuristic::Zero,
2523                    options: AstarOptions {
2524                        stop_at_first_goal: true,
2525                        beam: None,
2526                    },
2527                },
2528            )
2529            .unwrap_err();
2530
2531        if let IrtgError::AstarWeightPrecondition { weight, symbol } = &err {
2532            assert!((weight - 3.5).abs() < 1e-10);
2533            assert!(symbol.contains("my_rule"), "symbol = {symbol}");
2534        } else {
2535            panic!("expected AstarWeightPrecondition, got {:?}", err);
2536        }
2537
2538        // Check error message.
2539        let msg = err.to_string();
2540        assert!(msg.contains("3.5") || msg.contains("A*"), "message = {msg}");
2541    }
2542
2543    #[test]
2544    fn best_with_astar_returns_some_for_known_sentence() {
2545        let irtg = parse_irtg(tiny_irtg_bytes()).unwrap();
2546        let english = irtg.interpretation::<StringAlgebra>("english").unwrap();
2547
2548        let value = english.parse_object("john watches").unwrap();
2549        let result = irtg
2550            .best_with(
2551                [english.input(value)],
2552                &MaterializationStrategy::Astar {
2553                    heuristic: AstarHeuristic::Zero,
2554                    options: AstarOptions {
2555                        stop_at_first_goal: true,
2556                        beam: None,
2557                    },
2558                },
2559            )
2560            .unwrap();
2561
2562        assert!(result.is_some(), "expected a best tree for 'john watches'");
2563    }
2564
2565    #[test]
2566    fn best_with_chart_strategy_returns_some_for_known_sentence() {
2567        let irtg = parse_irtg(tiny_irtg_bytes()).unwrap();
2568        let english = irtg.interpretation::<StringAlgebra>("english").unwrap();
2569
2570        let value = english.parse_object("john watches").unwrap();
2571        let result = irtg
2572            .best_with(
2573                [english.input(value)],
2574                &MaterializationStrategy::TopDownCondensed,
2575            )
2576            .unwrap();
2577
2578        assert!(result.is_some(), "expected a best tree for 'john watches'");
2579    }
2580
2581    fn tiny_tag_irtg() -> &'static [u8] {
2582        br#"
2583        interpretation string: de.up.ling.irtg.algebra.TagStringAlgebra
2584        interpretation tree: de.up.ling.irtg.algebra.TagTreeAlgebra
2585
2586        S! -> r
2587          [string] *WRAP21*(*CONC12*(a, *EE*), b)
2588          [tree] @(*, f(a))
2589        "#
2590    }
2591
2592    #[test]
2593    fn tag_string_and_tree_are_both_parse_inputs() {
2594        let irtg = parse_irtg(tiny_tag_irtg()).unwrap();
2595        let string = irtg.interpretation::<TagStringAlgebra>("string").unwrap();
2596        let tree = irtg.interpretation::<TagTreeAlgebra>("tree").unwrap();
2597        let string_value = string.parse_object("a b").unwrap();
2598        let tree_value = tree.parse_object("f(a)").unwrap();
2599
2600        let chart = irtg
2601            .parse([string.input(string_value), tree.input(tree_value)])
2602            .unwrap();
2603        assert!(chart.automaton.viterbi().is_some());
2604    }
2605
2606    #[test]
2607    fn tag_inputs_support_generic_zero_astar() {
2608        let irtg = parse_irtg(tiny_tag_irtg()).unwrap();
2609        let string = irtg.interpretation::<TagStringAlgebra>("string").unwrap();
2610        let value = string.parse_object("a b").unwrap();
2611        let result = irtg
2612            .best_with(
2613                [string.input(value)],
2614                &MaterializationStrategy::Astar {
2615                    heuristic: AstarHeuristic::Zero,
2616                    options: AstarOptions {
2617                        stop_at_first_goal: true,
2618                        beam: None,
2619                    },
2620                },
2621            )
2622            .unwrap();
2623        assert!(result.is_some());
2624    }
2625
2626    #[test]
2627    fn generic_algebra_state_names_are_consistent_across_strategies() {
2628        let irtg = parse_irtg(tiny_tag_irtg()).unwrap();
2629
2630        for interpretation in ["string", "tree"] {
2631            let mut expected_names = None;
2632            let strategies = [
2633                MaterializationStrategy::TopDownCondensed,
2634                MaterializationStrategy::IndexedCondensed,
2635                MaterializationStrategy::Astar {
2636                    heuristic: AstarHeuristic::Zero,
2637                    options: AstarOptions {
2638                        stop_at_first_goal: false,
2639                        beam: None,
2640                    },
2641                },
2642            ];
2643
2644            for strategy in strategies {
2645                let input = irtg
2646                    .interpretation_ref(interpretation)
2647                    .unwrap()
2648                    .parse_object_erased(if interpretation == "string" {
2649                        "a b"
2650                    } else {
2651                        "f(a)"
2652                    })
2653                    .unwrap();
2654                let chart = irtg
2655                    .parse_with(
2656                        [irtg
2657                            .interpretation_ref(interpretation)
2658                            .unwrap()
2659                            .input_erased(input)],
2660                        &strategy,
2661                    )
2662                    .unwrap();
2663                let mut names = chart.state_names;
2664                names.sort();
2665                assert!(names.iter().all(|name| !name.starts_with('q')));
2666                assert!(names.iter().all(|name| name.contains(" × ")));
2667                assert!(chart.state_parts.iter().all(|parts| parts.len() == 2));
2668                if interpretation == "string" {
2669                    assert!(
2670                        chart
2671                            .state_parts
2672                            .iter()
2673                            .any(|parts| { parts[1] == "[0-2]" || parts[1] == "[0-1, 1-2]" })
2674                    );
2675                }
2676                if let Some(expected) = &expected_names {
2677                    assert_eq!(&names, expected);
2678                } else {
2679                    expected_names = Some(names);
2680                }
2681            }
2682        }
2683    }
2684
2685    #[test]
2686    fn binarized_tag_tree_is_inputable() {
2687        let irtg = parse_irtg(
2688            br#"
2689            interpretation tree: de.up.ling.irtg.algebra.BinarizingTagTreeAlgebra
2690
2691            S! -> r
2692              [tree] f(_@_(a, _@_(b, c)))
2693            "# as &[u8],
2694        )
2695        .unwrap();
2696        let tree = irtg
2697            .interpretation::<Binarizing<TagTreeAlgebra>>("tree")
2698            .unwrap();
2699        let value = tree.parse_object("f(a, b, c)").unwrap();
2700        let chart = irtg.parse([tree.input(value)]).unwrap();
2701        assert!(chart.automaton.viterbi().is_some());
2702    }
2703
2704    #[test]
2705    fn legacy_alto_aux_embedding_is_normalized_outside_the_algebra() {
2706        let irtg = parse_irtg(
2707            br#"
2708            interpretation ft: de.up.ling.irtg.algebra.FeatureStructureAlgebra
2709
2710            S! -> combine(A)
2711              [ft] emba_top_bottom(?1)
2712
2713            A -> value
2714              [ft] "[root: #x [], foot: #x]"
2715            "# as &[u8],
2716        )
2717        .unwrap();
2718        let ft = irtg
2719            .interpretation::<FeatureStructureAlgebra>("ft")
2720            .unwrap();
2721        assert!(ft.algebra_signature().get("emba_top_bottom").is_none());
2722        assert!(
2723            ft.algebra_signature()
2724                .get("remap_root=top,foot=bottom")
2725                .is_some()
2726        );
2727    }
2728}