vlib: add a datatypes.fsm module (#13668)
parent
3f1e232c9b
commit
d5b087de10
|
@ -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*
|
||||||
|
|
|
@ -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.
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue