Skip to main content

qubit_state_machine/fast/
fast_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
11//! Integer-coded state machine implementation.
12
13use qubit_cas::{
14    FastCas,
15    FastCasDecision,
16    FastCasState,
17};
18
19use super::{
20    FastStateMachineError,
21    FastStateMachineResult,
22    fast_state_machine_error_from_fast_cas_error,
23};
24
25const UNSET_TRANSITION: usize = usize::MAX;
26
27/// A compact, high-performance state machine backed by [`FastCas`].
28///
29/// States and events are represented as contiguous integer code spaces:
30///
31/// - states are in `[0, state_count)`
32/// - events are in `[0, event_count)`
33///
34/// Transition resolution is a single table index lookup:
35/// `index = source * event_count + event`.
36#[derive(Debug, Clone)]
37pub struct FastStateMachine {
38    pub(super) state_count: usize,
39    pub(super) event_count: usize,
40    pub(super) initial_states: Vec<bool>,
41    pub(super) final_states: Vec<bool>,
42    pub(super) transitions: Vec<usize>,
43    pub(super) cas: FastCas,
44}
45
46impl FastStateMachine {
47    /// Creates a builder used to configure counts, initial/final flags, transitions,
48    /// and CAS policy before calling [`super::FastStateMachineBuilder::build`].
49    ///
50    /// # Returns
51    /// A new, empty [`super::FastStateMachineBuilder`].
52    pub fn builder() -> super::FastStateMachineBuilder {
53        super::FastStateMachineBuilder::new()
54    }
55
56    /// Returns the number of distinct state codes configured for this machine.
57    ///
58    /// Valid state codes are integers in `0..state_count()`.
59    ///
60    /// # Returns
61    /// The configured state-space size (length of the transition table rows).
62    pub const fn state_count(&self) -> usize {
63        self.state_count
64    }
65
66    /// Returns the number of distinct event codes accepted by this machine.
67    ///
68    /// Valid event codes are integers in `0..event_count()`.
69    ///
70    /// # Returns
71    /// The configured event-space size (length of each row in the transition table).
72    pub const fn event_count(&self) -> usize {
73        self.event_count
74    }
75
76    /// Returns the dense transition table.
77    ///
78    /// The table is laid out row-major: source index first, then event index.
79    pub fn transitions(&self) -> &[usize] {
80        &self.transitions
81    }
82
83    /// Returns the CAS retry policy used for all transitions.
84    ///
85    /// This is the policy configured in the builder via
86    /// [`crate::FastStateMachineBuilder::cas_policy`], or
87    /// [`crate::FAST_STATE_MACHINE_DEFAULT_CAS_POLICY`] when no override is
88    /// supplied.
89    pub fn cas_policy(&self) -> qubit_cas::FastCasPolicy {
90        self.cas.policy()
91    }
92
93    /// Returns a read-only slice marking which state codes are initial.
94    ///
95    /// The slice has length [`Self::state_count`]; index `s` corresponds to state code `s`,
96    /// and is `true` if that state was registered as initial in the builder.
97    pub fn initial_states(&self) -> &[bool] {
98        &self.initial_states
99    }
100
101    /// Returns a read-only slice marking which state codes are final (accepting).
102    ///
103    /// The slice has length [`Self::state_count`]; index `s` corresponds to state code `s`,
104    /// and is `true` if that state was registered as final in the builder.
105    pub fn final_states(&self) -> &[bool] {
106        &self.final_states
107    }
108
109    /// Returns whether `state` is a valid code for this machine.
110    ///
111    /// # Arguments
112    /// * `state` — Candidate state code.
113    ///
114    /// # Returns
115    /// `true` if `state < state_count()`, otherwise `false`.
116    pub const fn contains_state(&self, state: usize) -> bool {
117        state < self.state_count
118    }
119
120    /// Returns whether `state` was configured as an initial state.
121    ///
122    /// # Arguments
123    /// * `state` — State code to test.
124    ///
125    /// # Returns
126    /// `true` if `state` is in range and marked initial; `false` if out of range or not initial.
127    pub fn is_initial_state(&self, state: usize) -> bool {
128        self.initial_states.get(state).copied().unwrap_or(false)
129    }
130
131    /// Returns whether `state` was configured as a final state.
132    ///
133    /// # Arguments
134    /// * `state` — State code to test.
135    ///
136    /// # Returns
137    /// `true` if `state` is in range and marked final; `false` if out of range or not final.
138    pub fn is_final_state(&self, state: usize) -> bool {
139        self.final_states.get(state).copied().unwrap_or(false)
140    }
141
142    /// Looks up the next state for a specific `(source, event)` pair.
143    ///
144    /// # Arguments
145    /// * `source` — Current state code.
146    /// * `event` — Event code.
147    ///
148    /// # Returns
149    /// `Some(target)` when a transition is configured; `None` if `source` or `event` is
150    /// out of range, or if no transition exists for that pair.
151    pub fn transition_target(&self, source: usize, event: usize) -> Option<usize> {
152        self.get_transition_target(source, event)
153            .filter(|&target| target != UNSET_TRANSITION)
154    }
155
156    /// Returns the raw table cell for `(source, event)` without treating the unset sentinel.
157    ///
158    /// Unlike [`Self::transition_target`], this does not map the internal “no transition”
159    /// sentinel to `None`; callers that need a public [`Option`] should use
160    /// [`Self::transition_target`].
161    ///
162    /// # Returns
163    /// `None` when `source` or `event` is out of range; otherwise `Some(cell)` where `cell`
164    /// may still denote “unset” in the packed table.
165    fn get_transition_target(&self, source: usize, event: usize) -> Option<usize> {
166        if !self.contains_state(source) || event >= self.event_count {
167            return None;
168        }
169        let index = source
170            .checked_mul(self.event_count)
171            .and_then(|base| base.checked_add(event));
172        index.map(|index| self.transitions[index])
173    }
174
175    /// Applies one event atomically on `state` using the configured [`FastCas`] policy.
176    ///
177    /// Reads the current code from `state`, resolves the transition for `event`, and stores
178    /// the new code back if the transition is valid.
179    ///
180    /// # Arguments
181    /// * `state` — Shared compact state updated by compare-and-swap.
182    /// * `event` — Event code to apply.
183    ///
184    /// # Returns
185    /// `Ok(new_state)` after a successful transition and CAS store.
186    ///
187    /// # Errors
188    /// * [`FastStateMachineError::UnknownState`] — current code not in this machine.
189    /// * [`FastStateMachineError::UnknownTransition`] — no transition for `(current, event)`.
190    /// * [`FastStateMachineError::CasConflict`] — CAS retries exhausted.
191    pub fn trigger(&self, state: &FastCasState, event: usize) -> FastStateMachineResult {
192        let (_old_state, new_state) = self.change_state(state, event)?;
193        Ok(new_state)
194    }
195
196    /// Like [`Self::trigger`], but invokes `on_success` after the CAS update succeeds.
197    ///
198    /// The callback receives `(old_state, new_state)` as observed for the successful
199    /// transition. It is not called when [`Self::trigger`] would return an error.
200    ///
201    /// # Arguments
202    /// * `state` — Shared compact state updated by compare-and-swap.
203    /// * `event` — Event code to apply.
204    /// * `on_success` — Called with previous and new state codes only on success.
205    ///
206    /// # Returns
207    /// Same as [`Self::trigger`]: `Ok(new_state)` on success.
208    ///
209    /// # Errors
210    /// Same as [`Self::trigger`].
211    pub fn trigger_with<F>(&self, state: &FastCasState, event: usize, on_success: F) -> FastStateMachineResult
212    where
213        F: Fn(usize, usize),
214    {
215        let (old_state, new_state) = self.change_state(state, event)?;
216        on_success(old_state, new_state);
217        Ok(new_state)
218    }
219
220    /// Attempts the same transition as [`Self::trigger`], discarding error details.
221    ///
222    /// # Returns
223    /// `true` if the transition and CAS update succeeded; `false` if validation failed or
224    /// CAS retries were exhausted.
225    pub fn try_trigger(&self, state: &FastCasState, event: usize) -> bool {
226        self.trigger(state, event).is_ok()
227    }
228
229    /// Like [`Self::trigger_with`], but returns only whether the transition succeeded.
230    ///
231    /// `on_success` runs only when the CAS update succeeds, matching [`Self::trigger_with`].
232    ///
233    /// # Returns
234    /// `true` on success; `false` on any error that [`Self::trigger_with`] would surface.
235    pub fn try_trigger_with<F>(&self, state: &FastCasState, event: usize, on_success: F) -> bool
236    where
237        F: Fn(usize, usize),
238    {
239        self.trigger_with(state, event, on_success).is_ok()
240    }
241
242    /// Runs one CAS-backed transition: validates `event` against the loaded current code and
243    /// installs the next code or aborts with [`FastStateMachineError`].
244    fn change_state(&self, state: &FastCasState, event: usize) -> Result<(usize, usize), FastStateMachineError> {
245        match self.cas.execute::<usize, FastStateMachineError, _>(state, |current| {
246            match self.next_state(current, event) {
247                Ok(new_state) => FastCasDecision::update(new_state, new_state),
248                Err(error) => FastCasDecision::abort(error),
249            }
250        }) {
251            Ok(success) => Ok((success.previous(), success.current())),
252            Err(error) => Err(fast_state_machine_error_from_fast_cas_error(error)),
253        }
254    }
255
256    /// Validates `state` and resolves the successor for `event` using the transition table.
257    fn next_state(&self, state: usize, event: usize) -> Result<usize, FastStateMachineError> {
258        if !self.contains_state(state) {
259            return Err(FastStateMachineError::UnknownState { state });
260        }
261
262        self.transition_target(state, event)
263            .ok_or(FastStateMachineError::UnknownTransition {
264                source_state: state,
265                event,
266            })
267    }
268}