Skip to main content

murk_core/
error.rs

1//! Error types for the Murk simulation framework.
2//!
3//! Maps the error code table from HLD §9.7 to Rust enums, organized
4//! by subsystem: step (tick engine), propagator, ingress, and observation.
5
6use std::error::Error;
7use std::fmt;
8
9/// Errors from the tick engine during `step()`.
10///
11/// Corresponds to the TickEngine and Pipeline subsystem codes in HLD §9.7.
12#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum StepError {
14    /// A propagator returned an error during execution
15    /// (`MURK_ERROR_PROPAGATOR_FAILED`).
16    PropagatorFailed {
17        /// Name of the failing propagator.
18        name: String,
19        /// The underlying propagator error.
20        reason: PropagatorError,
21    },
22    /// Arena allocation failed — OOM during generation staging
23    /// (`MURK_ERROR_ALLOCATION_FAILED`).
24    AllocationFailed,
25    /// The tick was rolled back due to a propagator failure
26    /// (`MURK_ERROR_TICK_ROLLBACK`).
27    TickRollback,
28    /// Ticking is disabled after consecutive rollbacks
29    /// (`MURK_ERROR_TICK_DISABLED`, Decision J).
30    TickDisabled,
31    /// The requested dt exceeds a propagator's `max_dt` constraint
32    /// (`MURK_ERROR_DT_OUT_OF_RANGE`).
33    DtOutOfRange,
34    /// The world is shutting down
35    /// (`MURK_ERROR_SHUTTING_DOWN`, Decision E).
36    ShuttingDown,
37}
38
39impl fmt::Display for StepError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::PropagatorFailed { name, reason } => {
43                write!(f, "propagator '{name}' failed: {reason}")
44            }
45            Self::AllocationFailed => write!(f, "arena allocation failed"),
46            Self::TickRollback => write!(f, "tick rolled back"),
47            Self::TickDisabled => write!(f, "ticking disabled after consecutive rollbacks"),
48            Self::DtOutOfRange => write!(f, "dt exceeds propagator max_dt constraint"),
49            Self::ShuttingDown => write!(f, "world is shutting down"),
50        }
51    }
52}
53
54impl Error for StepError {
55    fn source(&self) -> Option<&(dyn Error + 'static)> {
56        match self {
57            Self::PropagatorFailed { reason, .. } => Some(reason),
58            _ => None,
59        }
60    }
61}
62
63/// Errors from individual propagator execution.
64///
65/// Returned by `Propagator::step()` and wrapped in
66/// [`StepError::PropagatorFailed`] by the tick engine.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub enum PropagatorError {
69    /// The propagator's step function failed
70    /// (`MURK_ERROR_PROPAGATOR_FAILED`).
71    ExecutionFailed {
72        /// Human-readable description of the failure.
73        reason: String,
74    },
75    /// NaN detected in propagator output (sentinel checking).
76    NanDetected {
77        /// The field containing the NaN.
78        field_id: crate::FieldId,
79        /// Index of the first NaN cell, if known.
80        cell_index: Option<usize>,
81    },
82    /// A user-defined constraint was violated.
83    ConstraintViolation {
84        /// Description of the violated constraint.
85        constraint: String,
86    },
87}
88
89impl fmt::Display for PropagatorError {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        match self {
92            Self::ExecutionFailed { reason } => write!(f, "execution failed: {reason}"),
93            Self::NanDetected {
94                field_id,
95                cell_index,
96            } => {
97                write!(f, "NaN detected in field {field_id}")?;
98                if let Some(idx) = cell_index {
99                    write!(f, " at cell {idx}")?;
100                }
101                Ok(())
102            }
103            Self::ConstraintViolation { constraint } => {
104                write!(f, "constraint violation: {constraint}")
105            }
106        }
107    }
108}
109
110impl Error for PropagatorError {}
111
112/// Errors from the ingress (command submission) pipeline.
113///
114/// Used in [`Receipt::reason_code`](crate::command::Receipt) to explain
115/// why a command was rejected.
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
117pub enum IngressError {
118    /// The command queue is at capacity (`MURK_ERROR_QUEUE_FULL`).
119    QueueFull,
120    /// The command's `basis_tick_id` is too old (`MURK_ERROR_STALE`).
121    Stale,
122    /// The tick was rolled back; commands were dropped
123    /// (`MURK_ERROR_TICK_ROLLBACK`).
124    TickRollback,
125    /// Ticking is disabled after consecutive rollbacks
126    /// (`MURK_ERROR_TICK_DISABLED`).
127    TickDisabled,
128    /// The world is shutting down (`MURK_ERROR_SHUTTING_DOWN`).
129    ShuttingDown,
130    /// The command type is not supported by the current tick executor
131    /// (`MURK_ERROR_UNSUPPORTED_COMMAND`).
132    UnsupportedCommand,
133    /// The command was accepted but could not be applied (e.g. invalid
134    /// coordinate or unknown field) (`MURK_ERROR_NOT_APPLIED`).
135    NotApplied,
136    /// The referenced entity does not exist, is dead, or has a stale generation.
137    UnknownEntity,
138    /// Entity capacity is full.
139    EntityCapacityFull,
140}
141
142impl fmt::Display for IngressError {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        match self {
145            Self::QueueFull => write!(f, "command queue full"),
146            Self::Stale => write!(f, "command basis_tick_id is stale"),
147            Self::TickRollback => write!(f, "tick rolled back"),
148            Self::TickDisabled => write!(f, "ticking disabled"),
149            Self::ShuttingDown => write!(f, "world is shutting down"),
150            Self::UnsupportedCommand => write!(f, "command type not supported"),
151            Self::NotApplied => write!(
152                f,
153                "command accepted but not applied (invalid coordinate or unknown field)"
154            ),
155            Self::UnknownEntity => write!(f, "unknown entity"),
156            Self::EntityCapacityFull => write!(f, "entity capacity full"),
157        }
158    }
159}
160
161impl Error for IngressError {}
162
163/// Errors from the observation (egress) pipeline.
164///
165/// Covers ObsPlan compilation, execution, and snapshot access failures.
166#[derive(Clone, Debug, PartialEq, Eq)]
167pub enum ObsError {
168    /// ObsPlan generation does not match the current snapshot
169    /// (`MURK_ERROR_PLAN_INVALIDATED`).
170    PlanInvalidated {
171        /// Description of the generation mismatch.
172        reason: String,
173    },
174    /// Exact-tick egress request timed out — RealtimeAsync only
175    /// (`MURK_ERROR_TIMEOUT_WAITING_FOR_TICK`).
176    TimeoutWaitingForTick,
177    /// Requested tick has been evicted from the ring buffer
178    /// (`MURK_ERROR_NOT_AVAILABLE`).
179    NotAvailable,
180    /// ObsPlan `valid_ratio` is below the 0.35 threshold
181    /// (`MURK_ERROR_INVALID_COMPOSITION`).
182    InvalidComposition {
183        /// Description of the composition issue.
184        reason: String,
185    },
186    /// ObsPlan execution failed mid-fill
187    /// (`MURK_ERROR_EXECUTION_FAILED`).
188    ExecutionFailed {
189        /// Description of the execution failure.
190        reason: String,
191    },
192    /// Malformed ObsSpec at compilation time
193    /// (`MURK_ERROR_INVALID_OBSSPEC`).
194    InvalidObsSpec {
195        /// Description of the spec issue.
196        reason: String,
197    },
198    /// Egress worker exceeded `max_epoch_hold`
199    /// (`MURK_ERROR_WORKER_STALLED`).
200    WorkerStalled,
201}
202
203impl fmt::Display for ObsError {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match self {
206            Self::PlanInvalidated { reason } => write!(f, "plan invalidated: {reason}"),
207            Self::TimeoutWaitingForTick => write!(f, "timeout waiting for tick"),
208            Self::NotAvailable => write!(f, "requested tick not available"),
209            Self::InvalidComposition { reason } => write!(f, "invalid composition: {reason}"),
210            Self::ExecutionFailed { reason } => write!(f, "execution failed: {reason}"),
211            Self::InvalidObsSpec { reason } => write!(f, "invalid obsspec: {reason}"),
212            Self::WorkerStalled => write!(f, "egress worker stalled"),
213        }
214    }
215}
216
217impl Error for ObsError {}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn ingress_error_not_applied_display() {
225        let msg = IngressError::NotApplied.to_string();
226        assert!(
227            msg.contains("not applied"),
228            "NotApplied Display must mention 'not applied': {msg}"
229        );
230    }
231}