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();
102 let mut token_bytes = Vec::with_capacity(vocab_size);
103 for i in 0..vocab_size {
104 let id = TokenId::new(i as u32);
105 let bytes = if let Some(s) = tokenizer.token_text(id) {
106 s.as_bytes().to_vec()
107 } else {
108 tokenizer
109 .decode(&[id], false)
110 .map(|s| s.into_bytes())
111 .unwrap_or_default()
112 };
113 token_bytes.push(bytes);
114 }
115
116 Ok(Self {
117 dfa,
118 state: Mutex::new(DfaPosition {
119 state: start,
120 dead: false,
121 }),
122 token_bytes,
123 eos_token,
124 consumed: Mutex::new(0),
125 })
126 }
127
128 pub fn reset(&self) -> Result<()> {
130 let start = self
131 .dfa
132 .start_state(&StartConfig::new().anchored(Anchored::Yes))
133 .map_err(|e| FerrumError::internal(format!("regex start state: {e}")))?;
134 *self.state.lock() = DfaPosition {
135 state: start,
136 dead: false,
137 };
138 *self.consumed.lock() = 0;
139 Ok(())
140 }
141
142 pub fn can_accept(&self) -> bool {
144 let pos = *self.state.lock();
145 !pos.dead && self.dfa.is_match_state(self.dfa.next_eoi_state(pos.state))
146 }
147
148 fn advance(&self, mut state: StateID, bytes: &[u8]) -> Option<StateID> {
152 for &b in bytes {
153 state = self.dfa.next_state(state, b);
154 if self.dfa.is_dead_state(state) {
155 return None;
156 }
157 }
158 Some(state)
159 }
160
161 pub fn mask_logits(&self, logits: &mut [f32]) {
163 let pos = *self.state.lock();
164 if pos.dead {
165 self.force_eos(logits);
167 return;
168 }
169
170 let pattern_done = self.dfa.is_match_state(self.dfa.next_eoi_state(pos.state));
171
172 let vocab = logits.len().min(self.token_bytes.len());
176 let mut any_allowed = false;
177 for idx in 0..vocab {
178 let is_eos = self.eos_token.map_or(false, |e| e.get() as usize == idx);
179 let bytes = &self.token_bytes[idx];
180
181 let allowed = if is_eos {
182 pattern_done
183 } else if bytes.is_empty() {
184 pattern_done
188 } else {
189 self.advance(pos.state, bytes).is_some()
190 };
191
192 if allowed {
193 any_allowed = true;
194 } else {
195 logits[idx] = f32::NEG_INFINITY;
196 }
197 }
198
199 if !any_allowed {
204 self.force_eos(logits);
205 }
206 }
207
208 fn force_eos(&self, logits: &mut [f32]) {
209 if let Some(eos) = self.eos_token {
210 let eos_idx = eos.get() as usize;
211 for (i, l) in logits.iter_mut().enumerate() {
212 *l = if i == eos_idx { 0.0 } else { f32::NEG_INFINITY };
213 }
214 }
215 }
216
217 pub fn advance_with_tokens_public(&self, tokens: &[TokenId]) {
221 self.advance_with_tokens(tokens);
222 }
223
224 fn advance_with_tokens(&self, tokens: &[TokenId]) {
228 let mut consumed = self.consumed.lock();
229 if *consumed >= tokens.len() {
230 return;
231 }
232 let mut pos = self.state.lock();
233 for &tok in &tokens[*consumed..] {
234 if pos.dead {
235 break;
236 }
237 let idx = tok.get() as usize;
238 if idx >= self.token_bytes.len() {
239 continue;
240 }
241 let bytes = &self.token_bytes[idx];
242 if self.eos_token.map_or(false, |e| e == tok) {
244 continue;
245 }
246 if let Some(next) = self.advance(pos.state, bytes) {
247 pos.state = next;
248 } else {
249 pos.dead = true;
250 }
251 }
252 *consumed = tokens.len();
253 }
254}
255
256impl LogitsProcessor for RegexGuidedProcessor {
257 fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
258 self.advance_with_tokens(ctx.previous_tokens);
259 self.mask_logits(ctx.logits);
260 Ok(())
261 }
262
263 fn name(&self) -> &str {
264 "regex_guided"
265 }
266
267 fn priority(&self) -> ProcessorPriority {
268 ProcessorPriority::High
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use ferrum_interfaces::tokenizer::{ChatMessage, TokenizerInfo, TokenizerType};
278 use ferrum_types::{SpecialTokens, TokenId};
279
280 struct ByteTokenizer {
284 special: SpecialTokens,
285 byte_strings: Vec<String>,
286 }
287
288 impl ByteTokenizer {
289 fn new() -> Self {
290 let mut byte_strings = Vec::with_capacity(257);
291 for b in 0u8..=255 {
292 byte_strings.push(String::from_utf8(vec![b]).unwrap_or_default());
293 }
294 byte_strings.push("</s>".to_string());
295 Self {
296 special: SpecialTokens {
297 bos_token: None,
298 eos_token: Some(TokenId::new(256)),
299 unk_token: None,
300 pad_token: None,
301 sep_token: None,
302 cls_token: None,
303 mask_token: None,
304 },
305 byte_strings,
306 }
307 }
308 }
309
310 impl Tokenizer for ByteTokenizer {
311 fn encode(&self, text: &str, _add_special: bool) -> Result<Vec<TokenId>> {
312 Ok(text.bytes().map(|b| TokenId::new(b as u32)).collect())
313 }
314 fn decode(&self, tokens: &[TokenId], _skip_special: bool) -> Result<String> {
315 let mut out = String::new();
316 for t in tokens {
317 let idx = t.get() as usize;
318 if idx < 256 {
319 out.push(idx as u8 as char);
320 }
321 }
322 Ok(out)
323 }
324 fn decode_incremental(&self, _prev: &[TokenId], next: TokenId) -> Result<String> {
325 self.decode(&[next], false)
326 }
327 fn vocab_size(&self) -> usize {
328 257
329 }
330 fn special_tokens(&self) -> &SpecialTokens {
331 &self.special
332 }
333 fn token_id(&self, text: &str) -> Option<TokenId> {
334 if text.len() == 1 {
335 Some(TokenId::new(text.bytes().next().unwrap() as u32))
336 } else {
337 None
338 }
339 }
340 fn token_text(&self, token_id: TokenId) -> Option<&str> {
341 self.byte_strings
342 .get(token_id.get() as usize)
343 .map(|s| s.as_str())
344 }
345 fn apply_chat_template(&self, _messages: &[ChatMessage]) -> Result<String> {
346 Ok(String::new())
347 }
348 fn info(&self) -> TokenizerInfo {
349 TokenizerInfo {
350 tokenizer_type: TokenizerType::Custom,
351 vocab_size: 257,
352 special_tokens: self.special.clone(),
353 supports_incremental: true,
354 supports_chat_template: false,
355 max_token_length: Some(1),
356 model_name: Some("byte-tokenizer-test".into()),
357 }
358 }
359 }
360
361 fn processor(pattern: &str) -> RegexGuidedProcessor {
362 let tok: Arc<dyn Tokenizer> = Arc::new(ByteTokenizer::new());
363 RegexGuidedProcessor::new(pattern, tok, Some(TokenId::new(256))).unwrap()
364 }
365
366 #[test]
367 fn digits_only_allows_digits_at_start() {
368 let p = processor(r"[0-9]+");
369 let mut logits = vec![0.0f32; 257];
370 p.mask_logits(&mut logits);
371 for b in 0u8..=255 {
372 let expected_allowed = b.is_ascii_digit();
373 let got = logits[b as usize].is_finite();
374 assert_eq!(
375 got, expected_allowed,
376 "byte {b:?} ({}): expected allowed={expected_allowed}, got={got}",
377 b as char
378 );
379 }
380 assert!(logits[256].is_infinite() && logits[256].is_sign_negative());
382 }
383
384 #[test]
385 fn digits_only_allows_eos_after_a_digit() {
386 let p = processor(r"[0-9]+");
387 p.advance_with_tokens(&[TokenId::new(b'3' as u32)]);
388 let mut logits = vec![0.0f32; 257];
389 p.mask_logits(&mut logits);
390 assert!(
391 logits[256].is_finite(),
392 "EOS should be allowed after a digit"
393 );
394 assert!(
395 logits[b'7' as usize].is_finite(),
396 "another digit still allowed"
397 );
398 assert!(logits[b'a' as usize].is_infinite(), "alpha still forbidden");
399 }
400
401 #[test]
402 fn dead_state_forces_eos() {
403 let p = processor(r"[0-9]+");
404 p.advance_with_tokens(&[TokenId::new(b'a' as u32)]);
406 let mut logits = vec![0.0f32; 257];
407 p.mask_logits(&mut logits);
408 assert!(logits[256].is_finite(), "EOS forced as fallback");
409 for b in 0u8..=255 {
410 assert!(
411 logits[b as usize].is_infinite(),
412 "byte {b} should be masked when DFA dead"
413 );
414 }
415 }
416
417 #[test]
418 fn reset_restores_initial_state() {
419 let p = processor(r"[0-9]+");
420 p.advance_with_tokens(&[TokenId::new(b'3' as u32)]);
421 assert!(p.can_accept());
422 p.reset().unwrap();
423 assert!(!p.can_accept(), "fresh state should not accept empty input");
424 }
425
426 #[test]
427 fn hex_prefix_pattern() {
428 let p = processor(r"0x[0-9a-f]+");
429 let mut logits = vec![0.0f32; 257];
430 p.mask_logits(&mut logits);
431 assert!(logits[b'0' as usize].is_finite(), "'0' starts the pattern");
432 assert!(logits[b'1' as usize].is_infinite(), "'1' can't start");
433 p.advance_with_tokens(&[TokenId::new(b'0' as u32), TokenId::new(b'x' as u32)]);
434 let mut logits = vec![0.0f32; 257];
435 p.mask_logits(&mut logits);
436 assert!(logits[b'a' as usize].is_finite());
437 assert!(logits[b'f' as usize].is_finite());
438 assert!(logits[b'g' as usize].is_infinite());
439 assert!(logits[b'9' as usize].is_finite());
440 }
441}