Skip to main content

meta_language/grammar/emit/
abnf.rs

1use crate::grammar::{CharClassItem, Grammar, GrammarExpr, GrammarFormat};
2
3use super::{
4    finish_lines, ordered_rules, render_rule_line, unsupported_error, EmitReport, GrammarEmitError,
5    ABNF_RULE_TEMPLATE,
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::Capture { label, expr } => {
88                report_capture_loss(&mut self.report, GrammarFormat::Abnf, label.as_ref());
89                return self.emit_expr(expr, parent);
90            }
91        };
92
93        if precedence < parent {
94            Ok(format!("( {text} )"))
95        } else {
96            Ok(text)
97        }
98    }
99
100    fn emit_choice(
101        &mut self,
102        ordered: bool,
103        alternatives: &[GrammarExpr],
104    ) -> Result<String, GrammarEmitError> {
105        if ordered {
106            self.report
107                .add_lossy("ABNF treats ordered choice as unordered choice");
108        }
109        alternatives
110            .iter()
111            .map(|alternative| self.emit_expr(alternative, Precedence::Choice))
112            .collect::<Result<Vec<_>, _>>()
113            .map(|items| items.join(" / "))
114    }
115
116    fn emit_sequence(&mut self, items: &[GrammarExpr]) -> Result<String, GrammarEmitError> {
117        let mut emitted = Vec::new();
118        for item in items {
119            if matches!(item, GrammarExpr::Empty) {
120                continue;
121            }
122            let text = self.emit_expr(item, Precedence::Sequence)?;
123            if !text.is_empty() {
124                emitted.push(text);
125            }
126        }
127        if emitted.is_empty() {
128            Ok("\"\"".to_string())
129        } else {
130            Ok(emitted.join(" "))
131        }
132    }
133
134    fn emit_repeat(
135        &mut self,
136        expr: &GrammarExpr,
137        min: usize,
138        max: Option<usize>,
139    ) -> Result<String, GrammarEmitError> {
140        if max.is_some_and(|max| max < min) {
141            return Err(unsupported_error(
142                GrammarFormat::Abnf,
143                format!("Repeat with min {min} greater than max {max:?}"),
144            ));
145        }
146
147        let inner = self.emit_expr(expr, Precedence::Choice)?;
148        let prefix = max.map_or_else(
149            || {
150                if min == 0 {
151                    "*".to_string()
152                } else {
153                    format!("{min}*")
154                }
155            },
156            |max| {
157                if min == 0 {
158                    format!("*{max}")
159                } else {
160                    format!("{min}*{max}")
161                }
162            },
163        );
164        Ok(format!("{prefix}( {inner} )"))
165    }
166}
167
168fn emit_char_range(start: char, end: char) -> Result<String, GrammarEmitError> {
169    if start > end {
170        return Err(unsupported_error(
171            GrammarFormat::Abnf,
172            format!(
173                "CharRange has descending bounds U+{:04X}..=U+{:04X}",
174                start as u32, end as u32
175            ),
176        ));
177    }
178    Ok(format!("%x{}-{}", hex_char(start), hex_char(end)))
179}
180
181fn emit_char_class(negated: bool, items: &[CharClassItem]) -> Result<String, GrammarEmitError> {
182    if negated {
183        return Err(unsupported_error(GrammarFormat::Abnf, "negated CharClass"));
184    }
185    if items.is_empty() {
186        return Err(unsupported_error(GrammarFormat::Abnf, "empty CharClass"));
187    }
188
189    let items = items
190        .iter()
191        .map(emit_char_class_item)
192        .collect::<Result<Vec<_>, _>>()?
193        .join(" / ");
194    Ok(format!("( {items} )"))
195}
196
197fn emit_char_class_item(item: &CharClassItem) -> Result<String, GrammarEmitError> {
198    match item {
199        CharClassItem::Char(value) => Ok(format!("%x{}", hex_char(*value))),
200        CharClassItem::Range(start, end) => emit_char_range(*start, *end),
201    }
202}
203
204fn report_capture_loss(report: &mut EmitReport, format: GrammarFormat, label: Option<&String>) {
205    if let Some(label) = label {
206        report.add_lossy(format!("{format} dropped capture label {label:?}"));
207    } else {
208        report.add_lossy(format!("{format} dropped anonymous capture"));
209    }
210}
211
212fn quote_terminal(prefix: &str, value: &str) -> String {
213    if value.is_empty() {
214        return "\"\"".to_string();
215    }
216    if abnf_char_value_safe(value) {
217        return format!("{prefix}\"{value}\"");
218    }
219    numeric_terminal(value)
220}
221
222fn abnf_char_value_safe(value: &str) -> bool {
223    value
224        .chars()
225        .all(|character| matches!(character as u32, 0x20 | 0x21 | 0x23..=0x7e))
226}
227
228fn numeric_terminal(value: &str) -> String {
229    let values = value.chars().map(hex_char).collect::<Vec<_>>().join(".");
230    format!("%x{values}")
231}
232
233fn hex_char(value: char) -> String {
234    let code = value as u32;
235    if code <= 0xff {
236        format!("{code:02X}")
237    } else {
238        format!("{code:X}")
239    }
240}