#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
use std::fmt;
pub type StateId = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub struct NFAState {
pub id: StateId,
pub is_final: bool,
}
impl NFAState {
#[inline]
pub const fn new(id: StateId, is_final: bool) -> Self {
Self { id, is_final }
}
#[inline]
pub const fn non_final(id: StateId) -> Self {
Self::new(id, false)
}
#[inline]
pub const fn final_state(id: StateId) -> Self {
Self::new(id, true)
}
}
impl fmt::Display for NFAState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_final {
write!(f, "q{}*", self.id)
} else {
write!(f, "q{}", self.id)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub struct CharClassChar {
pub ranges: Vec<(char, char)>,
pub negated: bool,
}
impl CharClassChar {
#[inline]
pub fn new() -> Self {
Self {
ranges: Vec::new(),
negated: false,
}
}
#[inline]
pub fn from_range(start: char, end: char) -> Self {
Self {
ranges: vec![(start, end)],
negated: false,
}
}
pub fn from_chars(chars: &[char]) -> Self {
let ranges = chars.iter().map(|&c| (c, c)).collect();
Self {
ranges,
negated: false,
}
}
pub fn add_range(&mut self, start: char, end: char) {
self.ranges.push((start, end));
}
pub fn add_char(&mut self, c: char) {
self.ranges.push((c, c));
}
#[inline]
pub fn negated(mut self) -> Self {
self.negated = !self.negated;
self
}
pub fn matches(&self, c: char) -> bool {
let in_ranges = self
.ranges
.iter()
.any(|&(start, end)| c >= start && c <= end);
if self.negated {
!in_ranges
} else {
in_ranges
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
}
impl Default for CharClassChar {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for CharClassChar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[")?;
if self.negated {
write!(f, "^")?;
}
for (start, end) in &self.ranges {
if start == end {
write!(f, "{}", start)?;
} else {
write!(f, "{}-{}", start, end)?;
}
}
write!(f, "]")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub struct CharClass {
pub ranges: Vec<(u8, u8)>,
pub negated: bool,
}
impl CharClass {
#[inline]
pub fn new() -> Self {
Self {
ranges: Vec::new(),
negated: false,
}
}
#[inline]
pub fn from_range(start: u8, end: u8) -> Self {
Self {
ranges: vec![(start, end)],
negated: false,
}
}
pub fn from_bytes(bytes: &[u8]) -> Self {
let ranges = bytes.iter().map(|&b| (b, b)).collect();
Self {
ranges,
negated: false,
}
}
pub fn add_range(&mut self, start: u8, end: u8) {
self.ranges.push((start, end));
}
pub fn add_byte(&mut self, b: u8) {
self.ranges.push((b, b));
}
#[inline]
pub fn negated(mut self) -> Self {
self.negated = !self.negated;
self
}
pub fn matches(&self, b: u8) -> bool {
let in_ranges = self
.ranges
.iter()
.any(|&(start, end)| b >= start && b <= end);
if self.negated {
!in_ranges
} else {
in_ranges
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
}
impl Default for CharClass {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for CharClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[")?;
if self.negated {
write!(f, "^")?;
}
for (start, end) in &self.ranges {
if start == end {
write!(f, "{}", *start as char)?;
} else {
write!(f, "{}-{}", *start as char, *end as char)?;
}
}
write!(f, "]")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub enum TransitionLabelChar {
Epsilon,
Char(char),
CharClass(CharClassChar),
Any,
StartOfLine,
EndOfLine,
StartOfInput,
EndOfInput,
EndOfInputStrict,
}
impl TransitionLabelChar {
#[inline]
pub fn is_epsilon(&self) -> bool {
matches!(self, TransitionLabelChar::Epsilon)
}
#[inline]
pub fn is_anchor(&self) -> bool {
matches!(
self,
TransitionLabelChar::StartOfLine
| TransitionLabelChar::EndOfLine
| TransitionLabelChar::StartOfInput
| TransitionLabelChar::EndOfInput
| TransitionLabelChar::EndOfInputStrict
)
}
pub fn matches(&self, c: char) -> bool {
match self {
TransitionLabelChar::Epsilon => true,
TransitionLabelChar::Char(expected) => c == *expected,
TransitionLabelChar::CharClass(class) => class.matches(c),
TransitionLabelChar::Any => true,
TransitionLabelChar::StartOfLine
| TransitionLabelChar::EndOfLine
| TransitionLabelChar::StartOfInput
| TransitionLabelChar::EndOfInput
| TransitionLabelChar::EndOfInputStrict => false,
}
}
pub fn matches_at_position(&self, input: &str, pos: usize, multiline: bool) -> bool {
let len = input.len();
match self {
TransitionLabelChar::StartOfLine => {
if pos == 0 {
return true;
}
if multiline && pos > 0 {
let bytes = input.as_bytes();
if pos <= len && bytes.get(pos - 1) == Some(&b'\n') {
return true;
}
}
false
}
TransitionLabelChar::EndOfLine => {
if pos == len {
return true;
}
if multiline {
let bytes = input.as_bytes();
if bytes.get(pos) == Some(&b'\n') {
return true;
}
}
false
}
TransitionLabelChar::StartOfInput => {
pos == 0
}
TransitionLabelChar::EndOfInput => {
if pos == len {
return true;
}
if pos == len.saturating_sub(1) {
let bytes = input.as_bytes();
if bytes.last() == Some(&b'\n') {
return true;
}
}
false
}
TransitionLabelChar::EndOfInputStrict => {
pos == len
}
_ => false,
}
}
#[inline]
pub fn consumes_input(&self) -> bool {
!self.is_epsilon() && !self.is_anchor()
}
#[inline]
pub fn expected_char(&self) -> Option<char> {
match self {
TransitionLabelChar::Char(c) => Some(*c),
_ => None,
}
}
}
impl fmt::Display for TransitionLabelChar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransitionLabelChar::Epsilon => write!(f, "ε"),
TransitionLabelChar::Char(c) => write!(f, "{}", c),
TransitionLabelChar::CharClass(class) => write!(f, "{}", class),
TransitionLabelChar::Any => write!(f, "."),
TransitionLabelChar::StartOfLine => write!(f, "^"),
TransitionLabelChar::EndOfLine => write!(f, "$"),
TransitionLabelChar::StartOfInput => write!(f, "\\A"),
TransitionLabelChar::EndOfInput => write!(f, "\\Z"),
TransitionLabelChar::EndOfInputStrict => write!(f, "\\z"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub enum TransitionLabel {
Epsilon,
Byte(u8),
CharClass(CharClass),
Any,
StartOfLine,
EndOfLine,
StartOfInput,
EndOfInput,
EndOfInputStrict,
}
impl TransitionLabel {
#[inline]
pub fn is_epsilon(&self) -> bool {
matches!(self, TransitionLabel::Epsilon)
}
#[inline]
pub fn is_anchor(&self) -> bool {
matches!(
self,
TransitionLabel::StartOfLine
| TransitionLabel::EndOfLine
| TransitionLabel::StartOfInput
| TransitionLabel::EndOfInput
| TransitionLabel::EndOfInputStrict
)
}
pub fn matches(&self, b: u8) -> bool {
match self {
TransitionLabel::Epsilon => true,
TransitionLabel::Byte(expected) => b == *expected,
TransitionLabel::CharClass(class) => class.matches(b),
TransitionLabel::Any => true,
TransitionLabel::StartOfLine
| TransitionLabel::EndOfLine
| TransitionLabel::StartOfInput
| TransitionLabel::EndOfInput
| TransitionLabel::EndOfInputStrict => false,
}
}
pub fn matches_at_position(&self, input: &[u8], pos: usize, multiline: bool) -> bool {
let len = input.len();
match self {
TransitionLabel::StartOfLine => {
if pos == 0 {
return true;
}
if multiline && pos > 0 && input.get(pos - 1) == Some(&b'\n') {
return true;
}
false
}
TransitionLabel::EndOfLine => {
if pos == len {
return true;
}
if multiline && input.get(pos) == Some(&b'\n') {
return true;
}
false
}
TransitionLabel::StartOfInput => pos == 0,
TransitionLabel::EndOfInput => {
if pos == len {
return true;
}
if pos == len.saturating_sub(1) && input.last() == Some(&b'\n') {
return true;
}
false
}
TransitionLabel::EndOfInputStrict => pos == len,
_ => false,
}
}
#[inline]
pub fn consumes_input(&self) -> bool {
!self.is_epsilon() && !self.is_anchor()
}
}
impl fmt::Display for TransitionLabel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransitionLabel::Epsilon => write!(f, "ε"),
TransitionLabel::Byte(b) => write!(f, "{}", *b as char),
TransitionLabel::CharClass(class) => write!(f, "{}", class),
TransitionLabel::Any => write!(f, "."),
TransitionLabel::StartOfLine => write!(f, "^"),
TransitionLabel::EndOfLine => write!(f, "$"),
TransitionLabel::StartOfInput => write!(f, "\\A"),
TransitionLabel::EndOfInput => write!(f, "\\Z"),
TransitionLabel::EndOfInputStrict => write!(f, "\\z"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub struct TransitionChar {
pub from: StateId,
pub label: TransitionLabelChar,
pub to: StateId,
pub weight: f64,
}
impl TransitionChar {
#[inline]
pub fn new(from: StateId, label: TransitionLabelChar, to: StateId) -> Self {
Self {
from,
label,
to,
weight: 0.0,
}
}
#[inline]
pub fn with_weight(
from: StateId,
label: TransitionLabelChar,
to: StateId,
weight: f64,
) -> Self {
Self {
from,
label,
to,
weight,
}
}
#[inline]
pub fn epsilon(from: StateId, to: StateId) -> Self {
Self::new(from, TransitionLabelChar::Epsilon, to)
}
#[inline]
pub fn on_char(from: StateId, c: char, to: StateId) -> Self {
Self::new(from, TransitionLabelChar::Char(c), to)
}
#[inline]
pub fn on_class(from: StateId, class: CharClassChar, to: StateId) -> Self {
Self::new(from, TransitionLabelChar::CharClass(class), to)
}
#[inline]
pub fn on_any(from: StateId, to: StateId) -> Self {
Self::new(from, TransitionLabelChar::Any, to)
}
}
impl fmt::Display for TransitionChar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.weight != 0.0 {
write!(
f,
"q{} --{}[{:.2}]--> q{}",
self.from, self.label, self.weight, self.to
)
} else {
write!(f, "q{} --{}--> q{}", self.from, self.label, self.to)
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
pub struct Transition {
pub from: StateId,
pub label: TransitionLabel,
pub to: StateId,
pub weight: f64,
}
impl Transition {
#[inline]
pub fn new(from: StateId, label: TransitionLabel, to: StateId) -> Self {
Self {
from,
label,
to,
weight: 0.0,
}
}
#[inline]
pub fn with_weight(from: StateId, label: TransitionLabel, to: StateId, weight: f64) -> Self {
Self {
from,
label,
to,
weight,
}
}
#[inline]
pub fn epsilon(from: StateId, to: StateId) -> Self {
Self::new(from, TransitionLabel::Epsilon, to)
}
#[inline]
pub fn on_byte(from: StateId, b: u8, to: StateId) -> Self {
Self::new(from, TransitionLabel::Byte(b), to)
}
#[inline]
pub fn on_class(from: StateId, class: CharClass, to: StateId) -> Self {
Self::new(from, TransitionLabel::CharClass(class), to)
}
#[inline]
pub fn on_any(from: StateId, to: StateId) -> Self {
Self::new(from, TransitionLabel::Any, to)
}
}
impl fmt::Display for Transition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.weight != 0.0 {
write!(
f,
"q{} --{}[{:.2}]--> q{}",
self.from, self.label, self.weight, self.to
)
} else {
write!(f, "q{} --{}--> q{}", self.from, self.label, self.to)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nfa_state_new() {
let state = NFAState::new(0, false);
assert_eq!(state.id, 0);
assert!(!state.is_final);
}
#[test]
fn test_nfa_state_final() {
let state = NFAState::final_state(5);
assert_eq!(state.id, 5);
assert!(state.is_final);
}
#[test]
fn test_nfa_state_display() {
assert_eq!(format!("{}", NFAState::non_final(0)), "q0");
assert_eq!(format!("{}", NFAState::final_state(1)), "q1*");
}
#[test]
fn test_char_class_from_chars() {
let vowels = CharClassChar::from_chars(&['a', 'e', 'i', 'o', 'u']);
assert!(vowels.matches('a'));
assert!(vowels.matches('e'));
assert!(!vowels.matches('b'));
}
#[test]
fn test_char_class_from_range() {
let lowercase = CharClassChar::from_range('a', 'z');
assert!(lowercase.matches('a'));
assert!(lowercase.matches('m'));
assert!(lowercase.matches('z'));
assert!(!lowercase.matches('A'));
assert!(!lowercase.matches('0'));
}
#[test]
fn test_char_class_negated() {
let non_vowels = CharClassChar::from_chars(&['a', 'e', 'i', 'o', 'u']).negated();
assert!(!non_vowels.matches('a'));
assert!(non_vowels.matches('b'));
assert!(non_vowels.matches('z'));
}
#[test]
fn test_char_class_display() {
let vowels = CharClassChar::from_chars(&['a', 'e', 'i']);
assert_eq!(format!("{}", vowels), "[aei]");
let range = CharClassChar::from_range('a', 'z');
assert_eq!(format!("{}", range), "[a-z]");
let negated = CharClassChar::from_chars(&['x']).negated();
assert_eq!(format!("{}", negated), "[^x]");
}
#[test]
fn test_byte_class_from_bytes() {
let vowels = CharClass::from_bytes(&[b'a', b'e', b'i', b'o', b'u']);
assert!(vowels.matches(b'a'));
assert!(!vowels.matches(b'b'));
}
#[test]
fn test_byte_class_from_range() {
let lowercase = CharClass::from_range(b'a', b'z');
assert!(lowercase.matches(b'a'));
assert!(lowercase.matches(b'z'));
assert!(!lowercase.matches(b'A'));
}
#[test]
fn test_label_epsilon() {
let label = TransitionLabelChar::Epsilon;
assert!(label.is_epsilon());
assert!(!label.consumes_input());
assert!(label.matches('x')); }
#[test]
fn test_label_char() {
let label = TransitionLabelChar::Char('a');
assert!(!label.is_epsilon());
assert!(label.consumes_input());
assert!(label.matches('a'));
assert!(!label.matches('b'));
}
#[test]
fn test_label_char_class() {
let class = CharClassChar::from_chars(&['a', 'e', 'i', 'o', 'u']);
let label = TransitionLabelChar::CharClass(class);
assert!(label.matches('a'));
assert!(!label.matches('b'));
}
#[test]
fn test_label_any() {
let label = TransitionLabelChar::Any;
assert!(label.matches('a'));
assert!(label.matches('z'));
assert!(label.matches(' '));
}
#[test]
fn test_transition_epsilon() {
let trans = TransitionChar::epsilon(0, 1);
assert_eq!(trans.from, 0);
assert_eq!(trans.to, 1);
assert!(trans.label.is_epsilon());
assert_eq!(trans.weight, 0.0);
}
#[test]
fn test_transition_on_char() {
let trans = TransitionChar::on_char(1, 'a', 2);
assert_eq!(trans.from, 1);
assert_eq!(trans.to, 2);
assert!(trans.label.matches('a'));
}
#[test]
fn test_transition_with_weight() {
let trans = TransitionChar::with_weight(0, TransitionLabelChar::Char('x'), 1, 0.5);
assert_eq!(trans.weight, 0.5);
}
#[test]
fn test_transition_display() {
let eps = TransitionChar::epsilon(0, 1);
assert_eq!(format!("{}", eps), "q0 --ε--> q1");
let on_a = TransitionChar::on_char(1, 'a', 2);
assert_eq!(format!("{}", on_a), "q1 --a--> q2");
let weighted = TransitionChar::with_weight(0, TransitionLabelChar::Char('x'), 1, 0.15);
assert_eq!(format!("{}", weighted), "q0 --x[0.15]--> q1");
}
#[test]
fn test_byte_transition_epsilon() {
let trans = Transition::epsilon(0, 1);
assert!(trans.label.is_epsilon());
}
#[test]
fn test_byte_transition_on_byte() {
let trans = Transition::on_byte(0, b'a', 1);
assert!(trans.label.matches(b'a'));
assert!(!trans.label.matches(b'b'));
}
}