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    FastCasError,
17    FastCasState,
18};
19
20use super::{
21    FastStateMachineError,
22    FastStateMachineResult,
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            .and_then(|target| {
154                if target == UNSET_TRANSITION {
155                    None
156                } else {
157                    Some(target)
158                }
159            })
160    }
161
162    /// Returns the raw table cell for `(source, event)` without treating the unset sentinel.
163    ///
164    /// Unlike [`Self::transition_target`], this does not map the internal “no transition”
165    /// sentinel to `None`; callers that need a public [`Option`] should use
166    /// [`Self::transition_target`].
167    ///
168    /// # Returns
169    /// `None` when `source` or `event` is out of range; otherwise `Some(cell)` where `cell`
170    /// may still denote “unset” in the packed table.
171    fn get_transition_target(&self, source: usize, event: usize) -> Option<usize> {
172        if !self.contains_state(source) || event >= self.event_count {
173            return None;
174        }
175        let index = source
176            .checked_mul(self.event_count)
177            .and_then(|base| base.checked_add(event));
178        index.map(|index| self.transitions[index])
179    }
180
181    /// Applies one event atomically on `state` using the configured [`FastCas`] policy.
182    ///
183    /// Reads the current code from `state`, resolves the transition for `event`, and stores
184    /// the new code back if the transition is valid.
185    ///
186    /// # Arguments
187    /// * `state` — Shared compact state updated by compare-and-swap.
188    /// * `event` — Event code to apply.
189    ///
190    /// # Returns
191    /// `Ok(new_state)` after a successful transition and CAS store.
192    ///
193    /// # Errors
194    /// * [`FastStateMachineError::UnknownState`] — current code not in this machine.
195    /// * [`FastStateMachineError::UnknownTransition`] — no transition for `(current, event)`.
196    /// * [`FastStateMachineError::CasConflict`] — CAS retries exhausted.
197    pub fn trigger(&self, state: &FastCasState, event: usize) -> FastStateMachineResult {
198        let (_old_state, new_state) = self.change_state(state, event)?;
199        Ok(new_state)
200    }
201
202    /// Like [`Self::trigger`], but invokes `on_success` after the CAS update succeeds.
203    ///
204    /// The callback receives `(old_state, new_state)` as observed for the successful
205    /// transition. It is not called when [`Self::trigger`] would return an error.
206    ///
207    /// # Arguments
208    /// * `state` — Shared compact state updated by compare-and-swap.
209    /// * `event` — Event code to apply.
210    /// * `on_success` — Called with previous and new state codes only on success.
211    ///
212    /// # Returns
213    /// Same as [`Self::trigger`]: `Ok(new_state)` on success.
214    ///
215    /// # Errors
216    /// Same as [`Self::trigger`].
217    pub fn trigger_with<F>(
218        &self,
219        state: &FastCasState,
220        event: usize,
221        on_success: F,
222    ) -> FastStateMachineResult
223    where
224        F: Fn(usize, usize),
225    {
226        let (old_state, new_state) = self.change_state(state, event)?;
227        on_success(old_state, new_state);
228        Ok(new_state)
229    }
230
231    /// Attempts the same transition as [`Self::trigger`], discarding error details.
232    ///
233    /// # Returns
234    /// `true` if the transition and CAS update succeeded; `false` if validation failed or
235    /// CAS retries were exhausted.
236    pub fn try_trigger(&self, state: &FastCasState, event: usize) -> bool {
237        self.trigger(state, event).is_ok()
238    }
239
240    /// Like [`Self::trigger_with`], but returns only whether the transition succeeded.
241    ///
242    /// `on_success` runs only when the CAS update succeeds, matching [`Self::trigger_with`].
243    ///
244    /// # Returns
245    /// `true` on success; `false` on any error that [`Self::trigger_with`] would surface.
246    pub fn try_trigger_with<F>(&self, state: &FastCasState, event: usize, on_success: F) -> bool
247    where
248        F: Fn(usize, usize),
249    {
250        self.trigger_with(state, event, on_success).is_ok()
251    }
252
253    /// Runs one CAS-backed transition: validates `event` against the loaded current code and
254    /// installs the next code or aborts with [`FastStateMachineError`].
255    fn change_state(
256        &self,
257        state: &FastCasState,
258        event: usize,
259    ) -> Result<(usize, usize), FastStateMachineError> {
260        match self
261            .cas
262            .execute::<usize, FastStateMachineError, _>(state, |current| {
263                match self.next_state(current, event) {
264                    Ok(new_state) => FastCasDecision::update(new_state, new_state),
265                    Err(error) => FastCasDecision::abort(error),
266                }
267            }) {
268            Ok(success) => Ok((success.previous(), success.current())),
269            Err(error) => Err(Self::state_error_from_cas_error(error)),
270        }
271    }
272
273    /// Validates `state` and resolves the successor for `event` using the transition table.
274    fn next_state(&self, state: usize, event: usize) -> Result<usize, FastStateMachineError> {
275        if !self.contains_state(state) {
276            return Err(FastStateMachineError::UnknownState { state });
277        }
278
279        self.transition_target(state, event)
280            .ok_or(FastStateMachineError::UnknownTransition {
281                source_state: state,
282                event,
283            })
284    }
285
286    /// Converts [`FastCasError`] into [`FastStateMachineError`] for public APIs.
287    fn state_error_from_cas_error(
288        error: FastCasError<FastStateMachineError>,
289    ) -> FastStateMachineError {
290        match error {
291            FastCasError::Abort { error, .. } => error,
292            FastCasError::Conflict { attempts, .. } => {
293                FastStateMachineError::CasConflict { attempts }
294            }
295        }
296    }
297}