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    finish_lines, ordered_rules, peg_choice_alternatives, render_rule_line_with_modifier,
7    unsupported_error, EmitReport, GrammarEmitError, PEST_RULE_TEMPLATE,
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::Capture { label, expr } => {
116                if let Some(label) = label {
117                    self.report
118                        .add_lossy(format!("PEG dropped capture label {label:?}"));
119                }
120                let inner = self.emit_expr(expr, Precedence::Choice)?;
121                (format!("({inner})"), Precedence::Atom)
122            }
123        };
124
125        if precedence < parent {
126            Ok(format!("({text})"))
127        } else {
128            Ok(text)
129        }
130    }
131
132    fn emit_choice(
133        &mut self,
134        ordered: bool,
135        alternatives: &[GrammarExpr],
136    ) -> Result<String, GrammarEmitError> {
137        if alternatives.is_empty() {
138            return Err(unsupported_error(GrammarFormat::Peg, "empty Choice"));
139        }
140        if !ordered {
141            self.report
142                .add_lossy("PEG treats unordered choice as ordered choice");
143        }
144
145        peg_choice_alternatives(ordered, alternatives)
146            .into_iter()
147            .map(|alternative| self.emit_expr(alternative, Precedence::Choice))
148            .collect::<Result<Vec<_>, _>>()
149            .map(|items| items.join(" | "))
150    }
151
152    fn emit_sequence(&mut self, items: &[GrammarExpr]) -> Result<String, GrammarEmitError> {
153        let mut emitted = Vec::new();
154        for item in items {
155            if matches!(item, GrammarExpr::Empty) {
156                continue;
157            }
158            let text = self.emit_expr(item, Precedence::Sequence)?;
159            if !text.is_empty() {
160                emitted.push(text);
161            }
162        }
163        if emitted.is_empty() {
164            Ok(quote_terminal(""))
165        } else {
166            Ok(emitted.join(" ~ "))
167        }
168    }
169
170    fn emit_repeat(
171        &mut self,
172        expr: &GrammarExpr,
173        min: usize,
174        max: Option<usize>,
175    ) -> Result<String, GrammarEmitError> {
176        if max.is_some_and(|max| max < min) {
177            return Err(unsupported_error(
178                GrammarFormat::Peg,
179                format!("Repeat with min {min} greater than max {max:?}"),
180            ));
181        }
182
183        let inner = self.emit_expr(expr, Precedence::Postfix)?;
184        let suffix = match max {
185            Some(max) if min == max => format!("{{{min}}}"),
186            Some(max) => format!("{{{min},{max}}}"),
187            None => format!("{{{min},}}"),
188        };
189        Ok(format!("{inner}{suffix}"))
190    }
191}
192
193fn emit_char_class(negated: bool, items: &[CharClassItem]) -> Result<String, GrammarEmitError> {
194    if items.is_empty() {
195        return Err(unsupported_error(GrammarFormat::Peg, "empty CharClass"));
196    }
197
198    let inner = items
199        .iter()
200        .map(emit_char_class_item)
201        .collect::<Result<Vec<_>, _>>()?
202        .join(" | ");
203    if negated {
204        Ok(format!("(!({inner}) ~ ANY)"))
205    } else {
206        Ok(format!("({inner})"))
207    }
208}
209
210fn emit_char_class_item(item: &CharClassItem) -> Result<String, GrammarEmitError> {
211    match item {
212        CharClassItem::Char(value) => Ok(quote_terminal(&value.to_string())),
213        CharClassItem::Range(start, end) => emit_char_range(*start, *end),
214    }
215}
216
217fn emit_char_range(start: char, end: char) -> Result<String, GrammarEmitError> {
218    if start > end {
219        return Err(unsupported_error(
220            GrammarFormat::Peg,
221            format!(
222                "CharRange has descending bounds U+{:04X}..=U+{:04X}",
223                start as u32, end as u32
224            ),
225        ));
226    }
227    Ok(format!(
228        "{}..{}",
229        quote_char_literal(start),
230        quote_char_literal(end)
231    ))
232}
233
234const fn rule_modifier(kind: RuleKind) -> &'static str {
235    match kind {
236        RuleKind::Normal => "",
237        RuleKind::Atomic => "@",
238        RuleKind::Silent => "_",
239        RuleKind::Token => "$",
240    }
241}
242
243fn push_doc_lines(lines: &mut Vec<String>, doc: &str) {
244    if doc.is_empty() {
245        lines.push("//".to_string());
246        return;
247    }
248    for line in doc.lines() {
249        if line.is_empty() {
250            lines.push("//".to_string());
251        } else {
252            lines.push(format!("// {line}"));
253        }
254    }
255}
256
257fn contains_unordered_choice(expr: &GrammarExpr) -> bool {
258    match expr {
259        GrammarExpr::Choice { ordered: false, .. } => true,
260        GrammarExpr::Choice { alternatives, .. } | GrammarExpr::Sequence(alternatives) => {
261            alternatives.iter().any(contains_unordered_choice)
262        }
263        GrammarExpr::Optional(expr)
264        | GrammarExpr::ZeroOrMore(expr)
265        | GrammarExpr::OneOrMore(expr)
266        | GrammarExpr::And(expr)
267        | GrammarExpr::Not(expr)
268        | GrammarExpr::Capture { expr, .. }
269        | GrammarExpr::Repeat { expr, .. } => contains_unordered_choice(expr),
270        GrammarExpr::Empty
271        | GrammarExpr::Terminal(_)
272        | GrammarExpr::TerminalInsensitive(_)
273        | GrammarExpr::CharRange(_, _)
274        | GrammarExpr::CharClass { .. }
275        | GrammarExpr::AnyChar
276        | GrammarExpr::NonTerminal(_) => false,
277    }
278}
279
280fn quote_terminal(value: &str) -> String {
281    let mut output = String::with_capacity(value.len() + 2);
282    output.push('"');
283    for character in value.chars() {
284        push_escaped_string_char(&mut output, character);
285    }
286    output.push('"');
287    output
288}
289
290fn quote_char_literal(value: char) -> String {
291    let mut output = String::new();
292    output.push('\'');
293    push_escaped_char_literal_char(&mut output, value);
294    output.push('\'');
295    output
296}
297
298fn push_escaped_string_char(output: &mut String, character: char) {
299    match character {
300        '"' => output.push_str("\\\""),
301        '\\' => output.push_str("\\\\"),
302        '\n' => output.push_str("\\n"),
303        '\r' => output.push_str("\\r"),
304        '\t' => output.push_str("\\t"),
305        character if character.is_control() => {
306            let _ = write!(output, "\\u{{{:X}}}", character as u32);
307        }
308        character => output.push(character),
309    }
310}
311
312fn push_escaped_char_literal_char(output: &mut String, character: char) {
313    match character {
314        '\'' => output.push_str("\\'"),
315        '\\' => output.push_str("\\\\"),
316        '\n' => output.push_str("\\n"),
317        '\r' => output.push_str("\\r"),
318        '\t' => output.push_str("\\t"),
319        character if character.is_control() => {
320            let _ = write!(output, "\\u{{{:X}}}", character as u32);
321        }
322        character => output.push(character),
323    }
324}