Skip to main content

StateMachineImpl

Macro StateMachineImpl 

Source
macro_rules! StateMachineImpl {
    ($($input:tt)*) => { ... };
}
Expand description

Connects a runtime type to a state-machine definition and generates its effects.

Invoke this macro in the implementation crate, in the same module as the runtime type’s state-specific methods. It implements StateMachineImpl, creates a private transition token, and emits module-local extension traits consumed by transition!. Keeping the token private is what prevents callers from retagging states directly.

The macro also emits raw-state construction helpers for the implementation:

  • Runtime::with_state(value) is public and safe, but only works for states declared Initial by the definition crate. Treat Initial as a public constructor contract: anyone with raw Runtime can attach one of those states.
  • Runtime::with_state_priv(value) is private to the invocation module and works for states listed with priv Initial: StateName; inside this macro. Use this for target-owned conversions from another state machine.
  • Runtime::with_state_unsafe(value) is the explicit unsafe escape hatch for arbitrary concrete states.

A private raw construction state is declared before or between transitions:

StateMachineImpl! {
    Job: JobStandin;

    priv Initial: Authenticated;

    transition Authenticated => Authorised();
}

Transition headers must match transitions declared by StateMachineDefinition!. The definition macro proves that an edge is legal; this macro decides what effect runs when that edge is taken for a particular runtime type. In this macro, headers may also carry implementation bodies:

  • ; means the transition has no effect body. The runtime value is only retagged from From to To.
  • { ... } runs the body against a mutable runtime value before retagging. Inside the body, self refers to &mut Runtime, not to the state token.
  • , after a header queues that header and shares the next body with it. This is not punctuation sugar. It records that those transition edges use the same effect type. Static union transitions depend on that fact: transition!(const Online self) only compiles when every concrete member uses the same body and signature for the chosen target.
  • pinned transition From => To(...) { ... } declares an effect that runs with self bound to Pin<&mut Runtime> instead of &mut Runtime. Call it with transition!(pin state, ...) from a method whose storage is bounded by S: SPinMut. This is the form to use when the runtime is !Unpin and the effect must call pinned methods or update fields through interior mutability without ever exposing &mut T. Pinned and ordinary effects are separate contracts, so the same From => To edge may have both a normal body and a pinned body. The call form decides which one is used.

The macro intentionally generates awkward hidden helper names such as _magicsm_transition, _magicsm_transitionConst, _magicsm_transitionDyn, _magicsm_transitionPin, _magicsm_transitionPinConst, and _magicsm_transitionPinDyn. User code should call them through transition!, which keeps transition calls uniform and prevents method-name collisions with normal implementation methods.

A typical implementation keeps the public methods ordinary Rust methods and uses State<Storage, Self, StateMarker> as the receiver. The storage bound documents what the method needs: SRef for reading, SMut for mutation and transitions, and SMove when a backend must be movable by value.

use magicstatemachines::{transition, SMut, State, StateMachineImpl};
use test_def::{ConnectionStandin, InOnline, Online};
use test_def::states::{Authenticated, Connected, Disconnected};

pub struct Connection {
    user: Option<String>,
}

StateMachineImpl! {
    Connection: ConnectionStandin;

    transition Disconnected => Connected();

    transition Connected => Authenticated(user: String) {
        self.user = Some(user);
    }

    transition Connected | Authenticated => Disconnected(),
    transition Authenticated => Connected() {
        self.user = None;
    }
}

impl Connection {
    pub fn connect<S>(self: State<S, Self, Disconnected>) -> State<S, Self, Connected>
    where
        S: SMut,
    {
        transition!(self)
    }

    pub fn authenticate<S>(
        self: State<S, Self, Connected>,
        user: impl Into<String>,
    ) -> State<S, Self, Authenticated>
    where
        S: SMut,
    {
        transition!(self, user.into())
    }

    pub fn disconnect<S>(self: State<S, Self, impl InOnline>) -> State<S, Self, Disconnected>
    where
        S: SMut,
    {
        transition!(dyn Online self)
    }
}

In the example, the two => Disconnected headers share the same body, so either transition!(const Online self) or transition!(dyn Online self) can be used. If Connected => Disconnected and Authenticated => Disconnected had different bodies, the static form would stop compiling and the dynamic form would remain valid by discriminating the concrete runtime state first.

Use the static form when the union proof is part of the API contract:

fn disconnect<S>(self: State<S, Self, impl InOnline>) -> State<S, Self, Disconnected>
where
    S: SMut,
{
    transition!(const Online self)
}

Use the dynamic form when the body may differ per member, or when the value is already stored as a discriminated union state:

fn stop<S>(self: State<S, Self, impl InOnline>) -> State<S, Self, Disconnected>
where
    S: SMut,
{
    transition!(dyn Online self,)
}

Pinned transitions are intentionally separate from ordinary transitions. SPinBox<T, S> does not implement SMut for T: !Unpin, so a normal body cannot accidentally receive &mut T. The pinned body receives Pin<&mut T> and the method advertises that by requiring S: SPinMut:

use core::{cell::Cell, marker::PhantomPinned, pin::Pin};
use magicstatemachines::{transition, SPinMut, State};

struct Connection {
    ready: Cell<bool>,
    _pin: PhantomPinned,
}

impl Connection {
    fn mark_ready(self: Pin<&mut Self>) {
        self.as_ref().get_ref().ready.set(true);
    }
}

StateMachineImpl! {
    Connection: ConnectionStandin;

    pinned transition Disconnected => Connected() {
        self.as_mut().mark_ready();
    }
}

impl Connection {
    fn connect<S>(self: State<S, Self, Disconnected>) -> State<S, Self, Connected>
    where
        S: SPinMut,
    {
        transition!(pin self)
    }
}

If an edge can be used from both movable and pinned storage, define both bodies. The state-machine definition still has only one edge; the implementation macro records two effects for that edge:

StateMachineImpl! {
    Connection: ConnectionStandin;

    transition Disconnected => Connected() {
        self.connected = true;
    }

    pinned transition Disconnected => Connected() {
        self.as_ref().connected.set(true);
    }
}

impl Connection {
    fn connect<S>(self: State<S, Self, Disconnected>) -> State<S, Self, Connected>
    where
        S: SMut,
    {
        transition!(self)
    }

    fn connect_pinned<S>(self: State<S, Self, Disconnected>) -> State<S, Self, Connected>
    where
        S: SPinMut,
    {
        transition!(pin self)
    }
}

Pinned transitions also have static and dynamic union forms. Use transition!(pin const Online self) when every member of the union shares the same pinned body and signature for the selected target. Use transition!(pin dyn Online self) when the current concrete member should be discriminated first and each member’s own pinned body should run.