metrale_grammar/matcher/matcher.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//
3// GrammarMatcher — per-request grammar-matching state.
4//
5// Port of `class GrammarMatcher` / `GrammarMatcher::Impl` from
6// `cpp/grammar_matcher.cc` + `include/xgrammar/matcher.h`. This file
7// holds the struct, construction, token/string acceptance, rollback,
8// termination and reset. The bitmask fill and jump-forward search
9// (the long methods) live in the sibling `fill.rs`.
10//
11// In the C++ the `Impl` *inherits* `EarleyParser`; the Rust port
12// composes one instead (`parser` field) — the parser already exposes
13// `advance`, `pop_last_states`, `push_one_state_to_check`,
14// `is_completed`, `latest_scanable_states` and `reset`.
15//
16// Termination: the C++ tracks `stop_token_is_accepted_` on the parser
17// base. The Rust `EarleyParser` clears its own such flag on every
18// `pop_last_states`, which is *not* what the matcher wants (a rollback
19// past a stop token un-terminates, but a rollback that does not reach
20// the stop step must keep it). The matcher therefore owns its own
21// `stop_token_accepted` flag, undone precisely by `rollback`.
22
23use std::collections::VecDeque;
24use std::sync::Arc;
25
26use crate::compiler::{AdaptiveTokenMask, CompiledGrammar};
27use crate::earley::{EarleyParser, NO_PREV_INPUT_POS, ParserState};
28use crate::tokenizer::TokenizerInfo;
29
30/// Reusable scratch buffers for [`GrammarMatcher::fill_next_token_bitmask`].
31///
32/// `fill_next_token_bitmask` / `compute_partitions` run once per decoded
33/// token. Allocating their working sets fresh each call costs ~2.3 MB of
34/// heap traffic per token (a `vocab_size` bool vec, a clone of the
35/// ~1.5 MB sorted decoded vocab, etc). The C++ matcher instead keeps
36/// member buffers and `.clear()`s them. This struct mirrors that: it is
37/// held on the matcher, `std::mem::take`-n during the fill, `.clear()`ed
38/// and reused, then restored.
39#[derive(Debug, Default)]
40pub(super) struct FillScratch {
41 /// `accepted[token_id]` — union of every state's accepted set.
42 pub(super) accepted: Vec<bool>,
43 /// Running rejected intersection; `{-1}` is the universal set.
44 pub(super) rejected: Vec<i32>,
45 /// Snapshot of the parser's latest scanable states (`Copy`), taken
46 /// once so no parser borrow is held across the trial-loop mutation.
47 pub(super) live_states: Vec<ParserState>,
48 /// Per-live-state `(state, mask)` pairs resolved for this fill.
49 pub(super) states: Vec<(ParserState, Arc<AdaptiveTokenMask>)>,
50 /// Per-state delta of newly rejected uncertain-token indices.
51 pub(super) rejected_delta: Vec<i32>,
52 /// Reusable packed-bitmask buffer for the Coalescence fast-path
53 /// (`forced_token` / `next_forced_tokens`). Sized to
54 /// `bitmask_size(vocab_size)` on first use and reused thereafter, so
55 /// the forced-token check costs no per-call heap allocation.
56 pub(super) coalesce_bitmask: Vec<i32>,
57}
58
59/// A stateful matcher that matches sampled tokens against a compiled
60/// grammar — the core of grammar-guided generation.
61///
62/// One `GrammarMatcher` tracks one request's decoding state. Each
63/// decode step Metrale Engine calls [`Self::fill_next_token_bitmask`] to learn
64/// which tokens are legal, samples one, then [`Self::accept_token`].
65/// [`Self::rollback`] undoes accepted tokens (e.g. for speculative
66/// decoding); [`Self::reset`] returns to the initial state.
67#[derive(Debug)]
68pub struct GrammarMatcher {
69 /// The compiled grammar (shared, cheap to clone). `pub(super)` so
70 /// the `fill` hot path can borrow it as a field disjoint from
71 /// `parser` and `scratch` (the borrow checker needs the concrete
72 /// field, not the whole-`self` accessor).
73 pub(super) compiled_grammar: CompiledGrammar,
74 /// The Earley parser driving byte-level matching.
75 pub(super) parser: EarleyParser,
76 /// Token ids that terminate generation. Either the override set or
77 /// the tokenizer's detected stop tokens.
78 stop_token_ids: Vec<i32>,
79 /// When true the matcher completes (terminates) as soon as the
80 /// root rule is matched, without requiring a stop token.
81 terminate_without_stop_token: bool,
82 /// True once a stop token has been accepted — the matcher is then
83 /// terminated. Undone by [`Self::rollback`] past the stop step.
84 stop_token_accepted: bool,
85 /// Byte length consumed by each accepted token / string, in order.
86 /// One entry per `accept_token`/`accept_string` call; the rollback
87 /// unit. A stop token contributes a `0`-length entry.
88 token_length_history: VecDeque<usize>,
89 /// Reusable working buffers for the per-token bitmask fill. Taken
90 /// (`std::mem::take`) for the duration of the fill and restored
91 /// before return — see [`FillScratch`].
92 pub(super) scratch: FillScratch,
93}
94
95impl GrammarMatcher {
96 /// Construct a matcher from a [`CompiledGrammar`].
97 ///
98 /// * `override_stop_tokens` — when `Some`, replaces the tokenizer's
99 /// detected stop tokens; must be non-empty (panics otherwise,
100 /// matching the C++ `XGRAMMAR_CHECK`).
101 /// * `terminate_without_stop_token` — terminate on root-rule
102 /// completion without needing a stop token.
103 /// * `max_rollback_tokens` — accepted for API faithfulness; the C++
104 /// deprecated and ignores it (rollback is unbounded).
105 ///
106 /// Port of the `GrammarMatcher` constructor.
107 #[must_use]
108 pub fn new(
109 compiled_grammar: CompiledGrammar,
110 override_stop_tokens: Option<Vec<i32>>,
111 terminate_without_stop_token: bool,
112 max_rollback_tokens: i32,
113 ) -> Self {
114 let _ = max_rollback_tokens; // deprecated & unused, kept for ABI parity.
115 if let Some(ref ovr) = override_stop_tokens {
116 assert!(!ovr.is_empty(), "override_stop_tokens must not be empty");
117 }
118 let stop_token_ids = override_stop_tokens
119 .unwrap_or_else(|| compiled_grammar.tokenizer_info().stop_token_ids().to_vec());
120 let parser = EarleyParser::from_grammar(compiled_grammar.grammar_arc());
121 Self {
122 compiled_grammar,
123 parser,
124 stop_token_ids,
125 terminate_without_stop_token,
126 stop_token_accepted: false,
127 token_length_history: VecDeque::new(),
128 scratch: FillScratch::default(),
129 }
130 }
131
132 /// Convenience constructor with default options (no stop-token
133 /// override, requires a stop token, unbounded rollback).
134 #[must_use]
135 pub fn from_compiled_grammar(compiled_grammar: CompiledGrammar) -> Self {
136 Self::new(compiled_grammar, None, false, -1)
137 }
138
139 /// The compiled grammar this matcher runs.
140 #[must_use]
141 pub fn compiled_grammar(&self) -> &CompiledGrammar {
142 &self.compiled_grammar
143 }
144
145 /// The tokenizer info the grammar was compiled against.
146 #[must_use]
147 pub fn tokenizer_info(&self) -> &TokenizerInfo {
148 self.compiled_grammar.tokenizer_info()
149 }
150
151 /// Canonicalize a *live* parser state into the cache key the
152 /// compiler's mask cache is keyed by.
153 ///
154 /// The parser's live scanable states carry real positions
155 /// (`rule_start_pos`, `sub_element_id`, `repeat_count`,
156 /// `partial_codepoint`), but the adaptive-token-mask cache is
157 /// position-agnostic — the C++ `adaptive_token_mask_cache` hashes
158 /// by `StateHashForCache`. This reproduces the exact tuple the
159 /// eager compiler used as the cache key:
160 /// `(rule_id, body_expr_id, element_id, NO_PREV_INPUT_POS, 0)`.
161 /// The result is fed to [`CompiledGrammar::get_or_compute_mask`].
162 pub(super) fn canonical_mask_state(&self, live: &ParserState) -> ParserState {
163 let body_expr_id = self
164 .compiled_grammar
165 .grammar()
166 .rule(live.rule_id)
167 .body_expr_id;
168 ParserState::new(
169 live.rule_id,
170 body_expr_id,
171 live.element_id,
172 NO_PREV_INPUT_POS,
173 0,
174 )
175 }
176
177 /// The stop token ids that terminate this matcher.
178 #[must_use]
179 pub fn stop_token_ids(&self) -> &[i32] {
180 &self.stop_token_ids
181 }
182
183 /// The maximum number of rollback tokens — always `-1` (unbounded),
184 /// matching the deprecated C++ behavior.
185 #[must_use]
186 pub fn max_rollback_tokens(&self) -> i32 {
187 -1
188 }
189
190 /// True if the matcher has terminated. When
191 /// `terminate_without_stop_token` is set this is "root rule
192 /// completed"; otherwise it is "a stop token was accepted".
193 ///
194 /// Port of `IsTerminated`.
195 #[must_use]
196 pub fn is_terminated(&self) -> bool {
197 if self.terminate_without_stop_token {
198 self.parser.is_completed()
199 } else {
200 self.stop_token_accepted
201 }
202 }
203
204 /// True once an actual stop token has been accepted — distinct
205 /// from [`Self::is_terminated`], which also fires on root-rule
206 /// completion under `terminate_without_stop_token`. The bitmask
207 /// fill and jump-forward search guard on this flag specifically.
208 #[must_use]
209 pub(super) fn stop_token_accepted(&self) -> bool {
210 self.stop_token_accepted
211 }
212
213 /// True if the root rule has matched at the current position — a
214 /// stop token may be emitted now. Exposed for callers that need the
215 /// raw completion state independently of termination policy.
216 #[must_use]
217 pub fn is_grammar_completed(&self) -> bool {
218 self.parser.is_completed()
219 }
220
221 /// Try to accept the stop token, terminating the matcher.
222 /// Returns whether the stop token was acceptable. Port of
223 /// `AcceptStopToken`.
224 fn accept_stop_token(&mut self) -> bool {
225 if self.terminate_without_stop_token {
226 return false;
227 }
228 if !self.parser.is_completed() {
229 return false;
230 }
231 debug_assert!(!self.stop_token_accepted);
232 self.token_length_history.push_back(0);
233 self.stop_token_accepted = true;
234 true
235 }
236
237 /// Accept one sampled token and advance the matcher.
238 ///
239 /// Returns `false` (without mutating state) if the token is not
240 /// grammar-legal, is out of range, is a special token, or the
241 /// matcher has already terminated. A stop token terminates the
242 /// matcher. Port of `AcceptToken`.
243 pub fn accept_token(&mut self, token_id: i32, debug: bool) -> bool {
244 let _ = debug;
245 if self.stop_token_accepted {
246 return false;
247 }
248 let vocab_size = self.tokenizer_info().vocab_size() as i32;
249 if token_id < 0 || token_id >= vocab_size {
250 return false;
251 }
252 if self.stop_token_ids.contains(&token_id) {
253 return self.accept_stop_token();
254 }
255 if self
256 .tokenizer_info()
257 .special_token_ids()
258 .contains(&token_id)
259 {
260 return false;
261 }
262 // Decode the token to bytes and feed them one by one.
263 let token = self.tokenizer_info().decoded_vocab()[token_id as usize].clone();
264 self.advance_bytes_as_step(&token)
265 }
266
267 /// Accept a literal string, advancing the matcher byte by byte.
268 /// The whole string counts as one rollback step. Used to seed a
269 /// matcher or in tests. Port of `AcceptString`.
270 pub fn accept_string(&mut self, s: &str, debug: bool) -> bool {
271 self.accept_bytes(s.as_bytes(), debug)
272 }
273
274 /// Accept a literal byte string — the byte-level form of
275 /// [`Self::accept_string`] (token bytes are not always UTF-8).
276 pub fn accept_bytes(&mut self, bytes: &[u8], debug: bool) -> bool {
277 let _ = debug;
278 if self.stop_token_accepted {
279 return false;
280 }
281 self.advance_bytes_as_step(bytes)
282 }
283
284 /// Advance the parser over `bytes` as a single rollback step.
285 ///
286 /// On a rejected byte the bytes already consumed are rolled back so
287 /// the matcher state is unchanged — exactly as the C++
288 /// `AcceptToken` / `AcceptString` do. Returns whether all bytes
289 /// were accepted; on success one history entry is recorded.
290 fn advance_bytes_as_step(&mut self, bytes: &[u8]) -> bool {
291 for (consumed, &byte) in bytes.iter().enumerate() {
292 if !self.parser.advance(byte) {
293 if consumed > 0 {
294 self.parser.pop_last_states(consumed);
295 }
296 return false;
297 }
298 }
299 self.token_length_history.push_back(bytes.len());
300 true
301 }
302
303 /// Roll back the matcher by `n` accepted tokens / strings.
304 ///
305 /// Each prior `accept_token`/`accept_string` is one unit; a stop
306 /// token is a zero-byte unit whose rollback un-terminates the
307 /// matcher. Panics if `n` exceeds the recorded history. Port of
308 /// `Rollback`.
309 pub fn rollback(&mut self, n: i32) {
310 let n = n.max(0) as usize;
311 assert!(
312 n <= self.token_length_history.len(),
313 "cannot roll back {n} tokens: only {} steps recorded",
314 self.token_length_history.len()
315 );
316 for _ in 0..n {
317 let steps = self
318 .token_length_history
319 .pop_back()
320 .expect("history checked non-empty");
321 if steps == 0 {
322 // The stop-token step: un-terminate, no bytes consumed.
323 self.stop_token_accepted = false;
324 } else {
325 self.parser.pop_last_states(steps);
326 }
327 }
328 }
329
330 /// Reset the matcher to its initial state.
331 pub fn reset(&mut self) {
332 self.parser.reset();
333 self.stop_token_accepted = false;
334 self.token_length_history.clear();
335 }
336
337 /// Number of accepted tokens / strings currently in the rollback
338 /// history.
339 #[must_use]
340 pub fn num_history_steps(&self) -> usize {
341 self.token_length_history.len()
342 }
343}