diff --git a/Cargo.lock b/Cargo.lock index 80b7ab7..0bcadc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + [[package]] name = "cfg-if" version = "1.0.0" @@ -40,6 +46,7 @@ dependencies = [ "serde", "serde_yml", "thiserror 1.0.69", + "uuid", ] [[package]] @@ -216,6 +223,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.172" @@ -238,6 +255,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "memchr" version = "2.7.4" @@ -380,6 +403,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + [[package]] name = "ryu" version = "1.0.20" @@ -531,6 +560,17 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -552,6 +592,64 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index a10289b..ea8cc40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,10 @@ thiserror = "1" rand = "0.8" graphviz-rust = "0.9.4" itertools = "0.14.0" + +[dependencies.uuid] +version = "1.17.0" +# Lets you generate random UUIDs +features = [ + "v4", +] \ No newline at end of file diff --git a/src/configotron/fsm/builder/mod.rs b/src/configotron/fsm/builder/mod.rs deleted file mode 100644 index 6ecd7a0..0000000 --- a/src/configotron/fsm/builder/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::{cell::RefCell, collections::HashSet, rc::{Rc, Weak}}; - -use crate::configotron::fsm::{FSMState, FSMStateRef, FSM}; - -pub mod numbers; - -pub fn elimate_unused_states(fsm: &mut FSM) { - let start = fsm.start.as_ref().unwrap(); - let mut used: HashSet = HashSet::new(); - - pub fn explore_states(s: Weak>, used: &mut HashSet) -> Option<()> { - - let s_rc = s.upgrade()?; - let s_borrow = s_rc.borrow(); - let t = s_borrow.transition.as_ref()?; - let name = s_borrow.name.clone(); - - if !used.contains(&name) { - used.insert(name); - - for i in 0..(128 as u8) { - explore_states(t.get_next(i), used); - } - } - Some(()) - } - - explore_states(Rc::downgrade(start), &mut used); - - let mut new_state_array: Vec = Vec::new(); - - fsm.states.drain(..).for_each(|s| { - let s_name = s.borrow().name.clone(); - if used.contains(&s_name) { - new_state_array.push(s); - } - }); - - fsm.states = new_state_array; -} - - -#[cfg(test)] -mod test { - use std::rc::Rc; - - use graphviz_rust::cmd::Format; - - use crate::configotron::fsm::{builder::elimate_unused_states, masking::{MaskPresets, TransitionMask}, new_state_ref, test::debug_file_dump, transition::FSMTranistion, FSMState, FSMStateAction, FSM}; - - #[test] - fn state_elimitnation() { - let z0 = new_state_ref(FSMState::new( - "z0".to_string(), FSMStateAction::Terminal, - )); - let z1 = new_state_ref(FSMState::new( - "z1".to_string(), FSMStateAction::Terminal, - )); - let z2 = new_state_ref(FSMState::new( - "z2".to_string(), FSMStateAction::Terminal, - )); - let z3 = new_state_ref(FSMState::new( - "z3".to_string(), FSMStateAction::Terminal, - )); - let z4 = new_state_ref(FSMState::new( - "z4".to_string(), FSMStateAction::Terminal, - )); - let z5 = new_state_ref(FSMState::new( - "z5".to_string(), FSMStateAction::Terminal, - )); - - let start = z0.clone(); - - /* - * Links: - * (z0) -> (z1) - * (z1) -> (z2), (z3) - * (z2) -> (z2) - * (z3) -> (z1) - * X (z4) -> (z3) - * X (z5) -> (z5) - */ - - z0.borrow_mut().set_transitions({ - let t = FSMTranistion::new(Rc::downgrade(&z1)); - t - }); - z1.borrow_mut().set_transitions({ - let mut t = FSMTranistion::new(Rc::downgrade(&z2)); - t.apply(TransitionMask::from(MaskPresets::LowerCase), Rc::downgrade(&z3)); - t - }); - z2.borrow_mut().set_transitions({ - let t = FSMTranistion::new(Rc::downgrade(&z2)); - t - }); - z3.borrow_mut().set_transitions({ - let t = FSMTranistion::new(Rc::downgrade(&z1)); - t - }); - z4.borrow_mut().set_transitions({ - let t = FSMTranistion::new(Rc::downgrade(&z3)); - t - }); - z5.borrow_mut().set_transitions({ - let t = FSMTranistion::new(Rc::downgrade(&z5)); - t - }); - - - let state_prev = vec![z0.clone(), z1.clone(), z2.clone(), z3.clone(), z4.clone(), z5.clone()]; - let state_after = vec![z0.clone(), z1.clone(), z2.clone(), z3.clone()]; - - let mut fsm = FSM { - states: state_prev, - name: "test_fsm".to_string(), - start: Some(start) - }; - - debug_file_dump(fsm.render_graph(Format::Svg).unwrap(), "/tmp/state_elimitiation_before.svg".to_string()); - - elimate_unused_states(&mut fsm); - - debug_file_dump(fsm.render_graph(Format::Svg).unwrap(), "/tmp/state_elimitiation_after.svg".to_string()); - - - assert_eq!(fsm.states.len(), state_after.len()); - - for (i, j) in fsm.states.iter().zip(state_after) { - assert_eq!(i.borrow().name, j.borrow().name) - } - - - } -} \ No newline at end of file diff --git a/src/configotron/fsm/builder/numbers.rs b/src/configotron/fsm/builder/numbers.rs deleted file mode 100644 index d5a7738..0000000 --- a/src/configotron/fsm/builder/numbers.rs +++ /dev/null @@ -1,309 +0,0 @@ -use std::{rc::Rc}; - -use crate::{configotron::fsm::{builder::elimate_unused_states, masking::{MaskPresets, TransitionMask}, new_state_ref, transition::FSMTranistion, FSMState, FSMStateAction, FSMStateRef}, main}; - -use super::super::FSM; - -fn number_of_digits(i: i32) -> u32 { - if i == 0 { - 1 - } else { - let mut n = i.abs(); - let mut digits = 0; - while n > 0 { // Yes, with floating point math it does not work, sorry ;( - n /= 10; - digits += 1; - } - digits - } -} - -fn nth_decimal_digit(num: u32, n: u32) -> u32 { - let num_digits = number_of_digits(num as i32); - if n >= num_digits { - return 0; - } - let divisor = 10u32.pow(num_digits - n - 1); - (num / divisor) % 10 -} - -fn create_fsm_number_scafold(n: u32) -> (Vec, Vec, FSMStateRef, FSMStateRef) { - let length_of_number = number_of_digits(n as i32); - - // Creating All states - let error_state = new_state_ref(FSMState::new("e".to_string(), FSMStateAction::Error)); - let goal_state = new_state_ref(FSMState::new("g".to_string(), FSMStateAction::Terminal)); - - let mut main_line: Vec = Vec::new(); - let mut side_line: Vec = Vec::new(); - - // Create and Link Main to side - for i in 0..length_of_number { - let digit = nth_decimal_digit(n, i); - - let new_main_state = new_state_ref(FSMState::new( - format!("m{}", i), FSMStateAction::None)); - let new_side_state = new_state_ref(FSMState::new( - format!("s{}", i), FSMStateAction::None)); - - // Main -> Error - let mut main_state_transition = FSMTranistion::new(Rc::downgrade(&error_state)); - - // Side -> Error - new_side_state.borrow_mut().set_transitions( - FSMTranistion::new(Rc::downgrade(&error_state)) - ); - - // Main -> Side (-) Transition [0..n-1] - if digit > 0 { - main_state_transition.apply(TransitionMask::from(MaskPresets::NumberRange(0..=(digit as u8 - 1))), Rc::downgrade(&new_side_state)); - } - - new_main_state.borrow_mut().set_transitions( - main_state_transition - ); - - main_line.push(new_main_state.clone()); - side_line.push(new_side_state.clone()); - }; - - - // Link Main to main, side to side - for (i, - ( - (prev_main, next_main), - (prev_side, next_side) - ) - ) in - main_line.iter().zip(main_line.iter().skip(1)) - .zip(side_line.iter().zip(side_line.iter().skip(1))) - .enumerate() { - let digit = nth_decimal_digit(n, i as u32); - - match &mut prev_main.borrow_mut().transition { - None => {}, - Some(t) => { - - // prev_main -> next_side (+) Transition - if digit < 9 { - t.apply( - TransitionMask::from(MaskPresets::NumberRange((digit as u8 + 1)..=9)), - Rc::downgrade(next_side) - ); - } - // prev_main -> next_main (=) Transition - t.apply( - TransitionMask::from(MaskPresets::Char(std::char::from_digit(digit, 10).unwrap())), - Rc::downgrade(next_main) - ); - } - } - - // prev_side -> next_side - match &mut prev_side.borrow_mut().transition { - None => {}, - Some(t) => { - t.apply( - TransitionMask::from(MaskPresets::Numbers), - Rc::downgrade(next_side) - ); - } - } - } - - (main_line, side_line, goal_state, error_state) -} - -fn create_fsm_number_upper_bound(max: u32) -> FSM { - let ( - mut main_line, - mut side_line, - goal_state, - error_state - ) = create_fsm_number_scafold(max); - - // Setting main/side line as Terminal states - for (side, main) in side_line.iter_mut().zip(&mut main_line) { - side.borrow_mut().action = FSMStateAction::Terminal; - main.borrow_mut().action = FSMStateAction::Terminal; - }; - - // Link last main to goal - match &mut main_line.last() { - Some(main_last) => { - let last_digit = max % 10; - match &mut main_last.borrow_mut().transition { - Some(t) => { - // Main -> Goal (=) or (+) - t.apply(TransitionMask::from(MaskPresets::NumberRange(0..=(last_digit as u8))), - Rc::downgrade(&goal_state) - ); - }, - _ => {} - } - }, - _ => {} - } - - // Link goal to error - goal_state.borrow_mut().set_transitions(FSMTranistion::new(Rc::downgrade(&error_state))); - - // Linking error to error - error_state.borrow_mut().set_transitions(FSMTranistion::new(Rc::downgrade(&error_state))); - - // Combinding in one array - let mut states: Vec>> = vec![error_state, goal_state]; - let first_state: Option = match main_line.first() { - None => None, - Some(first) => { - Some(first.clone()) - } - }; - states.append(&mut main_line); - states.append(&mut side_line); - - let mut fsm = FSM { - name: format!("FSM_UpperBound_{}", max), - states: states, - start: first_state - }; - - elimate_unused_states(&mut fsm); - - fsm -} - -fn create_fsm_number_lower_bound(min: u32) -> FSM { - let ( - mut main_line, - mut side_line, - goal_state, - error_state - ) = create_fsm_number_scafold(min); - - // Setting main/side line as Error states - for (side, main) in side_line.iter_mut().zip(&mut main_line) { - side.borrow_mut().action = FSMStateAction::Error; - main.borrow_mut().action = FSMStateAction::Error; - }; - - // Link last main to goal,error,side - match &mut main_line.last() { - Some(main_last) => { - let last_digit = min % 10; - match &mut main_last.borrow_mut().transition { - Some(t) => { - // Main -> Goal (=) - t.apply(TransitionMask::from(MaskPresets::NumberRange(0..=(last_digit as u8))), - Rc::downgrade(&goal_state) - ); - - // Main -> Error (+) - t.apply(TransitionMask::from(MaskPresets::NumberRange(0..=(last_digit as u8))), - Rc::downgrade(&goal_state) - ); - - }, - _ => {} - } - }, - _ => {} - } - - // Link goal to error - goal_state.borrow_mut().set_transitions(FSMTranistion::new(Rc::downgrade(&error_state))); - - // Linking error to error - error_state.borrow_mut().set_transitions(FSMTranistion::new(Rc::downgrade(&error_state))); - - // Combinding in one array - let mut states: Vec>> = vec![error_state, goal_state]; - let first_state: Option = match main_line.first() { - None => None, - Some(first) => { - Some(first.clone()) - } - }; - states.append(&mut main_line); - states.append(&mut side_line); - - let mut fsm = FSM { - name: format!("FSM_LowerBound_{}", min), - states: states, - start: first_state - }; - - elimate_unused_states(&mut fsm); - - fsm -} - -#[cfg(test)] -mod test { - use graphviz_rust::cmd::Format; - - use crate::configotron::fsm::{builder::numbers::{create_fsm_number_upper_bound, number_of_digits}, run::run_fsm, test::debug_file_dump, FSMStateAction}; - - - #[test] - fn build_upper_bound() { - for max in vec![4026, 124, 9999, 1000, 0, 42398] { - let fsm = create_fsm_number_upper_bound(max); - let data = fsm.render_graph(Format::Svg).unwrap(); - println!("Testing upper Bound FSM {}", max); - - for i in 0..=(if max == 0 {10} else {max * 10} ) { - - assert_eq!(run_fsm(&i.to_string(), &fsm), if i <= max { - FSMStateAction::Terminal - } else { - FSMStateAction::Error - }, "{} yield the wrong final state", i); - } - - // Testing against refernce machine - let file_name = format!("build_number_upper_bound_{}.svg", max); - debug_file_dump(data.clone(), format!("/tmp/{}", file_name).to_string()); - - } - } - - #[test] - fn build_lower_bound() { - for max in vec![4026, 124, 9999, 1000, 0, 42398] { - let fsm = create_fsm_number_upper_bound(max); - let data = fsm.render_graph(Format::Svg).unwrap(); - println!("Testing Lower Bound FSM {}", max); - - for i in 0..=(if max == 0 {10} else {max * 10} ) { - - assert_eq!(run_fsm(&i.to_string(), &fsm), if i >= max { - FSMStateAction::Terminal - } else { - FSMStateAction::Error - }, "{} yield the wrong final state", i); - } - - // Testing against refernce machine - let file_name = format!("build_number_lower_bound_{}.svg", max); - debug_file_dump(data.clone(), format!("/tmp/{}", file_name).to_string()); - - } - } - - #[test] - fn number_of_digits_test() { - assert_eq!(number_of_digits(0), 1); - assert_eq!(number_of_digits(9), 1); - assert_eq!(number_of_digits(10), 2); - assert_eq!(number_of_digits(99), 2); - assert_eq!(number_of_digits(100), 3); - assert_eq!(number_of_digits(-1), 1); - assert_eq!(number_of_digits(-9), 1); - assert_eq!(number_of_digits(-10), 2); - assert_eq!(number_of_digits(-99), 2); - assert_eq!(number_of_digits(-100), 3); - assert_eq!(number_of_digits(123456), 6); - assert_eq!(number_of_digits(-123456), 6); - } -} \ No newline at end of file diff --git a/src/configotron/fsm/display.rs b/src/configotron/fsm/display.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/configotron/fsm/masking.rs b/src/configotron/fsm/masking.rs deleted file mode 100644 index 05911a1..0000000 --- a/src/configotron/fsm/masking.rs +++ /dev/null @@ -1,224 +0,0 @@ - -use itertools::Itertools; -use std::{ops::RangeInclusive, usize}; - -pub enum MaskPresets { - Char(char), - AnyCase, - LowerCase, - UpperCase, - AllHumanReadable, - Numbers, - NumberRange(RangeInclusive), - SpecialChars, - WhiteSpace, - LineTermination, - NoChar, -} - -#[derive(Debug, Clone)] -pub struct TransitionMask { - mask: u128, -} - -impl From for TransitionMask { - fn from(value: MaskPresets) -> Self { - let mut n: u128 = 0; - - let mut set_range = |r: RangeInclusive| { - r.for_each(|i| n |= 1 << i); - }; - - match value { - MaskPresets::NoChar => {} - MaskPresets::AnyCase => { - set_range(0x41..=0x5a); // UpperCase - set_range(0x61..=0x7a); // LowerCase - }, - MaskPresets::NumberRange(r) => { - let start = (*r.start() as i32) + 0x30; - let end = (*r.end() as i32) + 0x30; - set_range(start..=end); - } - MaskPresets::LowerCase => { - set_range(0x61..=0x7a); // LowerCase - } - MaskPresets::UpperCase => { - set_range(0x41..=0x5a); // UpperCase - } - MaskPresets::AllHumanReadable => { - set_range(0x20..=0x7e); // All Ascii Chars + Space + Tab - n |= 1 << 0x9; // Tab - } - MaskPresets::Numbers => { - set_range(0x30..=0x39); // Numbers - } - MaskPresets::SpecialChars => { - set_range(0x21..=0x2F); // ! to / - set_range(0x3A..=0x40); // : to @ - set_range(0x5B..=0x0D); // [ to ` - set_range(0x7B..=0x7E); // { to ~ - } - MaskPresets::WhiteSpace => { - n |= 1 << 0x9; // Tab - n |= 1 << 0x20; // Spaces - } - MaskPresets::LineTermination => { - n |= 1 << 0x0a; // Line Feed - n |= 1 << 0x0d; // Carriage Return - } - MaskPresets::Char(c) => { - if c.is_ascii() { - n |= 1 << (c as u32) - } - } - } - - TransitionMask { mask: n } - } -} - -impl TransitionMask { - pub fn new() -> Self { - TransitionMask { mask: 0 } - } - - pub fn or(&self, other: Self) -> TransitionMask { - TransitionMask { - mask: other.mask | self.mask, - } - } - - pub fn and(&self, other: Self) -> TransitionMask { - TransitionMask { - mask: other.mask & self.mask, - } - } - - pub fn inv(&self) -> TransitionMask { - TransitionMask { mask: !self.mask } - } - - pub fn get_bit(&self, i: usize) -> bool { - (self.mask & (1 << i)) > 0 - } - - pub fn set_bit(&mut self, i: usize) { - self.mask |= 1 << i; - } - - pub fn to_string(&self) -> String { - let mut res: Vec = Vec::new(); - let mut strike: Option = None; - - fn num_to_str(i: usize) -> String { - if (0x21..=0x7E).contains(&i) { - format!("'{}'", ((i as u8) as char).to_string()) - } else { - match i { - 0x20 => "SP".to_string(), - 0x0D => "LF".to_string(), - 0x0A => "CR".to_string(), - 0x09 => "TAB".to_string(), - _ => format!("{:02x}", i), - } - } - } - - for (p, n) in (0..128).tuple_windows() { - match (strike, self.get_bit(p), self.get_bit(n)) { - (None, true, true) => { - // First two - strike = Some(p); - } - (None, true, false) => { - // First, only one - res.push(num_to_str(p)); - } - (None, false, true) => { - // Strik Start - strike = Some(n) - } - (None, false, false) => { - // No Strike - } - (Some(_), true, true) => { - // Stike ongoing - } - (Some(s), true, false) => { - // Strik end - if s == p { - // Single Number - res.push(num_to_str(p)); - } else { - // Range - res.push(format!("{}-{}", num_to_str(s), num_to_str(p))); - } - - strike = None; - } - (Some(_), false, true) => { - // This should no happend - strike = Some(n); - } - (Some(_), false, false) => { - // Should also no happend - strike = None; - } - } - } - - match strike { - None => (), - Some(s) => { - if s == 127 { - res.push(num_to_str(s)); - } else { - res.push(format!("{}-{}", num_to_str(s), num_to_str(127))); - } - } - }; - - res.join(",") - } - } - - -mod test { - use super::TransitionMask; - - #[test] - fn transition_mask_to_string() { - let mut mask1 = TransitionMask::new(); - - mask1.set_bit(0); - - (10..=16).for_each(|i| { - mask1.set_bit(i); - }); - - (50..=51).for_each(|i| { - mask1.set_bit(i); - }); - - mask1.set_bit(60); - mask1.set_bit(62); - mask1.set_bit(64); - - (70..=73).for_each(|i| { - mask1.set_bit(i); - }); - - (126..=127).for_each(|i| { - mask1.set_bit(i); - }); - - println!("{}", mask1.to_string()); - - assert_eq!( - mask1.to_string(), - "00,CR-10,'2'-'3','<','>','@','F'-'I','~'-7f" - ); - } - -} \ No newline at end of file diff --git a/src/configotron/fsm/masks.rs b/src/configotron/fsm/masks.rs new file mode 100644 index 0000000..ec6b0a1 --- /dev/null +++ b/src/configotron/fsm/masks.rs @@ -0,0 +1,127 @@ +use std::{collections::{HashMap}, hash::Hash}; + +use crate::configotron::fsm::{TransitionArray, TransitionMask}; + +pub fn decompose_transition(array: &TransitionArray) -> Vec<(T, TransitionMask)> { + let mut results: HashMap = HashMap::new(); + + for (pos, i) in array.iter().enumerate() { + let m: TransitionMask = 1 << pos; + results.entry(i.clone()) + .and_modify(|val| *val |= m) + .or_insert(m); + } + + results.into_iter().collect() +} + +pub fn mask_number(i: u8) -> TransitionMask { + let i = i % 10; + 1 << (i + b'0') +} + +pub fn mask_any_number() -> TransitionMask { + let mut mask: TransitionMask = 0; + for n in 0..=9 { + mask |= 1 << (n + b'0'); + } + mask +} + +pub fn mask_number_range(start: u8, end: u8) -> TransitionMask { + let mut mask: TransitionMask = 0; + for n in (start % 10)..=(end % 10) { + mask |= 1 << (n + b'0'); + } + mask +} + +pub fn mask_number_char(c: char) -> TransitionMask { + if c.is_ascii() && (c as u8) <= 0x7F { + 1 << (c as u8) + } else { + 0 + } +} + +pub fn mask_all() -> TransitionMask { + u128::MAX +} + +#[cfg(test)] +mod test { + use crate::configotron::fsm::masks::decompose_transition; + + #[test] + fn basic_decomposition() { + let mut arr: [String; 128] = std::array::from_fn(|_| " ".to_string()); + + for i in 10..=20 { + arr[i] = "A".to_string(); + } + + for i in 30..=35 { + arr[i] = "A".to_string(); + } + + for i in 100..=110 { + arr[i] = "B".to_string(); + } + + for i in 111..=120 { + arr[i] = "C".to_string(); + } + + for i in 121..=127 { + arr[i] = "A".to_string(); + } + + let masks = decompose_transition(&arr); + + // Test that the number of unique masks is 3 ("A", "B", "C") + assert_eq!(masks.len(), 4); + + // Collect the keys for easier assertions + let mut keys: Vec<_> = masks.iter().map(|(k, _)| k.as_str()).collect(); + keys.sort(); + assert_eq!(keys, vec![" ", "A", "B", "C"]); + // Print out the masks for "A", "B", "C", and " " + for (k, v) in &masks { + if k == "A" || k == "B" || k == "C" || k == " " { + println!("mask for {:?}: {:#0128b}", k, v); + } + } + + // Check that the mask for "A" covers the correct indices + let mask_a = masks.iter().find(|(k, _)| k == "A").unwrap().1; + let mut expected_mask_a = 0; + for i in 10..=20 { + expected_mask_a |= 1 << i; + } + for i in 30..=35 { + expected_mask_a |= 1 << i; + } + for i in 121..=127 { + expected_mask_a |= 1 << i; + } + // Note: arr is length 128, so 128..=130 are out of bounds + assert_eq!(mask_a, expected_mask_a); + + // Check that the mask for "B" covers 100..=110 + let mask_b = masks.iter().find(|(k, _)| k == "B").unwrap().1; + let mut expected_mask_b = 0; + for i in 100..=110 { + expected_mask_b |= 1 << i; + } + assert_eq!(mask_b, expected_mask_b); + + // Check that the mask for "C" covers 111..=120 + let mask_c = masks.iter().find(|(k, _)| k == "C").unwrap().1; + let mut expected_mask_c = 0; + for i in 111..=120 { + expected_mask_c |= 1 << i; + } + assert_eq!(mask_c, expected_mask_c); + + } +} \ No newline at end of file diff --git a/src/configotron/fsm/mod.rs b/src/configotron/fsm/mod.rs index 700f433..147ae32 100644 --- a/src/configotron/fsm/mod.rs +++ b/src/configotron/fsm/mod.rs @@ -1,253 +1,223 @@ -pub mod masking; -pub mod transition; -pub mod builder; -pub mod run; +pub mod display; +pub mod masks; -use graphviz_rust::{ - cmd::Layout, dot_structures::{Attribute, Edge, EdgeTy, Graph, Id, NodeId, Stmt, Vertex}, exec, printer::PrinterContext -}; -use transition::FSMTranistion; -use std::{cell::RefCell, rc::Rc}; +use std::{collections::{HashMap, HashSet}}; -pub type FSMStateRef = Rc>; +use uuid::Uuid; -pub fn new_state_ref(s: FSMState) -> FSMStateRef { - Rc::new(RefCell::new(s)) +pub type StateRef = Uuid; + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum StateType { + EndState, + ErrorState, + None +} + +#[derive(Clone, Copy, Debug)] +pub enum Action { + None, + Push, + Pop, + Submit +} + +pub type TransitionArray = [T; 128]; +pub type UnfinishedTransitionArray = [Option; 128]; + +/// +/// Represents a State in consolidate State machine State +/// +/// 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 +pub struct State { + name: String, + transition: TransitionArray<(StateRef, Action)>, + state_type: StateType +} + +/// +/// Represents a finished compleate consolidate State machine +/// +/// `start` is expected to be a valid refernces in the `states` Hashmap +pub struct FiniteStateMachine { + states: HashMap, + start: StateRef } #[derive(Debug, Clone)] -pub struct FSM { +struct UnfinishedState { name: String, - states: Vec, - start: Option + transition: UnfinishedTransitionArray<(StateRef, Action)>, + state_type: StateType } -impl FSM { - pub fn new(state: Vec, name: &str) -> Self { - FSM { - states: state, - name: name.to_string(), - start: None - } +pub struct FSMBuilder { + states: HashMap +} + +pub type TransitionMask = u128; + +impl FSMBuilder { + pub fn new() -> Self { + FSMBuilder { states: HashMap::new() } } - fn render_stmts(&self) -> Vec { - let mut stmts: Vec = vec![Stmt::Attribute(Attribute( - Id::Plain("rankdir".to_string()), - Id::Plain("LR".to_string()), - ))]; - for s in &self.states { - let s_borrowed = s.borrow(); + 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!"); + } - stmts.push(Stmt::Node(graphviz_rust::dot_structures::Node { - id: NodeId(Id::Plain(s_borrowed.to_graph_label()), None), - attributes: { - let mut res = vec![]; - let label = s_borrowed.to_graph_label(); + if !self.states.contains_key(start_state) { + return Err("Start state is not in the State Set!"); + } - match s_borrowed.action { - FSMStateAction::None => { - res.push(Attribute( - Id::Plain("shape".to_string()), - Id::Plain("circle".to_string()), - )); - } - FSMStateAction::Error => { - res.push(Attribute( - Id::Plain("shape".to_string()), - Id::Plain("circle".to_string()), - )); - } - FSMStateAction::Reset => { - res.push(Attribute( - Id::Plain("shape".to_string()), - Id::Plain("circle".to_string()), - )); - } - FSMStateAction::Terminal => { - res.push(Attribute( - Id::Plain("shape".to_string()), - Id::Plain("doublecircle".to_string()), - )); - } - } + if !self.states.contains_key(default_state) { + return Err("Start state is not in the State Set!"); + } - res.push( - Attribute( - Id::Plain("label".to_string()), - Id::Plain(label), - ) - ); + // 1. Fill vacent Tranisitons + 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| match state.transition[i] { + Some(trans) => trans.clone(), + None => (*default_state, default_action.clone()) + }), + }); + } - res - }, - })); + // 2. Elimate all unsed states + let used_states = Self::get_used_states(&filled_states, start_state); + + filled_states.retain(|id, _| used_states.contains(id)); - match &s_borrowed.transition { - None => {} - Some(t) => { - for (t, range) in t.to_targets_vec() { - match t.upgrade() { - None => {} - Some(target) => { - stmts.push(Stmt::Edge(Edge { - ty: EdgeTy::Pair( - Vertex::N(NodeId( - Id::Plain(s_borrowed.to_graph_label()), - None, - )), - Vertex::N(NodeId( - Id::Plain(target.borrow().to_graph_label()), - None, - )), - ), - attributes: vec![Attribute( - Id::Plain("label".to_string()), - Id::Plain(format!("\"{}\"", range.to_string())), - )], - })); - } - } + Ok(FiniteStateMachine { states: filled_states, start: *start_state }) + } + + fn get_used_states(states: &HashMap, start: &StateRef) -> HashSet { + let mut used: HashSet = HashSet::new(); + + let mut stack: Vec = Vec::new(); + stack.push(*start); + + while let Some(current) = stack.pop() { + if used.insert(current) { + if let Some(s) = states.get(¤t) { + for next in s.transition.iter() { + stack.push((*next).0); } } } } - - stmts + used } - fn render_graph(&self, format: graphviz_rust::cmd::Format) -> Result, std::io::Error> { - let g = Graph::DiGraph { - id: Id::Plain("root".to_string()), - strict: false, - stmts: self.render_stmts(), - }; - let mut ctx: graphviz_rust::printer::PrinterContext = PrinterContext::default(); - println!("{}", graphviz_rust::print(g.clone(), &mut ctx)); - - let data = exec(g, &mut ctx, vec![ - format.into(), - graphviz_rust::cmd::CommandArg::Layout(Layout::Dot) - ])?; - - Ok(data) + pub fn add_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 + }); + id } + + pub fn add_link(&mut self, from_ref: &StateRef, to_ref: &StateRef, action: Action, mask: TransitionMask) -> Result<(), &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")?; + + for i in 0..128 { + if (mask & (1 << i)) != 0 { + from_state.transition[i] = Some((*to_ref, action.clone())); + } + } + + Ok(()) + } } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum FSMStateAction { - None, - Terminal, - Error, - Reset -} +pub fn run_fsm(fsm: &FiniteStateMachine, input: &String) -> StateType { + let mut current = fsm.start; -#[derive(Debug, Clone)] -pub struct FSMState { - name: String, - action: FSMStateAction, - transition: Option, -} - -impl FSMState { - pub fn new(name: String, action: FSMStateAction) -> Self { - FSMState { - name: name, - action: action, - transition: None, + for c in input.chars() { + if c.is_ascii() && (c as u32) <= 0x7F { + current = fsm.states.get(¤t).unwrap().transition[c as usize].0; } } - pub fn set_transitions(&mut self, t: FSMTranistion) { - self.transition = Some(t); - } - - pub fn to_graph_label(&self) -> String { - return format!("\"{}\"", self.name); - } - - pub fn to_graph_hash(&self) -> String { - self.to_graph_label() - } + fsm.states.get(¤t).unwrap().state_type } - #[cfg(test)] mod test { - use graphviz_rust::cmd::Format; - use std::{cell::RefCell, io::Write, rc::Rc}; - use super::{FSMState, FSMStateAction, FSM}; - use super::transition::FSMTranistion; - use super::masking::{MaskPresets, TransitionMask}; - - pub fn debug_file_dump(d: Vec, filepath: String) { - let mut output_file = std::fs::File::create(filepath).unwrap(); - output_file.write_all(&d).unwrap(); - } + use crate::configotron::fsm::{masks::{mask_all, mask_number_char}, run_fsm, Action, FSMBuilder, StateType}; #[test] - fn simple_fsm_render() { - let z0 = Rc::new(RefCell::new(FSMState::new( - "z0".to_string(), - FSMStateAction::None, - ))); - let z1 = Rc::new(RefCell::new(FSMState::new( - "z1".to_string(), - FSMStateAction::None, - ))); - let z2 = Rc::new(RefCell::new(FSMState::new( - "z2".to_string(), - FSMStateAction::Terminal, - ))); - let z3 = Rc::new(RefCell::new(FSMState::new( - "z3".to_string(), - FSMStateAction::None, - ))); + 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::EndState); + let z3 = fsm_builder.add_state("z3".to_string(), StateType::ErrorState); - let mut z0_transisions = FSMTranistion::new_from_mask( - TransitionMask::from(MaskPresets::Char('a')), - Rc::downgrade(&z1), - Rc::downgrade(&z3), - ); - z0_transisions.apply( - TransitionMask::from(MaskPresets::Char('b')), - Rc::downgrade(&z2), - ); + // These are unsed states and should be eliminated + fsm_builder.add_state("u4".to_string(), StateType::ErrorState); + let u5 =fsm_builder.add_state("u5".to_string(), StateType::ErrorState); - let mut z1_transisions = FSMTranistion::new_from_mask( - TransitionMask::from(MaskPresets::Char('a')), - Rc::downgrade(&z1), - Rc::downgrade(&z3), - ); - z1_transisions.apply( - TransitionMask::from(MaskPresets::Char('b')), - Rc::downgrade(&z2), - ); + fsm_builder.add_link(&u5, &u5, Action::None, mask_all()).unwrap(); + + // Transitions: + // z0: 'a' -> z1, 'b' -> z2 + // z1: 'a' -> z1, 'b' -> z2 + // z2: 'a' -> z2 + // z3: * -> z3 + // + // u4: * -> z3 + // u5: * -> u5 + // Error/Default: z3 - let z2_transisions = FSMTranistion::new_from_mask( - TransitionMask::from(MaskPresets::Char('a')), - Rc::downgrade(&z2), - Rc::downgrade(&z3), - ); + fsm_builder.add_link(&z0, &z1, Action::None, mask_number_char('a')).unwrap(); + fsm_builder.add_link(&z0, &z2, Action::None, mask_number_char('b')).unwrap(); + + fsm_builder.add_link(&z1, &z1, Action::None, mask_number_char('a')).unwrap(); + fsm_builder.add_link(&z1, &z2, Action::None, mask_number_char('b')).unwrap(); + + fsm_builder.add_link(&z2, &z2, Action::None, mask_number_char('a')).unwrap(); - let z3_transisions = FSMTranistion::new(Rc::downgrade(&z3)); + let res_fsm = fsm_builder.finish(&z3, Action::None, &z0); + assert!(res_fsm.is_ok()); - z0.borrow_mut().set_transitions(z0_transisions); - z1.borrow_mut().set_transitions(z1_transisions); - z2.borrow_mut().set_transitions(z2_transisions); - z3.borrow_mut().set_transitions(z3_transisions); + + if let Ok(fsm) = res_fsm { + // Unused state elimation worked? + assert_eq!(fsm.states.len(), 4); - let fsm = FSM::new(vec![z0, z1, z2, z3], "Test FSM #1"); - - let data = fsm.render_graph(Format::Svg).unwrap(); - - let mut output_file = std::fs::File::create("/tmp/debug-simle_fsm_building.svg").unwrap(); - output_file.write_all(&data).unwrap(); + assert_eq!(run_fsm(&fsm, &"".to_string()), StateType::None); + assert_eq!(run_fsm(&fsm, &"a".to_string()), StateType::None); + assert_eq!(run_fsm(&fsm, &"b".to_string()), StateType::EndState); + assert_eq!(run_fsm(&fsm, &"aa".to_string()), StateType::None); + assert_eq!(run_fsm(&fsm, &"ab".to_string()), StateType::EndState); + assert_eq!(run_fsm(&fsm, &"ba".to_string()), StateType::EndState); // 'b' leads to z2, then 'a' stays in z2 + assert_eq!(run_fsm(&fsm, &"bb".to_string()), StateType::ErrorState); + assert_eq!(run_fsm(&fsm, &"aaa".to_string()), StateType::None); + assert_eq!(run_fsm(&fsm, &"aab".to_string()), StateType::EndState); + assert_eq!(run_fsm(&fsm, &"abb".to_string()), StateType::ErrorState); + assert_eq!(run_fsm(&fsm, &"abc".to_string()), StateType::ErrorState); // 'c' is not defined, should go to default z3 + assert_eq!(run_fsm(&fsm, &"c".to_string()), StateType::ErrorState); + assert_eq!(run_fsm(&fsm, &"cab".to_string()), StateType::ErrorState); + assert_eq!(run_fsm(&fsm, &"aabc".to_string()), StateType::ErrorState); + assert_eq!(run_fsm(&fsm, &"bca".to_string()), StateType::ErrorState); + assert_eq!(run_fsm(&fsm, &"aaaaab".to_string()), StateType::EndState); + assert_eq!(run_fsm(&fsm, &"aaaaac".to_string()), StateType::ErrorState); + assert_eq!(run_fsm(&fsm, &"bbbb".to_string()), StateType::ErrorState); + } - let svg_content = std::fs::read_to_string("/tmp/debug-simle_fsm_building.svg").unwrap(); - assert_eq!(svg_content, String::from_utf8(data).unwrap()); } -} + +} \ No newline at end of file diff --git a/src/configotron/fsm/run.rs b/src/configotron/fsm/run.rs deleted file mode 100644 index 19e335c..0000000 --- a/src/configotron/fsm/run.rs +++ /dev/null @@ -1,18 +0,0 @@ -use std::{cell::RefCell, rc::{Rc, Weak}}; - -use crate::configotron::fsm::{FSMState, FSMStateAction, FSM}; - -pub fn run_fsm(input: &str, fsm: &FSM) -> FSMStateAction { - let mut state: Weak> = Rc::downgrade(fsm.start.as_ref().unwrap()); - - for c in input.chars() { - let n = c as u8; - if c.is_ascii() && n <= 0x7F { - let rc_state = state.upgrade().unwrap(); - let s = rc_state.borrow(); - state = s.transition.as_ref().unwrap().get_next(n); - } - }; - - state.upgrade().unwrap().borrow().action.clone() -} diff --git a/src/configotron/fsm/transition.rs b/src/configotron/fsm/transition.rs deleted file mode 100644 index 8d65551..0000000 --- a/src/configotron/fsm/transition.rs +++ /dev/null @@ -1,80 +0,0 @@ - -use super::masking::TransitionMask; -use super::FSMState; -use std::collections::HashMap; -use std::rc::Rc; -use std::{cell::RefCell, rc::Weak}; - -type StateRef = Weak>; - -#[derive(Debug, Clone)] -pub struct FSMTranistion { - array: [StateRef; 128], -} - -impl FSMTranistion { - pub fn new_from_mask(mask: TransitionMask, pos: StateRef, default: StateRef) -> Self { - let mut array: [StateRef; 128] = std::array::from_fn(|_| Weak::clone(&default)); - for i in 0..128 { - if mask.get_bit(i) { - array[i] = Weak::clone(&pos); - } - } - - FSMTranistion { array } - } - - pub fn get_next(&self, c: u8) -> StateRef { - self.array[c as usize].clone() - } - - pub fn new(default: StateRef) -> Self { - let array: [StateRef; 128] = std::array::from_fn(|_| Weak::clone(&default)); - - FSMTranistion { array } - } - - pub fn apply(&mut self, mask: TransitionMask, state: StateRef) { - for i in 0..128 { - if mask.get_bit(i) { - self.array[i] = Weak::clone(&state); - } - } - } - - pub fn to_targets_vec(&self) -> Vec<(StateRef, TransitionMask)> { - let mut target_maps: HashMap = HashMap::new(); - - self.array.iter().enumerate().for_each(|(index, target)| { - match target.upgrade() { - None => {} - Some(t) => { - let s_borrowd = t.borrow(); - let key = s_borrowd.to_graph_label(); - match target_maps.get_mut(&key) { - Some((_, mask)) => { - mask.set_bit(index); - } - None => { - let mut mask = TransitionMask::new(); - mask.set_bit(index); - target_maps.insert(key, (Rc::downgrade(&t), mask)); - } - }; - } - }; - }); - - let mut res: Vec<(StateRef, TransitionMask)> = Vec::new(); - - target_maps - .iter() - .for_each(|(_, (target, mask))| match target.upgrade() { - Some(t) => { - res.push((Rc::downgrade(&t), mask.clone())); - } - None => {} - }); - res - } -}