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}
134
135impl fmt::Display for IngressError {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 match self {
138 Self::QueueFull => write!(f, "command queue full"),
139 Self::Stale => write!(f, "command basis_tick_id is stale"),
140 Self::TickRollback => write!(f, "tick rolled back"),
141 Self::TickDisabled => write!(f, "ticking disabled"),
142 Self::ShuttingDown => write!(f, "world is shutting down"),
143 Self::UnsupportedCommand => write!(f, "command type not supported"),
144 }
145 }
146}
147
148impl Error for IngressError {}
149
150/// Errors from the observation (egress) pipeline.
151///
152/// Covers ObsPlan compilation, execution, and snapshot access failures.
153#[derive(Clone, Debug, PartialEq, Eq)]
154pub enum ObsError {
155 /// ObsPlan generation does not match the current snapshot
156 /// (`MURK_ERROR_PLAN_INVALIDATED`).
157 PlanInvalidated {
158 /// Description of the generation mismatch.
159 reason: String,
160 },
161 /// Exact-tick egress request timed out — RealtimeAsync only
162 /// (`MURK_ERROR_TIMEOUT_WAITING_FOR_TICK`).
163 TimeoutWaitingForTick,
164 /// Requested tick has been evicted from the ring buffer
165 /// (`MURK_ERROR_NOT_AVAILABLE`).
166 NotAvailable,
167 /// ObsPlan `valid_ratio` is below the 0.35 threshold
168 /// (`MURK_ERROR_INVALID_COMPOSITION`).
169 InvalidComposition {
170 /// Description of the composition issue.
171 reason: String,
172 },
173 /// ObsPlan execution failed mid-fill
174 /// (`MURK_ERROR_EXECUTION_FAILED`).
175 ExecutionFailed {
176 /// Description of the execution failure.
177 reason: String,
178 },
179 /// Malformed ObsSpec at compilation time
180 /// (`MURK_ERROR_INVALID_OBSSPEC`).
181 InvalidObsSpec {
182 /// Description of the spec issue.
183 reason: String,
184 },
185 /// Egress worker exceeded `max_epoch_hold`
186 /// (`MURK_ERROR_WORKER_STALLED`).
187 WorkerStalled,
188}
189
190impl fmt::Display for ObsError {
191 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192 match self {
193 Self::PlanInvalidated { reason } => write!(f, "plan invalidated: {reason}"),
194 Self::TimeoutWaitingForTick => write!(f, "timeout waiting for tick"),
195 Self::NotAvailable => write!(f, "requested tick not available"),
196 Self::InvalidComposition { reason } => write!(f, "invalid composition: {reason}"),
197 Self::ExecutionFailed { reason } => write!(f, "execution failed: {reason}"),
198 Self::InvalidObsSpec { reason } => write!(f, "invalid obsspec: {reason}"),
199 Self::WorkerStalled => write!(f, "egress worker stalled"),
200 }
201 }
202}
203
204impl Error for ObsError {}