From d5b087de107eae686d8668f22adfe8b92fed8033 Mon Sep 17 00:00:00 2001 From: Mihai Galos Date: Wed, 9 Mar 2022 11:14:54 +0100 Subject: [PATCH] vlib: add a datatypes.fsm module (#13668) --- CHANGELOG.md | 3 +- vlib/datatypes/fsm/README.md | 21 +++++++++ vlib/datatypes/fsm/fsm.v | 87 ++++++++++++++++++++++++++++++++++ vlib/datatypes/fsm/fsm_test.v | 88 +++++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 vlib/datatypes/fsm/README.md create mode 100644 vlib/datatypes/fsm/fsm.v create mode 100644 vlib/datatypes/fsm/fsm_test.v diff --git a/CHANGELOG.md b/CHANGELOG.md index 493ecf2b60..b75bbef459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ -## V 0.2.5 -*Not yet released, changelog is not full* -- Introduce `isize` and `usize` types, deprecate `size_t` in favor of `usize` +- Introduce `isize` and `usize` types, deprecate `size_t` in favor of `usize`. +- Add `datatypes` and `datatypes.fsm` modules. -## V 0.2.4 -*Not yet released, changelog is not full* diff --git a/vlib/datatypes/fsm/README.md b/vlib/datatypes/fsm/README.md new file mode 100644 index 0000000000..8b62a99cd0 --- /dev/null +++ b/vlib/datatypes/fsm/README.md @@ -0,0 +1,21 @@ +# fsm + +This module implements a Finite State Machine (FSM). +The FSM is composed of states and transitions between them. +These need to be specified by the client. + +## Usage + +Have a look at `fsm_test.v` for usage examples. + +On each `run()`, all the possible transitions from the current state are evaluated. +The first transition for the current state, whose condition evaluates to true is +taken (the condition is specified by a transition callback function). + +In a successfull transition, the current state changes to the new one. +When that happens: +* the client-specified `on_exit()` handler from the current state is called. +* the client-specified `on_entry()` handler of the new state is called. + +After all transitions are checked, and thus the state is changed, the client-specified +`on_run()` handler of the now current state is called. diff --git a/vlib/datatypes/fsm/fsm.v b/vlib/datatypes/fsm/fsm.v new file mode 100644 index 0000000000..3d8f309670 --- /dev/null +++ b/vlib/datatypes/fsm/fsm.v @@ -0,0 +1,87 @@ +module fsm + +pub type EventHandlerFn = fn (receiver voidptr, from string, to string) + +pub type ConditionFn = fn (receiver voidptr, from string, to string) bool + +struct State { +mut: + entry_handler EventHandlerFn + run_handler EventHandlerFn + exit_handler EventHandlerFn +} + +struct Transition { +mut: + to string + condition_handler ConditionFn = voidptr(0) +} + +pub struct StateMachine { +mut: + states map[string]State + transitions map[string][]Transition + current_state string +} + +pub fn new() StateMachine { + return StateMachine{} +} + +pub fn (mut s StateMachine) set_state(name string) ? { + if name in s.states { + s.current_state = name + } + return error('unknown state: $name') +} + +pub fn (mut s StateMachine) get_state() string { + return s.current_state +} + +pub fn (mut s StateMachine) add_state(name string, entry EventHandlerFn, run EventHandlerFn, exit EventHandlerFn) { + s.states[name] = State{ + entry_handler: entry + run_handler: run + exit_handler: exit + } + if s.states.len == 1 { + s.current_state = name + } +} + +pub fn (mut s StateMachine) add_transition(from string, to string, condition_handler ConditionFn) { + t := Transition{ + to: to + condition_handler: condition_handler + } + if from in s.transitions { + s.transitions[from] << t + return + } + s.transitions[from] = [t] +} + +pub fn (mut s StateMachine) run(receiver voidptr) ? { + from_state := s.current_state + mut to_state := s.current_state + if transitions := s.transitions[s.current_state] { + for transition in transitions { + if transition.condition_handler(receiver, from_state, transition.to) { + s.change_state(receiver, transition.to) + to_state = transition.to + break + } + } + } else { + s.states[s.current_state].run_handler(receiver, from_state, to_state) + return error('no more transitions') + } + s.states[s.current_state].run_handler(receiver, from_state, to_state) +} + +fn (mut s StateMachine) change_state(receiver voidptr, newstate string) { + s.states[s.current_state].exit_handler(receiver, s.current_state, newstate) + s.states[newstate].entry_handler(receiver, s.current_state, newstate) + s.current_state = newstate +} diff --git a/vlib/datatypes/fsm/fsm_test.v b/vlib/datatypes/fsm/fsm_test.v new file mode 100644 index 0000000000..ef4136ea36 --- /dev/null +++ b/vlib/datatypes/fsm/fsm_test.v @@ -0,0 +1,88 @@ +import datatypes.fsm + +struct MyReceiver { +mut: + data []string +} + +fn default_setup() (MyReceiver, fsm.StateMachine) { + mut receiver := MyReceiver{} + mut s := fsm.new() + s.add_state('A', on_state_entry, on_state_run, on_state_exit) + s.add_state('B', on_state_entry, on_state_run, on_state_exit) + s.add_transition('A', 'B', condition_transition) + return receiver, s +} + +fn test_statemachine_number_of_callbacks_correct_when_single_transition() ? { + mut receiver, mut s := default_setup() + + s.run(receiver) ? + + assert receiver.data.len == 3 +} + +fn test_statemachine_sequence_works_when_typical() ? { + mut receiver, mut s := default_setup() + + s.run(receiver) ? + + assert receiver.data[0] == 'on_state_exit: A -> B' + assert receiver.data[1] == 'on_state_entry: A -> B' + assert receiver.data[2] == 'on_state_run: A -> B' +} + +fn test_statemachine_works_when_final_state() ? { + mut receiver, mut s := default_setup() + + // current state `A`, with a possible transition to `B`: + s.run(receiver) ? // run should not error here + + // Note: run will now return error, because for state `B`, + // there are no more transitions: + s.run(receiver) or { assert true } + s.run(receiver) or { assert true } + + assert receiver.data.len == 5 + assert receiver.data[2] == 'on_state_run: A -> B' + assert receiver.data[3] == 'on_state_run: B -> B' + assert receiver.data[4] == 'on_state_run: B -> B' +} + +fn test_simple_loop() ? { + mut receiver, mut s := default_setup() + + // Add a transition back to `A` too: + s.add_transition('B', 'A', condition_transition) + + // Run the FSM for a while. + // It will loop forever between `A` -> `B` -> `A` -> `B` ... + for _ in 0 .. 100 { + s.run(receiver) or { assert false } + } + assert receiver.data[1] == 'on_state_entry: A -> B' + assert receiver.data[4] == 'on_state_entry: B -> A' + assert receiver.data[7] == 'on_state_entry: A -> B' + assert receiver.data[10] == 'on_state_entry: B -> A' +} + +// Helper functions + +fn on_state_entry(mut receiver MyReceiver, from string, to string) { + receiver.data << 'on_state_entry: ' + from + ' -> ' + to +} + +fn on_state_run(mut receiver MyReceiver, from string, to string) { + receiver.data << 'on_state_run: ' + from + ' -> ' + to +} + +fn on_state_exit(mut receiver MyReceiver, from string, to string) { + receiver.data << 'on_state_exit: ' + from + ' -> ' + to +} + +fn condition_transition(receiver &MyReceiver, from string, to string) bool { + // The condition callback is a way to provide input to the FSM + // It can return true or false, based on external events/state. + // For these tests however, that is not used, and it simply always returns true. + return true +}