#![no_std]
#![forbid(unsafe_code)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::expect_used)]
#![warn(clippy::missing_const_for_fn)]
mod err;
mod transition;
use core::marker::PhantomData;
pub use crate::err::ValidationError;
pub use crate::transition::Transition;
pub use crate::transition::TransitionMatrix;
#[rustfmt::skip]
#[cfg(feature = "derive")]
pub use tinystate_derive::{Events, States};
pub trait States: Copy + Default {
fn index(&self) -> usize;
fn from_index(index: usize) -> Self;
}
pub trait Events: Copy {
fn index(&self) -> usize;
fn from_index(index: usize) -> Self;
}
pub struct StateMachine<S: States, E: Events, const NS: usize, const NE: usize> {
matrix: TransitionMatrix<S, NS, NE>,
current: S,
_d: PhantomData<E>,
}
impl<S: States, E: Events, const NS: usize, const NE: usize> StateMachine<S, E, NS, NE> {
const fn new(initial: S, matrix: TransitionMatrix<S, NS, NE>) -> Self {
Self {
matrix,
current: initial,
_d: PhantomData,
}
}
#[inline]
pub fn trigger(&mut self, event: E) {
let (i, j) = (self.current.index(), event.index());
let t = self.matrix[i][j];
self.current = t.to;
}
#[inline]
pub fn can_trigger(&self, event: E) -> bool {
let (i, j) = (self.current.index(), event.index());
let t = self.matrix[i][j];
t.to.index() != self.current.index()
}
#[inline]
pub fn trigger_if<F>(&mut self, event: E, condition: F) -> bool
where
F: FnOnce(&S) -> bool,
{
if condition(&self.current) {
self.trigger(event);
true
} else {
false
}
}
#[cfg(feature = "costs")]
pub fn trigger_if_affordable(&mut self, event: E, budget: f32) -> Result<f32, f32> {
let (i, j) = (self.current.index(), event.index());
let cost = self.matrix[i][j].cost;
if cost <= budget {
self.trigger(event);
Ok(cost)
} else {
Err(cost)
}
}
#[inline]
pub fn try_trigger(&mut self, event: E) -> Result<S, S> {
let old_state = self.current;
self.trigger(event);
if self.current.index() == old_state.index() {
Err(old_state)
} else {
Ok(self.current)
}
}
#[inline]
pub fn next_state(&self, event: E) -> S {
let (i, j) = (self.current.index(), event.index());
self.matrix[i][j].to
}
#[inline]
pub const fn current(&self) -> &S {
&self.current
}
}
pub struct StateMachineBuilder<S: States, E: Events, const NS: usize, const NE: usize> {
matrix: TransitionMatrix<S, NS, NE>,
defined: [[bool; NE]; NS],
initial: Option<S>,
_d: PhantomData<E>,
}
impl<S: States, E: Events, const NS: usize, const NE: usize> StateMachineBuilder<S, E, NS, NE> {
#[must_use]
pub fn new() -> Self {
Self {
matrix: TransitionMatrix::new([[Transition::default(); NE]; NS]),
defined: [[false; NE]; NS],
initial: None,
_d: PhantomData,
}
}
#[must_use]
pub const fn initial(mut self, state: S) -> Self {
self.initial = Some(state);
self
}
#[must_use]
pub fn transition(mut self, from: S, event: E, to: S) -> Self {
let (i, j) = (from.index(), event.index());
self.matrix[i][j].to = to;
self.defined[i][j] = true;
self
}
#[must_use]
#[cfg(feature = "costs")]
pub fn transition_cost(mut self, from: S, cost: f32, event: E, to: S) -> Self {
let (i, j) = (from.index(), event.index());
let t = &mut self.matrix[i][j];
self.defined[i][j] = true;
t.to = to;
t.cost = cost;
self
}
#[must_use]
pub fn transitions_from(mut self, from: S, transitions: &[(E, S)]) -> Self {
let i = from.index();
for &(event, to) in transitions {
let j = event.index();
self.matrix[i][j].to = to;
self.defined[i][j] = true;
}
self
}
#[must_use]
pub fn self_loop(mut self, state: S, event: E) -> Self {
let (i, j) = (state.index(), event.index());
self.matrix[i][j].to = state;
self.defined[i][j] = true;
self
}
#[must_use]
#[cfg(feature = "costs")]
pub fn self_loop_cost(mut self, state: S, event: S, cost: f32) -> Self {
let (i, j) = (state.index(), event.index());
let t = &mut self.matrix[i][j];
self.defined[i][j] = true;
t.to = state;
t.cost = cost;
self
}
fn validate_dimensions() -> Result<(), ValidationError<S, E>> {
for i in 0..NS {
let state = S::from_index(i);
if state.index() != i {
return Err(ValidationError::InvalidStateIndex {
expected: i,
got: state.index(),
});
}
}
for j in 0..NE {
let event = E::from_index(j);
if event.index() != j {
return Err(ValidationError::InvalidEventIndex {
expected: j,
got: event.index(),
});
}
}
Ok(())
}
fn validated(self) -> Result<Self, ValidationError<S, E>> {
Self::validate_dimensions()?;
for i in 0..NS {
for j in 0..NE {
if !self.defined[i][j] {
return Err(ValidationError::UndefinedTransition {
from: S::from_index(i),
event: E::from_index(j),
});
}
}
}
Ok(self)
}
pub fn build(self) -> Result<StateMachine<S, E, NS, NE>, ValidationError<S, E>> {
let b = self.validated()?;
let initial = b.initial.ok_or(ValidationError::NoInitialState::<S, E>)?;
Ok(StateMachine::new(initial, b.matrix))
}
}
impl<S: States, E: Events, const NS: usize, const NE: usize> Default
for StateMachineBuilder<S, E, NS, NE>
{
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq, States)]
enum TrafficLightState {
Red,
Yellow,
Green,
}
#[derive(Debug, PartialEq, Events)]
enum TrafficLightEvent {
Timer,
}
#[test]
fn test_traffic_light() {
let mut sm = StateMachineBuilder::<TrafficLightState, TrafficLightEvent, 3, 1>::new()
.initial(TrafficLightState::Red)
.transition(
TrafficLightState::Red,
TrafficLightEvent::Timer,
TrafficLightState::Green,
)
.transition(
TrafficLightState::Green,
TrafficLightEvent::Timer,
TrafficLightState::Yellow,
)
.transition(
TrafficLightState::Yellow,
TrafficLightEvent::Timer,
TrafficLightState::Red,
)
.build()
.unwrap();
assert_eq!(*sm.current(), TrafficLightState::Red);
sm.trigger(TrafficLightEvent::Timer);
assert_eq!(*sm.current(), TrafficLightState::Green);
sm.trigger(TrafficLightEvent::Timer);
assert_eq!(*sm.current(), TrafficLightState::Yellow);
sm.trigger(TrafficLightEvent::Timer);
assert_eq!(*sm.current(), TrafficLightState::Red);
}
#[derive(Debug, PartialEq, States)]
enum DoorState {
Closed,
Open,
Locked,
}
#[derive(Debug, PartialEq, Events)]
enum DoorEvent {
Push,
Pull,
Lock,
Unlock,
}
#[test]
fn test_door_with_multiple_events() {
let mut sm = StateMachineBuilder::<DoorState, DoorEvent, 3, 4>::new()
.initial(DoorState::Closed)
.transition(DoorState::Closed, DoorEvent::Push, DoorState::Open)
.transition(DoorState::Closed, DoorEvent::Pull, DoorState::Closed)
.transition(DoorState::Closed, DoorEvent::Lock, DoorState::Locked)
.transition(DoorState::Closed, DoorEvent::Unlock, DoorState::Closed)
.transition(DoorState::Open, DoorEvent::Push, DoorState::Open)
.transition(DoorState::Open, DoorEvent::Pull, DoorState::Closed)
.transition(DoorState::Open, DoorEvent::Lock, DoorState::Open)
.transition(DoorState::Open, DoorEvent::Unlock, DoorState::Open)
.self_loop(DoorState::Locked, DoorEvent::Push)
.self_loop(DoorState::Locked, DoorEvent::Pull)
.self_loop(DoorState::Locked, DoorEvent::Lock)
.transition(DoorState::Locked, DoorEvent::Unlock, DoorState::Closed)
.build()
.unwrap();
assert_eq!(*sm.current(), DoorState::Closed);
sm.trigger(DoorEvent::Push);
assert_eq!(*sm.current(), DoorState::Open);
sm.trigger(DoorEvent::Pull);
assert_eq!(*sm.current(), DoorState::Closed);
sm.trigger(DoorEvent::Lock);
assert_eq!(*sm.current(), DoorState::Locked);
sm.trigger(DoorEvent::Push); assert_eq!(*sm.current(), DoorState::Locked);
sm.trigger(DoorEvent::Unlock);
assert_eq!(*sm.current(), DoorState::Closed);
sm.trigger(DoorEvent::Push);
assert_eq!(*sm.current(), DoorState::Open);
}
#[test]
fn test_can_trigger() {
let sm = StateMachineBuilder::<TrafficLightState, TrafficLightEvent, 3, 1>::new()
.initial(TrafficLightState::Red)
.transition(
TrafficLightState::Red,
TrafficLightEvent::Timer,
TrafficLightState::Green,
)
.transition(
TrafficLightState::Green,
TrafficLightEvent::Timer,
TrafficLightState::Yellow,
)
.transition(
TrafficLightState::Yellow,
TrafficLightEvent::Timer,
TrafficLightState::Red,
)
.build()
.unwrap();
assert!(sm.can_trigger(TrafficLightEvent::Timer));
}
#[test]
fn test_can_trigger_self_loop() {
let sm = StateMachineBuilder::<DoorState, DoorEvent, 3, 4>::new()
.initial(DoorState::Locked)
.self_loop(DoorState::Locked, DoorEvent::Push)
.transition(DoorState::Locked, DoorEvent::Pull, DoorState::Locked)
.transition(DoorState::Locked, DoorEvent::Lock, DoorState::Locked)
.transition(DoorState::Locked, DoorEvent::Unlock, DoorState::Closed)
.transition(DoorState::Closed, DoorEvent::Push, DoorState::Open)
.transition(DoorState::Closed, DoorEvent::Pull, DoorState::Closed)
.transition(DoorState::Closed, DoorEvent::Lock, DoorState::Locked)
.transition(DoorState::Closed, DoorEvent::Unlock, DoorState::Closed)
.transition(DoorState::Open, DoorEvent::Push, DoorState::Open)
.transition(DoorState::Open, DoorEvent::Pull, DoorState::Closed)
.transition(DoorState::Open, DoorEvent::Lock, DoorState::Open)
.transition(DoorState::Open, DoorEvent::Unlock, DoorState::Open)
.build()
.unwrap();
assert!(!sm.can_trigger(DoorEvent::Push));
assert!(sm.can_trigger(DoorEvent::Unlock));
}
#[test]
fn test_trigger_if() {
let mut sm = StateMachineBuilder::<DoorState, DoorEvent, 3, 4>::new()
.initial(DoorState::Closed)
.transition(DoorState::Closed, DoorEvent::Push, DoorState::Open)
.transition(DoorState::Closed, DoorEvent::Pull, DoorState::Closed)
.transition(DoorState::Closed, DoorEvent::Lock, DoorState::Locked)
.transition(DoorState::Closed, DoorEvent::Unlock, DoorState::Closed)
.transition(DoorState::Open, DoorEvent::Push, DoorState::Open)
.transition(DoorState::Open, DoorEvent::Pull, DoorState::Closed)
.transition(DoorState::Open, DoorEvent::Lock, DoorState::Open)
.transition(DoorState::Open, DoorEvent::Unlock, DoorState::Open)
.self_loop(DoorState::Locked, DoorEvent::Push)
.self_loop(DoorState::Locked, DoorEvent::Pull)
.self_loop(DoorState::Locked, DoorEvent::Lock)
.transition(DoorState::Locked, DoorEvent::Unlock, DoorState::Closed)
.build()
.unwrap();
let triggered = sm.trigger_if(DoorEvent::Push, |state| *state == DoorState::Closed);
assert!(triggered);
assert_eq!(*sm.current(), DoorState::Open);
let triggered = sm.trigger_if(DoorEvent::Lock, |state| *state == DoorState::Closed);
assert!(!triggered);
assert_eq!(*sm.current(), DoorState::Open); }
#[test]
fn test_try_trigger() {
let mut sm = StateMachineBuilder::<DoorState, DoorEvent, 3, 4>::new()
.initial(DoorState::Locked)
.self_loop(DoorState::Locked, DoorEvent::Push)
.transition(DoorState::Locked, DoorEvent::Pull, DoorState::Locked)
.transition(DoorState::Locked, DoorEvent::Lock, DoorState::Locked)
.transition(DoorState::Locked, DoorEvent::Unlock, DoorState::Closed)
.transition(DoorState::Closed, DoorEvent::Push, DoorState::Open)
.transition(DoorState::Closed, DoorEvent::Pull, DoorState::Closed)
.transition(DoorState::Closed, DoorEvent::Lock, DoorState::Locked)
.transition(DoorState::Closed, DoorEvent::Unlock, DoorState::Closed)
.transition(DoorState::Open, DoorEvent::Push, DoorState::Open)
.transition(DoorState::Open, DoorEvent::Pull, DoorState::Closed)
.transition(DoorState::Open, DoorEvent::Lock, DoorState::Open)
.transition(DoorState::Open, DoorEvent::Unlock, DoorState::Open)
.build()
.unwrap();
let result = sm.try_trigger(DoorEvent::Push);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), DoorState::Locked);
let result = sm.try_trigger(DoorEvent::Unlock);
assert!(result.is_ok());
assert_eq!(result.unwrap(), DoorState::Closed);
}
#[test]
fn test_next_state() {
let sm = StateMachineBuilder::<TrafficLightState, TrafficLightEvent, 3, 1>::new()
.initial(TrafficLightState::Red)
.transition(
TrafficLightState::Red,
TrafficLightEvent::Timer,
TrafficLightState::Green,
)
.transition(
TrafficLightState::Green,
TrafficLightEvent::Timer,
TrafficLightState::Yellow,
)
.transition(
TrafficLightState::Yellow,
TrafficLightEvent::Timer,
TrafficLightState::Red,
)
.build()
.unwrap();
let next = sm.next_state(TrafficLightEvent::Timer);
assert_eq!(next, TrafficLightState::Green);
assert_eq!(*sm.current(), TrafficLightState::Red);
}
}