Skip to main content

qubit_state_machine/
state_machine.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Immutable finite state machine rules and CAS-backed event triggering.
11
12use std::collections::{HashMap, HashSet};
13use std::fmt::Debug;
14use std::hash::Hash;
15
16use qubit_atomic::AtomicRef;
17use qubit_cas::{CasDecision, CasError, CasExecutor, CasSuccess};
18
19use crate::{StateMachineBuilder, StateMachineError, StateMachineResult, Transition};
20
21/// Immutable finite state machine rules.
22///
23/// `S` is the state type and `E` is the event type. Both should usually be
24/// small enum-like types. The state machine itself is immutable and can be
25/// shared across threads; mutable current state is kept in [`AtomicRef`] and
26/// updated through [`qubit_cas::CasExecutor`].
27///
28/// # Common usage
29///
30/// Define the valid states and events, build an immutable transition table, and
31/// keep each object's current state in an [`AtomicRef`].
32///
33/// ```
34/// use qubit_state_machine::{AtomicRef, StateMachine};
35///
36/// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
37/// enum JobState {
38///     Queued,
39///     Running,
40///     Succeeded,
41///     Failed,
42/// }
43///
44/// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
45/// enum JobEvent {
46///     Start,
47///     Complete,
48///     Fail,
49/// }
50///
51/// fn create_job_machine() -> StateMachine<JobState, JobEvent> {
52///     StateMachine::builder()
53///         .add_states(&[
54///             JobState::Queued,
55///             JobState::Running,
56///             JobState::Succeeded,
57///             JobState::Failed,
58///         ])
59///         .set_initial_state(JobState::Queued)
60///         .set_final_states(&[JobState::Succeeded, JobState::Failed])
61///         .add_transition(JobState::Queued, JobEvent::Start, JobState::Running)
62///         .add_transition(JobState::Running, JobEvent::Complete, JobState::Succeeded)
63///         .add_transition(JobState::Running, JobEvent::Fail, JobState::Failed)
64///         .build()
65///         .expect("job state machine should be valid")
66/// }
67///
68/// let machine = create_job_machine();
69/// let state = AtomicRef::from_value(JobState::Queued);
70///
71/// assert_eq!(machine.trigger(&state, JobEvent::Start).unwrap(), JobState::Running);
72/// assert_eq!(*state.load(), JobState::Running);
73///
74/// assert!(machine.try_trigger(&state, JobEvent::Complete));
75/// assert_eq!(*state.load(), JobState::Succeeded);
76/// ```
77#[derive(Debug, Clone)]
78pub struct StateMachine<S, E>
79where
80    S: Copy + Eq + Hash + Debug + 'static,
81    E: Copy + Eq + Hash + Debug + 'static,
82{
83    states: HashSet<S>,
84    initial_states: HashSet<S>,
85    final_states: HashSet<S>,
86    transitions: HashSet<Transition<S, E>>,
87    transition_map: HashMap<(S, E), S>,
88    cas_executor: CasExecutor<S, StateMachineError<S, E>>,
89}
90
91impl<S, E> StateMachine<S, E>
92where
93    S: Copy + Eq + Hash + Debug + 'static,
94    E: Copy + Eq + Hash + Debug + 'static,
95{
96    /// Creates a builder for immutable state machine rules.
97    ///
98    /// # Returns
99    /// A new empty [`StateMachineBuilder`].
100    ///
101    /// # Examples
102    ///
103    /// ```
104    /// use qubit_state_machine::StateMachine;
105    ///
106    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
107    /// enum State {
108    ///     New,
109    /// }
110    ///
111    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
112    /// enum Event {
113    ///     Start,
114    /// }
115    ///
116    /// let machine = StateMachine::<State, Event>::builder()
117    ///     .add_state(State::New)
118    ///     .set_initial_state(State::New)
119    ///     .build()
120    ///     .expect("single-state machine should build");
121    /// assert!(machine.contains_state(State::New));
122    /// ```
123    pub fn builder() -> StateMachineBuilder<S, E> {
124        StateMachineBuilder::new()
125    }
126
127    /// Creates a state machine from validated builder parts.
128    ///
129    /// # Parameters
130    /// - `builder`: Builder containing validated states and terminal markers.
131    /// - `transitions`: Validated transition set.
132    /// - `transition_map`: Lookup table keyed by `(source, event)`.
133    ///
134    /// # Returns
135    /// An immutable state machine.
136    ///
137    /// This constructor does not validate input. Rule validation belongs to
138    /// [`StateMachineBuilder::build`].
139    pub(crate) fn new(
140        builder: StateMachineBuilder<S, E>,
141        transitions: HashSet<Transition<S, E>>,
142        transition_map: HashMap<(S, E), S>,
143    ) -> Self {
144        Self {
145            states: builder.states,
146            initial_states: builder.initial_states,
147            final_states: builder.final_states,
148            transitions,
149            transition_map,
150            cas_executor: CasExecutor::latency_first(),
151        }
152    }
153
154    /// Returns all registered states.
155    ///
156    /// # Returns
157    /// An immutable view of the registered state set.
158    ///
159    /// # Examples
160    ///
161    /// ```
162    /// # use qubit_state_machine::StateMachine;
163    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
164    /// # enum State { New, Running }
165    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
166    /// # enum Event { Start }
167    /// # let machine = StateMachine::builder()
168    /// #     .add_states(&[State::New, State::Running])
169    /// #     .add_transition(State::New, Event::Start, State::Running)
170    /// #     .build()
171    /// #     .expect("rules should build");
172    /// assert!(machine.states().contains(&State::New));
173    /// assert_eq!(machine.states().len(), 2);
174    /// ```
175    pub const fn states(&self) -> &HashSet<S> {
176        &self.states
177    }
178
179    /// Returns all configured initial states.
180    ///
181    /// # Returns
182    /// An immutable view of the initial state set.
183    ///
184    /// # Examples
185    ///
186    /// ```
187    /// # use qubit_state_machine::StateMachine;
188    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
189    /// # enum State { New, Running }
190    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
191    /// # enum Event { Start }
192    /// # let machine = StateMachine::builder()
193    /// #     .add_states(&[State::New, State::Running])
194    /// #     .set_initial_state(State::New)
195    /// #     .add_transition(State::New, Event::Start, State::Running)
196    /// #     .build()
197    /// #     .expect("rules should build");
198    /// assert!(machine.initial_states().contains(&State::New));
199    /// ```
200    pub const fn initial_states(&self) -> &HashSet<S> {
201        &self.initial_states
202    }
203
204    /// Returns all configured final states.
205    ///
206    /// # Returns
207    /// An immutable view of the final state set.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// # use qubit_state_machine::StateMachine;
213    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
214    /// # enum State { New, Done }
215    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
216    /// # enum Event { Finish }
217    /// # let machine = StateMachine::builder()
218    /// #     .add_states(&[State::New, State::Done])
219    /// #     .set_final_state(State::Done)
220    /// #     .add_transition(State::New, Event::Finish, State::Done)
221    /// #     .build()
222    /// #     .expect("rules should build");
223    /// assert!(machine.final_states().contains(&State::Done));
224    /// ```
225    pub const fn final_states(&self) -> &HashSet<S> {
226        &self.final_states
227    }
228
229    /// Returns all registered transitions.
230    ///
231    /// # Returns
232    /// An immutable view of the transition set.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use qubit_state_machine::{StateMachine, Transition};
238    ///
239    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
240    /// enum State {
241    ///     New,
242    ///     Running,
243    /// }
244    ///
245    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
246    /// enum Event {
247    ///     Start,
248    /// }
249    ///
250    /// let machine = StateMachine::builder()
251    ///     .add_states(&[State::New, State::Running])
252    ///     .add_transition(State::New, Event::Start, State::Running)
253    ///     .build()
254    ///     .expect("rules should build");
255    ///
256    /// assert!(machine
257    ///     .transitions()
258    ///     .contains(&Transition::new(State::New, Event::Start, State::Running)));
259    /// ```
260    pub const fn transitions(&self) -> &HashSet<Transition<S, E>> {
261        &self.transitions
262    }
263
264    /// Tests whether a state is registered in this state machine.
265    ///
266    /// # Parameters
267    /// - `state`: State to test.
268    ///
269    /// # Returns
270    /// `true` if the state is registered.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// # use qubit_state_machine::StateMachine;
276    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
277    /// # enum State { New, Running, Detached }
278    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
279    /// # enum Event { Start }
280    /// # let machine = StateMachine::builder()
281    /// #     .add_states(&[State::New, State::Running])
282    /// #     .add_transition(State::New, Event::Start, State::Running)
283    /// #     .build()
284    /// #     .expect("rules should build");
285    /// assert!(machine.contains_state(State::Running));
286    /// assert!(!machine.contains_state(State::Detached));
287    /// ```
288    pub fn contains_state(&self, state: S) -> bool {
289        self.states.contains(&state)
290    }
291
292    /// Tests whether a state is configured as an initial state.
293    ///
294    /// # Parameters
295    /// - `state`: State to test.
296    ///
297    /// # Returns
298    /// `true` if the state is an initial state.
299    ///
300    /// # Examples
301    ///
302    /// ```
303    /// # use qubit_state_machine::StateMachine;
304    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
305    /// # enum State { New, Running }
306    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
307    /// # enum Event { Start }
308    /// # let machine = StateMachine::builder()
309    /// #     .add_states(&[State::New, State::Running])
310    /// #     .set_initial_state(State::New)
311    /// #     .add_transition(State::New, Event::Start, State::Running)
312    /// #     .build()
313    /// #     .expect("rules should build");
314    /// assert!(machine.is_initial_state(State::New));
315    /// assert!(!machine.is_initial_state(State::Running));
316    /// ```
317    pub fn is_initial_state(&self, state: S) -> bool {
318        self.initial_states.contains(&state)
319    }
320
321    /// Tests whether a state is configured as a final state.
322    ///
323    /// # Parameters
324    /// - `state`: State to test.
325    ///
326    /// # Returns
327    /// `true` if the state is a final state.
328    ///
329    /// # Examples
330    ///
331    /// ```
332    /// # use qubit_state_machine::StateMachine;
333    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
334    /// # enum State { Running, Done }
335    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
336    /// # enum Event { Finish }
337    /// # let machine = StateMachine::builder()
338    /// #     .add_states(&[State::Running, State::Done])
339    /// #     .set_final_state(State::Done)
340    /// #     .add_transition(State::Running, Event::Finish, State::Done)
341    /// #     .build()
342    /// #     .expect("rules should build");
343    /// assert!(machine.is_final_state(State::Done));
344    /// assert!(!machine.is_final_state(State::Running));
345    /// ```
346    pub fn is_final_state(&self, state: S) -> bool {
347        self.final_states.contains(&state)
348    }
349
350    /// Looks up the target state for a source state and event.
351    ///
352    /// This method only queries rules; it does not modify any current-state
353    /// storage.
354    ///
355    /// # Parameters
356    /// - `source`: Source state.
357    /// - `event`: Event to apply.
358    ///
359    /// # Returns
360    /// `Some(target)` if a transition exists, or `None` otherwise.
361    ///
362    /// # Examples
363    ///
364    /// ```
365    /// # use qubit_state_machine::StateMachine;
366    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
367    /// # enum State { New, Running }
368    /// # #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
369    /// # enum Event { Start, Finish }
370    /// # let machine = StateMachine::builder()
371    /// #     .add_states(&[State::New, State::Running])
372    /// #     .add_transition(State::New, Event::Start, State::Running)
373    /// #     .build()
374    /// #     .expect("rules should build");
375    /// assert_eq!(
376    ///     machine.transition_target(State::New, Event::Start),
377    ///     Some(State::Running),
378    /// );
379    /// assert_eq!(machine.transition_target(State::New, Event::Finish), None);
380    /// ```
381    pub fn transition_target(&self, source: S, event: E) -> Option<S> {
382        self.transition_map.get(&(source, event)).copied()
383    }
384
385    /// Triggers an event and updates the provided atomic state reference.
386    ///
387    /// # Parameters
388    /// - `state`: Current state atomic reference.
389    /// - `event`: Event to apply.
390    ///
391    /// # Returns
392    /// The new state after a successful transition.
393    ///
394    /// # Errors
395    /// Returns [`StateMachineError::UnknownState`] when the current state is not
396    /// registered. Returns [`StateMachineError::UnknownTransition`] when the
397    /// current state is registered but has no transition for `event`.
398    ///
399    /// # Examples
400    ///
401    /// ```
402    /// use qubit_state_machine::{AtomicRef, StateMachine};
403    ///
404    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
405    /// enum State {
406    ///     New,
407    ///     Running,
408    /// }
409    ///
410    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
411    /// enum Event {
412    ///     Start,
413    /// }
414    ///
415    /// let machine = StateMachine::builder()
416    ///     .add_states(&[State::New, State::Running])
417    ///     .add_transition(State::New, Event::Start, State::Running)
418    ///     .build()
419    ///     .expect("rules should build");
420    /// let state = AtomicRef::from_value(State::New);
421    ///
422    /// assert_eq!(machine.trigger(&state, Event::Start).unwrap(), State::Running);
423    /// assert_eq!(*state.load(), State::Running);
424    /// ```
425    pub fn trigger(&self, state: &AtomicRef<S>, event: E) -> StateMachineResult<S, E> {
426        let (_, new_state) = self.change_state(state, event)?;
427        Ok(new_state)
428    }
429
430    /// Triggers an event, updates the atomic state, and invokes a success callback.
431    ///
432    /// The callback runs after the CAS update has succeeded.
433    ///
434    /// # Parameters
435    /// - `state`: Current state atomic reference.
436    /// - `event`: Event to apply.
437    /// - `on_success`: Callback receiving `(old_state, new_state)`.
438    ///
439    /// # Returns
440    /// The new state after a successful transition.
441    ///
442    /// # Errors
443    /// Returns the same errors as [`StateMachine::trigger`]. The callback is not
444    /// invoked when the transition fails.
445    ///
446    /// # Examples
447    ///
448    /// ```
449    /// use qubit_state_machine::{AtomicRef, StateMachine};
450    ///
451    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
452    /// enum State {
453    ///     New,
454    ///     Running,
455    /// }
456    ///
457    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
458    /// enum Event {
459    ///     Start,
460    /// }
461    ///
462    /// let machine = StateMachine::builder()
463    ///     .add_states(&[State::New, State::Running])
464    ///     .add_transition(State::New, Event::Start, State::Running)
465    ///     .build()
466    ///     .expect("rules should build");
467    /// let state = AtomicRef::from_value(State::New);
468    /// let mut observed = None;
469    ///
470    /// let next = machine
471    ///     .trigger_with(&state, Event::Start, |old_state, new_state| {
472    ///         observed = Some((old_state, new_state));
473    ///     })
474    ///     .expect("start should be valid");
475    ///
476    /// assert_eq!(next, State::Running);
477    /// assert_eq!(observed, Some((State::New, State::Running)));
478    /// ```
479    pub fn trigger_with<F>(
480        &self,
481        state: &AtomicRef<S>,
482        event: E,
483        on_success: F,
484    ) -> StateMachineResult<S, E>
485    where
486        F: FnOnce(S, S),
487    {
488        let (old_state, new_state) = self.change_state(state, event)?;
489        on_success(old_state, new_state);
490        Ok(new_state)
491    }
492
493    /// Attempts to trigger an event without returning error details.
494    ///
495    /// # Parameters
496    /// - `state`: Current state atomic reference.
497    /// - `event`: Event to apply.
498    ///
499    /// # Returns
500    /// `true` if the state changed successfully; `false` if the transition was
501    /// invalid.
502    ///
503    /// # Examples
504    ///
505    /// ```
506    /// use qubit_state_machine::{AtomicRef, StateMachine};
507    ///
508    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
509    /// enum State {
510    ///     New,
511    ///     Running,
512    /// }
513    ///
514    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
515    /// enum Event {
516    ///     Start,
517    ///     Finish,
518    /// }
519    ///
520    /// let machine = StateMachine::builder()
521    ///     .add_states(&[State::New, State::Running])
522    ///     .add_transition(State::New, Event::Start, State::Running)
523    ///     .build()
524    ///     .expect("rules should build");
525    /// let state = AtomicRef::from_value(State::New);
526    ///
527    /// assert!(!machine.try_trigger(&state, Event::Finish));
528    /// assert_eq!(*state.load(), State::New);
529    /// assert!(machine.try_trigger(&state, Event::Start));
530    /// ```
531    pub fn try_trigger(&self, state: &AtomicRef<S>, event: E) -> bool {
532        self.trigger(state, event).is_ok()
533    }
534
535    /// Attempts to trigger an event and invokes a callback only on success.
536    ///
537    /// # Parameters
538    /// - `state`: Current state atomic reference.
539    /// - `event`: Event to apply.
540    /// - `on_success`: Callback receiving `(old_state, new_state)`.
541    ///
542    /// # Returns
543    /// `true` if the state changed successfully; `false` if the transition was
544    /// invalid. The callback is skipped when this method returns `false`.
545    ///
546    /// # Examples
547    ///
548    /// ```
549    /// use qubit_state_machine::{AtomicRef, StateMachine};
550    ///
551    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
552    /// enum State {
553    ///     New,
554    ///     Running,
555    /// }
556    ///
557    /// #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
558    /// enum Event {
559    ///     Start,
560    ///     Finish,
561    /// }
562    ///
563    /// let machine = StateMachine::builder()
564    ///     .add_states(&[State::New, State::Running])
565    ///     .add_transition(State::New, Event::Start, State::Running)
566    ///     .build()
567    ///     .expect("rules should build");
568    /// let state = AtomicRef::from_value(State::New);
569    /// let mut callback_count = 0;
570    ///
571    /// assert!(!machine.try_trigger_with(&state, Event::Finish, |_, _| {
572    ///     callback_count += 1;
573    /// }));
574    /// assert_eq!(callback_count, 0);
575    ///
576    /// assert!(machine.try_trigger_with(&state, Event::Start, |_, _| {
577    ///     callback_count += 1;
578    /// }));
579    /// assert_eq!(callback_count, 1);
580    /// ```
581    pub fn try_trigger_with<F>(&self, state: &AtomicRef<S>, event: E, on_success: F) -> bool
582    where
583        F: FnOnce(S, S),
584    {
585        self.trigger_with(state, event, on_success).is_ok()
586    }
587
588    /// Applies a transition through the CAS executor.
589    ///
590    /// # Parameters
591    /// - `state`: Current state atomic reference.
592    /// - `event`: Event to apply.
593    ///
594    /// # Returns
595    /// The old and new state when the transition succeeds.
596    ///
597    /// # Errors
598    /// Returns a runtime state machine error when no valid next state exists.
599    fn change_state(
600        &self,
601        state: &AtomicRef<S>,
602        event: E,
603    ) -> Result<(S, S), StateMachineError<S, E>> {
604        let outcome = self.cas_executor.execute(state, |current_state: &S| {
605            match self.next_state(*current_state, event) {
606                Ok(new_state) => CasDecision::update(new_state, new_state),
607                Err(error) => CasDecision::abort(error),
608            }
609        });
610        match outcome.into_result() {
611            Ok(success) => Ok(Self::state_change_from_success(success)),
612            Err(error) => Err(Self::state_error_from_cas_error(error)),
613        }
614    }
615
616    /// Resolves the next state for the current state and event.
617    ///
618    /// # Parameters
619    /// - `current_state`: State currently stored by the atomic reference.
620    /// - `event`: Event to apply.
621    ///
622    /// # Returns
623    /// The target state when a transition exists.
624    ///
625    /// # Errors
626    /// Returns an unknown-state error before checking transitions if the current
627    /// state is not registered. Returns an unknown-transition error if no rule
628    /// exists for the `(current_state, event)` pair.
629    fn next_state(&self, current_state: S, event: E) -> Result<S, StateMachineError<S, E>> {
630        if !self.contains_state(current_state) {
631            return Err(StateMachineError::UnknownState {
632                state: current_state,
633            });
634        }
635        self.transition_target(current_state, event)
636            .ok_or(StateMachineError::UnknownTransition {
637                source: current_state,
638                event,
639            })
640    }
641
642    /// Extracts old and new states from a successful CAS transition.
643    ///
644    /// # Parameters
645    /// - `success`: Successful CAS result returned by the executor.
646    ///
647    /// # Returns
648    /// The old state and current state after CAS completion.
649    fn state_change_from_success(success: CasSuccess<S, S>) -> (S, S) {
650        match success {
651            CasSuccess::Updated {
652                previous, current, ..
653            } => (*previous, *current),
654            CasSuccess::Finished { current, .. } => (*current, *current),
655        }
656    }
657
658    /// Maps terminal CAS failures into state machine errors.
659    ///
660    /// # Parameters
661    /// - `error`: Terminal CAS error returned by the executor.
662    ///
663    /// # Returns
664    /// The business state machine error when the operation aborted, or a CAS
665    /// conflict error when retry limits were exhausted by compare-and-swap
666    /// conflicts.
667    fn state_error_from_cas_error(
668        error: CasError<S, StateMachineError<S, E>>,
669    ) -> StateMachineError<S, E> {
670        match error.error() {
671            Some(error) => *error,
672            None => StateMachineError::CasConflict {
673                attempts: error.attempts(),
674            },
675        }
676    }
677}