1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::Write as _;
3
4use crate::grammar::{CharClassItem, Grammar, GrammarExpr, GrammarFormat, GrammarRule};
5
6use super::{
7 EmitReport, GBNF_RULE_TEMPLATE, GrammarEmitError, finish_lines, render_rule_line,
8 unsupported_error,
9};
10
11const ANY_CHAR_CLASS: &str = r"[\x00-\U0010FFFF]";
12
13pub fn emit_gbnf(grammar: &Grammar) -> Result<(String, EmitReport), GrammarEmitError> {
31 if grammar.rules().is_empty() {
32 return Ok((String::new(), EmitReport::default()));
33 }
34
35 let start_index = start_rule_index(grammar).ok_or_else(|| {
36 unsupported_error(
37 GrammarFormat::Gbnf,
38 "configured start rule is not present in the grammar",
39 )
40 })?;
41 let start_name = grammar.rules()[start_index].name();
42
43 let mut report = EmitReport::default();
44 let names = NamePlan::new(grammar, start_name, &mut report);
45 let mut emitter = GbnfEmitter { report, names };
46 let mut lines = Vec::new();
47
48 let start_rule = &grammar.rules()[start_index];
49 lines.extend(doc_comments(start_rule, &mut emitter.report));
50 let start_body = emitter.emit_expr(start_rule.expr(), Precedence::Choice)?;
51 lines.push(render_rule_line(GBNF_RULE_TEMPLATE, "root", &start_body));
52
53 for (index, rule) in grammar.rules().iter().enumerate() {
54 if index == start_index {
55 continue;
56 }
57 lines.extend(doc_comments(rule, &mut emitter.report));
58 let name = emitter.names.name_for(rule.name());
59 let body = emitter.emit_expr(rule.expr(), Precedence::Choice)?;
60 lines.push(render_rule_line(GBNF_RULE_TEMPLATE, &name, &body));
61 }
62
63 Ok((finish_lines(&lines), emitter.report))
64}
65
66fn doc_comments(rule: &GrammarRule, report: &mut EmitReport) -> Vec<String> {
70 let Some(doc) = rule.doc() else {
71 return Vec::new();
72 };
73 let lines: Vec<String> = doc
74 .split('\n')
75 .map(|line| {
76 if line.starts_with('#') {
77 line.to_string()
78 } else {
79 format!("# {line}")
80 }
81 })
82 .collect();
83 let reimported = lines
84 .iter()
85 .map(|line| line.trim())
86 .collect::<Vec<_>>()
87 .join("\n");
88 if lines.iter().any(|line| line.contains('\r')) || reimported != doc {
89 report.add_lossy(format!(
90 "GBNF re-imports the documentation of rule {:?} as {reimported:?}",
91 rule.name()
92 ));
93 }
94 lines
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
98enum Precedence {
99 Choice = 0,
100 Sequence = 1,
101 Postfix = 2,
102 Atom = 3,
103}
104
105#[derive(Clone, Debug)]
106struct GbnfEmitter {
107 report: EmitReport,
108 names: NamePlan,
109}
110
111impl GbnfEmitter {
112 fn emit_expr(
113 &mut self,
114 expr: &GrammarExpr,
115 parent: Precedence,
116 ) -> Result<String, GrammarEmitError> {
117 let (text, precedence) = match expr {
118 GrammarExpr::Empty => (quote_terminal(""), Precedence::Atom),
119 GrammarExpr::Terminal(value) => (quote_terminal(value), Precedence::Atom),
120 GrammarExpr::TerminalInsensitive(value) => {
121 self.report.add_lossy(format!(
122 "GBNF expands case-insensitive terminal {value:?} to character classes"
123 ));
124 (emit_case_insensitive_terminal(value), Precedence::Sequence)
125 }
126 GrammarExpr::CharRange(start, end) => {
127 (emit_char_range(*start, *end)?, Precedence::Atom)
128 }
129 GrammarExpr::CharClass { negated, items } => {
130 (emit_char_class(*negated, items)?, Precedence::Atom)
131 }
132 GrammarExpr::AnyChar => (ANY_CHAR_CLASS.to_string(), Precedence::Atom),
133 GrammarExpr::NonTerminal(name) => (self.names.name_for(name), Precedence::Atom),
134 GrammarExpr::Choice {
135 ordered,
136 alternatives,
137 } => (
138 self.emit_choice(*ordered, alternatives)?,
139 Precedence::Choice,
140 ),
141 GrammarExpr::Sequence(items) => (self.emit_sequence(items)?, Precedence::Sequence),
142 GrammarExpr::Optional(inner) => {
143 let inner = self.emit_expr(inner, Precedence::Choice)?;
144 (format!("({inner})?"), Precedence::Postfix)
145 }
146 GrammarExpr::ZeroOrMore(inner) => {
147 let inner = self.emit_expr(inner, Precedence::Choice)?;
148 (format!("({inner})*"), Precedence::Postfix)
149 }
150 GrammarExpr::OneOrMore(inner) => {
151 let inner = self.emit_expr(inner, Precedence::Choice)?;
152 (format!("({inner})+"), Precedence::Postfix)
153 }
154 GrammarExpr::Repeat { expr, min, max } => {
155 (self.emit_repeat(expr, *min, *max)?, Precedence::Postfix)
156 }
157 GrammarExpr::And(_) => {
158 return Err(unsupported_error(GrammarFormat::Gbnf, "positive-predicate"));
159 }
160 GrammarExpr::Not(_) => {
161 return Err(unsupported_error(GrammarFormat::Gbnf, "negative-predicate"));
162 }
163 GrammarExpr::Feature(feature) => {
164 return Err(unsupported_error(GrammarFormat::Gbnf, feature.head()));
165 }
166 GrammarExpr::Capture { label, expr } => {
167 report_capture_loss(&mut self.report, label.as_ref());
168 return self.emit_expr(expr, parent);
169 }
170 };
171
172 if precedence < parent {
173 Ok(format!("({text})"))
174 } else {
175 Ok(text)
176 }
177 }
178
179 fn emit_choice(
180 &mut self,
181 ordered: bool,
182 alternatives: &[GrammarExpr],
183 ) -> Result<String, GrammarEmitError> {
184 if alternatives.is_empty() {
185 return Err(unsupported_error(GrammarFormat::Gbnf, "empty Choice"));
186 }
187 if ordered {
188 self.report
189 .add_lossy("GBNF treats ordered choice as unordered choice");
190 }
191
192 alternatives
193 .iter()
194 .map(|alternative| self.emit_expr(alternative, Precedence::Choice))
195 .collect::<Result<Vec<_>, _>>()
196 .map(|items| items.join(" | "))
197 }
198
199 fn emit_sequence(&mut self, items: &[GrammarExpr]) -> Result<String, GrammarEmitError> {
200 let mut emitted = Vec::new();
201 let mut index = 0;
202 while index < items.len() {
203 if let Some(class_items) = negated_class_peephole(items, index) {
204 emitted.push(emit_char_class(true, &class_items)?);
205 index += 2;
206 continue;
207 }
208
209 let text = self.emit_expr(&items[index], Precedence::Sequence)?;
210 if !text.is_empty() {
211 emitted.push(text);
212 }
213 index += 1;
214 }
215
216 if emitted.is_empty() {
217 Ok(quote_terminal(""))
218 } else {
219 Ok(emitted.join(" "))
220 }
221 }
222
223 fn emit_repeat(
224 &mut self,
225 expr: &GrammarExpr,
226 min: usize,
227 max: Option<usize>,
228 ) -> Result<String, GrammarEmitError> {
229 if max.is_some_and(|max| max < min) {
230 return Err(unsupported_error(
231 GrammarFormat::Gbnf,
232 format!("Repeat with min {min} greater than max {max:?}"),
233 ));
234 }
235
236 let inner = self.emit_expr(expr, Precedence::Choice)?;
237 let suffix = match max {
238 Some(max) if min == max => format!("{{{min}}}"),
239 Some(max) => format!("{{{min},{max}}}"),
240 None => format!("{{{min},}}"),
241 };
242 Ok(format!("({inner}){suffix}"))
243 }
244}
245
246#[derive(Clone, Debug)]
247struct NamePlan {
248 names: BTreeMap<String, String>,
249}
250
251impl NamePlan {
252 fn new(grammar: &Grammar, start_name: &str, report: &mut EmitReport) -> Self {
253 let defined_names = grammar
254 .rules()
255 .iter()
256 .map(|rule| rule.name().to_string())
257 .collect::<BTreeSet<_>>();
258 let mut symbols = Vec::new();
259 let mut seen = BTreeSet::new();
260 for rule in grammar.rules() {
261 push_unique_symbol(&mut symbols, &mut seen, rule.name());
262 }
263 for reference in grammar.referenced_nonterminals() {
264 push_unique_symbol(&mut symbols, &mut seen, &reference);
265 }
266
267 let mut used = BTreeSet::from(["root".to_string()]);
268 let mut names = BTreeMap::new();
269 names.insert(start_name.to_string(), "root".to_string());
270
271 for symbol in symbols {
272 if symbol == start_name {
273 continue;
274 }
275 let base = sanitize_identifier(&symbol);
276 let emitted = unique_identifier(&base, &mut used);
277 if emitted != symbol {
278 report_name_change(report, &defined_names, &symbol, &emitted);
279 }
280 names.insert(symbol, emitted);
281 }
282
283 Self { names }
284 }
285
286 fn name_for(&self, source: &str) -> String {
287 self.names
288 .get(source)
289 .map_or_else(|| source.to_string(), Clone::clone)
290 }
291}
292
293fn push_unique_symbol(symbols: &mut Vec<String>, seen: &mut BTreeSet<String>, symbol: &str) {
294 if seen.insert(symbol.to_string()) {
295 symbols.push(symbol.to_string());
296 }
297}
298
299fn report_name_change(
300 report: &mut EmitReport,
301 defined_names: &BTreeSet<String>,
302 source: &str,
303 emitted: &str,
304) {
305 let kind = if defined_names.contains(source) {
306 "rule"
307 } else {
308 "non-terminal reference"
309 };
310 report.add_lossy(format!("GBNF renamed {kind} {source:?} to {emitted:?}"));
311}
312
313fn unique_identifier(base: &str, used: &mut BTreeSet<String>) -> String {
314 if used.insert(base.to_string()) {
315 return base.to_string();
316 }
317
318 for suffix in 1_usize.. {
319 let candidate = format!("{base}-{suffix}");
320 if used.insert(candidate.clone()) {
321 return candidate;
322 }
323 }
324 unreachable!("unbounded suffix search must eventually find a free identifier")
325}
326
327fn sanitize_identifier(source: &str) -> String {
328 let mut output = String::new();
329 let mut previous_hyphen = false;
330 for character in source.chars() {
331 let sanitized = if character.is_ascii_alphanumeric() {
332 character
333 } else {
334 '-'
335 };
336 if sanitized == '-' {
337 if !previous_hyphen {
338 output.push(sanitized);
339 previous_hyphen = true;
340 }
341 } else {
342 output.push(sanitized);
343 previous_hyphen = false;
344 }
345 }
346
347 let mut output = output.trim_matches('-').to_string();
348 if output.is_empty() {
349 output.push_str("ml");
350 }
351 if !output.starts_with(|character: char| character.is_ascii_alphabetic()) {
352 output.insert_str(0, "ml-");
353 }
354 output
355}
356
357fn start_rule_index(grammar: &Grammar) -> Option<usize> {
358 grammar.start().map_or(Some(0), |start| {
359 grammar.rules().iter().position(|rule| rule.name() == start)
360 })
361}
362
363fn negated_class_peephole(items: &[GrammarExpr], index: usize) -> Option<Vec<CharClassItem>> {
364 let GrammarExpr::Not(inner) = items.get(index)? else {
365 return None;
366 };
367 if !matches!(items.get(index + 1), Some(GrammarExpr::AnyChar)) {
368 return None;
369 }
370 predicate_negated_class_items(inner)
371}
372
373fn predicate_negated_class_items(expr: &GrammarExpr) -> Option<Vec<CharClassItem>> {
374 match expr {
375 GrammarExpr::CharClass {
376 negated: false,
377 items,
378 } => Some(items.clone()),
379 GrammarExpr::Terminal(value) => {
380 let mut chars = value.chars();
381 let character = chars.next()?;
382 chars
383 .next()
384 .is_none()
385 .then_some(vec![CharClassItem::Char(character)])
386 }
387 _ => None,
388 }
389}
390
391fn emit_case_insensitive_terminal(value: &str) -> String {
392 if value.is_empty() {
393 return quote_terminal("");
394 }
395
396 value
397 .chars()
398 .map(emit_case_insensitive_char)
399 .collect::<String>()
400}
401
402fn emit_case_insensitive_char(character: char) -> String {
403 if character.is_ascii_alphabetic() {
404 let upper = character.to_ascii_uppercase();
405 let lower = character.to_ascii_lowercase();
406 let mut output = String::new();
407 output.push('[');
408 push_escaped_class_char(&mut output, upper);
409 push_escaped_class_char(&mut output, lower);
410 output.push(']');
411 output
412 } else {
413 quote_terminal(&character.to_string())
414 }
415}
416
417fn emit_char_range(start: char, end: char) -> Result<String, GrammarEmitError> {
418 validate_range("CharRange", start, end)?;
419 Ok(format!(
420 "[{}-{}]",
421 escaped_class_char(start),
422 escaped_class_char(end)
423 ))
424}
425
426fn emit_char_class(negated: bool, items: &[CharClassItem]) -> Result<String, GrammarEmitError> {
427 if items.is_empty() {
428 return Err(unsupported_error(GrammarFormat::Gbnf, "empty CharClass"));
429 }
430
431 let mut output = String::new();
432 output.push('[');
433 if negated {
434 output.push('^');
435 }
436 for item in items {
437 output.push_str(&emit_char_class_item(item)?);
438 }
439 output.push(']');
440 Ok(output)
441}
442
443fn emit_char_class_item(item: &CharClassItem) -> Result<String, GrammarEmitError> {
444 match item {
445 CharClassItem::Char(value) => Ok(escaped_class_char(*value)),
446 CharClassItem::Range(start, end) => {
447 validate_range("CharClass range", *start, *end)?;
448 Ok(format!(
449 "{}-{}",
450 escaped_class_char(*start),
451 escaped_class_char(*end)
452 ))
453 }
454 }
455}
456
457fn validate_range(construct: &str, start: char, end: char) -> Result<(), GrammarEmitError> {
458 if start > end {
459 return Err(unsupported_error(
460 GrammarFormat::Gbnf,
461 format!(
462 "{construct} has descending bounds U+{:04X}..=U+{:04X}",
463 start as u32, end as u32
464 ),
465 ));
466 }
467 Ok(())
468}
469
470fn report_capture_loss(report: &mut EmitReport, label: Option<&String>) {
471 if let Some(label) = label {
472 report.add_lossy(format!("GBNF dropped capture label {label:?}"));
473 } else {
474 report.add_lossy("GBNF dropped anonymous capture");
475 }
476}
477
478fn quote_terminal(value: &str) -> String {
479 let mut output = String::with_capacity(value.len() + 2);
480 output.push('"');
481 for character in value.chars() {
482 push_escaped_string_char(&mut output, character);
483 }
484 output.push('"');
485 output
486}
487
488fn escaped_class_char(character: char) -> String {
489 let mut output = String::new();
490 push_escaped_class_char(&mut output, character);
491 output
492}
493
494fn push_escaped_string_char(output: &mut String, character: char) {
495 match character {
496 '"' => output.push_str("\\\""),
497 '\\' => output.push_str("\\\\"),
498 '\n' => output.push_str("\\n"),
499 '\r' => output.push_str("\\r"),
500 '\t' => output.push_str("\\t"),
501 '\u{08}' => output.push_str("\\b"),
502 '\u{0c}' => output.push_str("\\f"),
503 character if character.is_control() => push_hex_escape(output, character),
504 character => output.push(character),
505 }
506}
507
508fn push_escaped_class_char(output: &mut String, character: char) {
509 match character {
510 '\\' => output.push_str("\\\\"),
511 '[' => output.push_str("\\["),
512 ']' => output.push_str("\\]"),
513 '-' => output.push_str("\\-"),
514 '^' => output.push_str("\\^"),
515 '\n' => output.push_str("\\n"),
516 '\r' => output.push_str("\\r"),
517 '\t' => output.push_str("\\t"),
518 '\u{08}' => output.push_str("\\b"),
519 '\u{0c}' => output.push_str("\\f"),
520 character if character.is_control() => push_hex_escape(output, character),
521 character => output.push(character),
522 }
523}
524
525fn push_hex_escape(output: &mut String, character: char) {
526 let code = character as u32;
527 if code <= 0xff {
528 let _ = write!(output, "\\x{code:02X}");
529 } else if code <= 0xffff {
530 let _ = write!(output, "\\u{code:04X}");
531 } else {
532 let _ = write!(output, "\\U{code:08X}");
533 }
534}