Skip to main content

meta_language/grammar/emit/
abnf.rs

1use crate::grammar::{CharClassItem, Grammar, GrammarExpr, GrammarFormat};
2
3use super::{
4    ABNF_RULE_TEMPLATE, EmitReport, GrammarEmitError, finish_lines, ordered_rules,
5    render_rule_line, unsupported_error,
6};
7
8/// Emits Augmented Backus-Naur Form text from the grammar IR.
9///
10/// ABNF has native alternation, grouping, optional expressions, counted
11/// repetition, numeric value ranges, and RFC 7405 case-sensitive string
12/// prefixes.
13///
14/// # Errors
15///
16/// Returns [`GrammarEmitError`] when the grammar contains a construct ABNF
17/// cannot represent, such as PEG predicates or negated character classes.
18pub fn emit_abnf(grammar: &Grammar) -> Result<(String, EmitReport), GrammarEmitError> {
19    let mut emitter = AbnfEmitter::default();
20    let mut lines = Vec::new();
21
22    for rule in ordered_rules(grammar) {
23        let body = emitter.emit_expr(rule.expr(), Precedence::Choice)?;
24        lines.push(render_rule_line(ABNF_RULE_TEMPLATE, rule.name(), &body));
25    }
26
27    Ok((finish_lines(&lines), emitter.report))
28}
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
31enum Precedence {
32    Choice = 0,
33    Sequence = 1,
34    Atom = 2,
35}
36
37#[derive(Clone, Debug, Default)]
38struct AbnfEmitter {
39    report: EmitReport,
40}
41
42impl AbnfEmitter {
43    fn emit_expr(
44        &mut self,
45        expr: &GrammarExpr,
46        parent: Precedence,
47    ) -> Result<String, GrammarEmitError> {
48        let (text, precedence) = match expr {
49            GrammarExpr::Empty => ("\"\"".to_string(), Precedence::Atom),
50            GrammarExpr::Terminal(value) => (quote_terminal("%s", value), Precedence::Atom),
51            GrammarExpr::TerminalInsensitive(value) => {
52                (quote_terminal("%i", value), Precedence::Atom)
53            }
54            GrammarExpr::CharRange(start, end) => {
55                (emit_char_range(*start, *end)?, Precedence::Atom)
56            }
57            GrammarExpr::CharClass { negated, items } => {
58                (emit_char_class(*negated, items)?, Precedence::Atom)
59            }
60            GrammarExpr::AnyChar => ("%x00-10FFFF".to_string(), Precedence::Atom),
61            GrammarExpr::NonTerminal(name) => (name.clone(), Precedence::Atom),
62            GrammarExpr::Choice {
63                ordered,
64                alternatives,
65            } => (
66                self.emit_choice(*ordered, alternatives)?,
67                Precedence::Choice,
68            ),
69            GrammarExpr::Sequence(items) => (self.emit_sequence(items)?, Precedence::Sequence),
70            GrammarExpr::Optional(inner) => {
71                let inner = self.emit_expr(inner, Precedence::Choice)?;
72                (format!("[ {inner} ]"), Precedence::Atom)
73            }
74            GrammarExpr::ZeroOrMore(inner) => {
75                let inner = self.emit_expr(inner, Precedence::Choice)?;
76                (format!("*( {inner} )"), Precedence::Atom)
77            }
78            GrammarExpr::OneOrMore(inner) => {
79                let inner = self.emit_expr(inner, Precedence::Choice)?;
80                (format!("1*( {inner} )"), Precedence::Atom)
81            }
82            GrammarExpr::Repeat { expr, min, max } => {
83                (self.emit_repeat(expr, *min, *max)?, Precedence::Atom)
84            }
85            GrammarExpr::And(_) => return Err(unsupported_error(GrammarFormat::Abnf, "And")),
86            GrammarExpr::Not(_) => return Err(unsupported_error(GrammarFormat::Abnf, "Not")),
87            GrammarExpr::Feature(feature) => {
88                return Err(unsupported_error(GrammarFormat::Abnf, feature.head()));
89            }
90            GrammarExpr::Capture { label, expr } => {
91                report_capture_loss(&mut self.report, GrammarFormat::Abnf, label.as_ref());
92                return self.emit_expr(expr, parent);
93            }
94        };
95
96        if precedence < parent {
97            Ok(format!("( {text} )"))
98        } else {
99            Ok(text)
100        }
101    }
102
103    fn emit_choice(
104        &mut self,
105        ordered: bool,
106        alternatives: &[GrammarExpr],
107    ) -> Result<String, GrammarEmitError> {
108        if ordered {
109            self.report
110                .add_lossy("ABNF treats ordered choice as unordered choice");
111        }
112        alternatives
113            .iter()
114            .map(|alternative| self.emit_expr(alternative, Precedence::Choice))
115            .collect::<Result<Vec<_>, _>>()
116            .map(|items| items.join(" / "))
117    }
118
119    fn emit_sequence(&mut self, items: &[GrammarExpr]) -> Result<String, GrammarEmitError> {
120        let mut emitted = Vec::new();
121        for item in items {
122            if matches!(item, GrammarExpr::Empty) {
123                continue;
124            }
125            let text = self.emit_expr(item, Precedence::Sequence)?;
126            if !text.is_empty() {
127                emitted.push(text);
128            }
129        }
130        if emitted.is_empty() {
131            Ok("\"\"".to_string())
132        } else {
133            Ok(emitted.join(" "))
134        }
135    }
136
137    fn emit_repeat(
138        &mut self,
139        expr: &GrammarExpr,
140        min: usize,
141        max: Option<usize>,
142    ) -> Result<String, GrammarEmitError> {
143        if max.is_some_and(|max| max < min) {
144            return Err(unsupported_error(
145                GrammarFormat::Abnf,
146                format!("Repeat with min {min} greater than max {max:?}"),
147            ));
148        }
149
150        let inner = self.emit_expr(expr, Precedence::Choice)?;
151        let prefix = max.map_or_else(
152            || {
153                if min == 0 {
154                    "*".to_string()
155                } else {
156                    format!("{min}*")
157                }
158            },
159            |max| {
160                if min == 0 {
161                    format!("*{max}")
162                } else {
163                    format!("{min}*{max}")
164                }
165            },
166        );
167        Ok(format!("{prefix}( {inner} )"))
168    }
169}
170
171fn emit_char_range(start: char, end: char) -> Result<String, GrammarEmitError> {
172    if start > end {
173        return Err(unsupported_error(
174            GrammarFormat::Abnf,
175            format!(
176                "CharRange has descending bounds U+{:04X}..=U+{:04X}",
177                start as u32, end as u32
178            ),
179        ));
180    }
181    Ok(format!("%x{}-{}", hex_char(start), hex_char(end)))
182}
183
184fn emit_char_class(negated: bool, items: &[CharClassItem]) -> Result<String, GrammarEmitError> {
185    if negated {
186        return Err(unsupported_error(GrammarFormat::Abnf, "negated CharClass"));
187    }
188    if items.is_empty() {
189        return Err(unsupported_error(GrammarFormat::Abnf, "empty CharClass"));
190    }
191
192    let items = items
193        .iter()
194        .map(emit_char_class_item)
195        .collect::<Result<Vec<_>, _>>()?
196        .join(" / ");
197    Ok(format!("( {items} )"))
198}
199
200fn emit_char_class_item(item: &CharClassItem) -> Result<String, GrammarEmitError> {
201    match item {
202        CharClassItem::Char(value) => Ok(format!("%x{}", hex_char(*value))),
203        CharClassItem::Range(start, end) => emit_char_range(*start, *end),
204    }
205}
206
207fn report_capture_loss(report: &mut EmitReport, format: GrammarFormat, label: Option<&String>) {
208    if let Some(label) = label {
209        report.add_lossy(format!("{format} dropped capture label {label:?}"));
210    } else {
211        report.add_lossy(format!("{format} dropped anonymous capture"));
212    }
213}
214
215fn quote_terminal(prefix: &str, value: &str) -> String {
216    if value.is_empty() {
217        return "\"\"".to_string();
218    }
219    if abnf_char_value_safe(value) {
220        return format!("{prefix}\"{value}\"");
221    }
222    numeric_terminal(value)
223}
224
225fn abnf_char_value_safe(value: &str) -> bool {
226    value
227        .chars()
228        .all(|character| matches!(character as u32, 0x20 | 0x21 | 0x23..=0x7e))
229}
230
231fn numeric_terminal(value: &str) -> String {
232    let values = value.chars().map(hex_char).collect::<Vec<_>>().join(".");
233    format!("%x{values}")
234}
235
236fn hex_char(value: char) -> String {
237    let code = value as u32;
238    if code <= 0xff {
239        format!("{code:02X}")
240    } else {
241        format!("{code:X}")
242    }
243}