meta_language/grammar/emit/
pest.rs1use 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
10pub 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}