Skip to main content

ferrox_models/grammar/
machine.rs

1//! The pushdown stack machine, transcribed from the second half of
2//! llama.cpp's `src/llama-grammar.cpp`.
3//!
4//! A [`Grammar`] holds the *set* of parse stacks that are still viable.
5//! Every stack rests on an element that consumes something -- a character
6//! class or a token -- so "which characters may come next" is read
7//! straight off the stack tops, and a character that no stack accepts
8//! kills the parse.
9//!
10//! Two invariants earn their keep and are easy to get wrong:
11//!
12//! - `advance_stack` runs a rule reference out to *every* alternate before
13//!   settling, so one input stack becomes N output stacks. A grammar with
14//!   nested alternation has more viable stacks than it has rules.
15//! - An empty stack means "the grammar is satisfied". It is not an error
16//!   and it is not dropped; it is the only thing that lets end-of-
17//!   generation be accepted (`allows_eog`).
18
19use std::collections::HashSet;
20
21use super::element::{GrammarElement, GrammarRule, GrammarStack, GreType, RulePos};
22use super::error::GrammarError;
23use super::lazy::{LazyState, LazyTriggers, TriggerStep};
24use super::parser::{parse_with_vocab, GrammarVocab, ParsedGrammar};
25use super::utf8::{decode_piece, PartialUtf8};
26
27/// A compiled grammar plus the live set of viable parse stacks.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct Grammar {
30    rules: Vec<GrammarRule>,
31    stacks: Vec<GrammarStack>,
32    partial_utf8: PartialUtf8,
33    /// Set by [`Grammar::into_lazy`]. `None` is llama.cpp's `lazy = false`:
34    /// the grammar applies from the first token.
35    lazy: Option<LazyState>,
36}
37
38impl Grammar {
39    /// Parse GBNF text and start from `root_name`.
40    ///
41    /// `llama_grammar_init_impl(vocab, grammar_str, grammar_root, ...)`,
42    /// minus the lazy-trigger machinery.
43    pub fn from_str_with_root(src: &str, root_name: &str) -> Result<Self, GrammarError> {
44        Self::from_str_with_vocab(src, root_name, None)
45    }
46
47    /// As [`Self::from_str_with_root`], resolving `<name>` token elements
48    /// through a vocabulary.
49    pub fn from_str_with_vocab(
50        src: &str,
51        root_name: &str,
52        vocab: Option<&dyn GrammarVocab>,
53    ) -> Result<Self, GrammarError> {
54        let parsed = parse_with_vocab(src, vocab)?;
55        Self::from_parsed(&parsed, root_name)
56    }
57
58    /// Start a machine over an already-parsed grammar.
59    pub fn from_parsed(parsed: &ParsedGrammar, root_name: &str) -> Result<Self, GrammarError> {
60        let start = parsed
61            .symbol_id(root_name)
62            .ok_or_else(|| GrammarError::MissingRoot {
63                name: root_name.to_string(),
64            })?;
65        Self::from_rules(parsed.rules.clone(), start, |id| {
66            parsed.symbol_name(id).map(str::to_string)
67        })
68    }
69
70    /// Build from a raw rule table.
71    ///
72    /// `name_of` supplies a symbol name for diagnostics; pass `|_| None` if
73    /// there is no symbol table.
74    pub fn from_rules(
75        rules: Vec<GrammarRule>,
76        start_rule_index: u32,
77        name_of: impl Fn(u32) -> Option<String>,
78    ) -> Result<Self, GrammarError> {
79        let n_rules = rules.len();
80
81        // Every rule must be terminated, or `advance_stack` walks off the
82        // end of one. llama.cpp guarantees this by construction and does
83        // not check; a hand-built table can violate it.
84        for (i, rule) in rules.iter().enumerate() {
85            if rule.last().map(|e| e.gtype) != Some(GreType::End) {
86                return Err(GrammarError::UndefinedRule {
87                    name: name_of(i as u32).unwrap_or_else(|| "<unnamed>".into()),
88                    rule_id: i as u32,
89                });
90            }
91        }
92
93        // Every rule reference must resolve.
94        for rule in rules.iter() {
95            for elem in rule {
96                if elem.gtype == GreType::RuleRef {
97                    let idx = elem.value as usize;
98                    if idx >= n_rules || rules[idx].is_empty() {
99                        return Err(GrammarError::UndefinedRule {
100                            name: name_of(elem.value).unwrap_or_else(|| "<unnamed>".into()),
101                            rule_id: elem.value,
102                        });
103                    }
104                }
105            }
106        }
107
108        if start_rule_index as usize >= n_rules {
109            return Err(GrammarError::MissingRoot {
110                name: name_of(start_rule_index).unwrap_or_else(|| "root".into()),
111            });
112        }
113
114        detect_left_recursion_all(&rules, &name_of)?;
115
116        // Loop over the alternates of the start rule to build the initial
117        // stacks.
118        let mut stacks: Vec<GrammarStack> = Vec::new();
119        let mut pos = RulePos::new(start_rule_index, 0);
120        loop {
121            let mut stack = GrammarStack::new();
122            if !elem(&rules, pos).is_end_of_sequence() {
123                stack.push(pos);
124            }
125            advance_stack(&rules, &stack, &mut stacks)?;
126            while !elem(&rules, pos).is_end_of_sequence() {
127                pos = pos.next();
128            }
129            if elem(&rules, pos).gtype == GreType::Alt {
130                pos = pos.next();
131            } else {
132                break;
133            }
134        }
135
136        Ok(Grammar {
137            rules,
138            stacks,
139            partial_utf8: PartialUtf8::default(),
140            lazy: None,
141        })
142    }
143
144    /// Make this grammar LAZY: it constrains nothing until one of
145    /// `triggers` matches the output.
146    ///
147    /// `llama_grammar_init_impl`'s `lazy` / `trigger_patterns` /
148    /// `trigger_tokens` arguments. See [`super::lazy`] for what the
149    /// triggers match against and what happens to the text before one.
150    ///
151    /// Refuses an empty trigger set: upstream allows it, and the result is
152    /// a grammar that can never switch on -- an unconstrained generation
153    /// that looks constrained from the outside.
154    pub fn into_lazy(mut self, triggers: LazyTriggers) -> Result<Self, GrammarError> {
155        if triggers.is_empty() {
156            return Err(GrammarError::LazyWithoutTriggers);
157        }
158        self.lazy = Some(LazyState::new(triggers));
159        Ok(self)
160    }
161
162    /// Whether this grammar waits for a trigger before it constrains.
163    pub fn is_lazy(&self) -> bool {
164        self.lazy.is_some()
165    }
166
167    /// Whether this grammar is lazy and has NOT yet been triggered, i.e.
168    /// constrains nothing right now.
169    ///
170    /// `llama_grammar::awaiting_trigger`, which is the first thing both
171    /// `llama_grammar_apply_impl` and `llama_grammar_accept_impl` test.
172    pub fn is_awaiting_trigger(&self) -> bool {
173        self.lazy.as_ref().is_some_and(LazyState::awaiting)
174    }
175
176    /// The output accumulated while awaiting a trigger. Empty once one has
177    /// fired, and for a grammar that is not lazy.
178    pub fn trigger_buffer(&self) -> &[u8] {
179        self.lazy.as_ref().map_or(&[], LazyState::buffer)
180    }
181
182    /// The compiled rule table.
183    pub fn rules(&self) -> &[GrammarRule] {
184        &self.rules
185    }
186
187    /// The stacks still viable after everything accepted so far.
188    pub fn stacks(&self) -> &[GrammarStack] {
189        &self.stacks
190    }
191
192    /// The partial UTF-8 sequence carried over from the last piece.
193    pub fn partial_utf8(&self) -> PartialUtf8 {
194        self.partial_utf8
195    }
196
197    /// True when at least one viable parse is complete, so an
198    /// end-of-generation token is allowed.
199    ///
200    /// `llama_grammar_apply_impl`'s `allow_eog`. An empty stack is a
201    /// finished parse.
202    ///
203    /// A lazy grammar that has not triggered allows it unconditionally:
204    /// upstream's `awaiting_trigger` early-return sits *above* both the
205    /// `allow_eog` mask and the abort in `llama_grammar_accept_impl`, so
206    /// an untriggered grammar has no opinion about ending. It has not been
207    /// applied; a generation that never calls a tool must be able to stop.
208    ///
209    /// Unless its trigger is MANDATORY, which is this repo's own addition
210    /// and the one place it departs from upstream here: see
211    /// [`LazyTriggers::mandatory`].
212    pub fn allows_eog(&self) -> bool {
213        if self.is_awaiting_trigger() {
214            return !self.trigger_is_mandatory();
215        }
216        self.stacks.iter().any(|s| s.is_empty())
217    }
218
219    /// Whether this grammar's trigger must fire before the generation may
220    /// end. False for every grammar that is not lazy, and for every lazy
221    /// grammar whose triggers were not marked
222    /// [`mandatory`](LazyTriggers::mandatory).
223    pub fn trigger_is_mandatory(&self) -> bool {
224        self.lazy.as_ref().is_some_and(LazyState::is_mandatory)
225    }
226
227    /// True when no parse is viable at all. Reaching this means a token was
228    /// accepted that should have been masked out.
229    pub fn is_dead(&self) -> bool {
230        self.stacks.is_empty()
231    }
232
233    /// Advance every stack over one code point.
234    ///
235    /// `llama_grammar_accept`. Stacks that cannot take the character are
236    /// dropped; a stack resting on a token element is dropped too, since a
237    /// token element consumes a whole token, never a character.
238    pub fn accept_codepoint(&mut self, chr: u32) -> Result<(), GrammarError> {
239        let mut next: Vec<GrammarStack> = Vec::with_capacity(self.stacks.len());
240        for stack in &self.stacks {
241            accept_chr(&self.rules, stack, chr, &mut next)?;
242        }
243        self.stacks = next;
244        Ok(())
245    }
246
247    /// Accept a piece of generated text, carrying any partial UTF-8
248    /// sequence across the call.
249    ///
250    /// `llama_grammar_accept_str`. Errors if nothing survives.
251    pub fn accept_str(&mut self, piece: &str) -> Result<(), GrammarError> {
252        self.accept_bytes(piece.as_bytes())
253    }
254
255    /// As [`Self::accept_str`], for a piece that is not valid UTF-8 on its
256    /// own.
257    ///
258    /// This is the real signature: a BPE token piece is bytes, and a piece
259    /// holding one byte of a multi-byte character is not a `str` at all.
260    /// llama.cpp passes `std::string`, which has the same freedom.
261    pub fn accept_bytes(&mut self, piece: &[u8]) -> Result<(), GrammarError> {
262        let (code_points, partial) = decode_piece(piece, self.partial_utf8);
263        // The vector is 0-terminated; the terminator is not a code point.
264        for &cp in &code_points[..code_points.len() - 1] {
265            self.accept_codepoint(cp)?;
266        }
267        self.partial_utf8 = partial;
268        if self.stacks.is_empty() {
269            return Err(GrammarError::NoViableStack {
270                piece: String::from_utf8_lossy(piece).into_owned(),
271            });
272        }
273        Ok(())
274    }
275
276    /// Accept a sampled token, given its decoded piece.
277    ///
278    /// `llama_grammar_accept_token`. This is not `accept_str` plus a token
279    /// id: a stack resting on a `Token` / `TokenNot` element matches on the
280    /// **id** and ignores the piece entirely, which is how a grammar can
281    /// require a specific special token whose text is unreachable through
282    /// its characters.
283    ///
284    /// While a lazy grammar is awaiting its trigger this does NOT advance
285    /// the parse: the token goes to the trigger buffer instead, and the
286    /// grammar is fed only once a trigger fires, and only from where it
287    /// says. That dispatch lives here, on the one accept path, rather than
288    /// in a lazy-aware twin of it.
289    pub fn accept_token(&mut self, token: u32, piece: &[u8]) -> Result<(), GrammarError> {
290        if self.is_awaiting_trigger() {
291            let step = match self.lazy.as_mut() {
292                Some(lazy) => lazy.observe(token, piece)?,
293                None => return Err(GrammarError::Internal("lazy state vanished mid-accept")),
294            };
295            let replay = match step {
296                TriggerStep::Awaiting => return Ok(()),
297                TriggerStep::Fired(replay) => replay,
298            };
299            for (tok, piece) in replay {
300                self.accept_token_now(tok, &piece)?;
301            }
302            return Ok(());
303        }
304        self.accept_token_now(token, piece)
305    }
306
307    /// `llama_grammar_accept_token`: the acceptance itself, with no
308    /// trigger check. The replay above is upstream's direct call to it.
309    fn accept_token_now(&mut self, token: u32, piece: &[u8]) -> Result<(), GrammarError> {
310        let (code_points, partial) = decode_piece(piece, self.partial_utf8);
311        let chars = &code_points[..code_points.len() - 1];
312
313        let mut stacks_new: Vec<GrammarStack> = Vec::with_capacity(self.stacks.len());
314
315        for stack in &self.stacks {
316            // A completed parse cannot consume another token; only an
317            // end-of-generation token, handled by `accept_eog`.
318            let Some(&top) = stack.last() else {
319                continue;
320            };
321            let top_elem = elem(&self.rules, top);
322
323            if matches!(top_elem.gtype, GreType::Token | GreType::TokenNot) {
324                if match_token(top_elem, token) {
325                    let mut new_stack = stack[..stack.len() - 1].to_vec();
326                    if !elem(&self.rules, top.next()).is_end_of_sequence() {
327                        new_stack.push(top.next());
328                    }
329                    advance_stack(&self.rules, &new_stack, &mut stacks_new)?;
330                }
331                continue;
332            }
333
334            let mut current: Vec<GrammarStack> = vec![stack.clone()];
335            for &cp in chars {
336                let mut next: Vec<GrammarStack> = Vec::new();
337                for cur in &current {
338                    accept_chr(&self.rules, cur, cp, &mut next)?;
339                }
340                current = next;
341                if current.is_empty() {
342                    break;
343                }
344            }
345            for surviving in current {
346                if !stacks_new.contains(&surviving) {
347                    stacks_new.push(surviving);
348                }
349            }
350        }
351
352        self.stacks = stacks_new;
353        self.partial_utf8 = partial;
354
355        if self.stacks.is_empty() {
356            return Err(GrammarError::NoViableStack {
357                piece: String::from_utf8_lossy(piece).into_owned(),
358            });
359        }
360        Ok(())
361    }
362
363    /// Accept an end-of-generation token.
364    ///
365    /// The EOG branch of `llama_grammar_accept_impl`, which aborts if no
366    /// stack is empty. Here it is a refusal: EOG at a point where the
367    /// grammar is unsatisfied means the mask let it through.
368    ///
369    /// Upstream's EOG branch sits *below* the `awaiting_trigger` check, so
370    /// an untriggered lazy grammar never reaches it: an EOG token is
371    /// buffered like any other. A caller that has the token's piece --
372    /// [`crate::grammar_sampler::GrammarSampler`] does -- must therefore
373    /// send it to [`Self::accept_token`] while [`Self::is_awaiting_trigger`],
374    /// not here.
375    pub fn accept_eog(&mut self) -> Result<(), GrammarError> {
376        if self.allows_eog() {
377            Ok(())
378        } else {
379            Err(GrammarError::NoViableStack {
380                piece: "<eog>".to_string(),
381            })
382        }
383    }
384}
385
386/// `rules[pos.rule][pos.index]`.
387///
388/// Out of range is unreachable for a validated table: every rule ends with
389/// `End`, every walk stops there, and `from_rules` checks it. Returning
390/// `End` rather than panicking keeps a malformed hand-built table from
391/// taking the process down.
392#[inline]
393pub(crate) fn elem(rules: &[GrammarRule], pos: RulePos) -> GrammarElement {
394    rules
395        .get(pos.rule as usize)
396        .and_then(|r| r.get(pos.index as usize))
397        .copied()
398        .unwrap_or(GrammarElement::new(GreType::End, 0))
399}
400
401/// `llama_grammar_match_char`: does `chr` satisfy the character class at
402/// `pos`? Returns the verdict and the position just past the class.
403///
404/// The negation lives on the *first* element of the class only, so this
405/// walks the whole `CharAlt` chain accumulating `found`, and inverts once
406/// at the end. Testing each element against its own type instead would
407/// make `[^ab]` mean "not a, or not b", which is every character.
408pub(crate) fn match_char(
409    rules: &[GrammarRule],
410    mut pos: RulePos,
411    chr: u32,
412) -> Result<(bool, RulePos), GrammarError> {
413    let first = elem(rules, pos);
414    let is_positive_char = matches!(first.gtype, GreType::Char | GreType::CharAny);
415    if !is_positive_char && first.gtype != GreType::CharNot {
416        return Err(GrammarError::Internal(
417            "match_char called on an element that is not a character class",
418        ));
419    }
420
421    let mut found = false;
422    loop {
423        let cur = elem(rules, pos);
424        let nxt = elem(rules, pos.next());
425        if nxt.gtype == GreType::CharRngUpper {
426            // Inclusive range, e.g. [a-z].
427            found = found || (cur.value <= chr && chr <= nxt.value);
428            pos = pos.advance(2);
429        } else if cur.gtype == GreType::CharAny {
430            found = true;
431            pos = pos.next();
432        } else {
433            // Exact match, e.g. [a] or "a".
434            found = found || cur.value == chr;
435            pos = pos.next();
436        }
437        if elem(rules, pos).gtype != GreType::CharAlt {
438            break;
439        }
440    }
441
442    Ok((found == is_positive_char, pos))
443}
444
445/// `llama_grammar_match_partial_char`: could *some* continuation of this
446/// partial UTF-8 sequence satisfy the class at `pos`?
447///
448/// This is what keeps a token that ends mid-codepoint viable. Without it,
449/// every multi-byte character a BPE vocabulary splits across two tokens
450/// would be unreachable under any grammar.
451pub(crate) fn match_partial_char(
452    rules: &[GrammarRule],
453    mut pos: RulePos,
454    partial_utf8: PartialUtf8,
455) -> Result<bool, GrammarError> {
456    let first = elem(rules, pos);
457    let is_positive_char = matches!(first.gtype, GreType::Char | GreType::CharAny);
458    if !is_positive_char && first.gtype != GreType::CharNot {
459        return Err(GrammarError::Internal(
460            "match_partial_char called on an element that is not a character class",
461        ));
462    }
463
464    let partial_value = partial_utf8.value;
465    let n_remain = partial_utf8.n_remain;
466
467    // Invalid sequence, or a 7-bit character split across two bytes
468    // (overlong): no continuation can be legal UTF-8.
469    if n_remain < 0 || (n_remain == 1 && partial_value < 2) {
470        return Ok(false);
471    }
472    // A UTF-8 sequence never has more than 3 continuation bytes, so this
473    // is unreachable from `decode_piece`. It is a guard against a
474    // hand-built `PartialUtf8`, where upstream's `1 << (n_remain * 6)`
475    // would be undefined behaviour and Rust's would panic.
476    if n_remain > 3 {
477        return Ok(false);
478    }
479
480    // The range of code points this partial sequence could complete to.
481    let shift = (n_remain * 6) as u32;
482    let mut low = partial_value << shift;
483    let high = low | ((1u32 << shift) - 1);
484
485    if low == 0 {
486        if n_remain == 2 {
487            low = 1 << 11;
488        } else if n_remain == 3 {
489            low = 1 << 16;
490        }
491    }
492
493    loop {
494        let cur = elem(rules, pos);
495        let nxt = elem(rules, pos.next());
496        if nxt.gtype == GreType::CharRngUpper {
497            if cur.value <= high && low <= nxt.value {
498                return Ok(is_positive_char);
499            }
500            pos = pos.advance(2);
501        } else if cur.gtype == GreType::CharAny {
502            // Upstream returns an unconditional `true` here, not
503            // `is_positive_char`. `.` is never negated, so they agree.
504            return Ok(true);
505        } else {
506            if low <= cur.value && cur.value <= high {
507                return Ok(is_positive_char);
508            }
509            pos = pos.next();
510        }
511        if elem(rules, pos).gtype != GreType::CharAlt {
512            break;
513        }
514    }
515
516    Ok(!is_positive_char)
517}
518
519/// `llama_grammar_match_token`.
520pub(crate) fn match_token(pos_elem: GrammarElement, token: u32) -> bool {
521    match pos_elem.gtype {
522        GreType::Token => pos_elem.value == token,
523        GreType::TokenNot => pos_elem.value != token,
524        _ => false,
525    }
526}
527
528/// `llama_grammar_advance_stack`: expand rule references until every
529/// resulting stack rests on something that consumes input.
530///
531/// Appends to `new_stacks`, skipping duplicates, exactly as upstream does.
532pub(crate) fn advance_stack(
533    rules: &[GrammarRule],
534    stack: &GrammarStack,
535    new_stacks: &mut Vec<GrammarStack>,
536) -> Result<(), GrammarError> {
537    let mut todo: Vec<GrammarStack> = vec![stack.clone()];
538    let mut seen: HashSet<GrammarStack> = HashSet::new();
539
540    while let Some(curr_stack) = todo.pop() {
541        if !seen.insert(curr_stack.clone()) {
542            continue;
543        }
544
545        let Some(&pos) = curr_stack.last() else {
546            // An empty stack is a completed parse, and is kept.
547            if !new_stacks.contains(&curr_stack) {
548                new_stacks.push(curr_stack);
549            }
550            continue;
551        };
552
553        let pos_elem = elem(rules, pos);
554        match pos_elem.gtype {
555            GreType::RuleRef => {
556                let rule_id = pos_elem.value;
557                let mut subpos = RulePos::new(rule_id, 0);
558                loop {
559                    // The stack without its top, plus the continuation
560                    // after this reference, plus the alternate's start.
561                    let mut next_stack = curr_stack[..curr_stack.len() - 1].to_vec();
562                    if !elem(rules, pos.next()).is_end_of_sequence() {
563                        next_stack.push(pos.next());
564                    }
565                    if !elem(rules, subpos).is_end_of_sequence() {
566                        next_stack.push(subpos);
567                    }
568                    todo.push(next_stack);
569
570                    while !elem(rules, subpos).is_end_of_sequence() {
571                        subpos = subpos.next();
572                    }
573                    if elem(rules, subpos).gtype == GreType::Alt {
574                        subpos = subpos.next();
575                    } else {
576                        break;
577                    }
578                }
579            }
580            t if t.is_stack_terminal() => {
581                if !new_stacks.contains(&curr_stack) {
582                    new_stacks.push(curr_stack);
583                }
584            }
585            _ => {
586                // End / Alt / CharAlt / CharRngUpper. Upstream aborts the
587                // process; a stack is never left on one of these.
588                return Err(GrammarError::Internal(
589                    "parse stack came to rest on END, ALT, CHAR_ALT or CHAR_RNG_UPPER",
590                ));
591            }
592        }
593    }
594    Ok(())
595}
596
597/// `llama_grammar_accept_chr`: advance one stack over one code point.
598pub(crate) fn accept_chr(
599    rules: &[GrammarRule],
600    stack: &GrammarStack,
601    chr: u32,
602    new_stacks: &mut Vec<GrammarStack>,
603) -> Result<(), GrammarError> {
604    let Some(&pos) = stack.last() else {
605        return Ok(());
606    };
607
608    let pos_elem = elem(rules, pos);
609    // A token element consumes a token, not a character; such a stack
610    // simply does not advance here.
611    if matches!(pos_elem.gtype, GreType::Token | GreType::TokenNot) {
612        return Ok(());
613    }
614
615    let (matched, after) = match_char(rules, pos, chr)?;
616    if matched {
617        let mut new_stack = stack[..stack.len() - 1].to_vec();
618        if !elem(rules, after).is_end_of_sequence() {
619            new_stack.push(after);
620        }
621        advance_stack(rules, &new_stack, new_stacks)?;
622    }
623    Ok(())
624}
625
626// -- left recursion --
627
628/// `llama_grammar_detect_left_recursion`, run over every rule.
629fn detect_left_recursion_all(
630    rules: &[GrammarRule],
631    name_of: &impl Fn(u32) -> Option<String>,
632) -> Result<(), GrammarError> {
633    let n = rules.len();
634    let mut visited = vec![false; n];
635    let mut in_progress = vec![false; n];
636    let mut may_be_empty = vec![false; n];
637    for i in 0..n {
638        if visited[i] {
639            continue;
640        }
641        if detect_left_recursion(rules, i, &mut visited, &mut in_progress, &mut may_be_empty) {
642            return Err(GrammarError::LeftRecursion {
643                rule_id: i as u32,
644                name: name_of(i as u32),
645            });
646        }
647    }
648    Ok(())
649}
650
651fn detect_left_recursion(
652    rules: &[GrammarRule],
653    rule_index: usize,
654    visited: &mut [bool],
655    in_progress: &mut [bool],
656    may_be_empty: &mut [bool],
657) -> bool {
658    if in_progress[rule_index] {
659        return true;
660    }
661    in_progress[rule_index] = true;
662
663    let rule = &rules[rule_index];
664
665    // First: can this rule produce the empty string? An alternate whose
666    // very first element ends the sequence is empty.
667    let mut at_rule_start = true;
668    for e in rule {
669        if e.is_end_of_sequence() {
670            if at_rule_start {
671                may_be_empty[rule_index] = true;
672                break;
673            }
674            at_rule_start = true;
675        } else {
676            at_rule_start = false;
677        }
678    }
679
680    // Second: recurse into leftmost non-terminals, and into the next one
681    // along for as long as the previous one may be empty.
682    let mut recurse_into_nonterminal = true;
683    for e in rule {
684        if e.gtype == GreType::RuleRef && recurse_into_nonterminal {
685            let target = e.value as usize;
686            if target >= rules.len() {
687                continue;
688            }
689            if detect_left_recursion(rules, target, visited, in_progress, may_be_empty) {
690                return true;
691            }
692            if !may_be_empty[target] {
693                recurse_into_nonterminal = false;
694            }
695        } else {
696            // A new alternate starts fresh; anything else has consumed
697            // input, so nothing after it is leftmost any more.
698            recurse_into_nonterminal = e.is_end_of_sequence();
699        }
700    }
701
702    in_progress[rule_index] = false;
703    visited[rule_index] = true;
704    false
705}