Skip to main content

gazelle/
table.rs

1use crate::grammar::SymbolId;
2use crate::runtime::ErrorContext;
3
4/// Grammar metadata for error reporting.
5/// Only carries data not available through [`ParseTable`].
6#[doc(hidden)]
7#[derive(Debug, Clone, Copy)]
8pub struct ErrorInfo<'a> {
9    /// Symbol names indexed by SymbolId.
10    pub symbol_names: &'a [&'a str],
11    /// Active items (rule, dot) per state.
12    pub state_items: &'a [&'a [(u16, u8)]],
13    /// RHS symbol IDs per rule.
14    pub rule_rhs: &'a [&'a [u32]],
15    /// Accessing symbol for each state.
16    pub state_symbols: &'a [u32],
17}
18
19impl ErrorContext for ErrorInfo<'_> {
20    fn symbol_name(&self, id: SymbolId) -> &str {
21        self.symbol_names
22            .get(id.0 as usize)
23            .copied()
24            .unwrap_or("<?>")
25    }
26
27    fn state_symbol(&self, state: usize) -> SymbolId {
28        SymbolId(self.state_symbols.get(state).copied().unwrap_or(0))
29    }
30
31    fn state_items(&self, state: usize) -> &[(u16, u8)] {
32        self.state_items.get(state).copied().unwrap_or(&[])
33    }
34
35    fn rule_rhs(&self, rule: usize) -> &[u32] {
36        self.rule_rhs.get(rule).copied().unwrap_or(&[])
37    }
38}
39
40// ============================================================================
41// Everything below requires alloc
42// ============================================================================
43
44mod alloc_impl {
45    use alloc::collections::{BTreeMap, BTreeSet};
46    use alloc::{format, string::String, vec, vec::Vec};
47
48    use crate::grammar::{Grammar, SymbolId};
49    use crate::lr::{GrammarInternal, to_grammar_internal};
50    use crate::runtime::{ErrorContext, OpEntry, ParseTable};
51
52    type Row = Vec<(u32, u32)>;
53    type RowGroup = (Row, Vec<usize>);
54
55    /// A conflict between two actions in the parse table.
56    ///
57    #[derive(Debug, Clone, PartialEq, Eq)]
58    pub enum Conflict {
59        ShiftReduce {
60            terminal: SymbolId,
61            reduce_rule: usize,
62            example: String,
63        },
64        ReduceReduce {
65            terminal: SymbolId,
66            rule1: usize,
67            rule2: usize,
68            example: String,
69        },
70    }
71
72    /// Owned parse table data produced by [`CompiledTable::build`].
73    ///
74    /// This holds the compressed table arrays, grammar, and conflict info.
75    /// Use [`CompiledTable::table`] to get a lightweight [`ParseTable`] for parsing.
76    #[derive(Debug)]
77    pub struct CompiledTable {
78        // Shared data/check arrays (bison-style: action + goto share the same table)
79        data: Vec<u32>,
80        check: Vec<u32>,
81        // Separate base arrays
82        action_base: Vec<i32>, // indexed by state
83        goto_base: Vec<i32>,   // indexed by non-terminal
84
85        /// Rules: (lhs_id, rhs_len) for each rule.
86        rules: Vec<(u32, u8)>,
87
88        /// Number of terminals (including EOF) for goto default indexing.
89        num_terminals: u32,
90
91        /// The augmented grammar.
92        pub(crate) grammar: GrammarInternal,
93        /// Number of states.
94        pub(crate) num_states: usize,
95        /// Conflicts paired with example strings.
96        pub(crate) conflicts: Vec<Conflict>,
97
98        // Error reporting data
99        /// Active items (rule, dot) per state.
100        state_items: Vec<Vec<(u16, u8)>>,
101        /// RHS symbol IDs per rule.
102        rule_rhs: Vec<Vec<u32>>,
103        /// Accessing symbol for each state.
104        state_symbols: Vec<u32>,
105        /// Default reduce rule per state (0 = no default).
106        default_reduce: Vec<u32>,
107        /// Default goto target per non-terminal (u32::MAX = no default).
108        default_goto: Vec<u32>,
109    }
110
111    /// Return the most frequent value, or u32::MAX if empty.
112    fn most_frequent(iter: impl Iterator<Item = u32>) -> u32 {
113        let mut counts: BTreeMap<u32, usize> = BTreeMap::new();
114        for v in iter {
115            *counts.entry(v).or_default() += 1;
116        }
117        counts
118            .into_iter()
119            .max_by_key(|&(_, c)| c)
120            .map(|(v, _)| v)
121            .unwrap_or(u32::MAX)
122    }
123
124    /// Pack rows of (col, value) into shared data/check arrays.
125    /// Returns `(data, check, bases)` where `bases[i]` is the displacement for row `i`.
126    /// Identical rows share the same base (row deduplication).
127    fn compact_rows(rows: &[Row]) -> (Vec<u32>, Vec<u32>, Vec<i32>) {
128        let mut bases = vec![0i32; rows.len()];
129
130        // Dedup: identical rows share the same base.
131        let mut dedup: BTreeMap<Row, Vec<usize>> = BTreeMap::new();
132        for (i, row) in rows.iter().enumerate() {
133            dedup.entry(row.clone()).or_default().push(i);
134        }
135
136        let mut unique_rows: Vec<RowGroup> = dedup.into_iter().collect();
137        unique_rows.sort_by(|a, b| b.0.len().cmp(&a.0.len()).then_with(|| a.1[0].cmp(&b.1[0])));
138
139        let init_size = rows.len() * 2;
140        let mut data = vec![0u32; init_size];
141        let mut check: Vec<u32> = vec![u32::MAX; init_size];
142        let mut used_bases = BTreeSet::new();
143
144        for (row, members) in &unique_rows {
145            if row.is_empty() {
146                for &idx in members {
147                    let mut displacement = 0i32;
148                    while !used_bases.insert(displacement) {
149                        displacement += 1;
150                    }
151                    bases[idx] = displacement;
152                }
153                continue;
154            }
155
156            let min_col = row.iter().map(|(c, _)| *c).min().unwrap_or(0) as i32;
157
158            let mut displacement = -min_col;
159            loop {
160                if !used_bases.contains(&displacement) {
161                    let mut ok = true;
162                    for &(col, _) in row {
163                        let slot = (displacement + col as i32) as usize;
164                        if slot >= check.len() {
165                            let new_size = (slot + 1).max(data.len() * 2);
166                            data.resize(new_size, 0);
167                            check.resize(new_size, u32::MAX);
168                        }
169                        if check[slot] != u32::MAX {
170                            ok = false;
171                            break;
172                        }
173                    }
174                    if ok {
175                        break;
176                    }
177                }
178                displacement += 1;
179            }
180
181            used_bases.insert(displacement);
182            for &(col, value) in row {
183                let slot = (displacement + col as i32) as usize;
184                data[slot] = value;
185                check[slot] = col;
186            }
187            for &idx in members {
188                bases[idx] = displacement;
189            }
190        }
191
192        (data, check, bases)
193    }
194
195    impl CompiledTable {
196        /// Build parse tables from a grammar.
197        ///
198        /// Returns an error if grammar conversion fails (for example, unknown symbols).
199        pub fn build(grammar: &Grammar) -> Result<Self, String> {
200            let internal = to_grammar_internal(grammar)?;
201            Ok(Self::build_from_internal(&internal))
202        }
203
204        /// Build parse tables from internal grammar representation using NFA → DFA → Hopcroft.
205        pub(crate) fn build_from_internal(grammar: &GrammarInternal) -> Self {
206            let result = crate::lr::build_minimal_automaton(grammar);
207            let num_terminals = grammar.symbols.num_terminals();
208            let num_item_states = result.num_item_states;
209            let num_non_terminals = grammar.symbols.num_non_terminals() as usize;
210
211            // Classify each DFA transition from item states.
212            // For each item state: collect reduce rules (for default_reduce).
213            // For each non-terminal: collect goto targets (for default_goto).
214            let mut reduce_rules_per_state: Vec<Vec<u32>> = vec![Vec::new(); num_item_states];
215            let mut goto_targets_per_nt: Vec<Vec<u32>> = vec![Vec::new(); num_non_terminals];
216
217            for (state, transitions) in result
218                .dfa
219                .transitions
220                .iter()
221                .take(num_item_states)
222                .enumerate()
223            {
224                for &(sym, target) in transitions {
225                    if result.reduce_to_real.contains_key(&sym) {
226                        continue;
227                    }
228                    if sym < num_terminals && target >= num_item_states {
229                        reduce_rules_per_state[state].push((target - num_item_states) as u32);
230                    } else if sym >= num_terminals
231                        && sym < grammar.symbols.num_symbols()
232                        && target < num_item_states
233                    {
234                        let nt_idx = (sym - num_terminals) as usize;
235                        goto_targets_per_nt[nt_idx].push(target as u32);
236                    }
237                }
238            }
239
240            // Default reduce: most frequent reduce rule per state (skip accept = rule 0).
241            let default_reduce: Vec<u32> = reduce_rules_per_state
242                .iter()
243                .map(|rules| {
244                    let default = most_frequent(rules.iter().filter(|&&r| r > 0).copied());
245                    if default != u32::MAX { default } else { 0 }
246                })
247                .collect();
248
249            // Default goto: most frequent target per non-terminal.
250            let default_goto: Vec<u32> = goto_targets_per_nt
251                .iter()
252                .map(|targets| most_frequent(targets.iter().copied()))
253                .collect();
254
255            // State symbols: for each item state, what symbol reaches it.
256            let mut state_symbols = vec![0u32; num_item_states];
257            for state in 0..num_item_states {
258                for &(sym, target) in &result.dfa.transitions[state] {
259                    if target < num_item_states {
260                        state_symbols[target] = sym;
261                    }
262                }
263            }
264
265            // Reverse map: real terminal → virtual reduce symbol.
266            let mut real_to_virtual: BTreeMap<u32, u32> = BTreeMap::new();
267            for (&virtual_id, &real_id) in &result.reduce_to_real {
268                real_to_virtual.insert(real_id, virtual_id);
269            }
270
271            // Helper: look up transition target by symbol in a state.
272            let find_target = |state: usize, sym: u32| -> Option<usize> {
273                result.dfa.transitions[state]
274                    .iter()
275                    .find(|&&(s, _)| s == sym)
276                    .map(|&(_, t)| t)
277            };
278
279            // Build rows: action rows (indexed by state), then goto rows (indexed by non-terminal).
280            let mut rows: Vec<Row> = Vec::with_capacity(num_item_states + num_non_terminals);
281
282            for (state, &dr) in default_reduce.iter().take(num_item_states).enumerate() {
283                let mut row: Row = Vec::new();
284
285                for sym in grammar.symbols.terminal_ids() {
286                    let shift = find_target(state, sym.0).filter(|&t| t < num_item_states);
287                    let reduce = if let Some(&virtual_id) = real_to_virtual.get(&sym.0) {
288                        // Non-plain terminal: reduce comes from virtual symbol transition.
289                        find_target(state, virtual_id)
290                            .filter(|&t| t >= num_item_states)
291                            .map(|t| t - num_item_states)
292                    } else {
293                        // Plain: reduce comes from the same terminal's transition.
294                        find_target(state, sym.0)
295                            .filter(|&t| t >= num_item_states)
296                            .map(|t| t - num_item_states)
297                    };
298
299                    let entry = match (shift, reduce) {
300                        (Some(s), Some(r)) => {
301                            use crate::grammar::TerminalKind;
302                            match grammar.symbols.terminal_kind(sym) {
303                                TerminalKind::Shift => OpEntry::shift(s),
304                                TerminalKind::Reduce => OpEntry::reduce(r),
305                                TerminalKind::Prec | TerminalKind::Conflict => {
306                                    OpEntry::shift_or_reduce(s, r)
307                                }
308                                TerminalKind::Plain => unreachable!(),
309                            }
310                        }
311                        (Some(s), None) => OpEntry::shift(s),
312                        (None, Some(r)) => {
313                            if dr > 0 && r as u32 == dr {
314                                continue;
315                            }
316                            OpEntry::reduce(r)
317                        }
318                        (None, None) => continue,
319                    };
320                    row.push((sym.0, entry.0));
321                }
322
323                rows.push(row);
324            }
325
326            // Goto rows (transposed: indexed by non-terminal, col = state).
327            for (nt_idx, &default_target) in default_goto.iter().take(num_non_terminals).enumerate()
328            {
329                let mut row: Row = Vec::new();
330                let sym = num_terminals + nt_idx as u32;
331                for state in 0..num_item_states {
332                    if let Some(target) = find_target(state, sym)
333                        && target < num_item_states
334                        && target as u32 != default_target
335                    {
336                        row.push((state as u32, target as u32));
337                    }
338                }
339                rows.push(row);
340            }
341
342            let (data, check, bases) = compact_rows(&rows);
343            let (action_base, goto_base) = bases.split_at(num_item_states);
344
345            let rules: Vec<(u32, u8)> = grammar
346                .rules
347                .iter()
348                .map(|r| (r.lhs.id().0, r.rhs.len() as u8))
349                .collect();
350
351            let rule_rhs: Vec<Vec<u32>> = grammar
352                .rules
353                .iter()
354                .map(|r| r.rhs.iter().map(|s| s.id().0).collect())
355                .collect();
356
357            CompiledTable {
358                data,
359                check,
360                action_base: action_base.to_vec(),
361                goto_base: goto_base.to_vec(),
362                num_terminals: grammar.symbols.num_terminals(),
363                grammar: grammar.clone(),
364                rules,
365                num_states: num_item_states,
366                conflicts: result.conflicts,
367                state_items: result.state_items,
368                rule_rhs,
369                state_symbols,
370                default_reduce,
371                default_goto,
372            }
373        }
374
375        /// Get a lightweight [`ParseTable`] borrowing from this compiled table.
376        pub fn table(&self) -> ParseTable<'_> {
377            ParseTable::new(
378                &self.data,
379                &self.check,
380                &self.action_base,
381                &self.goto_base,
382                &self.rules,
383                self.num_terminals,
384                &self.default_reduce,
385                &self.default_goto,
386            )
387        }
388
389        /// Returns true if the table has conflicts.
390        pub fn has_conflicts(&self) -> bool {
391            !self.conflicts.is_empty()
392        }
393
394        /// Format conflicts as human-readable error messages (one string per conflict).
395        pub fn format_conflicts(&self) -> Vec<String> {
396            use alloc::collections::BTreeSet;
397            let mut seen = BTreeSet::new();
398            let mut messages = Vec::new();
399            for c in &self.conflicts {
400                let msg = match c {
401                    Conflict::ShiftReduce {
402                        terminal,
403                        reduce_rule,
404                        example,
405                    } => {
406                        let item =
407                            self.format_item(*reduce_rule, self.rule_rhs[*reduce_rule].len());
408                        let is_ambiguity = example.starts_with("Ambiguity");
409                        let mut msg = if is_ambiguity {
410                            "Shift/reduce conflict:".into()
411                        } else {
412                            let term_name = self.grammar.symbols.name(*terminal);
413                            format!("Shift/reduce conflict on '{}':", term_name)
414                        };
415                        msg.push_str(&format!("\n  Shift wins over:\n    {}", item));
416                        if !example.is_empty() {
417                            let indented = example.replace('\n', "\n  ");
418                            msg.push_str(&format!("\n  {}", indented));
419                        }
420                        msg
421                    }
422                    Conflict::ReduceReduce {
423                        terminal,
424                        rule1,
425                        rule2,
426                        example,
427                    } => {
428                        let item1 = self.format_item(*rule1, self.rule_rhs[*rule1].len());
429                        let item2 = self.format_item(*rule2, self.rule_rhs[*rule2].len());
430                        let is_ambiguity = example.starts_with("Ambiguity");
431                        let mut msg = if is_ambiguity {
432                            format!(
433                                "Reduce/reduce conflict:\n  \
434                                 Reduce: {} (wins)\n  \
435                                 Reduce: {}",
436                                item1, item2,
437                            )
438                        } else {
439                            let term_name = self.grammar.symbols.name(*terminal);
440                            format!(
441                                "Reduce/reduce conflict on '{}':\n  \
442                                 Reduce: {} (wins)\n  \
443                                 Reduce: {}",
444                                term_name, item1, item2,
445                            )
446                        };
447                        if !example.is_empty() {
448                            let indented = example.replace('\n', "\n  ");
449                            msg.push_str(&format!("\n  {}", indented));
450                        }
451                        msg
452                    }
453                };
454                if seen.insert(msg.clone()) {
455                    messages.push(msg);
456                }
457            }
458            messages
459        }
460
461        /// Format an item as "lhs -> rhs1 rhs2 • rhs3 ..."
462        fn format_item(&self, rule_idx: usize, dot: usize) -> String {
463            let rule = &self.grammar.rules[rule_idx];
464            let lhs_name = self.grammar.symbols.name(rule.lhs.id());
465            let rhs = &self.rule_rhs[rule_idx];
466            let mut s = format!("{} ->", lhs_name);
467            for (i, &sym_id) in rhs.iter().enumerate() {
468                if i == dot {
469                    s.push_str(" \u{2022}");
470                }
471                s.push(' ');
472                s.push_str(self.grammar.symbols.name(SymbolId(sym_id)));
473            }
474            if dot == rhs.len() {
475                s.push_str(" \u{2022}");
476            }
477            s
478        }
479
480        /// Lookup symbol ID by name.
481        pub fn symbol_id(&self, name: &str) -> Option<SymbolId> {
482            self.grammar.symbols.get_id(name)
483        }
484
485        /// Get the name of a symbol by ID.
486        pub fn symbol_name(&self, id: SymbolId) -> &str {
487            self.grammar.symbols.name(id)
488        }
489
490        /// Get the total number of symbols.
491        pub fn num_symbols(&self) -> u32 {
492            self.grammar.symbols.num_symbols()
493        }
494
495        /// Get the number of terminal symbols.
496        pub fn num_terminals(&self) -> u32 {
497            self.grammar.symbols.num_terminals()
498        }
499
500        /// Get the number of parser states.
501        pub fn num_states(&self) -> usize {
502            self.num_states
503        }
504
505        /// Get the conflicts detected during table construction, paired with examples.
506        pub fn conflicts(&self) -> &[Conflict] {
507            &self.conflicts
508        }
509
510        // Accessors for compressed table arrays (for codegen/serialization)
511
512        #[doc(hidden)]
513        pub fn table_data(&self) -> &[u32] {
514            &self.data
515        }
516
517        #[doc(hidden)]
518        pub fn table_check(&self) -> &[u32] {
519            &self.check
520        }
521
522        #[doc(hidden)]
523        pub fn action_base(&self) -> &[i32] {
524            &self.action_base
525        }
526
527        #[doc(hidden)]
528        pub fn goto_base(&self) -> &[i32] {
529            &self.goto_base
530        }
531
532        #[doc(hidden)]
533        pub fn rules(&self) -> &[(u32, u8)] {
534            &self.rules
535        }
536
537        #[doc(hidden)]
538        pub fn state_items(&self) -> &[Vec<(u16, u8)>] {
539            &self.state_items
540        }
541
542        #[doc(hidden)]
543        pub fn rule_rhs(&self) -> &[Vec<u32>] {
544            &self.rule_rhs
545        }
546
547        #[doc(hidden)]
548        pub fn rule_name(&self, rule: usize) -> Option<&str> {
549            self.grammar.rules.get(rule).and_then(|r| {
550                if let crate::lr::AltAction::Named(name) = &r.action {
551                    Some(name.as_str())
552                } else {
553                    None
554                }
555            })
556        }
557
558        #[doc(hidden)]
559        pub fn state_symbols(&self) -> &[u32] {
560            &self.state_symbols
561        }
562
563        #[doc(hidden)]
564        pub fn default_reduce(&self) -> &[u32] {
565            &self.default_reduce
566        }
567
568        #[doc(hidden)]
569        pub fn default_goto(&self) -> &[u32] {
570            &self.default_goto
571        }
572    }
573
574    impl ErrorContext for CompiledTable {
575        fn symbol_name(&self, id: SymbolId) -> &str {
576            self.grammar.symbols.name(id)
577        }
578
579        fn state_symbol(&self, state: usize) -> SymbolId {
580            SymbolId(self.state_symbols.get(state).copied().unwrap_or(0))
581        }
582
583        fn state_items(&self, state: usize) -> &[(u16, u8)] {
584            self.state_items
585                .get(state)
586                .map(|v| v.as_slice())
587                .unwrap_or(&[])
588        }
589
590        fn rule_rhs(&self, rule: usize) -> &[u32] {
591            self.rule_rhs.get(rule).map(|v| v.as_slice()).unwrap_or(&[])
592        }
593    }
594}
595
596pub use alloc_impl::{CompiledTable, Conflict};
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601    use crate::lr::{GrammarInternal, to_grammar_internal};
602    use crate::meta::parse_grammar;
603    use crate::runtime::ParserOp;
604
605    fn simple_grammar() -> GrammarInternal {
606        to_grammar_internal(
607            &parse_grammar(
608                r#"
609            start s; terminals { a } s = a => a;
610        "#,
611            )
612            .unwrap(),
613        )
614        .unwrap()
615    }
616
617    fn expr_grammar() -> GrammarInternal {
618        to_grammar_internal(
619            &parse_grammar(
620                r#"
621            start expr;
622            terminals { PLUS, NUM }
623            expr = expr PLUS term => add | term => term;
624            term = NUM => num;
625        "#,
626            )
627            .unwrap(),
628        )
629        .unwrap()
630    }
631
632    fn ambiguous_grammar() -> GrammarInternal {
633        to_grammar_internal(
634            &parse_grammar(
635                r#"
636            start expr;
637            terminals { PLUS, NUM }
638            expr = expr PLUS expr => add | NUM => num;
639        "#,
640            )
641            .unwrap(),
642        )
643        .unwrap()
644    }
645
646    fn prec_grammar() -> GrammarInternal {
647        to_grammar_internal(
648            &parse_grammar(
649                r#"
650            start expr;
651            terminals { prec OP, NUM }
652            expr = expr OP expr => binop | NUM => num;
653        "#,
654            )
655            .unwrap(),
656        )
657        .unwrap()
658    }
659
660    #[test]
661    fn test_simple_table() {
662        let grammar = simple_grammar();
663        let compiled = CompiledTable::build_from_internal(&grammar);
664        let table = compiled.table();
665
666        assert!(!compiled.has_conflicts());
667
668        let a_id = compiled.symbol_id("a").unwrap();
669        match table.action(0, a_id) {
670            ParserOp::Shift(_) => {}
671            other => panic!("Expected Shift, got {:?}", other),
672        }
673    }
674
675    #[test]
676    fn test_expr_table() {
677        let grammar = expr_grammar();
678        let compiled = CompiledTable::build_from_internal(&grammar);
679        let table = compiled.table();
680
681        assert!(!compiled.has_conflicts());
682
683        let num_id = compiled.symbol_id("NUM").unwrap();
684        match table.action(0, num_id) {
685            ParserOp::Shift(_) => {}
686            other => panic!("Expected Shift on NUM, got {:?}", other),
687        }
688    }
689
690    #[test]
691    fn test_ambiguous_grammar() {
692        let grammar = ambiguous_grammar();
693        let compiled = CompiledTable::build_from_internal(&grammar);
694
695        assert!(
696            compiled.has_conflicts(),
697            "Expected conflicts for ambiguous grammar"
698        );
699
700        let has_sr_conflict = compiled
701            .conflicts
702            .iter()
703            .any(|c| matches!(c, Conflict::ShiftReduce { .. }));
704        assert!(has_sr_conflict, "Expected shift/reduce conflict");
705
706        // Test conflict formatting (includes examples inline)
707        let messages = compiled.format_conflicts();
708        assert!(!messages.is_empty(), "Expected formatted conflict messages");
709        let msg = &messages[0];
710        assert!(
711            msg.contains("Shift/reduce conflict"),
712            "Should describe conflict type: {}",
713            msg
714        );
715        // Ambiguity conflicts omit the terminal
716        assert!(
717            !msg.contains("'PLUS'"),
718            "Ambiguity should not mention terminal: {}",
719            msg
720        );
721        // Should contain items with dots
722        assert!(
723            msg.contains("\u{2022}"),
724            "Should contain dot in item: {}",
725            msg
726        );
727        // Should contain ambiguity info
728        assert!(
729            msg.contains("Ambiguity in"),
730            "Should contain ambiguity: {}",
731            msg
732        );
733        assert!(msg.contains("expr"), "Should mention expr: {}", msg);
734    }
735
736    #[test]
737    fn test_conflict_example_rr() {
738        let grammar = to_grammar_internal(
739            &parse_grammar(
740                r#"
741            start s;
742            terminals { A }
743            s = x => x | y => y;
744            x = A => a;
745            y = A => a;
746        "#,
747            )
748            .unwrap(),
749        )
750        .unwrap();
751        let compiled = CompiledTable::build_from_internal(&grammar);
752
753        assert!(compiled.has_conflicts(), "Expected R/R conflict");
754        let has_rr = compiled
755            .conflicts
756            .iter()
757            .any(|c| matches!(c, Conflict::ReduceReduce { .. }));
758        assert!(has_rr, "Expected reduce/reduce conflict");
759
760        // Examples are now inline in format_conflicts
761        let messages = compiled.format_conflicts();
762        let msg = &messages[0];
763        assert!(
764            msg.contains("Reduce/reduce conflict"),
765            "Should describe R/R: {}",
766            msg
767        );
768        assert!(
769            msg.contains("Ambiguity in"),
770            "Should contain ambiguity info: {}",
771            msg,
772        );
773    }
774
775    #[test]
776    fn test_conflict_example_rr_epsilon_bracketing() {
777        let grammar = to_grammar_internal(
778            &parse_grammar(
779                r#"
780            start s;
781            terminals { A }
782            s = x => x | y => y;
783            x = _ => e;
784            y = _ => e;
785        "#,
786            )
787            .unwrap(),
788        )
789        .unwrap();
790        let compiled = CompiledTable::build_from_internal(&grammar);
791
792        let messages = compiled.format_conflicts();
793        let msg = &messages[0];
794        assert!(
795            msg.contains("Reduce/reduce conflict"),
796            "Should describe R/R: {}",
797            msg
798        );
799        assert!(
800            msg.contains("Ambiguity in"),
801            "Should contain ambiguity info: {}",
802            msg,
803        );
804    }
805
806    #[test]
807    fn test_shift_terminal_resolves_conflict() {
808        // Same ambiguous grammar but with `shift PLUS` — no conflicts.
809        let grammar = to_grammar_internal(
810            &parse_grammar(
811                r#"
812            start expr;
813            terminals { shift PLUS, NUM }
814            expr = expr PLUS expr => add | NUM => num;
815        "#,
816            )
817            .unwrap(),
818        )
819        .unwrap();
820
821        let compiled = CompiledTable::build_from_internal(&grammar);
822        assert!(
823            !compiled.has_conflicts(),
824            "shift terminal should resolve S/R conflict"
825        );
826    }
827
828    #[test]
829    fn test_reduce_terminal_resolves_conflict() {
830        // Same grammar with `reduce PLUS` — reduce wins, no conflicts.
831        let grammar = to_grammar_internal(
832            &parse_grammar(
833                r#"
834            start expr;
835            terminals { reduce PLUS, NUM }
836            expr = expr PLUS expr => add | NUM => num;
837        "#,
838            )
839            .unwrap(),
840        )
841        .unwrap();
842
843        let compiled = CompiledTable::build_from_internal(&grammar);
844        assert!(
845            !compiled.has_conflicts(),
846            "reduce terminal should resolve S/R conflict"
847        );
848    }
849
850    #[test]
851    fn test_conflict_terminal_resolves_conflict() {
852        // Same grammar with `conflict PLUS` — runtime decision, no conflicts.
853        let grammar = to_grammar_internal(
854            &parse_grammar(
855                r#"
856            start expr;
857            terminals { conflict PLUS, NUM }
858            expr = expr PLUS expr => add | NUM => num;
859        "#,
860            )
861            .unwrap(),
862        )
863        .unwrap();
864
865        let compiled = CompiledTable::build_from_internal(&grammar);
866        assert!(
867            !compiled.has_conflicts(),
868            "conflict terminal should suppress S/R conflict"
869        );
870    }
871
872    #[test]
873    fn test_no_conflict_examples_for_clean_grammar() {
874        let grammar = expr_grammar();
875        let compiled = CompiledTable::build_from_internal(&grammar);
876        assert!(!compiled.has_conflicts());
877        assert!(compiled.conflicts().is_empty());
878    }
879
880    #[test]
881    fn test_prec_terminal_no_conflict() {
882        let grammar = prec_grammar();
883        let compiled = CompiledTable::build_from_internal(&grammar);
884        let table = compiled.table();
885
886        assert!(
887            !compiled.has_conflicts(),
888            "PrecTerminal should not report conflicts"
889        );
890
891        // Find state with ShiftOrReduce
892        let op_id = compiled.symbol_id("OP").unwrap();
893        let mut found_shift_or_reduce = false;
894        for state in 0..compiled.num_states() {
895            if let ParserOp::ShiftOrReduce { .. } = table.action(state, op_id) {
896                found_shift_or_reduce = true;
897                break;
898            }
899        }
900        assert!(
901            found_shift_or_reduce,
902            "Expected ShiftOrReduce action for OP"
903        );
904    }
905
906    #[test]
907    fn test_conflict_example_lr2() {
908        // LR(2) but not LR(1): after 'x', lookahead ';' could be
909        // shift (B → x ;) or reduce x→A (S → A ; a). Need to see past ';'.
910        let grammar = to_grammar_internal(
911            &parse_grammar(
912                r#"
913            start s;
914            terminals { X, SEMI, A, B }
915            s = aa SEMI A => sa | bb B => sb;
916            aa = X => x;
917            bb = X SEMI => xs;
918        "#,
919            )
920            .unwrap(),
921        )
922        .unwrap();
923        let compiled = CompiledTable::build_from_internal(&grammar);
924        // This grammar needs LR(2) — expect conflicts
925        assert!(compiled.has_conflicts(), "Expected R/R conflict");
926    }
927
928    #[test]
929    fn test_goto() {
930        let grammar = expr_grammar();
931        let compiled = CompiledTable::build_from_internal(&grammar);
932        let table = compiled.table();
933
934        let expr_id = compiled.symbol_id("expr").unwrap();
935        let term_id = compiled.symbol_id("term").unwrap();
936
937        assert!(
938            table.goto(0, expr_id).is_some(),
939            "Expected goto on expr from state 0"
940        );
941        assert!(
942            table.goto(0, term_id).is_some(),
943            "Expected goto on term from state 0"
944        );
945    }
946}