metrale_grammar/grammar/
printer.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// GrammarPrinter — prints a `GrammarData` back to EBNF text.
4// Port of `cpp/grammar_printer.{h,cc}`.
5//
6// Used by tests and debugging to round-trip a grammar: parse -> pass ->
7// print. The output format mirrors xgrammar exactly so functor passes
8// can be verified against the C++ reference strings.
9
10use std::collections::HashMap;
11
12use super::data::{GrammarData, Rule};
13use super::expr::{GrammarExpr, GrammarExprType};
14use crate::support::escape::print_str_as_escaped;
15use crate::support::{char_to_utf8, print_as_escaped};
16
17/// Prints the BNF AST in xgrammar's standard EBNF format.
18pub struct GrammarPrinter<'g> {
19    grammar: &'g GrammarData,
20}
21
22impl<'g> GrammarPrinter<'g> {
23    /// Wrap a grammar for printing.
24    pub fn new(grammar: &'g GrammarData) -> Self {
25        Self { grammar }
26    }
27
28    /// Render the complete grammar to EBNF text — one rule per line.
29    pub fn render(&self) -> String {
30        let mut result = String::new();
31        for i in 0..self.grammar.num_rules() {
32            result.push_str(&self.print_rule(self.grammar.rule(i)));
33            result.push('\n');
34        }
35        result
36    }
37
38    /// Print one rule: `name ::= body [(=lookahead)]`.
39    pub fn print_rule(&self, rule: &Rule) -> String {
40        let mut res = format!(
41            "{} ::= {}",
42            rule.name,
43            self.print_expr_id(rule.body_expr_id)
44        );
45        if rule.lookahead_assertion_id != -1 {
46            res.push_str(" (=");
47            res.push_str(&self.print_expr_id(rule.lookahead_assertion_id));
48            res.push(')');
49        }
50        res
51    }
52
53    /// Print the rule with the given id.
54    pub fn print_rule_id(&self, rule_id: i32) -> String {
55        self.print_rule(self.grammar.rule(rule_id))
56    }
57
58    /// Print the expression with the given id.
59    pub fn print_expr_id(&self, expr_id: i32) -> String {
60        self.print_expr(&self.grammar.expr(expr_id))
61    }
62
63    /// Print a `GrammarExpr`, dispatching on its type.
64    pub fn print_expr(&self, e: &GrammarExpr<'_>) -> String {
65        match e.kind {
66            GrammarExprType::ByteString => Self::print_byte_string(e),
67            GrammarExprType::CharacterClass => Self::print_character_class(e),
68            GrammarExprType::CharacterClassStar => {
69                format!("{}*", Self::print_character_class(e))
70            }
71            GrammarExprType::EmptyStr => "\"\"".to_string(),
72            GrammarExprType::RuleRef => self.print_rule_ref(e),
73            GrammarExprType::Sequence => self.print_sequence(e),
74            GrammarExprType::Choices => self.print_choices(e),
75            GrammarExprType::TagDispatch => self.print_tag_dispatch(e),
76            GrammarExprType::Repeat => self.print_repeat(e),
77        }
78    }
79
80    fn print_byte_string(e: &GrammarExpr<'_>) -> String {
81        let bytes: Vec<u8> = e.data.iter().map(|&b| b as u8).collect();
82        format!("\"{}\"", print_str_as_escaped(&bytes))
83    }
84
85    fn print_character_class(e: &GrammarExpr<'_>) -> String {
86        let custom: HashMap<i32, String> = HashMap::from([
87            ('-' as i32, "\\-".to_string()),
88            (']' as i32, "\\]".to_string()),
89        ]);
90        let mut result = String::from("[");
91        if e[0] != 0 {
92            result.push('^');
93        }
94        let mut i = 1;
95        while i < e.len() {
96            result.push_str(&print_as_escaped(e[i], &custom));
97            if e[i] != e[i + 1] {
98                result.push('-');
99                result.push_str(&print_as_escaped(e[i + 1], &custom));
100            }
101            i += 2;
102        }
103        result.push(']');
104        result
105    }
106
107    fn print_rule_ref(&self, e: &GrammarExpr<'_>) -> String {
108        self.grammar.rule(e[0]).name.clone()
109    }
110
111    fn print_sequence(&self, e: &GrammarExpr<'_>) -> String {
112        let parts: Vec<String> = e.data.iter().map(|&id| self.print_expr_id(id)).collect();
113        format!("({})", parts.join(" "))
114    }
115
116    fn print_choices(&self, e: &GrammarExpr<'_>) -> String {
117        let parts: Vec<String> = e.data.iter().map(|&id| self.print_expr_id(id)).collect();
118        format!("({})", parts.join(" | "))
119    }
120
121    fn print_repeat(&self, e: &GrammarExpr<'_>) -> String {
122        format!("{}{{{}, {}}}", self.grammar.rule(e[0]).name, e[1], e[2])
123    }
124
125    fn print_string(s: &str) -> String {
126        format!("\"{}\"", print_str_as_escaped(s.as_bytes()))
127    }
128
129    fn print_tag_dispatch(&self, e: &GrammarExpr<'_>) -> String {
130        let td = self.grammar.tag_dispatch(self.expr_id_of(e));
131        let mut result = String::from("TagDispatch(\n");
132        let indent = "  ";
133        for (tag, rule_id) in &td.tag_rule_pairs {
134            result.push_str(&format!(
135                "{indent}({}, {}),\n",
136                Self::print_string(tag),
137                self.grammar.rule(*rule_id).name
138            ));
139        }
140        result.push_str(&format!("{indent}stop_eos={},\n", td.stop_eos));
141        let stop: Vec<String> = td.stop_str.iter().map(|s| Self::print_string(s)).collect();
142        result.push_str(&format!("{indent}stop_str=({}),\n", stop.join(", ")));
143        result.push_str(&format!(
144            "{indent}loop_after_dispatch={},\n",
145            td.loop_after_dispatch
146        ));
147        let excl: Vec<String> = td
148            .excluded_str
149            .iter()
150            .map(|s| Self::print_string(s))
151            .collect();
152        result.push_str(&format!("{indent}excludes=({})\n)", excl.join(", ")));
153        result
154    }
155
156    /// `tag_dispatch` decoding needs the expr id; recover it by matching
157    /// the data slice's start offset against the grammar's CSR layout.
158    fn expr_id_of(&self, e: &GrammarExpr<'_>) -> i32 {
159        for id in 0..self.grammar.num_exprs() {
160            let cand = self.grammar.expr(id);
161            if cand.kind == e.kind && std::ptr::eq(cand.data.as_ptr(), e.data.as_ptr()) {
162                return id;
163            }
164        }
165        panic!("printer: tag dispatch expr not found in grammar");
166    }
167}
168
169/// Convenience: render `grammar` as EBNF text.
170pub fn print_grammar(grammar: &GrammarData) -> String {
171    GrammarPrinter::new(grammar).render()
172}
173
174/// Convert a unicode codepoint to its UTF-8 byte values as `i32`s.
175/// Helper shared with `functor` (single-element char-class -> byte string).
176pub(crate) fn codepoint_to_bytes(cp: i32) -> Vec<i32> {
177    char_to_utf8(cp).into_iter().map(|b| b as i32).collect()
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::grammar::parse_ebnf_default;
184
185    fn roundtrip(ebnf: &str) -> String {
186        let g = parse_ebnf_default(ebnf).expect("parse");
187        print_grammar(&g)
188    }
189
190    #[test]
191    fn prints_byte_string_rule() {
192        let out = roundtrip("root ::= \"abc\"\n");
193        assert!(out.contains("root ::="));
194        assert!(out.contains("\"abc\""));
195    }
196
197    #[test]
198    fn prints_character_class() {
199        let out = roundtrip("root ::= [a-z]\n");
200        assert!(out.contains("[a-z]"));
201    }
202
203    #[test]
204    fn prints_character_class_star() {
205        let out = roundtrip("root ::= [a-z]*\n");
206        assert!(out.contains("[a-z]*"));
207    }
208
209    #[test]
210    fn prints_negated_class() {
211        let out = roundtrip("root ::= [^a-z]\n");
212        assert!(out.contains("[^a-z]"));
213    }
214
215    #[test]
216    fn prints_choices() {
217        let out = roundtrip("root ::= \"a\" | \"b\"\n");
218        assert!(out.contains(" | "));
219    }
220
221    #[test]
222    fn prints_empty_string() {
223        let out = roundtrip("root ::= \"\"\n");
224        assert!(out.contains("\"\""));
225    }
226}