Skip to main content

ferrox_models/grammar/
candidates.rs

1//! Which candidate tokens no viable parse stack accepts.
2//!
3//! `llama_grammar_reject_candidates` and
4//! `llama_grammar_reject_candidates_for_stack`. This is the piece a
5//! sampler hook sits on: it answers "of these tokens, which are
6//! impossible?" without touching logits, a sampler, or a vocabulary.
7//!
8//! The algorithm is a shared-prefix walk, not a per-token replay. All
9//! candidates are advanced one code point together, the stack set is
10//! advanced once, and the survivors recurse. A token whose first character
11//! is impossible costs one comparison; a token that fully matches costs
12//! one pass per character. Replaying the whole machine per token would be
13//! correct and roughly `vocab_size` times slower.
14//!
15//! Two things upstream does in `llama_grammar_apply_impl` are **not** here,
16//! because they need a vocabulary rather than a grammar, and belong to the
17//! sampler hook that does not exist yet:
18//!
19//! - an end-of-generation token is masked unless
20//!   [`Grammar::allows_eog`](super::Grammar::allows_eog);
21//! - a token whose piece is empty, or starts with a NUL byte, is masked
22//!   unconditionally.
23//!
24//! Everything else about a candidate is decided here.
25
26use super::element::{GrammarRule, GrammarStack, GreType};
27use super::error::GrammarError;
28use super::machine::{advance_stack, elem, match_char, match_partial_char, match_token, Grammar};
29use super::utf8::{decode_piece, PartialUtf8};
30
31/// One token offered to the grammar.
32///
33/// `index` is the caller's own index -- a position in a logit array, say --
34/// and is what [`reject_candidates`] hands back. The grammar never
35/// interprets it.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct Candidate<'a> {
38    pub index: usize,
39    pub id: u32,
40    /// The token's decoded piece, as bytes. Not `&str`: a BPE piece can
41    /// hold a fragment of a multi-byte character and still be viable.
42    pub piece: &'a [u8],
43}
44
45impl<'a> Candidate<'a> {
46    pub fn new(index: usize, id: u32, piece: &'a [u8]) -> Self {
47        Self { index, id, piece }
48    }
49}
50
51/// A token piece decoded against the grammar's carried partial sequence.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct DecodedPiece {
54    /// Complete code points, always terminated by a `0`.
55    pub code_points: Vec<u32>,
56    /// Whatever incomplete sequence the piece ends with.
57    pub partial_utf8: PartialUtf8,
58}
59
60/// Decode a piece the way the grammar would, continuing from the partial
61/// UTF-8 sequence the grammar currently carries.
62pub fn decode_for(grammar: &Grammar, piece: &[u8]) -> DecodedPiece {
63    let (code_points, partial_utf8) = decode_piece(piece, grammar.partial_utf8());
64    DecodedPiece {
65        code_points,
66        partial_utf8,
67    }
68}
69
70/// The internal candidate: an index into the decoded arena plus a cursor
71/// along it. `llama_grammar_candidate` keeps a raw `const uint32_t *` here
72/// and walks it forwards on descent and backwards on return; `slot`/`off`
73/// is the same cursor, and `off` can go back down the same way.
74#[derive(Debug, Clone, Copy)]
75struct Cand {
76    index: usize,
77    id: u32,
78    slot: usize,
79    off: usize,
80    partial_utf8: PartialUtf8,
81}
82
83/// Return the `index` of every candidate that no viable stack accepts.
84///
85/// The result order follows the algorithm's traversal, not the input
86/// order; callers mask by index, so it does not matter. If the grammar has
87/// no viable stack left at all, every candidate is rejected.
88pub fn reject_candidates(
89    grammar: &Grammar,
90    candidates: &[Candidate<'_>],
91) -> Result<Vec<usize>, GrammarError> {
92    if grammar.is_awaiting_trigger() {
93        // A lazy grammar that has not fired constrains nothing, and its
94        // stacks are still at the start of the parse -- so answering this
95        // question from them would forbid the very prose the trigger
96        // exists to allow. Upstream's `apply` returns before it ever asks;
97        // a caller that gets here has skipped that check.
98        return Err(GrammarError::AwaitingTrigger);
99    }
100    if candidates.is_empty() {
101        return Ok(Vec::new());
102    }
103    if grammar.stacks().is_empty() {
104        // A dead grammar accepts nothing. Upstream asserts this cannot
105        // happen; it can only be reached by accepting a token the mask
106        // should have removed, so say so rather than abort.
107        return Ok(candidates.iter().map(|c| c.index).collect());
108    }
109
110    // Decode every piece once, against the grammar's carried partial.
111    let mut arena: Vec<Vec<u32>> = Vec::with_capacity(candidates.len());
112    let mut cands: Vec<Cand> = Vec::with_capacity(candidates.len());
113    for c in candidates {
114        let (code_points, partial_utf8) = decode_piece(c.piece, grammar.partial_utf8());
115        arena.push(code_points);
116        cands.push(Cand {
117            index: c.index,
118            id: c.id,
119            slot: arena.len() - 1,
120            off: 0,
121            partial_utf8,
122        });
123    }
124
125    let rejects = reject_over_stacks(grammar.rules(), grammar.stacks(), &arena, &cands)?;
126    Ok(rejects.into_iter().map(|c| c.index).collect())
127}
128
129/// Convenience for a single token: does any viable stack accept it?
130pub fn accepts_token(grammar: &Grammar, id: u32, piece: &[u8]) -> Result<bool, GrammarError> {
131    let c = [Candidate::new(0, id, piece)];
132    Ok(reject_candidates(grammar, &c)?.is_empty())
133}
134
135/// `llama_grammar_reject_candidates`: a candidate is rejected only if
136/// *every* stack rejects it, so the reject set is intersected across
137/// stacks by feeding each stack the previous stack's rejects.
138fn reject_over_stacks(
139    rules: &[GrammarRule],
140    stacks: &[GrammarStack],
141    arena: &[Vec<u32>],
142    candidates: &[Cand],
143) -> Result<Vec<Cand>, GrammarError> {
144    if stacks.is_empty() {
145        return Err(GrammarError::Internal(
146            "reject_over_stacks called with no stacks",
147        ));
148    }
149    if candidates.is_empty() {
150        return Ok(Vec::new());
151    }
152
153    let mut rejects = reject_for_stack(rules, &stacks[0], arena, candidates)?;
154    for stack in &stacks[1..] {
155        if rejects.is_empty() {
156            break;
157        }
158        rejects = reject_for_stack(rules, stack, arena, &rejects)?;
159    }
160    Ok(rejects)
161}
162
163/// `llama_grammar_reject_candidates_for_stack`.
164fn reject_for_stack(
165    rules: &[GrammarRule],
166    stack: &GrammarStack,
167    arena: &[Vec<u32>],
168    candidates: &[Cand],
169) -> Result<Vec<Cand>, GrammarError> {
170    let mut rejects: Vec<Cand> = Vec::with_capacity(candidates.len());
171
172    let Some(&stack_pos) = stack.last() else {
173        // The grammar is satisfied. Only a token that contributes nothing
174        // more survives: no complete code points and no partial sequence.
175        for tok in candidates {
176            if arena[tok.slot][tok.off] != 0 || tok.partial_utf8.n_remain != 0 {
177                rejects.push(*tok);
178            }
179        }
180        return Ok(rejects);
181    };
182
183    let stack_elem = elem(rules, stack_pos);
184
185    // A stack resting on a token element decides on the token id alone.
186    if matches!(stack_elem.gtype, GreType::Token | GreType::TokenNot) {
187        for tok in candidates {
188            if arena[tok.slot][tok.off] == 0 {
189                // The character rules consumed this token's code points;
190                // reject only if it ended mid-codepoint.
191                if tok.partial_utf8.n_remain != 0 {
192                    rejects.push(*tok);
193                }
194            } else if !match_token(stack_elem, tok.id) {
195                rejects.push(*tok);
196            }
197        }
198        return Ok(rejects);
199    }
200
201    let mut next_candidates: Vec<Cand> = Vec::with_capacity(candidates.len());
202
203    for tok in candidates {
204        if arena[tok.slot][tok.off] == 0 {
205            // Out of complete code points. Reject only if the trailing
206            // partial sequence could not possibly satisfy this position.
207            if tok.partial_utf8.n_remain != 0
208                && !match_partial_char(rules, stack_pos, tok.partial_utf8)?
209            {
210                rejects.push(*tok);
211            }
212        } else if match_char(rules, stack_pos, arena[tok.slot][tok.off])?.0 {
213            let mut advanced = *tok;
214            advanced.off += 1;
215            next_candidates.push(advanced);
216        } else {
217            rejects.push(*tok);
218        }
219    }
220
221    // Advance the stack past this character class once, for everyone.
222    let (_, stack_pos_after) = match_char(rules, stack_pos, 0)?;
223    let mut stack_after = stack[..stack.len() - 1].to_vec();
224    if !elem(rules, stack_pos_after).is_end_of_sequence() {
225        stack_after.push(stack_pos_after);
226    }
227    let mut next_stacks: Vec<GrammarStack> = Vec::new();
228    advance_stack(rules, &stack_after, &mut next_stacks)?;
229
230    let next_rejects = reject_over_stacks(rules, &next_stacks, arena, &next_candidates)?;
231    for tok in next_rejects {
232        let mut back = tok;
233        back.off -= 1;
234        rejects.push(back);
235    }
236
237    Ok(rejects)
238}