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