ironstate 0.1.3

Verified state machines for humans and AI agents
Documentation
//! The transition traits and the runtime `Machine` that drives them.

use crate::error::RestoreError;
use crate::error::TransitionError;
use crate::kind::{self, Kind};
use crate::listener::{
    Clock, RejectionListener, TransitionListener, TransitionRecord, default_clock,
};
use crate::metadata::{self, MachineMetadata};
use crate::migrate::Versioned;
use core::fmt;
use std::time::Instant;

/// The transition function — the developer's decision logic.
///
/// This is the one piece written by hand: a pure function returning `Some(next)`
/// for a legal transition and `None` for an illegal one. It never reads a clock
/// or performs I/O; machines that care about time model it as events.
pub trait TransitionRules: Sized {
    /// The event type that drives transitions.
    ///
    /// Bounding the event here (rather than on `StateMachine`) means every
    /// holder of `S: TransitionRules` automatically knows `S::Event: EventKind`,
    /// `Clone`, and `Debug` — the structural checks and verification macros all
    /// rely on that being implied rather than restated everywhere.
    type Event: EventKind + Clone + core::fmt::Debug;

    /// The state this machine moves to on `event`, or `None` if there is none.
    fn transition(&self, event: &Self::Event) -> Option<Self>;
}

/// Event metadata read by the runtime and the verification macros.
///
/// Generated by `#[derive(Event)]`. The kind methods feed structural
/// enforcement in `apply`; the enumeration methods let `analyze!` and `test!`
/// walk every variant without the developer listing them by hand.
pub trait EventKind: Sized {
    /// The kinds this event carries, or `None` for the implicit default kind.
    fn kinds(&self) -> Option<&'static [Kind]>;

    /// The name of this event's variant.
    fn variant_name(&self) -> &'static str;

    /// One representative value per event variant.
    ///
    /// Variants carrying data use `Default` values — analysis is variant-level,
    /// so the payload only has to exist, not be meaningful.
    fn event_variants() -> Vec<Self>;

    /// Generation weight for randomized testing; higher means more frequent.
    fn likelihood(&self) -> f64 {
        0.35
    }
}

/// A verified state machine: an enum with a declared initial state, terminal
/// states, and optional per-state event-kind restrictions.
///
/// Generated by `#[derive(StateMachine)]`. The runtime enforces the structure
/// declared here on every `apply`, and the verification macros read it to walk
/// the state graph.
pub trait StateMachine: TransitionRules + Clone + fmt::Debug + PartialEq + Sized {
    /// The state a fresh machine starts in.
    fn initial() -> Self;

    /// Whether this state is terminal (no outbound transitions).
    fn is_terminal(&self) -> bool;

    /// The kinds this state restricts incoming events to, or `None` if it
    /// accepts events of any kind.
    fn restriction(&self) -> Option<&'static [Kind]>;

    /// One representative value per state variant, for graph analysis.
    fn state_variants() -> Vec<Self>;

    /// The name of this state's variant.
    fn variant_name(&self) -> &'static str;
}

/// A running state machine.
///
/// `Machine` owns the current state and enforces the structure declared by
/// `#[derive(StateMachine)]` before ever calling the transition function:
/// terminal states reject everything, and a restricted state rejects events
/// whose kind it does not accept. Both checks cost a single branch.
///
/// # Examples
///
/// ```rust,no_run
/// use ironstate::prelude::*;
///
/// #[derive(StateMachine, Clone, Debug, PartialEq)]
/// #[state_machine(initial = Draft, terminal = [Published])]
/// enum Article { Draft, Review, Published }
///
/// #[derive(Event, Clone, Debug, PartialEq)]
/// enum Edit { Submit, Approve }
///
/// impl TransitionRules for Article {
///     type Event = Edit;
///     fn transition(&self, edit: &Edit) -> Option<Self> {
///         match (self, edit) {
///             (Article::Draft, Edit::Submit) => Some(Article::Review),
///             (Article::Review, Edit::Approve) => Some(Article::Published),
///             _ => None,
///         }
///     }
/// }
///
/// let mut article = Machine::<Article>::new();
/// article.apply(Edit::Submit).unwrap();   // Draft -> Review
/// article.apply(Edit::Approve).unwrap();  // Review -> Published
/// assert_eq!(article.state(), &Article::Published);
/// ```
pub struct Machine<S: StateMachine>
where
    S::Event: EventKind,
{
    state: S,
    on_transition: Vec<TransitionListener<S>>,
    on_rejection: Vec<RejectionListener<S>>,
    clock: Clock,
}

impl<S: StateMachine> Machine<S>
where
    S::Event: EventKind,
{
    /// Create a machine in the declared initial state.
    pub fn new() -> Self {
        Self {
            state: S::initial(),
            on_transition: Vec::new(),
            on_rejection: Vec::new(),
            clock: default_clock(),
        }
    }

    /// Load a machine from a known state value (e.g. read from a database).
    ///
    /// Because the state is a typed enum value, it is by construction a valid
    /// variant — there is nothing further to validate.
    pub fn restore(state: S) -> Self {
        Self {
            state,
            on_transition: Vec::new(),
            on_rejection: Vec::new(),
            clock: default_clock(),
        }
    }

    /// Decode a `{version, payload}` envelope and migrate it forward to the
    /// current schema before loading it.
    pub fn restore_versioned(bytes: &[u8]) -> Result<Self, RestoreError>
    where
        S: Versioned,
    {
        Ok(Self::restore(S::restore_versioned(bytes)?))
    }

    /// The current state.
    pub fn state(&self) -> &S {
        &self.state
    }

    /// Apply an event, enforcing structure before the transition function runs.
    ///
    /// On success the new state is returned. On rejection the event moves into
    /// the returned error, so a caller can recover it without a clone.
    ///
    /// # Errors
    ///
    /// Returns [`TransitionError::TerminalState`] if the current state is
    /// terminal, [`TransitionError::EventKindRejected`] if the state restricts
    /// the event's kind, and [`TransitionError::NoTransition`] if the transition
    /// function declines the event. The structural checks run first, so a
    /// terminal or restricted state is rejected before any of your logic runs.
    pub fn apply(&mut self, event: S::Event) -> Result<S, TransitionError<S, S::Event>> {
        if self.state.is_terminal() {
            let err = TransitionError::TerminalState {
                state: self.state.clone(),
                event,
            };
            self.fire_rejection(&err);
            return Err(err);
        }

        if let Some(expected) = self.state.restriction() {
            let event_kind = event.kinds();
            // Default-kind events (None) are blocked by any restricted state.
            let accepted = matches!(event_kind, Some(ek) if kind::intersects(expected, ek));
            if !accepted {
                let err = TransitionError::EventKindRejected {
                    state: self.state.clone(),
                    event,
                    expected_kinds: expected,
                    event_kind,
                };
                self.fire_rejection(&err);
                return Err(err);
            }
        }

        match self.state.transition(&event) {
            Some(next) => {
                let from = self.state.clone();
                self.state = next.clone();
                let record = TransitionRecord {
                    from_state: from,
                    event,
                    to_state: next.clone(),
                    timestamp: (self.clock)(),
                };
                self.fire_transition(&record);
                Ok(next)
            }
            None => {
                let err = TransitionError::NoTransition {
                    state: self.state.clone(),
                    event,
                };
                self.fire_rejection(&err);
                Err(err)
            }
        }
    }

    /// Whether `apply(event)` would succeed — the cheapest of the three probes,
    /// allocating nothing and firing no listeners.
    ///
    /// Pair it with `event_variants()` to show only the moves that are legal
    /// right now:
    ///
    /// ```ignore
    /// let legal: Vec<Edit> = Edit::event_variants()
    ///     .into_iter()
    ///     .filter(|edit| article.could_apply(edit))
    ///     .collect();
    /// ```
    pub fn could_apply(&self, event: &S::Event) -> bool {
        if self.state.is_terminal() {
            return false;
        }
        if let Some(expected) = self.state.restriction()
            && !matches!(event.kinds(), Some(ek) if kind::intersects(expected, ek))
        {
            return false;
        }
        self.state.transition(event).is_some()
    }

    /// The exact error `apply(event)` would return, or `None` if it would
    /// succeed. Checks run in the same order as `apply`; nothing is mutated and
    /// no listeners fire.
    pub fn why_not(&self, event: &S::Event) -> Option<TransitionError<S, S::Event>> {
        if self.state.is_terminal() {
            return Some(TransitionError::TerminalState {
                state: self.state.clone(),
                event: event.clone(),
            });
        }
        if let Some(expected) = self.state.restriction() {
            let event_kind = event.kinds();
            if !matches!(event_kind, Some(ek) if kind::intersects(expected, ek)) {
                return Some(TransitionError::EventKindRejected {
                    state: self.state.clone(),
                    event: event.clone(),
                    expected_kinds: expected,
                    event_kind,
                });
            }
        }
        if self.state.transition(event).is_none() {
            return Some(TransitionError::NoTransition {
                state: self.state.clone(),
                event: event.clone(),
            });
        }
        None
    }

    /// The state `apply(event)` would move to, or `None` if it would be
    /// rejected. Calls the transition function but mutates nothing and fires no
    /// listeners.
    pub fn peek_transition(&self, event: &S::Event) -> Option<S> {
        if !self.could_apply(event) {
            return None;
        }
        self.state.transition(event)
    }

    /// The static description of this machine's state graph.
    pub fn metadata() -> MachineMetadata<S> {
        metadata::build::<S>()
    }

    /// Register a listener fired after every successful transition.
    pub fn on_transition(
        &mut self,
        listener: impl Fn(&TransitionRecord<S, S::Event>) + 'static,
    ) -> &mut Self {
        self.on_transition.push(Box::new(listener));
        self
    }

    /// Register a listener fired after every rejected transition.
    pub fn on_rejection(
        &mut self,
        listener: impl Fn(&TransitionError<S, S::Event>) + 'static,
    ) -> &mut Self {
        self.on_rejection.push(Box::new(listener));
        self
    }

    /// Override the clock used for transition-record timestamps, so a
    /// deterministic harness can keep records reproducible.
    pub fn set_clock(&mut self, clock: impl Fn() -> Instant + 'static) -> &mut Self {
        self.clock = Box::new(clock);
        self
    }

    fn fire_transition(&self, record: &TransitionRecord<S, S::Event>) {
        for listener in &self.on_transition {
            listener(record);
        }
    }

    fn fire_rejection(&self, error: &TransitionError<S, S::Event>) {
        for listener in &self.on_rejection {
            listener(error);
        }
    }
}

impl<S: StateMachine> Default for Machine<S>
where
    S::Event: EventKind,
{
    fn default() -> Self {
        Self::new()
    }
}

impl<S: StateMachine + fmt::Debug> fmt::Debug for Machine<S>
where
    S::Event: EventKind,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Machine")
            .field("state", &self.state)
            .finish_non_exhaustive()
    }
}