Skip to main content

magicstatemachines/state/
owned.rs

1#[cfg(not(feature = "tracing"))]
2use crate::StateCopy;
3#[cfg(feature = "decompose")]
4use crate::{DecomposedData, DecomposedState, RecomposeError};
5use crate::{Initial, StateClone, StateMachineImpl, Transition};
6use core::marker::PhantomData;
7use core::ops::{Deref, DerefMut};
8#[cfg(feature = "tracing")]
9use core::panic::Location;
10use core::pin::Pin;
11
12/// A directly owned runtime implementation `T` whose compile-time state is `S`.
13///
14/// Without the `tracing` feature, the state marker has no runtime storage and
15/// `StateOwned<T, S>` has the same size and alignment as `T`.
16/// With `tracing`, the wrapper also stores a `Vec<TraceEntry>` containing the
17/// transition history for this value.
18///
19/// `StateOwned` is the simplest storage representation. Generic implementation
20/// methods usually use [`State<SOwned, T, S>`](crate::State) instead, because
21/// the same method can then work for owned, boxed, pinned, shared-guard, and
22/// discriminated storage. `StateOwned<T, S>` remains useful when you want the
23/// direct transparent wrapper explicitly.
24///
25/// The state marker `S` is compile-time authority, not runtime data. A
26/// transition consumes one state token and returns another:
27///
28/// ```ignore
29/// use magicstatemachines::{StateOwned, transition};
30/// use test_def::states::{Connected, Disconnected};
31///
32/// let disconnected: StateOwned<Connection, Disconnected> =
33///     StateOwned::new(Connection::new("localhost:8080"));
34/// let connected: StateOwned<Connection, Connected> = transition!(disconnected);
35/// ```
36///
37/// When `tracing` is enabled, the same transition appends a diagnostic record.
38/// The trace is historical only; it is not consulted to decide which methods
39/// are callable. The compiler-enforced state remains the `S` type parameter.
40///
41/// ```ignore
42/// let connected = transition!(disconnected);
43/// let entry = &connected.trace()[0];
44///
45/// assert!(entry.from().type_name().ends_with("::Disconnected"));
46/// assert!(entry.to().type_name().ends_with("::Connected"));
47/// ```
48///
49/// ```ignore
50/// use magicstatemachines::{State, StateOwned, SOwned, transition};
51/// use test_def::states::{Connected, Disconnected};
52///
53/// let disconnected: StateOwned<Connection, Disconnected> =
54///     StateOwned::new(Connection::new("localhost:8080"));
55///
56/// // Generic APIs usually spell the same owned state like this:
57/// let disconnected: State<SOwned, Connection, Disconnected> =
58///     State::new(Connection::new("localhost:8080"));
59/// // State-specific methods are implemented on `Connection` with arbitrary
60/// // self types, so they can accept this generic owned state directly.
61/// let connected: State<SOwned, Connection, Connected> =
62///     connection_method_that_connects(disconnected);
63/// ```
64///
65/// State tokens are linear and shared ownership is not valid state storage:
66///
67/// ```compile_fail
68/// use std::rc::Rc;
69/// use magicstatemachines::{Initial, StateMachineImpl, StateOwned};
70///
71/// struct Machine;
72/// struct Ready;
73/// struct Runtime;
74/// struct Token;
75///
76/// impl Initial<Ready> for Machine {}
77/// impl StateMachineImpl for Runtime {
78///     type Standin = Machine;
79///     type Impl = Self;
80///     type TransitionToken = Token;
81/// }
82///
83/// let _: StateOwned<Rc<Runtime>, Ready> = StateOwned::new(Rc::new(Runtime));
84/// ```
85#[cfg_attr(not(feature = "tracing"), repr(transparent))]
86pub struct StateOwned<T, S> {
87    pub(crate) value: T,
88    pub(crate) state: PhantomData<fn() -> S>,
89    #[cfg(feature = "tracing")]
90    pub(crate) trace: Vec<crate::TraceEntry>,
91}
92
93/// Owned state whose runtime value is pinned by an arbitrary pinning pointer.
94///
95/// This is the direct owned alias for `StateOwned<Pin<P>, S>`. In generic
96/// state-machine methods prefer [`State`](crate::State) plus a storage backend
97/// such as [`SPinBox`](crate::SPinBox), because that keeps the same method
98/// usable for owned, boxed, pinned, and guard-backed states.
99pub type SPin<T, S> = StateOwned<Pin<T>, S>;
100
101/// A one-shot callable that completes a state transition.
102#[doc(hidden)]
103pub struct TransitionCall<T, From, To> {
104    state: StateOwned<T, From>,
105    #[cfg(feature = "tracing")]
106    callsite: &'static Location<'static>,
107    to: PhantomData<fn() -> To>,
108}
109
110/// Creates a callable transition requiring the definition's arguments.
111///
112/// This low-level function requires the implementation's private transition
113/// capability. It is mainly used by generated code and crate-internal tests.
114/// Implementation methods should normally use
115/// [`transition!`](macro@crate::transition), which expands to the private
116/// helpers generated by [`StateMachineImpl!`](macro@crate::StateMachineImpl).
117///
118/// The returned [`TransitionCall`] is one-shot. Calling `.call(args)` checks
119/// `args` against the declaration's [`Transition::F`] signature and then
120/// retags the wrapper from `S` to `Next`. With `tracing`, the callsite captured
121/// here is appended to the returned state's trace.
122#[must_use]
123#[track_caller]
124pub fn transition<T, S, Next>(
125    state: StateOwned<T, S>,
126    _token: T::TransitionToken,
127) -> TransitionCall<T, S, Next>
128where
129    T: StateMachineImpl,
130    T::Standin: Transition<S, Next>,
131{
132    TransitionCall {
133        state,
134        #[cfg(feature = "tracing")]
135        callsite: Location::caller(),
136        to: PhantomData,
137    }
138}
139
140#[cfg(not(feature = "tracing"))]
141impl<T, From, To> TransitionCall<T, From, To>
142where
143    T: StateMachineImpl,
144{
145    #[doc(hidden)]
146    pub fn call<Args>(self, _args: Args) -> StateOwned<T, To>
147    where
148        T::Standin: Transition<From, To>,
149        <T::Standin as Transition<From, To>>::F: crate::TransitionSignature<Args>,
150    {
151        StateOwned {
152            value: self.state.value,
153            state: PhantomData,
154        }
155    }
156}
157
158#[cfg(feature = "tracing")]
159impl<T, From, To> TransitionCall<T, From, To>
160where
161    T: StateMachineImpl,
162{
163    #[doc(hidden)]
164    pub fn call<Args>(self, _args: Args) -> StateOwned<T, To>
165    where
166        T::Standin: Transition<From, To>,
167        <T::Standin as Transition<From, To>>::F: crate::TransitionSignature<Args>,
168        From: crate::StateTrait,
169        To: crate::ConcreteStateTrait,
170    {
171        let mut trace = self.state.trace;
172        trace.push(crate::TraceEntry::new::<From, To>(self.callsite));
173
174        StateOwned {
175            value: self.state.value,
176            state: PhantomData,
177            trace,
178        }
179    }
180}
181
182impl<T, S> StateOwned<T, S> {
183    /// Separates the compile-time state token from the runtime data.
184    ///
185    /// This is a provenance API for temporarily splitting a valid typestate
186    /// value into two opaque halves. The [`DecomposedState`] half carries the
187    /// authority that the value was in state `S`; the [`DecomposedData`] half
188    /// carries the runtime data `T`. Both halves contain the same generated UID,
189    /// and [`StateOwned::recompose`] checks that UID before it recreates a
190    /// usable `StateOwned<T, S>`.
191    ///
192    /// This is useful when state proof and runtime data need to pass through
193    /// different layers, but neither layer should be able to manufacture a new
194    /// valid state by itself. The state half cannot expose or mutate `T`, and
195    /// the data half cannot recreate typestate authority without the matching
196    /// state half.
197    ///
198    /// ```
199    /// use magicstatemachines::{Initial, StateMachineImpl, StateOwned};
200    ///
201    /// struct Machine;
202    /// struct Runtime {
203    ///     value: u32,
204    /// }
205    /// struct Ready;
206    /// struct Token;
207    ///
208    /// impl Initial<Ready> for Machine {}
209    ///
210    /// impl StateMachineImpl for Runtime {
211    ///     type Standin = Machine;
212    ///     type Impl = Self;
213    ///     type TransitionToken = Token;
214    /// }
215    ///
216    /// let ready: StateOwned<Runtime, Ready> = StateOwned::new(Runtime { value: 7 });
217    /// let (state, data) = ready.decompose();
218    ///
219    /// // The halves can now move through different code paths. Only the
220    /// // original matching pair can be recomposed into a valid state token.
221    /// let ready = StateOwned::recompose(state, data).expect("matching decomposition");
222    ///
223    /// assert_eq!(ready.value, 7);
224    /// ```
225    ///
226    /// This method is available only with `decompose`. The UID generator is
227    /// selected separately: `decompose-rand` uses the stable `rand` dependency,
228    /// while `decompose nightly-random` uses nightly `std::random` and does not
229    /// need `rand`. If `tracing` is also enabled, the trace stays with the state
230    /// half and is restored by recomposition.
231    #[cfg(feature = "decompose")]
232    #[must_use]
233    pub fn decompose(self) -> (DecomposedState<S>, DecomposedData<T>) {
234        let uid = decompose_uid();
235
236        (
237            DecomposedState {
238                uid,
239                state: PhantomData,
240                #[cfg(feature = "tracing")]
241                trace: self.trace,
242            },
243            DecomposedData {
244                uid,
245                value: self.value,
246            },
247        )
248    }
249
250    /// Recombines state and data produced by the same [`StateOwned::decompose`] call.
251    ///
252    /// Recomposition restores the exact same state type `S`. After it succeeds,
253    /// the returned value can be used like any other `StateOwned<T, S>`: it
254    /// dereferences to `T`, can keep transitioning according to the declared
255    /// state-machine contract, and, with `tracing`, continues carrying the
256    /// existing trace.
257    ///
258    /// A mismatched UID returns [`RecomposeError`]. The check is deliberately
259    /// narrow: it does not inspect `T`, and it does not infer whether two
260    /// runtime values happen to be equal. It only verifies that both opaque UID
261    /// halves came from the same decomposition. The UID is non-cryptographic and
262    /// exists to catch accidental or invalid recombination, not malicious input.
263    ///
264    /// ```
265    /// use magicstatemachines::{Initial, StateMachineImpl, StateOwned};
266    ///
267    /// struct Machine;
268    /// struct Runtime;
269    /// struct Ready;
270    /// struct Token;
271    ///
272    /// impl Initial<Ready> for Machine {}
273    ///
274    /// impl StateMachineImpl for Runtime {
275    ///     type Standin = Machine;
276    ///     type Impl = Self;
277    ///     type TransitionToken = Token;
278    /// }
279    ///
280    /// let ready: StateOwned<Runtime, Ready> = StateOwned::new(Runtime);
281    /// let (state, data) = ready.decompose();
282    /// let ready: StateOwned<Runtime, Ready> =
283    ///     StateOwned::recompose(state, data).expect("matching decomposition");
284    ///
285    /// let _: StateOwned<Runtime, Ready> = ready;
286    /// ```
287    #[cfg(feature = "decompose")]
288    pub fn recompose(
289        state: DecomposedState<S>,
290        data: DecomposedData<T>,
291    ) -> Result<Self, RecomposeError> {
292        if state.uid != data.uid {
293            return Err(RecomposeError);
294        }
295
296        Ok(Self {
297            value: data.value,
298            state: PhantomData,
299            #[cfg(feature = "tracing")]
300            trace: state.trace,
301        })
302    }
303
304    /// Recorded transitions in call order.
305    ///
306    /// Each entry stores the source state, destination state, and callsite
307    /// captured by the transition wrapper. This is available only with the
308    /// `tracing` feature and is intended for diagnostics, not for enforcing
309    /// the state machine.
310    ///
311    /// Cloning a traced state clones the erased state markers in every entry.
312    /// The public API of each entry stays the same whether erased markers are
313    /// backed by `&'static dyn StateTrait` or by the `dynZST` feature.
314    #[cfg(feature = "tracing")]
315    #[must_use]
316    pub fn trace(&self) -> &[crate::TraceEntry] {
317        &self.trace
318    }
319}
320
321#[cfg(all(feature = "decompose", feature = "nightly-random"))]
322fn decompose_uid() -> u64 {
323    std::random::random(..)
324}
325
326#[cfg(all(
327    feature = "decompose",
328    not(feature = "nightly-random"),
329    feature = "decompose-rand"
330))]
331fn decompose_uid() -> u64 {
332    use rand::TryRngCore;
333
334    rand::rngs::OsRng
335        .try_next_u64()
336        .expect("OS random source failed while decomposing state")
337}
338
339#[cfg(all(
340    feature = "decompose",
341    not(feature = "nightly-random"),
342    not(feature = "decompose-rand")
343))]
344compile_error!(
345    "feature `decompose` requires a random backend: enable `nightly-random` or `decompose-rand`"
346);
347
348impl<T, S> StateOwned<T, S>
349where
350    T: StateMachineImpl,
351    T::Standin: Initial<S>,
352{
353    /// Wraps an implementation in a state declared initial by its definition.
354    ///
355    /// This only compiles when `T::Standin: Initial<S>`. Use transition
356    /// methods generated in the implementation module to reach every later
357    /// state.
358    #[must_use]
359    pub const fn new(value: T) -> Self {
360        Self {
361            value,
362            state: PhantomData,
363            #[cfg(feature = "tracing")]
364            trace: Vec::new(),
365        }
366    }
367}
368
369impl<T, S> Deref for StateOwned<T, S> {
370    type Target = T;
371
372    fn deref(&self) -> &Self::Target {
373        &self.value
374    }
375}
376
377impl<T, S> DerefMut for StateOwned<T, S> {
378    fn deref_mut(&mut self) -> &mut Self::Target {
379        &mut self.value
380    }
381}
382
383impl<T, S> Clone for StateOwned<T, S>
384where
385    T: Clone,
386    S: StateClone,
387{
388    fn clone(&self) -> Self {
389        Self {
390            value: self.value.clone(),
391            state: PhantomData,
392            #[cfg(feature = "tracing")]
393            trace: self.trace.clone(),
394        }
395    }
396}
397
398#[cfg(not(feature = "tracing"))]
399impl<T, S> Copy for StateOwned<T, S>
400where
401    T: Copy,
402    S: StateClone + StateCopy,
403{
404}
405
406impl<T: core::fmt::Debug, S> core::fmt::Debug for StateOwned<T, S> {
407    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
408        self.value.fmt(formatter)
409    }
410}
411
412#[cfg(not(feature = "tracing"))]
413pub(super) fn complete_transition<T, From, To>(
414    state: StateOwned<T, From>,
415    _callsite: super::TransitionCallsite,
416) -> StateOwned<T, To> {
417    StateOwned {
418        value: state.value,
419        state: PhantomData,
420    }
421}
422
423#[cfg(feature = "tracing")]
424pub(super) fn complete_transition<T, From, To>(
425    state: StateOwned<T, From>,
426    callsite: super::TransitionCallsite,
427) -> StateOwned<T, To>
428where
429    From: crate::StateTrait,
430    To: crate::ConcreteStateTrait,
431{
432    let mut trace = state.trace;
433    trace.push(crate::TraceEntry::new::<From, To>(callsite));
434
435    StateOwned {
436        value: state.value,
437        state: PhantomData,
438        trace,
439    }
440}