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}