diff --git a/docs/configotron-whiteboard.xopp b/docs/configotron-whiteboard.xopp index ca8c0cb..f393ca2 100644 Binary files a/docs/configotron-whiteboard.xopp and b/docs/configotron-whiteboard.xopp differ diff --git a/src/configotron/fsm/component_builder.rs b/src/configotron/fsm/component_builder.rs index eb20855..1e4b951 100644 --- a/src/configotron/fsm/component_builder.rs +++ b/src/configotron/fsm/component_builder.rs @@ -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 { 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 { - let mut scaffold = build_fsm_bound_selecton(max, StateType::Error)?; +pub fn build_fsm_lower_bound(min: u32) -> Result { + 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 { Ok(fsm) } +fn fsm_cartesian_product(fsm1: &FiniteStateMachine, fsm2: &FiniteStateMachine, state_type_operator: F, action_operator: G) -> Result +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(¤t_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 { + 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 { + 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 = 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 = 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); + } + } + } + } } \ No newline at end of file diff --git a/src/configotron/fsm/display.rs b/src/configotron/fsm/display.rs index f691400..1ebe265 100644 --- a/src/configotron/fsm/display.rs +++ b/src/configotron/fsm/display.rs @@ -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() { diff --git a/src/configotron/fsm/mod.rs b/src/configotron/fsm/mod.rs index 3f81e80..80c1b81 100644 --- a/src/configotron/fsm/mod.rs +++ b/src/configotron/fsm/mod.rs @@ -37,6 +37,7 @@ impl fmt::Display for Action { pub type TransitionArray = [T; 128]; pub type UnfinishedTransitionArray = [Option; 128]; +pub type StateTransition = TransitionArray<(StateRef, Action)>; /// /// Represents a State in consolidate State machine State @@ -44,9 +45,10 @@ pub type UnfinishedTransitionArray = [Option; 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, start: StateRef @@ -77,7 +80,7 @@ impl FSMBuilder { FSMBuilder { states: HashMap::new() } } - pub fn finish (&self, default_state: &StateRef, default_action: Action, start_state: &StateRef) -> Result { + pub fn finish(&self, default_state: &StateRef, default_action: Action, start_state: &StateRef) -> Result { if self.states.len() < 1 { return Err("State Machine must have at least one state!"); } @@ -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 { + 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 = 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 })