use regex::Regex;
use regex_automata::dfa::{dense, Automaton, StartKind};
use regex_automata::util::primitives::StateID;
use regex_automata::{Anchored, Input, MatchKind};
use crate::errors::{Result, TrustformersError};
const PREFIX_DFA_SIZE_LIMIT: usize = 8 * (1 << 20);
#[derive(Debug)]
pub struct RegexConstraint {
pattern: String,
full_match: Regex,
prefix_dfa: dense::DFA<Vec<u32>>,
}
impl RegexConstraint {
pub fn new(pattern: &str) -> Result<Self> {
let full_match = Regex::new(&format!(r"\A(?:{pattern})\z")).map_err(|error| {
TrustformersError::invalid_input(format!("invalid regex pattern `{pattern}`: {error}"))
})?;
let prefix_dfa = dense::Builder::new()
.configure(
dense::Config::new()
.start_kind(StartKind::Anchored)
.match_kind(MatchKind::All)
.unicode_word_boundary(true)
.dfa_size_limit(Some(PREFIX_DFA_SIZE_LIMIT))
.determinize_size_limit(Some(PREFIX_DFA_SIZE_LIMIT)),
)
.build(pattern)
.map_err(|error| {
TrustformersError::invalid_input(format!(
"regex pattern `{pattern}` cannot be compiled into a prefix automaton for \
constrained decoding: {error}"
))
})?;
Ok(Self {
pattern: pattern.to_string(),
full_match,
prefix_dfa,
})
}
pub fn pattern(&self) -> &str {
&self.pattern
}
pub fn is_full_match(&self, text: &str) -> bool {
self.full_match.is_match(text)
}
pub fn is_viable_prefix(&self, text: &str) -> bool {
let input = Input::new(text).anchored(Anchored::Yes);
let mut state = match self.prefix_dfa.start_state_forward(&input) {
Ok(state) => state,
Err(_) => return true,
};
for &byte in text.as_bytes() {
state = self.prefix_dfa.next_state(state, byte);
if self.prefix_dfa.is_dead_state(state) {
return false;
}
if self.prefix_dfa.is_quit_state(state) {
return true;
}
}
if self.prefix_dfa.is_match_state(state) {
return self.has_live_continuation(state);
}
true
}
fn has_live_continuation(&self, state: StateID) -> bool {
if self.prefix_dfa.is_match_state(self.prefix_dfa.next_eoi_state(state)) {
return true;
}
(u8::MIN..=u8::MAX).any(|byte| {
let next = self.prefix_dfa.next_state(state, byte);
!self.prefix_dfa.is_dead_state(next)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_full_match_is_anchored_at_both_ends() {
let constraint = RegexConstraint::new(r"\d+").expect("compile");
assert!(constraint.is_full_match("123"));
assert!(!constraint.is_full_match("abc123"));
assert!(!constraint.is_full_match("123abc"));
assert!(!constraint.is_full_match(""));
}
#[test]
fn test_viable_prefix_accepts_prefixes_no_hard_coded_list_could_know() {
let constraint = RegexConstraint::new(r"hello\s+there").expect("compile");
assert!(constraint.is_viable_prefix(""));
assert!(constraint.is_viable_prefix("h"));
assert!(constraint.is_viable_prefix("hello"));
assert!(constraint.is_viable_prefix("hello "));
assert!(constraint.is_viable_prefix("hello the"));
assert!(constraint.is_viable_prefix("hello there"));
assert!(constraint.is_full_match("hello there"));
assert!(!constraint.is_viable_prefix("help"));
assert!(!constraint.is_viable_prefix("hello x"));
assert!(!constraint.is_viable_prefix("hello there!"));
}
#[test]
fn test_viable_prefix_handles_multibyte_text_without_panicking() {
let constraint = RegexConstraint::new(r"\d+").expect("compile");
assert!(!constraint.is_viable_prefix("日本語"));
assert!(!constraint.is_full_match("日本語"));
let unicode = RegexConstraint::new(r"日本\p{Han}+").expect("compile");
assert!(unicode.is_viable_prefix("日"));
assert!(unicode.is_viable_prefix("日本"));
assert!(unicode.is_viable_prefix("日本語"));
assert!(unicode.is_full_match("日本語"));
assert!(!unicode.is_viable_prefix("日x"));
}
#[test]
fn test_viable_prefix_keeps_longer_alternatives_alive_after_a_match() {
let constraint = RegexConstraint::new("a|ab").expect("compile");
assert!(constraint.is_viable_prefix("a"));
assert!(constraint.is_viable_prefix("ab"));
assert!(constraint.is_full_match("a"));
assert!(constraint.is_full_match("ab"));
assert!(!constraint.is_viable_prefix("abc"));
}
#[test]
fn test_viable_prefix_of_a_bounded_repetition() {
let constraint = RegexConstraint::new("[0-9]{3}").expect("compile");
assert!(constraint.is_viable_prefix("1"));
assert!(constraint.is_viable_prefix("12"));
assert!(constraint.is_viable_prefix("123"));
assert!(!constraint.is_viable_prefix("1234"));
assert!(!constraint.is_full_match("12"));
assert!(constraint.is_full_match("123"));
}
#[test]
fn test_alternation_prefix_narrows_as_text_grows() {
let constraint = RegexConstraint::new("(?:yes|no|maybe)").expect("compile");
assert!(constraint.is_viable_prefix("y"));
assert!(constraint.is_viable_prefix("m"));
assert!(!constraint.is_viable_prefix("ye5"));
assert!(constraint.is_full_match("maybe"));
assert!(!constraint.is_full_match("may"));
}
#[test]
fn test_viable_prefix_after_a_repeated_match_stays_exact() {
let constraint = RegexConstraint::new("(?:ab)+").expect("compile");
assert!(constraint.is_viable_prefix("a"));
assert!(constraint.is_viable_prefix("ab"));
assert!(constraint.is_viable_prefix("aba"));
assert!(constraint.is_viable_prefix("abab"));
assert!(constraint.is_full_match("abab"));
assert!(!constraint.is_full_match("aba"));
assert!(!constraint.is_viable_prefix("abx"));
assert!(!constraint.is_viable_prefix("ababx"));
}
#[test]
fn test_invalid_pattern_is_rejected() {
assert!(RegexConstraint::new("[invalid(").is_err());
}
#[test]
fn test_pattern_accessor_round_trips() {
let constraint = RegexConstraint::new(r"\w+").expect("compile");
assert_eq!(constraint.pattern(), r"\w+");
}
#[test]
fn test_word_boundary_pattern_compiles_and_matches() {
let constraint = RegexConstraint::new(r"\bcat\b").expect("compile");
assert!(constraint.is_full_match("cat"));
assert!(constraint.is_viable_prefix("c"));
assert!(!constraint.is_viable_prefix("dog"));
}
}