Skip to main content

rill_core/
error.rs

1//! # Rill Core error system
2//!
3//! Centralised error handling for the entire Rill ecosystem.
4//! Provides a hierarchy of error types with context and cross-level
5//! conversion.
6
7use std::error::Error as StdError;
8use std::fmt;
9
10// =============================================================================
11// Main error types
12// =============================================================================
13
14/// Primary error type for the entire Rill ecosystem.
15#[derive(Debug, Clone)]
16pub struct Error {
17    /// High-level error category for grouping.
18    pub category: ErrorCategory,
19    /// Machine-processable error code.
20    pub code: ErrorCode,
21    /// Human-readable error description.
22    pub message: String,
23    /// Optional chained cause (builder-style via [`Error::with_cause`]).
24    pub cause: Option<Box<Error>>,
25    /// Optional source location (attached via [`Error::at`]).
26    pub location: Option<ErrorLocation>,
27}
28
29/// Error category for grouping related error codes.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ErrorCategory {
32    /// Core errors (buffers, queues, basic types).
33    Core,
34    /// DSP errors (filters, effects, generators).
35    Dsp,
36    /// Graph errors (connections, topology).
37    Graph,
38    /// I/O errors (ALSA, JACK, PipeWire).
39    Io,
40    /// Control errors (OSC, automation).
41    Control,
42    /// Configuration errors.
43    Config,
44    /// Runtime errors.
45    Runtime,
46    /// Internal errors (should never occur).
47    Internal,
48}
49
50impl ErrorCategory {
51    /// Return the string representation of this category.
52    pub fn as_str(&self) -> &'static str {
53        match self {
54            ErrorCategory::Core => "core",
55            ErrorCategory::Dsp => "dsp",
56            ErrorCategory::Graph => "graph",
57            ErrorCategory::Io => "io",
58            ErrorCategory::Control => "control",
59            ErrorCategory::Config => "config",
60            ErrorCategory::Runtime => "runtime",
61            ErrorCategory::Internal => "internal",
62        }
63    }
64}
65
66impl fmt::Display for ErrorCategory {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(f, "{}", self.as_str())
69    }
70}
71
72/// Machine-processable error code.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ErrorCode {
75    // ── Core errors (0-99) ──────────────────────────────────────
76    /// Unknown or uncategorised error.
77    Unknown = 0,
78    /// Invalid parameter value.
79    InvalidParameter = 1,
80    /// Operation attempted in an invalid state.
81    InvalidState = 2,
82    /// Unsupported operation.
83    Unsupported = 3,
84    /// Feature not yet implemented.
85    NotImplemented = 4,
86    /// Operation timed out.
87    Timeout = 5,
88
89    // ── Buffer errors (100-119) ─────────────────────────────────
90    /// Buffer is full and cannot accept more data.
91    BufferFull = 100,
92    /// Buffer is empty and has no data to read.
93    BufferEmpty = 101,
94    /// Requested buffer size is invalid.
95    InvalidBufferSize = 102,
96    /// Buffer is misaligned for SIMD operations.
97    BufferMisaligned = 103,
98    /// Buffer has not been initialised yet.
99    BufferNotInitialized = 104,
100
101    // ── Queue errors (120-139) ──────────────────────────────────
102    /// Command or telemetry queue is full.
103    QueueFull = 120,
104    /// Queue is empty (no pending items).
105    QueueEmpty = 121,
106    /// Queue has been closed.
107    QueueClosed = 122,
108    /// Queue index is out of bounds.
109    InvalidQueueIndex = 123,
110
111    // ── Graph errors (200-299) ──────────────────────────────────
112    /// Referenced node does not exist in the graph.
113    NodeNotFound = 200,
114    /// Referenced port does not exist on the node.
115    PortNotFound = 201,
116    /// The requested connection is invalid.
117    InvalidConnection = 202,
118    /// A cycle was detected in the graph (forbidden in a DAG).
119    CycleDetected = 203,
120    /// Node with the same ID already exists.
121    NodeAlreadyExists = 204,
122    /// Port is already connected.
123    PortAlreadyConnected = 205,
124
125    // ── I/O errors (300-399) ────────────────────────────────────
126    /// I/O device not found.
127    DeviceNotFound = 300,
128    /// I/O device is busy.
129    DeviceBusy = 301,
130    /// ALSA-specific error.
131    AlsaError = 310,
132    /// JACK-specific error.
133    JackError = 311,
134    /// PipeWire-specific error.
135    PipeWireError = 312,
136    /// Buffer underrun or overrun.
137    XRun = 320,
138
139    // ── Control errors (400-499) ────────────────────────────────
140    /// OSC protocol error.
141    OscError = 401,
142    /// Control mapping not found.
143    MappingNotFound = 402,
144    /// Automaton instance not found.
145    AutomatonNotFound = 403,
146    /// Parameter value is outside the allowed range.
147    InvalidParameterValue = 404,
148
149    // ── Config errors (500-599) ─────────────────────────────────
150    /// Configuration path not found.
151    ConfigNotFound = 500,
152    /// Configuration format is invalid.
153    InvalidConfigFormat = 501,
154    /// Required field is missing from configuration.
155    MissingField = 502,
156
157    // ── Runtime errors (600-699) ────────────────────────────────
158    /// Real-time safety violation detected.
159    RealtimeViolation = 600,
160    /// Failed to set thread priority for RT scheduling.
161    PriorityError = 601,
162    /// Operation failed because the component is already running.
163    AlreadyRunning = 602,
164    /// Operation failed because the component is not running.
165    NotRunning = 603,
166}
167
168impl ErrorCode {
169    /// Return the error category for this code.
170    pub fn category(&self) -> ErrorCategory {
171        match *self {
172            ErrorCode::Unknown
173            | ErrorCode::InvalidParameter
174            | ErrorCode::InvalidState
175            | ErrorCode::Unsupported
176            | ErrorCode::NotImplemented
177            | ErrorCode::Timeout
178            | ErrorCode::BufferFull
179            | ErrorCode::BufferEmpty
180            | ErrorCode::InvalidBufferSize
181            | ErrorCode::BufferMisaligned
182            | ErrorCode::BufferNotInitialized
183            | ErrorCode::QueueFull
184            | ErrorCode::QueueEmpty
185            | ErrorCode::QueueClosed
186            | ErrorCode::InvalidQueueIndex => ErrorCategory::Core,
187
188            ErrorCode::NodeNotFound
189            | ErrorCode::PortNotFound
190            | ErrorCode::InvalidConnection
191            | ErrorCode::CycleDetected
192            | ErrorCode::NodeAlreadyExists
193            | ErrorCode::PortAlreadyConnected => ErrorCategory::Graph,
194
195            ErrorCode::DeviceNotFound
196            | ErrorCode::DeviceBusy
197            | ErrorCode::AlsaError
198            | ErrorCode::JackError
199            | ErrorCode::PipeWireError
200            | ErrorCode::XRun => ErrorCategory::Io,
201
202            ErrorCode::OscError
203            | ErrorCode::MappingNotFound
204            | ErrorCode::AutomatonNotFound
205            | ErrorCode::InvalidParameterValue => ErrorCategory::Control,
206
207            ErrorCode::ConfigNotFound
208            | ErrorCode::InvalidConfigFormat
209            | ErrorCode::MissingField => ErrorCategory::Config,
210
211            ErrorCode::RealtimeViolation
212            | ErrorCode::PriorityError
213            | ErrorCode::AlreadyRunning
214            | ErrorCode::NotRunning => ErrorCategory::Runtime,
215        }
216    }
217
218    /// Return a human-readable description of this error code.
219    pub fn description(&self) -> &'static str {
220        match self {
221            ErrorCode::Unknown => "Unknown error",
222            ErrorCode::InvalidParameter => "Invalid parameter",
223            ErrorCode::InvalidState => "Invalid state",
224            ErrorCode::Unsupported => "Unsupported operation",
225            ErrorCode::NotImplemented => "Not implemented",
226            ErrorCode::Timeout => "Operation timed out",
227
228            ErrorCode::BufferFull => "Buffer is full",
229            ErrorCode::BufferEmpty => "Buffer is empty",
230            ErrorCode::InvalidBufferSize => "Invalid buffer size",
231            ErrorCode::BufferMisaligned => "Buffer is misaligned for SIMD operations",
232            ErrorCode::BufferNotInitialized => "Buffer not initialized",
233
234            ErrorCode::QueueFull => "Queue is full",
235            ErrorCode::QueueEmpty => "Queue is empty",
236            ErrorCode::QueueClosed => "Queue is closed",
237            ErrorCode::InvalidQueueIndex => "Invalid queue index",
238
239            ErrorCode::NodeNotFound => "Node not found",
240            ErrorCode::PortNotFound => "Port not found",
241            ErrorCode::InvalidConnection => "Invalid connection",
242            ErrorCode::CycleDetected => "Cycle detected in graph",
243            ErrorCode::NodeAlreadyExists => "Node already exists",
244            ErrorCode::PortAlreadyConnected => "Port already connected",
245
246            ErrorCode::DeviceNotFound => "Device not found",
247            ErrorCode::DeviceBusy => "Device is busy",
248            ErrorCode::AlsaError => "ALSA error",
249            ErrorCode::JackError => "JACK error",
250            ErrorCode::PipeWireError => "PipeWire error",
251            ErrorCode::XRun => "Buffer underrun/overrun detected",
252
253            ErrorCode::OscError => "OSC error",
254            ErrorCode::MappingNotFound => "Mapping not found",
255            ErrorCode::AutomatonNotFound => "Automaton not found",
256            ErrorCode::InvalidParameterValue => "Invalid parameter value",
257
258            ErrorCode::ConfigNotFound => "Configuration not found",
259            ErrorCode::InvalidConfigFormat => "Invalid configuration format",
260            ErrorCode::MissingField => "Missing required field",
261
262            ErrorCode::RealtimeViolation => "Real-time violation detected",
263            ErrorCode::PriorityError => "Failed to set thread priority",
264            ErrorCode::AlreadyRunning => "Already running",
265            ErrorCode::NotRunning => "Not running",
266        }
267    }
268}
269
270/// Source location where an error originated.
271#[derive(Debug, Clone)]
272pub struct ErrorLocation {
273    /// Source file name.
274    pub file: &'static str,
275    /// Line number in the source file.
276    pub line: u32,
277    /// Column number in the source file.
278    pub column: u32,
279}
280
281impl fmt::Display for ErrorLocation {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        write!(f, "{}:{}:{}", self.file, self.line, self.column)
284    }
285}
286
287// =============================================================================
288// Error implementation
289// =============================================================================
290
291impl Error {
292    /// Create a new error with the given code and message.
293    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
294        Self {
295            category: code.category(),
296            code,
297            message: message.into(),
298            cause: None,
299            location: None,
300        }
301    }
302
303    /// Add a cause to this error (builder-style).
304    pub fn with_cause(mut self, cause: Error) -> Self {
305        self.cause = Some(Box::new(cause));
306        self
307    }
308
309    /// Attach source location info (builder-style).
310    pub fn at(mut self, file: &'static str, line: u32, column: u32) -> Self {
311        self.location = Some(ErrorLocation { file, line, column });
312        self
313    }
314
315    /// Walk the cause chain to find the root cause.
316    pub fn root_cause(&self) -> &Error {
317        let mut current = self;
318        while let Some(cause) = &current.cause {
319            current = cause;
320        }
321        current
322    }
323
324    /// Whether this error is critical for a real-time thread.
325    pub fn is_realtime_critical(&self) -> bool {
326        matches!(
327            self.code,
328            ErrorCode::RealtimeViolation
329                | ErrorCode::PriorityError
330                | ErrorCode::BufferFull
331                | ErrorCode::XRun
332        )
333    }
334
335    /// Whether this error is recoverable (non-critical).
336    pub fn is_recoverable(&self) -> bool {
337        !self.is_realtime_critical()
338    }
339}
340
341impl fmt::Display for Error {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        if let Some(loc) = &self.location {
344            write!(
345                f,
346                "[{}] at {}: {} ({})",
347                self.category,
348                loc,
349                self.message,
350                self.code.description()
351            )?;
352        } else {
353            write!(
354                f,
355                "[{}]: {} ({})",
356                self.category,
357                self.message,
358                self.code.description()
359            )?;
360        }
361
362        if let Some(cause) = &self.cause {
363            write!(f, "\n  caused by: {}", cause)?;
364        }
365
366        Ok(())
367    }
368}
369
370impl StdError for Error {
371    fn source(&self) -> Option<&(dyn StdError + 'static)> {
372        self.cause.as_ref().map(|c| c as &dyn StdError)
373    }
374}
375
376// =============================================================================
377// Result type
378// =============================================================================
379
380/// Result type alias for Rill Core operations.
381pub type Result<T> = std::result::Result<T, Error>;
382
383// =============================================================================
384// Conversion from standard errors
385// =============================================================================
386
387impl From<std::io::Error> for Error {
388    fn from(err: std::io::Error) -> Self {
389        Error::new(ErrorCode::Unknown, err.to_string())
390    }
391}
392
393impl From<std::num::ParseIntError> for Error {
394    fn from(err: std::num::ParseIntError) -> Self {
395        Error::new(ErrorCode::InvalidParameter, err.to_string())
396    }
397}
398
399impl From<std::num::ParseFloatError> for Error {
400    fn from(err: std::num::ParseFloatError) -> Self {
401        Error::new(ErrorCode::InvalidParameter, err.to_string())
402    }
403}
404
405impl From<std::str::Utf8Error> for Error {
406    fn from(err: std::str::Utf8Error) -> Self {
407        Error::new(ErrorCode::InvalidParameter, err.to_string())
408    }
409}
410
411// =============================================================================
412// Macros for convenient error creation
413// =============================================================================
414
415/// Create an error with a code and message.
416#[macro_export]
417macro_rules! error {
418    ($code:expr, $msg:expr) => {
419        $crate::error::Error::new($code, $msg)
420    };
421    ($code:expr, $fmt:expr, $($arg:tt)*) => {
422        $crate::error::Error::new($code, format!($fmt, $($arg)*))
423    };
424}
425
426/// Create an error with source location attached.
427#[macro_export]
428macro_rules! error_at {
429    ($code:expr, $msg:expr) => {
430        $crate::error::Error::new($code, $msg).at(file!(), line!(), column!())
431    };
432    ($code:expr, $fmt:expr, $($arg:tt)*) => {
433        $crate::error::Error::new($code, format!($fmt, $($arg)*))
434            .at(file!(), line!(), column!())
435    };
436}
437
438/// Return early with an error (convenience for `return Err(...)`).
439#[macro_export]
440macro_rules! bail {
441    ($code:expr, $msg:expr) => {
442        return Err($crate::error::Error::new($code, $msg))
443    };
444    ($code:expr, $fmt:expr, $($arg:tt)*) => {
445        return Err($crate::error::Error::new($code, format!($fmt, $($arg)*)))
446    };
447}
448
449/// Transform a `Result` by mapping the error with additional context.
450#[macro_export]
451macro_rules! context {
452    ($expr:expr, $code:expr, $msg:expr) => {
453        $expr.map_err(|e| $crate::error::Error::new($code, $msg).with_cause(e))
454    };
455    ($expr:expr, $code:expr, $fmt:expr, $($arg:tt)*) => {
456        $expr.map_err(|e| $crate::error::Error::new($code, format!($fmt, $($arg)*)).with_cause(e))
457    };
458}
459
460// =============================================================================
461// Specialized error types for different components
462// =============================================================================
463
464/// I/O error constructors.
465pub mod io {
466    #![allow(unused)]
467    use super::*;
468
469    /// Create a `DeviceNotFound` error.
470    pub fn device_not_found(name: &str) -> Error {
471        error!(ErrorCode::DeviceNotFound, "Device not found: {}", name)
472    }
473
474    /// Create a `DeviceBusy` error.
475    pub fn device_busy(name: &str) -> Error {
476        error!(ErrorCode::DeviceBusy, "Device is busy: {}", name)
477    }
478
479    /// Create an `AlsaError` with a description.
480    pub fn alsa_error(desc: &str) -> Error {
481        error!(ErrorCode::AlsaError, "ALSA error: {}", desc)
482    }
483
484    /// Create a `JackError` with a description.
485    pub fn jack_error(desc: &str) -> Error {
486        error!(ErrorCode::JackError, "JACK error: {}", desc)
487    }
488
489    /// Create a `PipeWireError` with a description.
490    pub fn pipewire_error(desc: &str) -> Error {
491        error!(ErrorCode::PipeWireError, "PipeWire error: {}", desc)
492    }
493
494    /// Create an `XRun` (buffer underrun/overrun) error.
495    pub fn xrun() -> Error {
496        Error::new(ErrorCode::XRun, "Buffer underrun/overrun detected")
497    }
498}
499
500/// Control error constructors (OSC, automation).
501pub mod control {
502    use super::*;
503
504    /// Create an `OscError` with a description.
505    pub fn osc_error(desc: &str) -> Error {
506        error!(ErrorCode::OscError, "OSC error: {}", desc)
507    }
508
509    /// Create a `MappingNotFound` error for the given mapping ID.
510    pub fn mapping_not_found(id: &str) -> Error {
511        error!(ErrorCode::MappingNotFound, "Mapping not found: {}", id)
512    }
513
514    /// Create an `AutomatonNotFound` error for the given automaton ID.
515    pub fn automaton_not_found(id: &str) -> Error {
516        error!(ErrorCode::AutomatonNotFound, "Automaton not found: {}", id)
517    }
518
519    /// Create an `InvalidParameterValue` error for a value outside the allowed range.
520    pub fn invalid_parameter_value(param: &str, value: f64, min: f64, max: f64) -> Error {
521        error!(
522            ErrorCode::InvalidParameterValue,
523            "Invalid value for parameter {}: {} (allowed range: {} - {})", param, value, min, max
524        )
525    }
526}
527
528/// Configuration error constructors.
529pub mod config {
530    use super::*;
531
532    /// Create a `ConfigNotFound` error for the given path.
533    pub fn not_found(path: &str) -> Error {
534        error!(
535            ErrorCode::ConfigNotFound,
536            "Configuration not found: {}", path
537        )
538    }
539
540    /// Create an `InvalidConfigFormat` error with details.
541    pub fn invalid_format(details: &str) -> Error {
542        error!(
543            ErrorCode::InvalidConfigFormat,
544            "Invalid configuration format: {}", details
545        )
546    }
547
548    /// Create a `MissingField` error for the required field name.
549    pub fn missing_field(field: &str) -> Error {
550        error!(ErrorCode::MissingField, "Missing required field: {}", field)
551    }
552}
553
554/// Runtime error constructors (thread priority, critical violations).
555pub mod runtime {
556    use super::*;
557
558    /// Create a `RealtimeViolation` error with details.
559    pub fn realtime_violation(details: &str) -> Error {
560        error!(
561            ErrorCode::RealtimeViolation,
562            "Real-time violation: {}", details
563        )
564    }
565
566    /// Create a `PriorityError` with details about the failure.
567    pub fn priority_error(details: &str) -> Error {
568        error!(
569            ErrorCode::PriorityError,
570            "Failed to set thread priority: {}", details
571        )
572    }
573
574    /// Create an `AlreadyRunning` error.
575    pub fn already_running() -> Error {
576        Error::new(ErrorCode::AlreadyRunning, "Already running")
577    }
578
579    /// Create a `NotRunning` error.
580    pub fn not_running() -> Error {
581        Error::new(ErrorCode::NotRunning, "Not running")
582    }
583}
584
585// =============================================================================
586// Tests
587// =============================================================================
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592
593    #[test]
594    fn test_error_creation() {
595        let err = Error::new(ErrorCode::BufferFull, "Test error");
596        assert_eq!(err.code, ErrorCode::BufferFull);
597        assert_eq!(err.message, "Test error");
598        assert_eq!(err.category, ErrorCategory::Core);
599    }
600
601    #[test]
602    fn test_error_with_cause() {
603        let cause = Error::new(ErrorCode::BufferEmpty, "Cause");
604        let err = Error::new(ErrorCode::BufferFull, "Main error").with_cause(cause);
605
606        assert!(err.cause.is_some());
607        assert_eq!(err.root_cause().code, ErrorCode::BufferEmpty);
608    }
609
610    #[test]
611    fn test_error_macros() {
612        let err = error!(ErrorCode::BufferFull, "Buffer is full");
613        assert_eq!(err.code, ErrorCode::BufferFull);
614
615        let err = error!(ErrorCode::BufferFull, "Buffer {} is full", "test");
616        assert_eq!(err.message, "Buffer test is full");
617    }
618
619    #[test]
620    fn test_specialized_errors() {
621        let err = io::device_not_found("hw:0");
622        assert_eq!(err.code, ErrorCode::DeviceNotFound);
623        assert!(err.message.contains("hw:0"));
624    }
625
626    #[test]
627    fn test_error_category() {
628        assert_eq!(ErrorCode::BufferFull.category(), ErrorCategory::Core);
629        assert_eq!(ErrorCode::NodeNotFound.category(), ErrorCategory::Graph);
630        assert_eq!(ErrorCode::AlsaError.category(), ErrorCategory::Io);
631        assert_eq!(ErrorCode::OscError.category(), ErrorCategory::Control);
632        assert_eq!(ErrorCode::ConfigNotFound.category(), ErrorCategory::Config);
633        assert_eq!(
634            ErrorCode::RealtimeViolation.category(),
635            ErrorCategory::Runtime
636        );
637    }
638
639    #[test]
640    fn test_realtime_critical() {
641        assert!(io::xrun().is_realtime_critical());
642        assert!(runtime::realtime_violation("test").is_realtime_critical());
643
644        assert!(!config::not_found("test").is_realtime_critical());
645    }
646}