Skip to main content

ferrox_models/grammar/
parser.rs

1//! The GBNF parser, transcribed from `llama_grammar_parser` in llama.cpp's
2//! `src/llama-grammar.cpp`.
3//!
4//! It compiles grammar text into a flat rule table. Every construct that
5//! is not a literal character, a character class, a rule reference or a
6//! token is rewritten into extra synthesized rules at parse time, so the
7//! stack machine in [`super::machine`] only ever sees five element kinds.
8//!
9//! The rewrites are upstream's, quoted from the comment in
10//! `parse_sequence`:
11//!
12//! ```text
13//! S{m,n} --> S S S (m times) S'(n-m)
14//!            S'(x)   ::= S S'(x-1) |
15//!            S'(1)   ::= S |
16//! S{m,}  --> S S S (m times) S'
17//!            S'      ::= S S' |
18//! S*     --> S{0,}   -->  S'  ::= S S' |
19//! S+     --> S{1,}   -->  S S'    with S' ::= S S' |
20//! S?     --> S{0,1}  -->  S'  ::= S |
21//! ```
22//!
23//! Getting these exactly right matters more than it looks: the synthesized
24//! rule *ids* are observable, because `S'` is named `<rule>_<id>` and the
25//! id is the symbol count at the time it is generated. Two parsers that
26//! accept the same language can still build different rule tables, and
27//! llama.cpp's own `tests/test-grammar-parser.cpp` pins the tables, not
28//! the language. Those pinned tables are transcribed in
29//! [`super::parser_tests`].
30
31use std::collections::BTreeMap;
32
33use super::element::{GrammarElement, GrammarRule, GreType};
34use super::error::GrammarError;
35use super::utf8::{byte_at, decode_char};
36
37/// `MAX_REPETITION_THRESHOLD`: the ceiling on both a single repetition
38/// count and on the running product of nested repetitions.
39pub const MAX_REPETITION_THRESHOLD: u64 = 2000;
40
41/// Resolves the `<name>` form of a grammar token element to a token id.
42///
43/// The `<[42]>` form needs no vocabulary. `<name>` does, and llama.cpp
44/// requires that the text (angle brackets included) tokenizes to exactly
45/// one token, with special tokens enabled.
46pub trait GrammarVocab {
47    /// Tokenize `text`, which includes its surrounding `<` and `>`, with
48    /// special-token parsing on and no BOS.
49    fn tokenize_special(&self, text: &str) -> Vec<u32>;
50}
51
52/// A parsed grammar: the rule table plus the symbol names that produced it.
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub struct ParsedGrammar {
55    /// `rules[id]` is the definition of symbol `id`, terminated by
56    /// [`GreType::End`].
57    pub rules: Vec<GrammarRule>,
58    /// Symbol name to rule id. Ordered, mirroring upstream's `std::map`,
59    /// so error messages and diagnostics are reproducible.
60    pub symbol_ids: BTreeMap<String, u32>,
61}
62
63impl ParsedGrammar {
64    /// The rule id for a symbol name, if the grammar defines or references
65    /// it.
66    pub fn symbol_id(&self, name: &str) -> Option<u32> {
67        self.symbol_ids.get(name).copied()
68    }
69
70    /// The name of a rule id, for diagnostics.
71    pub fn symbol_name(&self, rule_id: u32) -> Option<&str> {
72        self.symbol_ids
73            .iter()
74            .find(|(_, &v)| v == rule_id)
75            .map(|(k, _)| k.as_str())
76    }
77}
78
79/// Parse GBNF text with no vocabulary. The `<name>` token form is refused
80/// by name; `<[id]>` works.
81pub fn parse(src: &str) -> Result<ParsedGrammar, GrammarError> {
82    parse_with_vocab(src, None)
83}
84
85/// Parse GBNF text, resolving `<name>` token elements through `vocab`.
86pub fn parse_with_vocab(
87    src: &str,
88    vocab: Option<&dyn GrammarVocab>,
89) -> Result<ParsedGrammar, GrammarError> {
90    let mut p = Parser {
91        src: src.as_bytes(),
92        pos: 0,
93        vocab,
94        out: ParsedGrammar::default(),
95    };
96    p.parse_all()?;
97    Ok(p.out)
98}
99
100struct Parser<'a> {
101    src: &'a [u8],
102    pos: usize,
103    vocab: Option<&'a dyn GrammarVocab>,
104    out: ParsedGrammar,
105}
106
107impl<'a> Parser<'a> {
108    #[inline]
109    fn at(&self, i: usize) -> u8 {
110        byte_at(self.src, i)
111    }
112
113    #[inline]
114    fn cur(&self) -> u8 {
115        self.at(self.pos)
116    }
117
118    fn err(&self, expected: impl Into<String>) -> GrammarError {
119        GrammarError::syntax(expected, self.src, self.pos)
120    }
121
122    fn err_at(&self, expected: impl Into<String>, offset: usize) -> GrammarError {
123        GrammarError::syntax(expected, self.src, offset)
124    }
125
126    // -- character classification, `is_digit_char` / `is_word_char` --
127
128    fn is_digit_char(c: u8) -> bool {
129        c.is_ascii_digit()
130    }
131
132    fn is_word_char(c: u8) -> bool {
133        c.is_ascii_alphabetic() || c == b'-' || Self::is_digit_char(c)
134    }
135
136    // -- lexical helpers --
137
138    /// `parse_space`. Skips spaces, tabs and `#` comments; newlines too
139    /// when `newline_ok`. A comment runs to the end of the line but does
140    /// not eat the newline, so a comment inside a rule body terminates the
141    /// rule exactly as a bare newline would.
142    fn parse_space(&mut self, newline_ok: bool) {
143        loop {
144            let c = self.cur();
145            if c == b' ' || c == b'\t' {
146                self.pos += 1;
147            } else if c == b'#' {
148                while self.cur() != 0 && self.cur() != b'\r' && self.cur() != b'\n' {
149                    self.pos += 1;
150                }
151            } else if newline_ok && (c == b'\r' || c == b'\n') {
152                self.pos += 1;
153            } else {
154                return;
155            }
156        }
157    }
158
159    /// `parse_name`. Returns the end offset of the name starting at
160    /// `self.pos`; does not advance.
161    fn parse_name(&self) -> Result<usize, GrammarError> {
162        let mut end = self.pos;
163        while Self::is_word_char(self.at(end)) {
164            end += 1;
165        }
166        if end == self.pos {
167            return Err(self.err("expecting name"));
168        }
169        Ok(end)
170    }
171
172    /// `parse_int`. Returns the end offset; does not advance.
173    fn parse_int_end(&self) -> Result<usize, GrammarError> {
174        let mut end = self.pos;
175        while Self::is_digit_char(self.at(end)) {
176            end += 1;
177        }
178        if end == self.pos {
179            return Err(self.err("expecting integer"));
180        }
181        Ok(end)
182    }
183
184    /// `parse_int` plus the `std::stoull` upstream applies to the result.
185    fn parse_u64(&mut self) -> Result<u64, GrammarError> {
186        let end = self.parse_int_end()?;
187        let text = std::str::from_utf8(&self.src[self.pos..end])
188            .map_err(|_| self.err("expecting integer"))?;
189        let value: u64 = text
190            .parse()
191            .map_err(|_| self.err_at("integer is too large", self.pos))?;
192        self.pos = end;
193        Ok(value)
194    }
195
196    /// `parse_hex`. Consumes exactly `size` hex digits.
197    fn parse_hex(&mut self, size: usize) -> Result<u32, GrammarError> {
198        let start = self.pos;
199        let end = start + size;
200        let mut value: u32 = 0;
201        let mut p = start;
202        while p < end && self.at(p) != 0 {
203            let c = self.at(p);
204            let digit = match c {
205                b'a'..=b'f' => c - b'a' + 10,
206                b'A'..=b'F' => c - b'A' + 10,
207                b'0'..=b'9' => c - b'0',
208                _ => break,
209            };
210            value = (value << 4) + digit as u32;
211            p += 1;
212        }
213        if p != end {
214            self.pos = p;
215            return Err(self.err_at(format!("expecting {size} hex chars"), start));
216        }
217        self.pos = p;
218        Ok(value)
219    }
220
221    /// `parse_char`. One literal character, escape sequences included.
222    fn parse_char(&mut self) -> Result<u32, GrammarError> {
223        if self.cur() == b'\\' {
224            let start = self.pos;
225            let next = self.at(self.pos + 1);
226            return match next {
227                b'x' => {
228                    self.pos += 2;
229                    self.parse_hex(2)
230                }
231                b'u' => {
232                    self.pos += 2;
233                    self.parse_hex(4)
234                }
235                b'U' => {
236                    self.pos += 2;
237                    self.parse_hex(8)
238                }
239                b't' => {
240                    self.pos += 2;
241                    Ok(u32::from(b'\t'))
242                }
243                b'r' => {
244                    self.pos += 2;
245                    Ok(u32::from(b'\r'))
246                }
247                b'n' => {
248                    self.pos += 2;
249                    Ok(u32::from(b'\n'))
250                }
251                b'\\' | b'"' | b'[' | b']' => {
252                    self.pos += 2;
253                    Ok(u32::from(next))
254                }
255                _ => Err(self.err_at("unknown escape", start)),
256            };
257        }
258        if self.cur() != 0 {
259            let (value, next) = decode_char(self.src, self.pos);
260            self.pos = next;
261            return Ok(value);
262        }
263        Err(self.err("unexpected end of input"))
264    }
265
266    /// `parse_token`. Either `<[id]>` or `<name>`.
267    fn parse_token(&mut self) -> Result<u32, GrammarError> {
268        let start = self.pos;
269        if self.cur() != b'<' {
270            return Err(self.err("expecting '<'"));
271        }
272        self.pos += 1;
273
274        if self.cur() == b'[' {
275            self.pos += 1;
276            let id = self.parse_u64()?;
277            let id = u32::try_from(id).map_err(|_| self.err_at("token id is too large", start))?;
278            if self.cur() != b']' {
279                return Err(self.err("expecting ']'"));
280            }
281            self.pos += 1;
282            if self.cur() != b'>' {
283                return Err(self.err("expecting '>'"));
284            }
285            self.pos += 1;
286            return Ok(id);
287        }
288
289        while self.cur() != 0 && self.cur() != b'>' {
290            self.pos += 1;
291        }
292        if self.cur() != b'>' {
293            return Err(self.err("expecting '>'"));
294        }
295        self.pos += 1;
296
297        let text = std::str::from_utf8(&self.src[start..self.pos])
298            .map_err(|_| self.err_at("token name is not valid UTF-8", start))?
299            .to_string();
300
301        let Some(vocab) = self.vocab else {
302            return Err(GrammarError::TokenNeedsVocabulary {
303                token: text,
304                offset: start,
305            });
306        };
307        let ids = vocab.tokenize_special(&text);
308        if ids.len() != 1 {
309            return Err(GrammarError::TokenNotSingle {
310                token: text,
311                n_tokens: ids.len(),
312            });
313        }
314        Ok(ids[0])
315    }
316
317    // -- symbol table --
318
319    /// `get_symbol_id`: intern a name, reusing the id if it is already
320    /// interned. The id is the symbol count *before* insertion.
321    fn get_symbol_id(&mut self, name: &str) -> u32 {
322        let next_id = self.out.symbol_ids.len() as u32;
323        *self
324            .out
325            .symbol_ids
326            .entry(name.to_string())
327            .or_insert(next_id)
328    }
329
330    /// `generate_symbol_id`: a fresh `<base>_<id>` symbol, always new.
331    fn generate_symbol_id(&mut self, base_name: &str) -> u32 {
332        let next_id = self.out.symbol_ids.len() as u32;
333        self.out
334            .symbol_ids
335            .insert(format!("{base_name}_{next_id}"), next_id);
336        next_id
337    }
338
339    /// `add_rule`, growing the table with empty rules as needed. An empty
340    /// rule left behind at the end is an undefined symbol.
341    fn add_rule(&mut self, rule_id: u32, rule: GrammarRule) {
342        let idx = rule_id as usize;
343        if self.out.rules.len() <= idx {
344            self.out.rules.resize(idx + 1, GrammarRule::new());
345        }
346        self.out.rules[idx] = rule;
347    }
348
349    // -- the grammar of the grammar --
350
351    /// `parse_alternates`.
352    fn parse_alternates(
353        &mut self,
354        rule_name: &str,
355        rule_id: u32,
356        is_nested: bool,
357    ) -> Result<(), GrammarError> {
358        let mut rule = GrammarRule::new();
359        self.parse_sequence(rule_name, &mut rule, is_nested)?;
360        while self.cur() == b'|' {
361            rule.push(GrammarElement::new(GreType::Alt, 0));
362            self.pos += 1;
363            self.parse_space(true);
364            self.parse_sequence(rule_name, &mut rule, is_nested)?;
365        }
366        rule.push(GrammarElement::new(GreType::End, 0));
367        self.add_rule(rule_id, rule);
368        Ok(())
369    }
370
371    /// The `handle_repetitions` lambda of `parse_sequence`, hoisted to a
372    /// method. `last_sym_start` is read, never written, upstream too.
373    fn handle_repetitions(
374        &mut self,
375        rule: &mut GrammarRule,
376        rule_name: &str,
377        last_sym_start: usize,
378        n_prev_rules: &mut u64,
379        min_times: u64,
380        max_times: Option<u64>,
381    ) -> Result<(), GrammarError> {
382        let no_max = max_times.is_none();
383        if last_sym_start == rule.len() {
384            return Err(self.err("expecting preceding item to */+/?/{"));
385        }
386
387        let prev_rule: GrammarRule = rule[last_sym_start..].to_vec();
388
389        // Total rules this repetition will generate, before nesting.
390        let mut total_rules: u64 = 1;
391        match max_times {
392            Some(max) if max > 0 => total_rules = max,
393            _ => {
394                if min_times > 0 {
395                    total_rules = min_times;
396                }
397            }
398        }
399
400        let product = n_prev_rules.saturating_mul(total_rules);
401        if product >= MAX_REPETITION_THRESHOLD {
402            return Err(GrammarError::RepetitionTooLarge {
403                requested: product,
404                limit: MAX_REPETITION_THRESHOLD,
405                offset: self.pos,
406            });
407        }
408
409        if min_times == 0 {
410            rule.truncate(last_sym_start);
411        } else {
412            for _ in 1..min_times {
413                rule.extend_from_slice(&prev_rule);
414            }
415        }
416
417        let mut last_rec_rule_id: u32 = 0;
418        // `max_times - min_times` in upstream, which wraps on `{4,2}` and
419        // then loops ~2^64 times. Saturating gives zero optional copies,
420        // so `{4,2}` reads as `{4}` instead of hanging.
421        let n_opt = match max_times {
422            None => 1,
423            Some(max) => max.saturating_sub(min_times),
424        };
425
426        let mut rec_rule = prev_rule.clone();
427        for i in 0..n_opt {
428            rec_rule.truncate(prev_rule.len());
429            let rec_rule_id = self.generate_symbol_id(rule_name);
430            if i > 0 || no_max {
431                rec_rule.push(GrammarElement::new(
432                    GreType::RuleRef,
433                    if no_max {
434                        rec_rule_id
435                    } else {
436                        last_rec_rule_id
437                    },
438                ));
439            }
440            rec_rule.push(GrammarElement::new(GreType::Alt, 0));
441            rec_rule.push(GrammarElement::new(GreType::End, 0));
442            self.add_rule(rec_rule_id, rec_rule.clone());
443            last_rec_rule_id = rec_rule_id;
444        }
445        if n_opt > 0 {
446            rule.push(GrammarElement::new(GreType::RuleRef, last_rec_rule_id));
447        }
448        // Upstream asserts `n_prev_rules >= 1` here; it holds because
449        // `total_rules` is at least 1 and the product was bounds-checked
450        // above, so this is a plain assignment.
451        *n_prev_rules = product;
452        Ok(())
453    }
454
455    /// `parse_sequence`.
456    fn parse_sequence(
457        &mut self,
458        rule_name: &str,
459        rule: &mut GrammarRule,
460        is_nested: bool,
461    ) -> Result<(), GrammarError> {
462        let mut last_sym_start = rule.len();
463        let mut n_prev_rules: u64 = 1;
464
465        while self.cur() != 0 {
466            match self.cur() {
467                b'"' => {
468                    // Literal string.
469                    self.pos += 1;
470                    last_sym_start = rule.len();
471                    n_prev_rules = 1;
472                    while self.cur() != b'"' {
473                        if self.cur() == 0 {
474                            return Err(self.err("unexpected end of input"));
475                        }
476                        let value = self.parse_char()?;
477                        rule.push(GrammarElement::new(GreType::Char, value));
478                    }
479                    self.pos += 1;
480                    self.parse_space(is_nested);
481                }
482                b'[' => {
483                    // Character class, possibly negated, possibly ranged.
484                    self.pos += 1;
485                    let mut start_type = GreType::Char;
486                    if self.cur() == b'^' {
487                        self.pos += 1;
488                        start_type = GreType::CharNot;
489                    }
490                    last_sym_start = rule.len();
491                    n_prev_rules = 1;
492                    while self.cur() != b']' {
493                        if self.cur() == 0 {
494                            return Err(self.err("unexpected end of input"));
495                        }
496                        let value = self.parse_char()?;
497                        // Only the FIRST element of the class carries the
498                        // negation; every later one is CHAR_ALT. That is
499                        // what makes `[^ab]` one negated set rather than
500                        // two.
501                        let gtype = if last_sym_start < rule.len() {
502                            GreType::CharAlt
503                        } else {
504                            start_type
505                        };
506                        rule.push(GrammarElement::new(gtype, value));
507                        if self.at(self.pos) == b'-' && self.at(self.pos + 1) != b']' {
508                            if self.at(self.pos + 1) == 0 {
509                                return Err(self.err("unexpected end of input"));
510                            }
511                            self.pos += 1;
512                            let endchar = self.parse_char()?;
513                            rule.push(GrammarElement::new(GreType::CharRngUpper, endchar));
514                        }
515                    }
516                    self.pos += 1;
517                    self.parse_space(is_nested);
518                }
519                b'<' | b'!' => {
520                    // Token, or inverted token.
521                    let mut gtype = GreType::Token;
522                    if self.cur() == b'!' {
523                        gtype = GreType::TokenNot;
524                        self.pos += 1;
525                    }
526                    let token_id = self.parse_token()?;
527                    last_sym_start = rule.len();
528                    n_prev_rules = 1;
529                    rule.push(GrammarElement::new(gtype, token_id));
530                    self.parse_space(is_nested);
531                }
532                c if Self::is_word_char(c) => {
533                    // Rule reference.
534                    let name_end = self.parse_name()?;
535                    let name = std::str::from_utf8(&self.src[self.pos..name_end])
536                        .map_err(|_| self.err("rule name is not valid UTF-8"))?
537                        .to_string();
538                    let ref_rule_id = self.get_symbol_id(&name);
539                    self.pos = name_end;
540                    self.parse_space(is_nested);
541                    last_sym_start = rule.len();
542                    n_prev_rules = 1;
543                    rule.push(GrammarElement::new(GreType::RuleRef, ref_rule_id));
544                }
545                b'(' => {
546                    // Grouping: parse nested alternates into a synthesized
547                    // rule and refer to it.
548                    self.pos += 1;
549                    self.parse_space(true);
550                    let n_rules_before = self.out.symbol_ids.len() as u64;
551                    let sub_rule_id = self.generate_symbol_id(rule_name);
552                    self.parse_alternates(rule_name, sub_rule_id, true)?;
553                    n_prev_rules = (self.out.symbol_ids.len() as u64 - n_rules_before).max(1);
554                    last_sym_start = rule.len();
555                    rule.push(GrammarElement::new(GreType::RuleRef, sub_rule_id));
556                    if self.cur() != b')' {
557                        return Err(self.err("expecting ')'"));
558                    }
559                    self.pos += 1;
560                    self.parse_space(is_nested);
561                }
562                b'.' => {
563                    last_sym_start = rule.len();
564                    n_prev_rules = 1;
565                    rule.push(GrammarElement::new(GreType::CharAny, 0));
566                    self.pos += 1;
567                    self.parse_space(is_nested);
568                }
569                b'*' => {
570                    self.pos += 1;
571                    self.parse_space(is_nested);
572                    self.handle_repetitions(
573                        rule,
574                        rule_name,
575                        last_sym_start,
576                        &mut n_prev_rules,
577                        0,
578                        None,
579                    )?;
580                }
581                b'+' => {
582                    self.pos += 1;
583                    self.parse_space(is_nested);
584                    self.handle_repetitions(
585                        rule,
586                        rule_name,
587                        last_sym_start,
588                        &mut n_prev_rules,
589                        1,
590                        None,
591                    )?;
592                }
593                b'?' => {
594                    self.pos += 1;
595                    self.parse_space(is_nested);
596                    self.handle_repetitions(
597                        rule,
598                        rule_name,
599                        last_sym_start,
600                        &mut n_prev_rules,
601                        0,
602                        Some(1),
603                    )?;
604                }
605                b'{' => {
606                    self.pos += 1;
607                    self.parse_space(is_nested);
608
609                    if !Self::is_digit_char(self.cur()) {
610                        return Err(self.err("expecting an int"));
611                    }
612                    let min_times = self.parse_u64()?;
613                    self.parse_space(is_nested);
614
615                    let mut max_times: Option<u64> = None;
616
617                    if self.cur() == b'}' {
618                        max_times = Some(min_times);
619                        self.pos += 1;
620                        self.parse_space(is_nested);
621                    } else if self.cur() == b',' {
622                        self.pos += 1;
623                        self.parse_space(is_nested);
624
625                        if Self::is_digit_char(self.cur()) {
626                            max_times = Some(self.parse_u64()?);
627                            self.parse_space(is_nested);
628                        }
629
630                        if self.cur() != b'}' {
631                            return Err(self.err("expecting '}'"));
632                        }
633                        self.pos += 1;
634                        self.parse_space(is_nested);
635                    } else {
636                        return Err(self.err("expecting ','"));
637                    }
638                    if min_times > MAX_REPETITION_THRESHOLD
639                        || max_times.is_some_and(|m| m > MAX_REPETITION_THRESHOLD)
640                    {
641                        return Err(GrammarError::RepetitionTooLarge {
642                            requested: max_times.unwrap_or(min_times),
643                            limit: MAX_REPETITION_THRESHOLD,
644                            offset: self.pos,
645                        });
646                    }
647                    self.handle_repetitions(
648                        rule,
649                        rule_name,
650                        last_sym_start,
651                        &mut n_prev_rules,
652                        min_times,
653                        max_times,
654                    )?;
655                }
656                _ => break,
657            }
658        }
659        Ok(())
660    }
661
662    /// `parse_rule`: one `name ::= alternates` line.
663    fn parse_rule(&mut self) -> Result<(), GrammarError> {
664        let name_end = self.parse_name()?;
665        let name = std::str::from_utf8(&self.src[self.pos..name_end])
666            .map_err(|_| self.err("rule name is not valid UTF-8"))?
667            .to_string();
668        self.pos = name_end;
669        self.parse_space(false);
670        let rule_id = self.get_symbol_id(&name);
671
672        if !(self.cur() == b':' && self.at(self.pos + 1) == b':' && self.at(self.pos + 2) == b'=') {
673            return Err(self.err("expecting ::="));
674        }
675        self.pos += 3;
676        self.parse_space(true);
677
678        self.parse_alternates(&name, rule_id, false)?;
679
680        if self.cur() == b'\r' {
681            self.pos += if self.at(self.pos + 1) == b'\n' { 2 } else { 1 };
682        } else if self.cur() == b'\n' {
683            self.pos += 1;
684        } else if self.cur() != 0 {
685            return Err(self.err("expecting newline or end"));
686        }
687        self.parse_space(true);
688        Ok(())
689    }
690
691    /// `parse`, plus the validation pass that follows it.
692    fn parse_all(&mut self) -> Result<(), GrammarError> {
693        self.parse_space(true);
694        while self.cur() != 0 {
695            self.parse_rule()?;
696        }
697
698        // Every symbol that was referenced must also have been defined.
699        // A rule left empty by `add_rule`'s resize is a reference with no
700        // `::=`; a RULE_REF past the end of the table is the same thing
701        // when the missing symbol has the highest id.
702        for id in 0..self.out.rules.len() {
703            if self.out.rules[id].is_empty() {
704                return Err(self.undefined(id as u32));
705            }
706        }
707        for rule in &self.out.rules {
708            for elem in rule {
709                if elem.gtype == GreType::RuleRef {
710                    let idx = elem.value as usize;
711                    if idx >= self.out.rules.len() || self.out.rules[idx].is_empty() {
712                        return Err(self.undefined(elem.value));
713                    }
714                }
715            }
716        }
717        Ok(())
718    }
719
720    fn undefined(&self, rule_id: u32) -> GrammarError {
721        GrammarError::UndefinedRule {
722            name: self
723                .out
724                .symbol_name(rule_id)
725                .unwrap_or("<unnamed>")
726                .to_string(),
727            rule_id,
728        }
729    }
730}