1use std::sync::Arc;
26
27use ferrum_interfaces::sampler::{LogitsProcessor, ProcessorPriority, SamplingContext};
28use ferrum_interfaces::tokenizer::Tokenizer;
29use ferrum_types::{FerrumError, Result, TokenId};
30use parking_lot::Mutex;
31use regex_automata::{
32 dfa::{dense::DFA, Automaton, StartKind},
33 util::{primitives::StateID, start::Config as StartConfig},
34 Anchored,
35};
36
37pub struct RegexGuidedProcessor {
43 dfa: DFA<Vec<u32>>,
44 state: Mutex<DfaPosition>,
47 token_bytes: Vec<Vec<u8>>,
49 eos_token: Option<TokenId>,
51 consumed: Mutex<usize>,
53}
54
55impl std::fmt::Debug for RegexGuidedProcessor {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.debug_struct("RegexGuidedProcessor")
58 .field("vocab_size", &self.token_bytes.len())
59 .field("consumed", &*self.consumed.lock())
60 .finish()
61 }
62}
63
64#[derive(Copy, Clone, Debug)]
65struct DfaPosition {
66 state: StateID,
67 dead: bool,
71}
72
73impl RegexGuidedProcessor {
74 pub fn new(
77 pattern: &str,
78 tokenizer: Arc<dyn Tokenizer + Send + Sync>,
79 eos_token: Option<TokenId>,
80 ) -> Result<Self> {
81 let wrapped = format!(r"(?:{pattern})\z");
86 let dfa = DFA::builder()
87 .configure(DFA::config().start_kind(StartKind::Anchored))
88 .build(&wrapped)
89 .map_err(|e| FerrumError::invalid_request(format!("regex compile: {e}")))?;
90
91 let start = dfa
92 .start_state(&StartConfig::new().anchored(Anchored::Yes))
93 .map_err(|e| FerrumError::invalid_request(format!("regex start state: {e}")))?;
94
95 let vocab_size = tokenizer.vocab_size();
100 let mut token_bytes = Vec::with_capacity(vocab_size);
101 for i in 0..vocab_size {
102 let id = TokenId::new(i as u32);
103 let bytes = tokenizer
104 .decode(&[id], false)
105 .ok()
106 .or_else(|| tokenizer.token_text(id).map(str::to_string))
107 .map(String::into_bytes)
108 .unwrap_or_default();
109 token_bytes.push(bytes);
110 }
111
112 Ok(Self {
113 dfa,
114 state: Mutex::new(DfaPosition {
115 state: start,
116 dead: false,
117 }),
118 token_bytes,
119 eos_token,
120 consumed: Mutex::new(0),
121 })
122 }
123
124 pub fn reset(&self) -> Result<()> {
126 let start = self
127 .dfa
128 .start_state(&StartConfig::new().anchored(Anchored::Yes))
129 .map_err(|e| FerrumError::internal(format!("regex start state: {e}")))?;
130 *self.state.lock() = DfaPosition {
131 state: start,
132 dead: false,
133 };
134 *self.consumed.lock() = 0;
135 Ok(())
136 }
137
138 pub fn can_accept(&self) -> bool {
140 let pos = *self.state.lock();
141 !pos.dead && self.dfa.is_match_state(self.dfa.next_eoi_state(pos.state))
142 }
143
144 fn advance(&self, mut state: StateID, bytes: &[u8]) -> Option<StateID> {
148 for &b in bytes {
149 state = self.dfa.next_state(state, b);
150 if self.dfa.is_dead_state(state) {
151 return None;
152 }
153 }
154 Some(state)
155 }
156
157 pub fn mask_logits(&self, logits: &mut [f32]) {
159 let pos = *self.state.lock();
160 if pos.dead {
161 self.force_eos(logits);
163 return;
164 }
165
166 let pattern_done = self.dfa.is_match_state(self.dfa.next_eoi_state(pos.state));
167
168 let vocab = logits.len().min(self.token_bytes.len());
172 let mut any_allowed = false;
173 for idx in 0..vocab {
174 let is_eos = self.eos_token.map_or(false, |e| e.get() as usize == idx);
175 let bytes = &self.token_bytes[idx];
176
177 let allowed = if is_eos {
178 pattern_done
179 } else if bytes.is_empty() {
180 pattern_done
184 } else {
185 self.advance(pos.state, bytes).is_some()
186 };
187
188 if allowed {
189 any_allowed = true;
190 } else {
191 logits[idx] = f32::NEG_INFINITY;
192 }
193 }
194
195 if !any_allowed {
200 self.force_eos(logits);
201 }
202 }
203
204 fn force_eos(&self, logits: &mut [f32]) {
205 if let Some(eos) = self.eos_token {
206 let eos_idx = eos.get() as usize;
207 for (i, l) in logits.iter_mut().enumerate() {
208 *l = if i == eos_idx { 0.0 } else { f32::NEG_INFINITY };
209 }
210 }
211 }
212
213 pub fn advance_with_tokens_public(&self, tokens: &[TokenId]) {
217 self.advance_with_tokens(tokens);
218 }
219
220 fn advance_with_tokens(&self, tokens: &[TokenId]) {
224 let mut consumed = self.consumed.lock();
225 if *consumed >= tokens.len() {
226 return;
227 }
228 let mut pos = self.state.lock();
229 for &tok in &tokens[*consumed..] {
230 if pos.dead {
231 break;
232 }
233 let idx = tok.get() as usize;
234 if idx >= self.token_bytes.len() {
235 continue;
236 }
237 let bytes = &self.token_bytes[idx];
238 if self.eos_token.map_or(false, |e| e == tok) {
240 continue;
241 }
242 if let Some(next) = self.advance(pos.state, bytes) {
243 pos.state = next;
244 } else {
245 pos.dead = true;
246 }
247 }
248 *consumed = tokens.len();
249 }
250}
251
252impl LogitsProcessor for RegexGuidedProcessor {
253 fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
254 self.advance_with_tokens(ctx.previous_tokens);
255 self.mask_logits(ctx.logits);
256 Ok(())
257 }
258
259 fn name(&self) -> &str {
260 "regex_guided"
261 }
262
263 fn priority(&self) -> ProcessorPriority {
264 ProcessorPriority::High
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use ferrum_interfaces::tokenizer::{ChatMessage, TokenizerInfo, TokenizerType};
274 use ferrum_types::{SpecialTokens, TokenId};
275
276 struct ByteTokenizer {
280 special: SpecialTokens,
281 byte_strings: Vec<String>,
282 }
283
284 impl ByteTokenizer {
285 fn new() -> Self {
286 let mut byte_strings = Vec::with_capacity(257);
287 for b in 0u8..=255 {
288 byte_strings.push(String::from_utf8(vec![b]).unwrap_or_default());
289 }
290 byte_strings.push("</s>".to_string());
291 Self {
292 special: SpecialTokens {
293 bos_token: None,
294 eos_token: Some(TokenId::new(256)),
295 unk_token: None,
296 pad_token: None,
297 sep_token: None,
298 cls_token: None,
299 mask_token: None,
300 },
301 byte_strings,
302 }
303 }
304 }
305
306 impl Tokenizer for ByteTokenizer {
307 fn encode(&self, text: &str, _add_special: bool) -> Result<Vec<TokenId>> {
308 Ok(text.bytes().map(|b| TokenId::new(b as u32)).collect())
309 }
310 fn decode(&self, tokens: &[TokenId], _skip_special: bool) -> Result<String> {
311 let mut out = String::new();
312 for t in tokens {
313 let idx = t.get() as usize;
314 if idx < 256 {
315 out.push(idx as u8 as char);
316 }
317 }
318 Ok(out)
319 }
320 fn decode_incremental(&self, _prev: &[TokenId], next: TokenId) -> Result<String> {
321 self.decode(&[next], false)
322 }
323 fn vocab_size(&self) -> usize {
324 257
325 }
326 fn special_tokens(&self) -> &SpecialTokens {
327 &self.special
328 }
329 fn token_id(&self, text: &str) -> Option<TokenId> {
330 if text.len() == 1 {
331 Some(TokenId::new(text.bytes().next().unwrap() as u32))
332 } else {
333 None
334 }
335 }
336 fn token_text(&self, token_id: TokenId) -> Option<&str> {
337 self.byte_strings
338 .get(token_id.get() as usize)
339 .map(|s| s.as_str())
340 }
341 fn apply_chat_template(&self, _messages: &[ChatMessage]) -> Result<String> {
342 Ok(String::new())
343 }
344 fn info(&self) -> TokenizerInfo {
345 TokenizerInfo {
346 tokenizer_type: TokenizerType::Custom,
347 vocab_size: 257,
348 special_tokens: self.special.clone(),
349 supports_incremental: true,
350 supports_chat_template: false,
351 max_token_length: Some(1),
352 model_name: Some("byte-tokenizer-test".into()),
353 }
354 }
355 }
356
357 fn processor(pattern: &str) -> RegexGuidedProcessor {
358 let tok: Arc<dyn Tokenizer> = Arc::new(ByteTokenizer::new());
359 RegexGuidedProcessor::new(pattern, tok, Some(TokenId::new(256))).unwrap()
360 }
361
362 #[test]
363 fn digits_only_allows_digits_at_start() {
364 let p = processor(r"[0-9]+");
365 let mut logits = vec![0.0f32; 257];
366 p.mask_logits(&mut logits);
367 for b in 0u8..=255 {
368 let expected_allowed = b.is_ascii_digit();
369 let got = logits[b as usize].is_finite();
370 assert_eq!(
371 got, expected_allowed,
372 "byte {b:?} ({}): expected allowed={expected_allowed}, got={got}",
373 b as char
374 );
375 }
376 assert!(logits[256].is_infinite() && logits[256].is_sign_negative());
378 }
379
380 #[test]
381 fn digits_only_allows_eos_after_a_digit() {
382 let p = processor(r"[0-9]+");
383 p.advance_with_tokens(&[TokenId::new(b'3' as u32)]);
384 let mut logits = vec![0.0f32; 257];
385 p.mask_logits(&mut logits);
386 assert!(
387 logits[256].is_finite(),
388 "EOS should be allowed after a digit"
389 );
390 assert!(
391 logits[b'7' as usize].is_finite(),
392 "another digit still allowed"
393 );
394 assert!(logits[b'a' as usize].is_infinite(), "alpha still forbidden");
395 }
396
397 #[test]
398 fn dead_state_forces_eos() {
399 let p = processor(r"[0-9]+");
400 p.advance_with_tokens(&[TokenId::new(b'a' as u32)]);
402 let mut logits = vec![0.0f32; 257];
403 p.mask_logits(&mut logits);
404 assert!(logits[256].is_finite(), "EOS forced as fallback");
405 for b in 0u8..=255 {
406 assert!(
407 logits[b as usize].is_infinite(),
408 "byte {b} should be masked when DFA dead"
409 );
410 }
411 }
412
413 #[test]
414 fn reset_restores_initial_state() {
415 let p = processor(r"[0-9]+");
416 p.advance_with_tokens(&[TokenId::new(b'3' as u32)]);
417 assert!(p.can_accept());
418 p.reset().unwrap();
419 assert!(!p.can_accept(), "fresh state should not accept empty input");
420 }
421
422 #[test]
423 fn hex_prefix_pattern() {
424 let p = processor(r"0x[0-9a-f]+");
425 let mut logits = vec![0.0f32; 257];
426 p.mask_logits(&mut logits);
427 assert!(logits[b'0' as usize].is_finite(), "'0' starts the pattern");
428 assert!(logits[b'1' as usize].is_infinite(), "'1' can't start");
429 p.advance_with_tokens(&[TokenId::new(b'0' as u32), TokenId::new(b'x' as u32)]);
430 let mut logits = vec![0.0f32; 257];
431 p.mask_logits(&mut logits);
432 assert!(logits[b'a' as usize].is_finite());
433 assert!(logits[b'f' as usize].is_finite());
434 assert!(logits[b'g' as usize].is_infinite());
435 assert!(logits[b'9' as usize].is_finite());
436 }
437}