Added Range number and fsm union
Some checks failed
Test Rust / test (push) Failing after 7m7s

This commit is contained in:
AlexanderHD27
2025-06-11 00:03:20 +02:00
parent 85b7fcd8ab
commit 833e3c56b8
4 changed files with 349 additions and 17 deletions

Binary file not shown.

View File

@@ -1,3 +1,5 @@
use std::{collections::HashMap};
use crate::configotron::fsm::{masks::{mask_any_number, mask_number, mask_number_range}, Action, FSMBuilder, FiniteStateMachine, StateRef, StateType};
@@ -74,14 +76,16 @@ pub fn build_fsm_upper_bound(max: u32) -> Result<FiniteStateMachine, String> {
scaffold.builder.add_link(&scaffold.last_side, &scaffold.error_state, Action::None, mask_any_number())?;
scaffold.builder.add_link(&scaffold.goal_state, &scaffold.error_state, Action::None, mask_any_number())?;
scaffold.builder.set_state_type(&scaffold.start_state, StateType::Error);
let fsm = scaffold.builder.finish(&scaffold.error_state, Action::None, &scaffold.start_state)?;
Ok(fsm)
}
pub fn build_fsm_lower_bound(max: u32) -> Result<FiniteStateMachine, String> {
let mut scaffold = build_fsm_bound_selecton(max, StateType::Error)?;
pub fn build_fsm_lower_bound(min: u32) -> Result<FiniteStateMachine, String> {
let mut scaffold = build_fsm_bound_selecton(min, StateType::Error)?;
let last_digit = (max % 10) as u8;
let last_digit = (min % 10) as u8;
scaffold.builder.add_link(&scaffold.last_main, &scaffold.goal_state, Action::None, mask_number_range(last_digit, 9))?;
scaffold.builder.add_link(&scaffold.last_side, &scaffold.goal_state, Action::None, mask_any_number())?;
scaffold.builder.add_link(&scaffold.goal_state, &scaffold.goal_state, Action::None, mask_any_number())?;
@@ -90,21 +94,113 @@ pub fn build_fsm_lower_bound(max: u32) -> Result<FiniteStateMachine, String> {
Ok(fsm)
}
fn fsm_cartesian_product<F, G>(fsm1: &FiniteStateMachine, fsm2: &FiniteStateMachine, state_type_operator: F, action_operator: G) -> Result<FiniteStateMachine, String>
where F: Fn(StateType, StateType) -> StateType, G: Fn(Action, Action) -> Action {
let mut builder = FSMBuilder::new();
let mut states_matrix: HashMap<(StateRef, StateRef), StateRef> = HashMap::new();
// Create all States (Q1 x Q2)
for (
(state_ref1, state1),
(state_ref2, state2)
) in itertools::iproduct!(&fsm1.states, &fsm2.states) {
let name = format!("{}_{}", state1.name, state2.name);
states_matrix.insert((*state_ref1, *state_ref2), builder.add_named_state(name, state_type_operator(state1.state_type, state2.state_type)));
}
// Adding Transitions
for (
(state_ref1, state1),
(state_ref2, state2)
) in itertools::iproduct!(&fsm1.states, &fsm2.states) {
let current_ref = match states_matrix.get(&(*state_ref1, *state_ref2)) {
Some(r) => r.clone(),
None => return Err(format!("State pair ({:?}, {:?}) not found in states_matrix [internal]", state_ref1, state_ref2)),
};
for ch in 0..128 {
let (next_state1, action1) = state1.transition[ch];
let (next_state2, action2) = state2.transition[ch];
let new_ref = match states_matrix.get(&(next_state1, next_state2)) {
Some(r) => r.clone(),
None => return Err(format!("State pair ({:?}, {:?}) not found in states_matrix [internal]", next_state1, next_state2)),
};
builder.add_link(&current_ref, &new_ref, action_operator(action1, action2), 1 << ch)?;
}
};
let start_state = if let Some(s) = states_matrix.get(&(fsm1.start, fsm2.start)) {
s
} else {
return Err("Start state pair not found in states_matrix [internal]".to_string());
};
Ok(builder.finish_without_default(start_state)?)
}
pub fn union_fsm(fsm1: &FiniteStateMachine, fsm2: &FiniteStateMachine) -> Result<FiniteStateMachine, String> {
fsm_cartesian_product(fsm1, fsm2,
|t1, t2| {
use StateType::{Terminal, Error, None};
match (t1, t2) {
(Error, _) => Error,
(_, Error) => Error,
(Terminal, Terminal) => Terminal,
(Terminal, None) => None,
(None, Terminal) => None,
(None, None) => None,
}
},
|a1, a2| {
use Action::{Add, Pop, Submit, None};
match (a1, a2) {
(None, _) => None,
(_, None) => None,
(Add, Add) => Add,
(Pop, Pop) => Pop,
(Submit, Submit) => Submit,
(_, _) => None
}
}
)
}
pub fn build_fsm_in_bound(min: u32, max: u32) -> Result<FiniteStateMachine, String> {
if min > max {
return Err(String::from("Lower bound must be <= Upper bound"));
}
let lower_fsm = build_fsm_lower_bound(min)?;
let upper_fsm = build_fsm_upper_bound(max)?;
Ok(union_fsm(&lower_fsm, &upper_fsm)?)
}
#[cfg(test)]
mod test {
use crate::configotron::fsm::{component_builder::build_fsm_upper_bound, display::{debug_dump_fsm_graph}, run_fsm, StateType};
use crate::configotron::fsm::{component_builder::{build_fsm_in_bound, build_fsm_upper_bound, union_fsm}, display::debug_dump_fsm_graph, masks::mask_char, run_fsm, StateType};
use crate::configotron::fsm::component_builder::build_fsm_lower_bound;
use crate::configotron::fsm::{FSMBuilder, Action};
static TEST_NUMBERS: &[u32] = &[1256, 1, 9999, 1000, 0, 10, 42, 123, 500, 2024, 751394];
#[test]
fn upper_bound() {
let test_vec: Vec<u32> = vec![1256, 1, 9999, 1000, 0, 10, 42, 123, 500, 2024, 751394];
for max in test_vec {
for &max in TEST_NUMBERS {
let fsm = build_fsm_upper_bound(max);
assert!(fsm.is_ok(), "{:?}", if let Err(e) = fsm { e } else { String::new() });
if let Ok(machine) = fsm {
assert_eq!(StateType::Error, run_fsm(&machine, &"".to_string()), "Empty String");
assert_eq!(StateType::Error, run_fsm(&machine, &"this-is-not-a-number".to_string()), "Text");
assert_eq!(StateType::Error, run_fsm(&machine, &"13373 is a nice number".to_string()), "Number + Text");
debug_dump_fsm_graph(&machine, format!(".debug/upper_bound{}.svg", max));
for i in 0..=max*10 {
@@ -125,14 +221,15 @@ mod test {
#[test]
fn lower_bound() {
let test_vec: Vec<u32> = vec![1256, 1, 9999, 1000, 0, 10, 42, 123, 500, 2024, 751394];
for max in test_vec {
for &max in TEST_NUMBERS {
let fsm = build_fsm_lower_bound(max);
assert!(fsm.is_ok(), "{:?}", if let Err(e) = fsm { e } else { String::new() });
if let Ok(machine) = fsm {
debug_dump_fsm_graph(&machine, format!(".debug/lower_bound{}.svg", max));
assert_eq!(StateType::Error, run_fsm(&machine, &"".to_string()), "Empty String");
assert_eq!(StateType::Error, run_fsm(&machine, &"this-is-not-a-number".to_string()), "Text");
assert_eq!(StateType::Error, run_fsm(&machine, &"13373 is a nice number".to_string()), "Number + Text");
for i in 0..=max*10 {
let input_string = i.to_string();
@@ -147,4 +244,183 @@ mod test {
}
}
}
#[test]
fn union_fsm_test() {
// FSM A:
// States: z0, z1, z2, z3
// Start: z0
//
// Types:
// - Terminal: z2
// - Error: z0, z1, z3
//
// Transitions:
// z0 -a-> z0
// z0 -b-> z1
// z1 -a-> z2
// default: z3
//
// Language: /a+ba/
let mut builder_a = FSMBuilder::new();
let z0 = builder_a.add_named_state("z0".to_string(), StateType::Error);
let z1 = builder_a.add_named_state("z1".to_string(), StateType::Error);
let z2 = builder_a.add_named_state("z2".to_string(), StateType::Terminal);
let z3 = builder_a.add_named_state("z3".to_string(), StateType::Error);
builder_a.add_link(&z0, &z0, Action::None, mask_char('a')).unwrap();
builder_a.add_link(&z0, &z1, Action::None, mask_char('b')).unwrap();
builder_a.add_link(&z1, &z2, Action::None, mask_char('a')).unwrap();
let fsm_a_res = builder_a.finish(&z3, Action::None, &z0);
let fsm_a_res_clone = fsm_a_res.clone();
let accepted_a = ["ba", "aba", "aaba", "aaaba", "aaaaba", "aaaaaba"];
let rejected_a = ["a", "b", "ab", "aab", "abb", "baba", "", "bb", "aaa", "bab"];
// Test FSM A with different inputs
if let Ok(fsm_a) = fsm_a_res {
debug_dump_fsm_graph(&fsm_a, ".debug/union_a.svg".to_string());
// Accepts "ba", "aba", "aaba", "aaaba", etc.
for input in &accepted_a {
let res = run_fsm(&fsm_a, &input.to_string());
assert_eq!(res, StateType::Terminal, "FSM A should accept '{}'", input);
}
// Rejects "a", "b", "ab", "aab", "abb", "baba", "", "bb", "aaa", "bab"
for input in &rejected_a {
let res = run_fsm(&fsm_a, &input.to_string());
assert_eq!(res, StateType::Error, "FSM A should reject '{}'", input);
}
}
// FSM B:
// States: z0, z1, z2, z3
// Start: z0
//
// Types:
// - Terminal: z2
// - Error: z0, z1, z2, z3
//
// Transitions:
// z0 -a-> z1
// z1 -b-> z2
// z2 -a-> z2
// default: z3
//
// Language: /aba*/
let mut builder_a = FSMBuilder::new();
let z0 = builder_a.add_named_state("z0".to_string(), StateType::Error);
let z1 = builder_a.add_named_state("z1".to_string(), StateType::Error);
let z2 = builder_a.add_named_state("z2".to_string(), StateType::Terminal);
let z3 = builder_a.add_named_state("z3".to_string(), StateType::Terminal);
let z4 = builder_a.add_named_state("z4".to_string(), StateType::Error);
builder_a.add_link(&z0, &z1, Action::None, mask_char('a')).unwrap();
builder_a.add_link(&z1, &z2, Action::None, mask_char('b')).unwrap();
builder_a.add_link(&z2, &z3, Action::None, mask_char('a')).unwrap();
builder_a.add_link(&z3, &z3, Action::None, mask_char('a')).unwrap();
let fsm_b_res = builder_a.finish(&z4, Action::None, &z0);
let fsm_b_res_clone = fsm_b_res.clone();
let accepted_b = ["ab", "aba", "abaa", "abaaa", "abaaaa", "abaaaaa"];
let rejected_b = ["a", "b", "ba", "aab", "abb", "baba", "", "bb", "aaa"];
// Test FSM B with different inputs
if let Ok(fsm_b) = fsm_b_res {
debug_dump_fsm_graph(&fsm_b, ".debug/union_b.svg".to_string());
// Accepts "ab", "aba", "abaa", "abaaa", etc.
for input in &accepted_b {
let res = run_fsm(&fsm_b, &input.to_string());
assert_eq!(res, StateType::Terminal, "FSM B should accept '{}'", input);
}
// Rejects "a", "b", "ba", "aab", "abb", "baba", "", "bb", "aaa"
for input in &rejected_b {
let res = run_fsm(&fsm_b, &input.to_string());
assert_eq!(res, StateType::Error, "FSM B should reject '{}'", input);
}
}
let fsm_a= fsm_a_res_clone.unwrap();
let fsm_b = fsm_b_res_clone.unwrap();
let fsm_res = union_fsm(&fsm_a, &fsm_b);
assert!(fsm_res.is_ok());
if let Ok(fsm) = fsm_res {
assert_eq!(StateType::Error, run_fsm(&fsm, &"".to_string()), "Empty String");
assert_eq!(StateType::Error, run_fsm(&fsm, &"this-is-not-a-number".to_string()), "Text");
assert_eq!(StateType::Error, run_fsm(&fsm, &"13373 is a nice number".to_string()), "Number + Text");
debug_dump_fsm_graph(&fsm, ".debug/union_res.svg".to_string());
let all_rejected = rejected_a.iter()
.chain(rejected_b.iter())
.chain(accepted_a.iter())
.chain(accepted_b.iter());
for i in all_rejected {
let input = i.to_string();
let res_a = run_fsm(&fsm_a, &input);
let res_b = run_fsm(&fsm_b, &input);
let res = run_fsm(&fsm, &input);
let expected = match (res_a, res_b) {
(StateType::Terminal, StateType::Terminal) => StateType::Terminal,
(_, _) => StateType::Error,
};
assert_eq!(res, expected, "For input {} Union FSM output '{:?}', got '{:?}' (A: {:?}, B: {:?})", input, expected, res, res_a, res_b);
}
}
}
#[test]
fn in_bound() {
assert!(build_fsm_in_bound(2, 1).is_err());
let test_number_pairs = vec![
(0, 0),
(0, 1),
(1, 10),
(5, 15),
(10, 100),
(42, 123),
(100, 1000),
(500, 2024),
(751394, 751400),
(1, 9999),
(0, 9999),
];
for (min, max) in test_number_pairs {
if min > max {
continue;
}
let fsm_result = build_fsm_in_bound(min, max);
assert!(fsm_result.is_ok());
println!("Testing {}..{}", min, max);
if let Ok(fsm) = fsm_result {
debug_dump_fsm_graph(&fsm, format!(".debug/in_bound/{}_{}.svg", min, max));
for i in 0..=max*10 {
let input_string = i.to_string();
let expected = if i >= min && i <= max {
StateType::Terminal
} else {
StateType::Error
};
let res = run_fsm(&fsm, &input_string);
assert_eq!(res, expected, "Expecet '{:?}' (got '{:?}') for {} (Range: {}..{})", expected, res, i, min, max);
}
}
}
}
}

View File

@@ -131,6 +131,27 @@ pub fn create_graphviz_graph(fsm: &FiniteStateMachine) -> Graph {
}));
}
}
stmts.push(
Stmt::Node(Node {
id: NodeId(Id::Plain("Start".to_string()), None),
attributes: vec![
Attribute(Id::Plain("shape".to_string()), Id::Plain("plain".to_string())),
Attribute(Id::Plain("label".to_string()), Id::Plain("\" \"".to_string())),
]
})
);
stmts.push(
Stmt::Edge(Edge {
ty: EdgeTy::Pair(
Vertex::N(NodeId(Id::Plain("Start".to_string()), None)),
Vertex::N(NodeId(format_id(&fsm.start), None))
),
attributes: vec![]
})
);
Graph::DiGraph { id: Id::Plain("FSM".to_string()), strict: true, stmts: stmts }
}
@@ -157,8 +178,6 @@ mod test {
use graphviz_rust::{printer::{DotPrinter, PrinterContext}};
use crate::configotron::fsm::{display::{create_graphviz_graph, debug_dump_fsm_graph, mask_to_string}, masks::{mask_all, mask_char, mask_char_range}, Action, FSMBuilder, StateType};
use std::fs::File;
use std::io::Write;
#[test]
fn mask_to_string_test() {

View File

@@ -37,6 +37,7 @@ impl fmt::Display for Action {
pub type TransitionArray<T> = [T; 128];
pub type UnfinishedTransitionArray<T> = [Option<T>; 128];
pub type StateTransition = TransitionArray<(StateRef, Action)>;
///
/// Represents a State in consolidate State machine State
@@ -44,9 +45,10 @@ pub type UnfinishedTransitionArray<T> = [Option<T>; 128];
/// The `transition` field contains the outgoing transitions for this state,
/// where each entry corresponds to a possible input symbol.
/// Its expected to have ONLY valid StateRefs
#[derive(Clone, Debug)]
pub struct State {
name: String,
transition: TransitionArray<(StateRef, Action)>,
transition: StateTransition,
state_type: StateType
}
@@ -54,6 +56,7 @@ pub struct State {
/// Represents a finished compleate consolidate State machine
///
/// `start` is expected to be a valid refernces in the `states` Hashmap
#[derive(Clone, Debug)]
pub struct FiniteStateMachine {
states: HashMap<StateRef, State>,
start: StateRef
@@ -105,7 +108,41 @@ impl FSMBuilder {
// 2. Elimate all unsed states
let used_states = Self::get_used_states(&filled_states, start_state);
filled_states.retain(|id, _| used_states.contains(id));
Ok(FiniteStateMachine { states: filled_states, start: *start_state })
}
pub fn finish_without_default(&self, start_state: &StateRef) -> Result<FiniteStateMachine, &str> {
if self.states.len() < 1 {
return Err("State Machine must have at least one state!");
}
if !self.states.contains_key(start_state) {
return Err("Start state is not in the State Set!");
}
// 1. Check for any vacant transitions
for (_, state) in self.states.iter() {
for trans in state.transition.iter() {
if trans.is_none() {
return Err("Not all transitions are filled!");
}
}
}
// 2. All transitions are filled, so build the FSM
let mut filled_states: HashMap<Uuid, State> = HashMap::new();
for (id, state) in self.states.iter() {
filled_states.insert(*id, State {
name: state.name.clone(),
state_type: state.state_type,
transition: std::array::from_fn(|i| state.transition[i].clone().unwrap()),
});
}
// 3. Eliminate all unused states
let used_states = Self::get_used_states(&filled_states, start_state);
filled_states.retain(|id, _| used_states.contains(id));
Ok(FiniteStateMachine { states: filled_states, start: *start_state })