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