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 ¤t {
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}