tinystate/
lib.rs

1//! # tinystate
2//!
3//! A small, fast and `no_std` finite state machine for Rust.
4//!
5//! `tinystate` provides a compile-time validated state machine implementation with zero runtime overhead (only when building it its O(n^2), but since NE and NS are known is O(1) :P).
6//! All state transitions are defined at compile time using const generics.
7//!
8//! ## Quick Start
9//!
10//! ```rust
11//! use tinystate::{StateMachineBuilder, States, Events};
12//!
13//! #[derive(Debug, Clone, Copy, Default)]
14//! enum TrafficLight {
15//!     #[default]
16//!     Red,
17//!     Yellow,
18//!     Green,
19//! }
20//!
21//! impl States for TrafficLight {
22//!     fn index(&self) -> usize {
23//!         *self as usize
24//!     }
25//!     fn from_index(i: usize) -> Self {
26//!         match i {
27//!             0 => Self::Red,
28//!             1 => Self::Yellow,
29//!             2 => Self::Green,
30//!             _ => unreachable!(),
31//!         }
32//!     }
33//! }
34//!
35//! #[derive(Debug, Clone, Copy)]
36//! enum Timer { Tick }
37//!
38//! impl Events for Timer {
39//!     fn index(&self) -> usize { 0 }
40//!     fn from_index(_: usize) -> Self { Self::Tick }
41//! }
42//!
43//! let mut light = StateMachineBuilder::<TrafficLight, Timer, 3, 1>::new()
44//!     .initial(TrafficLight::Red)
45//!     .transition(TrafficLight::Red, Timer::Tick, TrafficLight::Green)
46//!     .transition(TrafficLight::Green, Timer::Tick, TrafficLight::Yellow)
47//!     .transition(TrafficLight::Yellow, Timer::Tick, TrafficLight::Red)
48//!     .build()
49//!     .unwrap();
50//!
51//! light.trigger(Timer::Tick);
52//! assert!(matches!(light.current(), TrafficLight::Green));
53//! ```
54
55#![no_std]
56#![forbid(unsafe_code)]
57#![warn(clippy::pedantic)]
58#![warn(clippy::nursery)]
59#![warn(clippy::unwrap_used)]
60#![warn(clippy::expect_used)]
61#![warn(clippy::missing_const_for_fn)]
62
63mod err;
64mod transition;
65
66use core::marker::PhantomData;
67
68pub use crate::err::ValidationError;
69pub use crate::transition::Transition;
70pub use crate::transition::TransitionMatrix;
71
72#[rustfmt::skip]
73#[cfg(feature = "derive")]
74pub use tinystate_derive::{Events, States};
75
76/// Trait for state types in a state machine.
77///
78/// Implement this trait to define the states of your DFA. Each state must be
79/// convertible to and from a unique index for efficient matrix-based lookups.
80///
81/// # Example
82///
83/// ```rust
84/// use tinystate::States;
85///
86/// #[derive(Debug, Clone, Copy, Default)]
87/// enum DoorState {
88///     #[default]
89///     Closed,
90///     Open,
91///     Locked,
92/// }
93///
94/// impl States for DoorState {
95///     fn index(&self) -> usize {
96///         *self as usize
97///     }
98///
99///     fn from_index(i: usize) -> Self {
100///         match i {
101///             0 => Self::Closed,
102///             1 => Self::Open,
103///             2 => Self::Locked,
104///             _ => unreachable!(),
105///         }
106///     }
107/// }
108/// ```
109///
110/// Or you can use the #[derive(States)] macro to automatically do this.
111/// (Works only on unfielded enums)
112pub trait States: Copy + Default {
113    /// Returns the unique index for this state.
114    fn index(&self) -> usize;
115
116    /// Constructs a state from its index.
117    fn from_index(index: usize) -> Self;
118}
119
120/// Trait for event types that trigger state transitions.
121///
122/// Implement this trait to define the events that can cause state transitions
123/// in your DFA. Each event must be convertible to and from a unique index.
124///
125/// # Example
126///
127/// ```rust
128/// use tinystate::Events;
129///
130/// #[derive(Debug, Clone, Copy)]
131/// enum DoorEvent {
132///     Push,
133///     Pull,
134///     Lock,
135/// }
136///
137/// impl Events for DoorEvent {
138///     fn index(&self) -> usize {
139///         *self as usize
140///     }
141///
142///     fn from_index(i: usize) -> Self {
143///         match i {
144///             0 => Self::Push,
145///             1 => Self::Pull,
146///             2 => Self::Lock,
147///             _ => unreachable!(),
148///         }
149///     }
150/// }
151/// ```
152/// Or you can use the #[derive(Events)] macro to automatically do this.
153/// (Works only on unfielded enums)
154pub trait Events: Copy {
155    /// Returns the unique index for this event.
156    fn index(&self) -> usize;
157
158    /// Constructs an event from its index.
159    fn from_index(index: usize) -> Self;
160}
161
162/// A deterministic finite automaton (DFA) state machine.
163///
164/// The state machine stores a transition matrix and tracks the current state.
165/// All transitions are defined at compile time through the const generics `NS` (number of states)
166/// and `NE` (number of events).
167///
168/// # Type Parameters
169///
170/// - `S`: The state type implementing [`States`]
171/// - `E`: The event type implementing [`Events`]
172/// - `NS`: The total number of states (const generic)
173/// - `NE`: The total number of events (const generic)
174///
175/// # Example
176///
177/// ```rust
178/// use tinystate::StateMachineBuilder;
179/// # use tinystate::{States, Events};
180/// # #[derive(Debug, Clone, Copy, Default)]
181/// # enum MyState { #[default] A, B }
182/// # impl States for MyState {
183/// #     fn index(&self) -> usize { *self as usize }
184/// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
185/// # }
186///
187/// # #[derive(Debug, Clone, Copy)]
188/// # enum MyEvent { X }
189/// # impl Events for MyEvent {
190/// #     fn index(&self) -> usize { 0 }
191/// #     fn from_index(_: usize) -> Self { Self::X }
192/// # }
193///
194/// let mut sm = StateMachineBuilder::<MyState, MyEvent, 2, 1>::new()
195///     .initial(MyState::A)
196///     .transition(MyState::A, MyEvent::X, MyState::B)
197///     .transition(MyState::B, MyEvent::X, MyState::A)
198///     .build()
199///     .unwrap();
200/// ```
201pub struct StateMachine<S: States, E: Events, const NS: usize, const NE: usize> {
202    /// Transition matrix: `[current_state][event] = next_state`
203    matrix: TransitionMatrix<S, NS, NE>,
204    current: S,
205    _d: PhantomData<E>,
206}
207
208impl<S: States, E: Events, const NS: usize, const NE: usize> StateMachine<S, E, NS, NE> {
209    const fn new(initial: S, matrix: TransitionMatrix<S, NS, NE>) -> Self {
210        Self {
211            matrix,
212            current: initial,
213            _d: PhantomData,
214        }
215    }
216
217    /// Triggers a state transition based on the given event.
218    ///
219    /// This unconditionally transitions to the next state defined in the transition matrix.
220    ///
221    /// # Example
222    ///
223    /// ```rust
224    /// # use tinystate::{StateMachineBuilder, States, Events};
225    ///
226    /// # #[derive(Debug, Clone, Copy, Default, PartialEq)]
227    /// # enum S { #[default] A, B }
228    /// # impl States for S {
229    /// #     fn index(&self) -> usize { *self as usize }
230    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
231    /// # }
232    ///
233    /// # #[derive(Debug, Clone, Copy)]
234    /// # enum E { X }
235    /// # impl Events for E {
236    /// #     fn index(&self) -> usize { 0 }
237    /// #     fn from_index(_: usize) -> Self { Self::X }
238    /// # }
239    ///
240    /// let mut sm = StateMachineBuilder::<S, E, 2, 1>::new()
241    ///     .initial(S::A)
242    ///     .transition(S::A, E::X, S::B)
243    ///     .transition(S::B, E::X, S::A)
244    ///     .build()
245    ///     .unwrap();
246    ///
247    /// sm.trigger(E::X);
248    /// assert_eq!(sm.current(), &S::B);
249    /// ```
250    #[inline]
251    pub fn trigger(&mut self, event: E) {
252        let (i, j) = (self.current.index(), event.index());
253        let t = self.matrix[i][j];
254
255        self.current = t.to;
256    }
257
258    /// Checks if an event would cause a state change.
259    ///
260    /// Returns `true` if triggering the event would transition to a different state,
261    /// `false` if it would remain in the current state (self-loop).
262    ///
263    /// # Example
264    ///
265    /// ```rust
266    /// # use tinystate::{StateMachineBuilder, States, Events};
267    /// # #[derive(Debug, Clone, Copy, Default)]
268    /// # enum S { #[default] A, B }
269    /// # impl States for S {
270    /// #     fn index(&self) -> usize { *self as usize }
271    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
272    /// # }
273    /// # #[derive(Debug, Clone, Copy)]
274    /// # enum E { X }
275    /// # impl Events for E {
276    /// #     fn index(&self) -> usize { 0 }
277    /// #     fn from_index(_: usize) -> Self { Self::X }
278    /// # }
279    ///
280    /// let sm = StateMachineBuilder::<S, E, 2, 1>::new()
281    ///     .initial(S::A)
282    ///     .transition(S::A, E::X, S::B)
283    ///     .self_loop(S::B, E::X)
284    ///     .build()
285    ///     .unwrap();
286    ///
287    /// assert!(sm.can_trigger(E::X)); // Would transition A -> B
288    /// ```
289    #[inline]
290    pub fn can_trigger(&self, event: E) -> bool {
291        let (i, j) = (self.current.index(), event.index());
292        let t = self.matrix[i][j];
293
294        // Returns true if the event causes a state change
295        t.to.index() != self.current.index()
296    }
297
298    /// Conditionally triggers a state transition.
299    ///
300    /// Only transitions if the provided condition returns `true` for the current state.
301    /// Returns `true` if the transition occurred, `false` otherwise.
302    ///
303    /// # Example
304    ///
305    /// ```rust
306    /// # use tinystate::{StateMachineBuilder, States, Events};
307    /// # #[derive(Debug, Clone, Copy, Default, PartialEq)]
308    /// # enum S { #[default] A, B }
309    /// # impl States for S {
310    /// #     fn index(&self) -> usize { *self as usize }
311    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
312    /// # }
313    /// # #[derive(Debug, Clone, Copy)]
314    /// # enum E { X }
315    /// # impl Events for E {
316    /// #     fn index(&self) -> usize { 0 }
317    /// #     fn from_index(_: usize) -> Self { Self::X }
318    /// # }
319    /// let mut sm = StateMachineBuilder::<S, E, 2, 1>::new()
320    ///     .initial(S::A)
321    ///     .transition(S::A, E::X, S::B)
322    ///     .transition(S::B, E::X, S::A)
323    ///     .build()
324    ///     .unwrap();
325    ///
326    /// let transitioned = sm.trigger_if(E::X, |s| matches!(s, S::A));
327    /// assert!(transitioned);
328    /// assert_eq!(sm.current(), &S::B);
329    /// ```
330    #[inline]
331    pub fn trigger_if<F>(&mut self, event: E, condition: F) -> bool
332    where
333        F: FnOnce(&S) -> bool,
334    {
335        if condition(&self.current) {
336            self.trigger(event);
337            true
338        } else {
339            false
340        }
341    }
342
343    /// Triggers a transition only if its cost is within budget.
344    ///
345    /// Only available with the `costs` feature enabled.
346    /// Returns `true` if the transition occurred, `false` if the cost exceeded the budget.
347    ///
348    /// # Feature Flag
349    ///
350    /// This method requires the `costs` feature to be enabled.
351    #[cfg(feature = "costs")]
352    pub fn trigger_if_affordable(&mut self, event: E, budget: f32) -> Result<f32, f32> {
353        let (i, j) = (self.current.index(), event.index());
354        let cost = self.matrix[i][j].cost;
355
356        if cost <= budget {
357            self.trigger(event);
358
359            Ok(cost)
360        } else {
361            Err(cost)
362        }
363    }
364
365    /// Attempts to trigger a transition, returning the result.
366    ///
367    /// Returns `Ok(new_state)` if the state changed, or `Err(old_state)` if it remained the same.
368    ///
369    /// # Errors
370    ///
371    /// Returns `Err(old_state)` if the event triggered a self-loop (state did not change).
372    ///
373    /// # Example
374    ///
375    /// ```rust
376    /// # use tinystate::{StateMachineBuilder, States, Events};
377    /// # #[derive(Debug, Clone, Copy, Default, PartialEq)]
378    /// # enum S { #[default] A, B }
379    /// # impl States for S {
380    /// #     fn index(&self) -> usize { *self as usize }
381    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
382    /// # }
383    /// # #[derive(Debug, Clone, Copy)]
384    /// # enum E { X }
385    /// # impl Events for E {
386    /// #     fn index(&self) -> usize { 0 }
387    /// #     fn from_index(_: usize) -> Self { Self::X }
388    /// # }
389    /// let mut sm = StateMachineBuilder::<S, E, 2, 1>::new()
390    ///     .initial(S::A)
391    ///     .transition(S::A, E::X, S::B)
392    ///     .self_loop(S::B, E::X)
393    ///     .build()
394    ///     .unwrap();
395    ///
396    /// assert_eq!(sm.try_trigger(E::X), Ok(S::B));
397    /// assert_eq!(sm.try_trigger(E::X), Err(S::B)); // Self-loop
398    /// ```
399    #[inline]
400    pub fn try_trigger(&mut self, event: E) -> Result<S, S> {
401        let old_state = self.current;
402        self.trigger(event);
403
404        if self.current.index() == old_state.index() {
405            Err(old_state)
406        } else {
407            Ok(self.current)
408        }
409    }
410
411    /// Returns the next state without transitioning.
412    ///
413    /// Useful for previewing what state an event would lead to.
414    ///
415    /// # Example
416    ///
417    /// ```rust
418    /// # use tinystate::{StateMachineBuilder, States, Events};
419    /// # #[derive(Debug, Clone, Copy, Default, PartialEq)]
420    /// # enum S { #[default] A, B }
421    /// # impl States for S {
422    /// #     fn index(&self) -> usize { *self as usize }
423    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
424    /// # }
425    /// # #[derive(Debug, Clone, Copy)]
426    /// # enum E { X }
427    /// # impl Events for E {
428    /// #     fn index(&self) -> usize { 0 }
429    /// #     fn from_index(_: usize) -> Self { Self::X }
430    /// # }
431    /// let sm = StateMachineBuilder::<S, E, 2, 1>::new()
432    ///     .initial(S::A)
433    ///     .transition(S::A, E::X, S::B)
434    ///     .transition(S::B, E::X, S::A)
435    ///     .build()
436    ///     .unwrap();
437    ///
438    /// assert_eq!(sm.next_state(E::X), S::B);
439    /// ```
440    #[inline]
441    pub fn next_state(&self, event: E) -> S {
442        let (i, j) = (self.current.index(), event.index());
443        self.matrix[i][j].to
444    }
445
446    /// Returns a reference to the current state.
447    ///
448    /// # Example
449    ///
450    /// ```rust
451    /// # use tinystate::{StateMachineBuilder, States, Events};
452    /// # #[derive(Debug, Clone, Copy, Default, PartialEq)]
453    /// # enum S { #[default] A, B }
454    /// # impl States for S {
455    /// #     fn index(&self) -> usize { *self as usize }
456    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
457    /// # }
458    /// # #[derive(Debug, Clone, Copy)]
459    /// # enum E { X }
460    /// # impl Events for E {
461    /// #     fn index(&self) -> usize { 0 }
462    /// #     fn from_index(_: usize) -> Self { Self::X }
463    /// # }
464    /// let sm = StateMachineBuilder::<S, E, 2, 1>::new()
465    ///     .initial(S::A)
466    ///     .transition(S::A, E::X, S::B)
467    ///     .transition(S::B, E::X, S::A)
468    ///     .build()
469    ///     .unwrap();
470    ///
471    /// assert_eq!(sm.current(), &S::A);
472    /// ```
473    #[inline]
474    pub const fn current(&self) -> &S {
475        &self.current
476    }
477}
478
479/// Builder for constructing and validating state machines.
480///
481/// The builder pattern ensures all transitions are defined before the state machine is created.
482/// Validation occurs at build time to catch configuration errors early.
483///
484/// # Type Parameters
485///
486/// - `S`: The state type implementing [`States`]
487/// - `E`: The event type implementing [`Events`]
488/// - `NS`: The total number of states (const generic)
489/// - `NE`: The total number of events (const generic)
490///
491/// # Example
492///
493/// ```rust
494/// use tinystate::StateMachineBuilder;
495/// # use tinystate::{States, Events};
496/// # #[derive(Debug, Clone, Copy, Default)]
497/// # enum S { #[default] A, B }
498/// # impl States for S {
499/// #     fn index(&self) -> usize { *self as usize }
500/// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
501/// # }
502/// # #[derive(Debug, Clone, Copy)]
503/// # enum E { X }
504/// # impl Events for E {
505/// #     fn index(&self) -> usize { 0 }
506/// #     fn from_index(_: usize) -> Self { Self::X }
507/// # }
508///
509/// let sm = StateMachineBuilder::<S, E, 2, 1>::new()
510///     .initial(S::A)
511///     .transition(S::A, E::X, S::B)
512///     .transition(S::B, E::X, S::A)
513///     .build()
514///     .unwrap();
515/// ```
516pub struct StateMachineBuilder<S: States, E: Events, const NS: usize, const NE: usize> {
517    matrix: TransitionMatrix<S, NS, NE>,
518    defined: [[bool; NE]; NS],
519    initial: Option<S>,
520    _d: PhantomData<E>,
521}
522
523impl<S: States, E: Events, const NS: usize, const NE: usize> StateMachineBuilder<S, E, NS, NE> {
524    /// Creates a new state machine builder.
525    ///
526    /// All transitions are initially undefined and must be configured before calling [`build`](Self::build).
527    ///
528    /// # Example
529    ///
530    /// ```rust
531    /// use tinystate::StateMachineBuilder;
532    /// # use tinystate::{States, Events};
533    /// # #[derive(Debug, Clone, Copy, Default)]
534    /// # enum S { #[default] A }
535    /// # impl States for S {
536    /// #     fn index(&self) -> usize { 0 }
537    /// #     fn from_index(_: usize) -> Self { Self::A }
538    /// # }
539    /// # #[derive(Debug, Clone, Copy)]
540    /// # enum E { X }
541    /// # impl Events for E {
542    /// #     fn index(&self) -> usize { 0 }
543    /// #     fn from_index(_: usize) -> Self { Self::X }
544    /// # }
545    ///
546    /// let builder = StateMachineBuilder::<S, E, 1, 1>::new();
547    /// ```
548    #[must_use]
549    pub fn new() -> Self {
550        Self {
551            matrix: TransitionMatrix::new([[Transition::default(); NE]; NS]),
552            defined: [[false; NE]; NS],
553            initial: None,
554            _d: PhantomData,
555        }
556    }
557
558    /// Sets the initial state of the state machine.
559    ///
560    /// This must be called before building the state machine.
561    ///
562    /// # Example
563    ///
564    /// ```rust
565    /// # use tinystate::{StateMachineBuilder, States, Events};
566    /// # #[derive(Debug, Clone, Copy, Default)]
567    /// # enum S { #[default] A, B }
568    /// # impl States for S {
569    /// #     fn index(&self) -> usize { *self as usize }
570    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
571    /// # }
572    /// # #[derive(Debug, Clone, Copy)]
573    /// # enum E { X }
574    /// # impl Events for E {
575    /// #     fn index(&self) -> usize { 0 }
576    /// #     fn from_index(_: usize) -> Self { Self::X }
577    /// # }
578    /// let builder = StateMachineBuilder::<S, E, 2, 1>::new()
579    ///     .initial(S::A);
580    /// ```
581    #[must_use]
582    pub const fn initial(mut self, state: S) -> Self {
583        self.initial = Some(state);
584        self
585    }
586
587    /// Adds a single state transition.
588    ///
589    /// Defines that when in `from` state and `event` occurs, transition to `to` state.
590    ///
591    /// # Example
592    ///
593    /// ```rust
594    /// # use tinystate::{StateMachineBuilder, States, Events};
595    ///
596    /// # #[derive(Debug, Clone, Copy, Default)]
597    /// # enum S { #[default] A, B }
598    /// # impl States for S {
599    /// #     fn index(&self) -> usize { *self as usize }
600    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
601    /// # }
602    ///
603    /// # #[derive(Debug, Clone, Copy)]
604    /// # enum E { X }
605    /// # impl Events for E {
606    /// #     fn index(&self) -> usize { 0 }
607    /// #     fn from_index(_: usize) -> Self { Self::X }
608    /// # }
609    ///
610    /// let builder = StateMachineBuilder::<S, E, 2, 1>::new()
611    ///     .transition(S::A, E::X, S::B);
612    /// ```
613    #[must_use]
614    pub fn transition(mut self, from: S, event: E, to: S) -> Self {
615        let (i, j) = (from.index(), event.index());
616
617        self.matrix[i][j].to = to;
618        self.defined[i][j] = true;
619        self
620    }
621
622    /// Adds a state transition with an associated cost.
623    ///
624    /// Only available with the `costs` feature enabled.
625    ///
626    /// # Feature Flag
627    ///
628    /// This method requires the `costs` feature to be enabled.
629    #[must_use]
630    #[cfg(feature = "costs")]
631    pub fn transition_cost(mut self, from: S, cost: f32, event: E, to: S) -> Self {
632        let (i, j) = (from.index(), event.index());
633        let t = &mut self.matrix[i][j];
634
635        self.defined[i][j] = true;
636
637        t.to = to;
638        t.cost = cost;
639
640        self
641    }
642
643    /// Adds multiple transitions from the same source state.
644    ///
645    /// Convenient for defining all outgoing transitions from a state at once.
646    ///
647    /// # Example
648    ///
649    /// ```rust
650    /// # use tinystate::{StateMachineBuilder, States, Events};
651    ///
652    /// # #[derive(Debug, Clone, Copy, Default)]
653    /// # enum S { #[default] A, B, C }
654    /// # impl States for S {
655    /// #     fn index(&self) -> usize { *self as usize }
656    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, 1 => Self::B, _ => Self::C } }
657    /// # }
658    ///
659    /// # #[derive(Debug, Clone, Copy)]
660    /// # enum E { X, Y }
661    /// # impl Events for E {
662    /// #     fn index(&self) -> usize { *self as usize }
663    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::X, _ => Self::Y } }
664    /// # }
665    ///
666    /// let builder = StateMachineBuilder::<S, E, 3, 2>::new()
667    ///     .transitions_from(S::A, &[(E::X, S::B), (E::Y, S::C)]);
668    /// ```
669    #[must_use]
670    pub fn transitions_from(mut self, from: S, transitions: &[(E, S)]) -> Self {
671        let i = from.index();
672
673        for &(event, to) in transitions {
674            let j = event.index();
675
676            self.matrix[i][j].to = to;
677            self.defined[i][j] = true;
678        }
679
680        self
681    }
682
683    /// Adds a self-loop transition where a state transitions back to itself.
684    ///
685    /// # Example
686    ///
687    /// ```rust
688    /// # use tinystate::{StateMachineBuilder, States, Events};
689    ///
690    /// # #[derive(Debug, Clone, Copy, Default)]
691    /// # enum S { #[default] A }
692    ///
693    /// # impl States for S {
694    /// #     fn index(&self) -> usize { 0 }
695    /// #     fn from_index(_: usize) -> Self { Self::A }
696    /// # }
697    ///
698    /// # #[derive(Debug, Clone, Copy)]
699    /// # enum E { X }
700    /// # impl Events for E {
701    /// #     fn index(&self) -> usize { 0 }
702    /// #     fn from_index(_: usize) -> Self { Self::X }
703    /// # }
704    ///
705    /// let builder = StateMachineBuilder::<S, E, 1, 1>::new()
706    ///     .self_loop(S::A, E::X);
707    /// ```
708    #[must_use]
709    pub fn self_loop(mut self, state: S, event: E) -> Self {
710        let (i, j) = (state.index(), event.index());
711
712        self.matrix[i][j].to = state;
713        self.defined[i][j] = true;
714        self
715    }
716
717    /// Adds a self-loop transition with an associated cost.
718    ///
719    /// Only available with the `costs` feature enabled.
720    ///
721    /// # Feature Flag
722    ///
723    /// This method requires the `costs` feature to be enabled.
724    #[must_use]
725    #[cfg(feature = "costs")]
726    pub fn self_loop_cost(mut self, state: S, event: S, cost: f32) -> Self {
727        let (i, j) = (state.index(), event.index());
728        let t = &mut self.matrix[i][j];
729
730        self.defined[i][j] = true;
731
732        t.to = state;
733        t.cost = cost;
734
735        self
736    }
737
738    fn validate_dimensions() -> Result<(), ValidationError<S, E>> {
739        for i in 0..NS {
740            let state = S::from_index(i);
741
742            if state.index() != i {
743                return Err(ValidationError::InvalidStateIndex {
744                    expected: i,
745                    got: state.index(),
746                });
747            }
748        }
749
750        for j in 0..NE {
751            let event = E::from_index(j);
752
753            if event.index() != j {
754                return Err(ValidationError::InvalidEventIndex {
755                    expected: j,
756                    got: event.index(),
757                });
758            }
759        }
760
761        Ok(())
762    }
763
764    fn validated(self) -> Result<Self, ValidationError<S, E>> {
765        Self::validate_dimensions()?;
766
767        for i in 0..NS {
768            for j in 0..NE {
769                if !self.defined[i][j] {
770                    return Err(ValidationError::UndefinedTransition {
771                        from: S::from_index(i),
772                        event: E::from_index(j),
773                    });
774                }
775            }
776        }
777
778        Ok(self)
779    }
780
781    /// Builds and validates the state machine.
782    ///
783    /// Returns an error if:
784    /// - The initial state is not set
785    /// - Any transition is undefined
786    /// - State or event indices are invalid
787    ///
788    /// # Errors
789    ///
790    /// Returns a [`ValidationError`] if the state machine configuration is invalid.
791    ///
792    /// # Example
793    ///
794    /// ```rust
795    /// # use tinystate::{StateMachineBuilder, States, Events};
796    /// # #[derive(Debug, Clone, Copy, Default)]
797    /// # enum S { #[default] A, B }
798    /// # impl States for S {
799    /// #     fn index(&self) -> usize { *self as usize }
800    /// #     fn from_index(i: usize) -> Self { match i { 0 => Self::A, _ => Self::B } }
801    /// # }
802    ///
803    /// # #[derive(Debug, Clone, Copy)]
804    /// # enum E { X }
805    /// # impl Events for E {
806    /// #     fn index(&self) -> usize { 0 }
807    /// #     fn from_index(_: usize) -> Self { Self::X }
808    /// # }
809    ///
810    /// let result = StateMachineBuilder::<S, E, 2, 1>::new()
811    ///     .initial(S::A)
812    ///     .transition(S::A, E::X, S::B)
813    ///     .transition(S::B, E::X, S::A)
814    ///     .build();
815    ///
816    /// assert!(result.is_ok());
817    /// ```
818    pub fn build(self) -> Result<StateMachine<S, E, NS, NE>, ValidationError<S, E>> {
819        let b = self.validated()?;
820        let initial = b.initial.ok_or(ValidationError::NoInitialState::<S, E>)?;
821
822        Ok(StateMachine::new(initial, b.matrix))
823    }
824}
825
826impl<S: States, E: Events, const NS: usize, const NE: usize> Default
827    for StateMachineBuilder<S, E, NS, NE>
828{
829    fn default() -> Self {
830        Self::new()
831    }
832}
833
834#[cfg(test)]
835mod tests {
836    use super::*;
837
838    #[derive(Debug, PartialEq, States)]
839    enum TrafficLightState {
840        Red,
841        Yellow,
842        Green,
843    }
844
845    #[derive(Debug, PartialEq, Events)]
846    enum TrafficLightEvent {
847        Timer,
848    }
849
850    #[test]
851    fn test_traffic_light() {
852        let mut sm = StateMachineBuilder::<TrafficLightState, TrafficLightEvent, 3, 1>::new()
853            .initial(TrafficLightState::Red)
854            .transition(
855                TrafficLightState::Red,
856                TrafficLightEvent::Timer,
857                TrafficLightState::Green,
858            )
859            .transition(
860                TrafficLightState::Green,
861                TrafficLightEvent::Timer,
862                TrafficLightState::Yellow,
863            )
864            .transition(
865                TrafficLightState::Yellow,
866                TrafficLightEvent::Timer,
867                TrafficLightState::Red,
868            )
869            .build()
870            .unwrap();
871
872        assert_eq!(*sm.current(), TrafficLightState::Red);
873
874        sm.trigger(TrafficLightEvent::Timer);
875        assert_eq!(*sm.current(), TrafficLightState::Green);
876
877        sm.trigger(TrafficLightEvent::Timer);
878        assert_eq!(*sm.current(), TrafficLightState::Yellow);
879
880        sm.trigger(TrafficLightEvent::Timer);
881        assert_eq!(*sm.current(), TrafficLightState::Red);
882    }
883
884    #[derive(Debug, PartialEq, States)]
885    enum DoorState {
886        Closed,
887        Open,
888        Locked,
889    }
890
891    #[derive(Debug, PartialEq, Events)]
892    enum DoorEvent {
893        Push,
894        Pull,
895        Lock,
896        Unlock,
897    }
898
899    #[test]
900    fn test_door_with_multiple_events() {
901        let mut sm = StateMachineBuilder::<DoorState, DoorEvent, 3, 4>::new()
902            .initial(DoorState::Closed)
903            // From Closed
904            .transition(DoorState::Closed, DoorEvent::Push, DoorState::Open)
905            .transition(DoorState::Closed, DoorEvent::Pull, DoorState::Closed)
906            .transition(DoorState::Closed, DoorEvent::Lock, DoorState::Locked)
907            .transition(DoorState::Closed, DoorEvent::Unlock, DoorState::Closed)
908            // From Open
909            .transition(DoorState::Open, DoorEvent::Push, DoorState::Open)
910            .transition(DoorState::Open, DoorEvent::Pull, DoorState::Closed)
911            .transition(DoorState::Open, DoorEvent::Lock, DoorState::Open)
912            .transition(DoorState::Open, DoorEvent::Unlock, DoorState::Open)
913            // From Locked
914            .self_loop(DoorState::Locked, DoorEvent::Push)
915            .self_loop(DoorState::Locked, DoorEvent::Pull)
916            .self_loop(DoorState::Locked, DoorEvent::Lock)
917            .transition(DoorState::Locked, DoorEvent::Unlock, DoorState::Closed)
918            .build()
919            .unwrap();
920
921        assert_eq!(*sm.current(), DoorState::Closed);
922
923        sm.trigger(DoorEvent::Push);
924        assert_eq!(*sm.current(), DoorState::Open);
925
926        sm.trigger(DoorEvent::Pull);
927        assert_eq!(*sm.current(), DoorState::Closed);
928
929        sm.trigger(DoorEvent::Lock);
930        assert_eq!(*sm.current(), DoorState::Locked);
931
932        sm.trigger(DoorEvent::Push); // Should stay locked
933        assert_eq!(*sm.current(), DoorState::Locked);
934
935        sm.trigger(DoorEvent::Unlock);
936        assert_eq!(*sm.current(), DoorState::Closed);
937
938        sm.trigger(DoorEvent::Push);
939        assert_eq!(*sm.current(), DoorState::Open);
940    }
941
942    #[test]
943    fn test_can_trigger() {
944        let sm = StateMachineBuilder::<TrafficLightState, TrafficLightEvent, 3, 1>::new()
945            .initial(TrafficLightState::Red)
946            .transition(
947                TrafficLightState::Red,
948                TrafficLightEvent::Timer,
949                TrafficLightState::Green,
950            )
951            .transition(
952                TrafficLightState::Green,
953                TrafficLightEvent::Timer,
954                TrafficLightState::Yellow,
955            )
956            .transition(
957                TrafficLightState::Yellow,
958                TrafficLightEvent::Timer,
959                TrafficLightState::Red,
960            )
961            .build()
962            .unwrap();
963
964        // Should return true because Timer causes a state change from Red to Green
965        assert!(sm.can_trigger(TrafficLightEvent::Timer));
966    }
967
968    #[test]
969    fn test_can_trigger_self_loop() {
970        let sm = StateMachineBuilder::<DoorState, DoorEvent, 3, 4>::new()
971            .initial(DoorState::Locked)
972            .self_loop(DoorState::Locked, DoorEvent::Push)
973            .transition(DoorState::Locked, DoorEvent::Pull, DoorState::Locked)
974            .transition(DoorState::Locked, DoorEvent::Lock, DoorState::Locked)
975            .transition(DoorState::Locked, DoorEvent::Unlock, DoorState::Closed)
976            .transition(DoorState::Closed, DoorEvent::Push, DoorState::Open)
977            .transition(DoorState::Closed, DoorEvent::Pull, DoorState::Closed)
978            .transition(DoorState::Closed, DoorEvent::Lock, DoorState::Locked)
979            .transition(DoorState::Closed, DoorEvent::Unlock, DoorState::Closed)
980            .transition(DoorState::Open, DoorEvent::Push, DoorState::Open)
981            .transition(DoorState::Open, DoorEvent::Pull, DoorState::Closed)
982            .transition(DoorState::Open, DoorEvent::Lock, DoorState::Open)
983            .transition(DoorState::Open, DoorEvent::Unlock, DoorState::Open)
984            .build()
985            .unwrap();
986
987        // Should return false because Push causes a self-loop (no state change)
988        assert!(!sm.can_trigger(DoorEvent::Push));
989
990        // Should return true because Unlock causes a state change from Locked to Closed
991        assert!(sm.can_trigger(DoorEvent::Unlock));
992    }
993
994    #[test]
995    fn test_trigger_if() {
996        let mut sm = StateMachineBuilder::<DoorState, DoorEvent, 3, 4>::new()
997            .initial(DoorState::Closed)
998            .transition(DoorState::Closed, DoorEvent::Push, DoorState::Open)
999            .transition(DoorState::Closed, DoorEvent::Pull, DoorState::Closed)
1000            .transition(DoorState::Closed, DoorEvent::Lock, DoorState::Locked)
1001            .transition(DoorState::Closed, DoorEvent::Unlock, DoorState::Closed)
1002            .transition(DoorState::Open, DoorEvent::Push, DoorState::Open)
1003            .transition(DoorState::Open, DoorEvent::Pull, DoorState::Closed)
1004            .transition(DoorState::Open, DoorEvent::Lock, DoorState::Open)
1005            .transition(DoorState::Open, DoorEvent::Unlock, DoorState::Open)
1006            .self_loop(DoorState::Locked, DoorEvent::Push)
1007            .self_loop(DoorState::Locked, DoorEvent::Pull)
1008            .self_loop(DoorState::Locked, DoorEvent::Lock)
1009            .transition(DoorState::Locked, DoorEvent::Unlock, DoorState::Closed)
1010            .build()
1011            .unwrap();
1012
1013        // Trigger only if door is closed
1014        let triggered = sm.trigger_if(DoorEvent::Push, |state| *state == DoorState::Closed);
1015        assert!(triggered);
1016        assert_eq!(*sm.current(), DoorState::Open);
1017
1018        // Try to lock, but only if door is closed (it's open, so shouldn't trigger)
1019        let triggered = sm.trigger_if(DoorEvent::Lock, |state| *state == DoorState::Closed);
1020        assert!(!triggered);
1021        assert_eq!(*sm.current(), DoorState::Open); // Still open
1022    }
1023
1024    #[test]
1025    fn test_try_trigger() {
1026        let mut sm = StateMachineBuilder::<DoorState, DoorEvent, 3, 4>::new()
1027            .initial(DoorState::Locked)
1028            .self_loop(DoorState::Locked, DoorEvent::Push)
1029            .transition(DoorState::Locked, DoorEvent::Pull, DoorState::Locked)
1030            .transition(DoorState::Locked, DoorEvent::Lock, DoorState::Locked)
1031            .transition(DoorState::Locked, DoorEvent::Unlock, DoorState::Closed)
1032            .transition(DoorState::Closed, DoorEvent::Push, DoorState::Open)
1033            .transition(DoorState::Closed, DoorEvent::Pull, DoorState::Closed)
1034            .transition(DoorState::Closed, DoorEvent::Lock, DoorState::Locked)
1035            .transition(DoorState::Closed, DoorEvent::Unlock, DoorState::Closed)
1036            .transition(DoorState::Open, DoorEvent::Push, DoorState::Open)
1037            .transition(DoorState::Open, DoorEvent::Pull, DoorState::Closed)
1038            .transition(DoorState::Open, DoorEvent::Lock, DoorState::Open)
1039            .transition(DoorState::Open, DoorEvent::Unlock, DoorState::Open)
1040            .build()
1041            .unwrap();
1042
1043        // Try to push (self-loop, should return Err)
1044        let result = sm.try_trigger(DoorEvent::Push);
1045        assert!(result.is_err());
1046        assert_eq!(result.unwrap_err(), DoorState::Locked);
1047
1048        // Try to unlock (state change, should return Ok)
1049        let result = sm.try_trigger(DoorEvent::Unlock);
1050        assert!(result.is_ok());
1051        assert_eq!(result.unwrap(), DoorState::Closed);
1052    }
1053
1054    #[test]
1055    fn test_next_state() {
1056        let sm = StateMachineBuilder::<TrafficLightState, TrafficLightEvent, 3, 1>::new()
1057            .initial(TrafficLightState::Red)
1058            .transition(
1059                TrafficLightState::Red,
1060                TrafficLightEvent::Timer,
1061                TrafficLightState::Green,
1062            )
1063            .transition(
1064                TrafficLightState::Green,
1065                TrafficLightEvent::Timer,
1066                TrafficLightState::Yellow,
1067            )
1068            .transition(
1069                TrafficLightState::Yellow,
1070                TrafficLightEvent::Timer,
1071                TrafficLightState::Red,
1072            )
1073            .build()
1074            .unwrap();
1075
1076        // Check what the next state would be without actually transitioning
1077        let next = sm.next_state(TrafficLightEvent::Timer);
1078        assert_eq!(next, TrafficLightState::Green);
1079
1080        // Current state should still be Red
1081        assert_eq!(*sm.current(), TrafficLightState::Red);
1082    }
1083}