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}