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