Skip to main content

meta_language/grammar/emit/
pest.rs

1use std::fmt::Write as _;
2
3use crate::grammar::{CharClassItem, Grammar, GrammarExpr, GrammarFormat, RuleKind};
4
5use super::{
6    EmitReport, GrammarEmitError, PEST_RULE_TEMPLATE, finish_lines, ordered_rules,
7    peg_choice_alternatives, render_rule_line_with_modifier, unsupported_error,
8};
9
10/// Emits pest PEG grammar text from the grammar IR.
11///
12/// pest maps closely to the grammar IR: ordered choice uses `|`, sequences use
13/// `~`, predicates use `&`/`!`, and repetition forms are native. `RuleKind::Token`
14/// is emitted as pest's compound-atomic `$` rule modifier, the closest pest
15/// analogue to a token-level rule and the syntax a future round-trip importer
16/// should invert. Rust `peg::parser!` and `winnow` combinator emission follow the
17/// same PEG algebra but are intentionally left to the Rust codegen path.
18///
19/// # Errors
20///
21/// Returns [`GrammarEmitError`] when the IR contains an internal invariant that
22/// cannot form valid pest text, such as an empty choice, an empty character
23/// class, a descending character range, or counted repetition with `max < min`.
24pub fn emit_pest(grammar: &Grammar) -> Result<(String, EmitReport), GrammarEmitError> {
25    let mut emitter = PestEmitter::default();
26    let mut lines = Vec::new();
27
28    for rule in ordered_rules(grammar) {
29        if let Some(doc) = rule.doc() {
30            push_doc_lines(&mut lines, doc);
31        }
32        if contains_unordered_choice(rule.expr()) {
33            lines.push(
34                "// NOTE: unordered choice in source is emitted as ordered pest choice."
35                    .to_string(),
36            );
37        }
38        let body = emitter.emit_expr(rule.expr(), Precedence::Choice)?;
39        lines.push(render_rule_line_with_modifier(
40            PEST_RULE_TEMPLATE,
41            rule.name(),
42            rule_modifier(rule.kind()),
43            &body,
44        ));
45    }
46
47    Ok((finish_lines(&lines), emitter.report))
48}
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
51enum Precedence {
52    Choice = 0,
53    Sequence = 1,
54    Prefix = 2,
55    Postfix = 3,
56    Atom = 4,
57}
58
59#[derive(Clone, Debug, Default)]
60struct PestEmitter {
61    report: EmitReport,
62}
63
64impl PestEmitter {
65    fn emit_expr(
66        &mut self,
67        expr: &GrammarExpr,
68        parent: Precedence,
69    ) -> Result<String, GrammarEmitError> {
70        let (text, precedence) = match expr {
71            GrammarExpr::Empty => (quote_terminal(""), Precedence::Atom),
72            GrammarExpr::Terminal(value) => (quote_terminal(value), Precedence::Atom),
73            GrammarExpr::TerminalInsensitive(value) => {
74                (format!("^{}", quote_terminal(value)), Precedence::Atom)
75            }
76            GrammarExpr::CharRange(start, end) => {
77                (emit_char_range(*start, *end)?, Precedence::Atom)
78            }
79            GrammarExpr::CharClass { negated, items } => {
80                (emit_char_class(*negated, items)?, Precedence::Atom)
81            }
82            GrammarExpr::AnyChar => ("ANY".to_string(), Precedence::Atom),
83            GrammarExpr::NonTerminal(name) => (name.clone(), Precedence::Atom),
84            GrammarExpr::Choice {
85                ordered,
86                alternatives,
87            } => (
88                self.emit_choice(*ordered, alternatives)?,
89                Precedence::Choice,
90            ),
91            GrammarExpr::Sequence(items) => (self.emit_sequence(items)?, Precedence::Sequence),
92            GrammarExpr::Optional(inner) => {
93                let inner = self.emit_expr(inner, Precedence::Postfix)?;
94                (format!("{inner}?"), Precedence::Postfix)
95            }
96            GrammarExpr::ZeroOrMore(inner) => {
97                let inner = self.emit_expr(inner, Precedence::Postfix)?;
98                (format!("{inner}*"), Precedence::Postfix)
99            }
100            GrammarExpr::OneOrMore(inner) => {
101                let inner = self.emit_expr(inner, Precedence::Postfix)?;
102                (format!("{inner}+"), Precedence::Postfix)
103            }
104            GrammarExpr::Repeat { expr, min, max } => {
105                (self.emit_repeat(expr, *min, *max)?, Precedence::Postfix)
106            }
107            GrammarExpr::And(inner) => {
108                let inner = self.emit_expr(inner, Precedence::Prefix)?;
109                (format!("&{inner}"), Precedence::Prefix)
110            }
111            GrammarExpr::Not(inner) => {
112                let inner = self.emit_expr(inner, Precedence::Prefix)?;
113                (format!("!{inner}"), Precedence::Prefix)
114            }
115            GrammarExpr::Feature(feature) => {
116                return Err(unsupported_error(GrammarFormat::Peg, feature.head()));
117            }
118            GrammarExpr::Capture { label, expr } => {
119                if let Some(label) = label {
120                    self.report
121                        .add_lossy(format!("PEG dropped capture label {label:?}"));
122                }
123                let inner = self.emit_expr(expr, Precedence::Choice)?;
124                (format!("({inner})"), Precedence::Atom)
125            }
126        };
127
128        if precedence < parent {
129            Ok(format!("({text})"))
130        } else {
131            Ok(text)
132        }
133    }
134
135    fn emit_choice(
136        &mut self,
137        ordered: bool,
138        alternatives: &[GrammarExpr],
139    ) -> Result<String, GrammarEmitError> {
140        if alternatives.is_empty() {
141            return Err(unsupported_error(GrammarFormat::Peg, "empty Choice"));
142        }
143        if !ordered {
144            self.report
145                .add_lossy("PEG treats unordered choice as ordered choice");
146        }
147
148        peg_choice_alternatives(ordered, alternatives)
149            .into_iter()
150            .map(|alternative| self.emit_expr(alternative, Precedence::Choice))
151            .collect::<Result<Vec<_>, _>>()
152            .map(|items| items.join(" | "))
153    }
154
155    fn emit_sequence(&mut self, items: &[GrammarExpr]) -> Result<String, GrammarEmitError> {
156        let mut emitted = Vec::new();
157        for item in items {
158            if matches!(item, GrammarExpr::Empty) {
159                continue;
160            }
161            let text = self.emit_expr(item, Precedence::Sequence)?;
162            if !text.is_empty() {
163                emitted.push(text);
164            }
165        }
166        if emitted.is_empty() {
167            Ok(quote_terminal(""))
168        } else {
169            Ok(emitted.join(" ~ "))
170        }
171    }
172
173    fn emit_repeat(
174        &mut self,
175        expr: &GrammarExpr,
176        min: usize,
177        max: Option<usize>,
178    ) -> Result<String, GrammarEmitError> {
179        if max.is_some_and(|max| max < min) {
180            return Err(unsupported_error(
181                GrammarFormat::Peg,
182                format!("Repeat with min {min} greater than max {max:?}"),
183            ));
184        }
185
186        let inner = self.emit_expr(expr, Precedence::Postfix)?;
187        let suffix = match max {
188            Some(max) if min == max => format!("{{{min}}}"),
189            Some(max) => format!("{{{min},{max}}}"),
190            None => format!("{{{min},}}"),
191        };
192        Ok(format!("{inner}{suffix}"))
193    }
194}
195
196fn emit_char_class(negated: bool, items: &[CharClassItem]) -> Result<String, GrammarEmitError> {
197    if items.is_empty() {
198        return Err(unsupported_error(GrammarFormat::Peg, "empty CharClass"));
199    }
200
201    let inner = items
202        .iter()
203        .map(emit_char_class_item)
204        .collect::<Result<Vec<_>, _>>()?
205        .join(" | ");
206    if negated {
207        Ok(format!("(!({inner}) ~ ANY)"))
208    } else {
209        Ok(format!("({inner})"))
210    }
211}
212
213fn emit_char_class_item(item: &CharClassItem) -> Result<String, GrammarEmitError> {
214    match item {
215        CharClassItem::Char(value) => Ok(quote_terminal(&value.to_string())),
216        CharClassItem::Range(start, end) => emit_char_range(*start, *end),
217    }
218}
219
220fn emit_char_range(start: char, end: char) -> Result<String, GrammarEmitError> {
221    if start > end {
222        return Err(unsupported_error(
223            GrammarFormat::Peg,
224            format!(
225                "CharRange has descending bounds U+{:04X}..=U+{:04X}",
226                start as u32, end as u32
227            ),
228        ));
229    }
230    Ok(format!(
231        "{}..{}",
232        quote_char_literal(start),
233        quote_char_literal(end)
234    ))
235}
236
237const fn rule_modifier(kind: RuleKind) -> &'static str {
238    match kind {
239        RuleKind::Normal => "",
240        RuleKind::Atomic => "@",
241        RuleKind::Silent => "_",
242        RuleKind::Token => "$",
243    }
244}
245
246fn push_doc_lines(lines: &mut Vec<String>, doc: &str) {
247    if doc.is_empty() {
248        lines.push("//".to_string());
249        return;
250    }
251    for line in doc.lines() {
252        if line.is_empty() {
253            lines.push("//".to_string());
254        } else {
255            lines.push(format!("// {line}"));
256        }
257    }
258}
259
260fn contains_unordered_choice(expr: &GrammarExpr) -> bool {
261    match expr {
262        GrammarExpr::Choice { ordered: false, .. } => true,
263        GrammarExpr::Choice { alternatives, .. } | GrammarExpr::Sequence(alternatives) => {
264            alternatives.iter().any(contains_unordered_choice)
265        }
266        GrammarExpr::Optional(expr)
267        | GrammarExpr::ZeroOrMore(expr)
268        | GrammarExpr::OneOrMore(expr)
269        | GrammarExpr::And(expr)
270        | GrammarExpr::Not(expr)
271        | GrammarExpr::Capture { expr, .. }
272        | GrammarExpr::Repeat { expr, .. } => contains_unordered_choice(expr),
273        GrammarExpr::Feature(_)
274        | GrammarExpr::Empty
275        | GrammarExpr::Terminal(_)
276        | GrammarExpr::TerminalInsensitive(_)
277        | GrammarExpr::CharRange(_, _)
278        | GrammarExpr::CharClass { .. }
279        | GrammarExpr::AnyChar
280        | GrammarExpr::NonTerminal(_) => false,
281    }
282}
283
284fn quote_terminal(value: &str) -> String {
285    let mut output = String::with_capacity(value.len() + 2);
286    output.push('"');
287    for character in value.chars() {
288        push_escaped_string_char(&mut output, character);
289    }
290    output.push('"');
291    output
292}
293
294fn quote_char_literal(value: char) -> String {
295    let mut output = String::new();
296    output.push('\'');
297    push_escaped_char_literal_char(&mut output, value);
298    output.push('\'');
299    output
300}
301
302fn push_escaped_string_char(output: &mut String, character: char) {
303    match character {
304        '"' => output.push_str("\\\""),
305        '\\' => output.push_str("\\\\"),
306        '\n' => output.push_str("\\n"),
307        '\r' => output.push_str("\\r"),
308        '\t' => output.push_str("\\t"),
309        character if character.is_control() => {
310            let _ = write!(output, "\\u{{{:X}}}", character as u32);
311        }
312        character => output.push(character),
313    }
314}
315
316fn push_escaped_char_literal_char(output: &mut String, character: char) {
317    match character {
318        '\'' => output.push_str("\\'"),
319        '\\' => output.push_str("\\\\"),
320        '\n' => output.push_str("\\n"),
321        '\r' => output.push_str("\\r"),
322        '\t' => output.push_str("\\t"),
323        character if character.is_control() => {
324            let _ = write!(output, "\\u{{{:X}}}", character as u32);
325        }
326        character => output.push(character),
327    }
328}