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}