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