vlib: add a datatypes.fsm module (#13668)

pull/13701/head
Mihai Galos 2022-03-09 11:14:54 +01:00 committed by GitHub
parent 3f1e232c9b
commit d5b087de10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 198 additions and 1 deletions

View File

@ -1,6 +1,7 @@
-## V 0.2.5 -## V 0.2.5
-*Not yet released, changelog is not full* -*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 -## V 0.2.4
-*Not yet released, changelog is not full* -*Not yet released, changelog is not full*

View File

@ -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.

View File

@ -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
}

View File

@ -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
}