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}