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}