diff --git a/.gitignore b/.gitignore index ea8c4bf..bc842f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.debug/ \ No newline at end of file diff --git a/src/configotron/fsm/component_builder.rs b/src/configotron/fsm/component_builder.rs new file mode 100644 index 0000000..eb20855 --- /dev/null +++ b/src/configotron/fsm/component_builder.rs @@ -0,0 +1,150 @@ +use crate::configotron::fsm::{masks::{mask_any_number, mask_number, mask_number_range}, Action, FSMBuilder, FiniteStateMachine, StateRef, StateType}; + + +fn decimal_digits(x: u32) -> std::vec::IntoIter { + x.to_string() + .chars() + .map(|c| c.to_digit(10).unwrap() as u8) + .collect::>() + .into_iter() +} + +struct BoundFSMScaffold { + builder: FSMBuilder, + error_state: StateRef, + goal_state: StateRef, + last_main: StateRef, + last_side: StateRef, + start_state: StateRef +} + +fn build_fsm_bound_selecton(n: u32, lines_state_type: StateType) -> Result { + let mut builder = FSMBuilder::new(); + let error_state = builder.add_named_state("e".to_string(), StateType::Error); + let goal_state = builder.add_named_state("g".to_string(),StateType::Terminal); + + let mut last_main_ref: StateRef = builder.add_named_state("m0".to_string(), lines_state_type); + let start_state = last_main_ref.clone(); + let mut last_side_ref: StateRef = builder.add_named_state("s0".to_string(), lines_state_type); + + // Link lines + let digits: Vec = decimal_digits(n).collect(); + let mut digits = digits; + digits.pop(); + for (i, &digit) in digits.iter().enumerate() { + let new_main_ref = builder.add_named_state(format!("m{}", i+1), lines_state_type); + let new_side_ref = builder.add_named_state(format!("s{}", i+1), lines_state_type); + + if digit > 0 { + builder.add_link(&last_main_ref, &last_side_ref, Action::None, mask_number_range(0, digit - 1))?; + } + + if digit < 9 { + builder.add_link(&last_main_ref, &new_side_ref, Action::None, mask_number_range(digit + 1, 9))?; + } + + builder.add_link(&last_main_ref, &new_main_ref, Action::None, mask_number(digit))?; + builder.add_link(&last_side_ref, &new_side_ref, Action::None, mask_any_number())?; + + last_main_ref = new_main_ref; + last_side_ref = new_side_ref; + } + + let last_digit = (n % 10) as u8; + if last_digit > 0 { + builder.add_link(&last_main_ref, &last_side_ref, Action::None, mask_number_range(0, last_digit - 1))?; + } + + Ok(BoundFSMScaffold { + builder: builder, + error_state: error_state, + goal_state: goal_state, + last_main: last_main_ref, + last_side: last_side_ref, + start_state: start_state + }) +} + + +pub fn build_fsm_upper_bound(max: u32) -> Result { + let mut scaffold = build_fsm_bound_selecton(max, StateType::Terminal)?; + + let last_digit = (max % 10) as u8; + scaffold.builder.add_link(&scaffold.last_main, &scaffold.goal_state, Action::None, mask_number(last_digit))?; + 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())?; + + 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)?; + + let last_digit = (max % 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())?; + + let fsm = scaffold.builder.finish(&scaffold.error_state, Action::None, &scaffold.start_state)?; + Ok(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_lower_bound; + + #[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 { + 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 { + debug_dump_fsm_graph(&machine, format!(".debug/upper_bound{}.svg", max)); + + for i in 0..=max*10 { + let input_string = i.to_string(); + let res = run_fsm(&machine, &input_string); + + assert_eq!(res, if i <= max { + StateType::Terminal + } else { + StateType::Error + }, "Wrong '{:?}' output for {} (Max: {})", res, i, max); + } + } + } + + } + + + #[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 { + 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)); + + for i in 0..=max*10 { + let input_string = i.to_string(); + let res = run_fsm(&machine, &input_string); + + assert_eq!(res, if i >= max { + StateType::Terminal + } else { + StateType::Error + }, "Wrong '{:?}' output for {} (Max: {})", res, i, max); + } + } + } + } +} \ No newline at end of file diff --git a/src/configotron/fsm/display.rs b/src/configotron/fsm/display.rs index 82d2312..f691400 100644 --- a/src/configotron/fsm/display.rs +++ b/src/configotron/fsm/display.rs @@ -1,5 +1,7 @@ +use std::fs::File; + use crate::configotron::fsm::{masks::decompose_transition, FiniteStateMachine, TransitionMask}; -use graphviz_rust::dot_structures::{Attribute, Edge, EdgeTy, Graph, Id, Node, NodeId, Stmt, Vertex}; +use graphviz_rust::{cmd::{CommandArg, Format}, dot_structures::{Attribute, Edge, EdgeTy, Graph, Id, Node, NodeId, Stmt, Vertex}, exec, printer::PrinterContext}; use uuid::Uuid; @@ -132,11 +134,31 @@ pub fn create_graphviz_graph(fsm: &FiniteStateMachine) -> Graph { Graph::DiGraph { id: Id::Plain("FSM".to_string()), strict: true, stmts: stmts } } +pub fn debug_dump_fsm_graph(fsm: &FiniteStateMachine, file: String) { + let graph = create_graphviz_graph(fsm); + let data = exec(graph, &mut PrinterContext::default(), vec![ + CommandArg::Format(Format::Svg) + ]); + + match File::create(&file) { + Ok(mut file) => { + // You probably want to write the SVG data to the file + if let Ok(svg_data) = data { + use std::io::Write; + file.write_all(&svg_data).expect("Unable to write DOT data"); + } + } + _ => {} + } +} + #[cfg(test)] mod test { - use graphviz_rust::{cmd::{CommandArg, Format}, exec, printer::{DotPrinter, PrinterContext}}; + use graphviz_rust::{printer::{DotPrinter, PrinterContext}}; - use crate::configotron::fsm::{display::{create_graphviz_graph, mask_to_string}, masks::{mask_all, mask_char, mask_char_range}, Action, FSMBuilder, StateType}; + 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() { @@ -176,12 +198,12 @@ mod test { } #[test] - fn create_graph_dot_lang() { + fn basic_fsm() { let mut fsm_builder = FSMBuilder::new(); - let z0 = fsm_builder.add_state("z0".to_string(), StateType::None); - let z1 = fsm_builder.add_state("z1".to_string(), StateType::None); - let z2 = fsm_builder.add_state("z2".to_string(), StateType::Terminal); - let z3 = fsm_builder.add_state("z3".to_string(), StateType::Error); + let z0 = fsm_builder.add_named_state("z0".to_string(), StateType::None); + let z1 = fsm_builder.add_named_state("z1".to_string(), StateType::None); + let z2 = fsm_builder.add_named_state("z2".to_string(), StateType::Terminal); + let z3 = fsm_builder.add_named_state("z3".to_string(), StateType::Error); // Transitions: // z0: 'a' -> z1, 'b' -> z2 @@ -206,6 +228,8 @@ mod test { let string = graph.print(&mut PrinterContext::default()); println!("Original: {}", string); + debug_dump_fsm_graph(&fsm, ".debug/basic_fsm.svg".to_string()); + // Test that the DOT output contains the expected structure, ignoring node ids (UUIDs) let string = graph.print(&mut PrinterContext::default()); // Remove all UUIDs from the output for comparison diff --git a/src/configotron/fsm/mod.rs b/src/configotron/fsm/mod.rs index 199d103..3f81e80 100644 --- a/src/configotron/fsm/mod.rs +++ b/src/configotron/fsm/mod.rs @@ -1,5 +1,6 @@ pub mod display; pub mod masks; +pub mod component_builder; use std::{collections::{HashMap, HashSet}, fmt}; @@ -18,7 +19,7 @@ pub enum StateType { #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum Action { None, - Push, + Add, Pop, Submit } @@ -27,7 +28,7 @@ impl fmt::Display for Action { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Action::None => write!(f, "None"), - Action::Push => write!(f, "Push"), + Action::Add => write!(f, "Push"), Action::Pop => write!(f, "Pop"), Action::Submit => write!(f, "Submit"), } @@ -128,9 +129,7 @@ impl FSMBuilder { used } - - - pub fn add_state(&mut self, name: String, state_type: StateType) -> StateRef { + pub fn add_named_state(&mut self, name: String, state_type: StateType) -> StateRef { let id = Uuid::new_v4(); self.states.insert(id, UnfinishedState { name: name, transition: [None; 128], state_type: state_type @@ -138,7 +137,21 @@ impl FSMBuilder { id } - pub fn add_link(&mut self, from_ref: &StateRef, to_ref: &StateRef, action: Action, mask: TransitionMask) -> Result<(), &str> { + pub fn add_state(&mut self, state_type: StateType) -> StateRef { + let id = Uuid::new_v4(); + self.states.insert(id, UnfinishedState { + name: "".to_string(), transition: [None; 128], state_type: state_type + }); + id + } + + pub fn set_state_type(&mut self, state_ref: &StateRef, state_type: StateType) { + if let Some(state) = self.states.get_mut(state_ref) { + state.state_type = state_type; + } + } + + pub fn add_link(&mut self, from_ref: &StateRef, to_ref: &StateRef, action: Action, mask: TransitionMask) -> Result<(), &'static str> { if !self.states.contains_key(to_ref) { return Err("to-State does not exist!"); } let from_state = self.states.get_mut(from_ref).ok_or("from-State does not exist")?; @@ -172,14 +185,14 @@ mod test { #[test] pub fn basic_fsm_build() { let mut fsm_builder = FSMBuilder::new(); - let z0 = fsm_builder.add_state("z0".to_string(), StateType::None); - let z1 = fsm_builder.add_state("z1".to_string(), StateType::None); - let z2 = fsm_builder.add_state("z2".to_string(), StateType::Terminal); - let z3 = fsm_builder.add_state("z3".to_string(), StateType::Error); + let z0 = fsm_builder.add_named_state("z0".to_string(), StateType::None); + let z1 = fsm_builder.add_named_state("z1".to_string(), StateType::None); + let z2 = fsm_builder.add_named_state("z2".to_string(), StateType::Terminal); + let z3 = fsm_builder.add_named_state("z3".to_string(), StateType::Error); // These are unsed states and should be eliminated - fsm_builder.add_state("u4".to_string(), StateType::Error); - let u5 =fsm_builder.add_state("u5".to_string(), StateType::Error); + fsm_builder.add_named_state("u4".to_string(), StateType::Error); + let u5 =fsm_builder.add_named_state("u5".to_string(), StateType::Error); fsm_builder.add_link(&u5, &u5, Action::None, mask_all()).unwrap();