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 (MIDI, 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    /// Audio device not found.
127    DeviceNotFound = 300,
128    /// Audio 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    /// MIDI protocol error.
141    MidiError = 400,
142    /// OSC protocol error.
143    OscError = 401,
144    /// Control mapping not found.
145    MappingNotFound = 402,
146    /// Automaton instance not found.
147    AutomatonNotFound = 403,
148    /// Parameter value is outside the allowed range.
149    InvalidParameterValue = 404,
150
151    // ── Config errors (500-599) ─────────────────────────────────
152    /// Configuration path not found.
153    ConfigNotFound = 500,
154    /// Configuration format is invalid.
155    InvalidConfigFormat = 501,
156    /// Required field is missing from configuration.
157    MissingField = 502,
158
159    // ── Runtime errors (600-699) ────────────────────────────────
160    /// Real-time safety violation detected.
161    RealtimeViolation = 600,
162    /// Failed to set thread priority for RT scheduling.
163    PriorityError = 601,
164    /// Operation failed because the component is already running.
165    AlreadyRunning = 602,
166    /// Operation failed because the component is not running.
167    NotRunning = 603,
168}
169
170impl ErrorCode {
171    /// Return the error category for this code.
172    pub fn category(&self) -> ErrorCategory {
173        match *self {
174            ErrorCode::Unknown
175            | ErrorCode::InvalidParameter
176            | ErrorCode::InvalidState
177            | ErrorCode::Unsupported
178            | ErrorCode::NotImplemented
179            | ErrorCode::Timeout
180            | ErrorCode::BufferFull
181            | ErrorCode::BufferEmpty
182            | ErrorCode::InvalidBufferSize
183            | ErrorCode::BufferMisaligned
184            | ErrorCode::BufferNotInitialized
185            | ErrorCode::QueueFull
186            | ErrorCode::QueueEmpty
187            | ErrorCode::QueueClosed
188            | ErrorCode::InvalidQueueIndex => ErrorCategory::Core,
189
190            ErrorCode::NodeNotFound
191            | ErrorCode::PortNotFound
192            | ErrorCode::InvalidConnection
193            | ErrorCode::CycleDetected
194            | ErrorCode::NodeAlreadyExists
195            | ErrorCode::PortAlreadyConnected => ErrorCategory::Graph,
196
197            ErrorCode::DeviceNotFound
198            | ErrorCode::DeviceBusy
199            | ErrorCode::AlsaError
200            | ErrorCode::JackError
201            | ErrorCode::PipeWireError
202            | ErrorCode::XRun => ErrorCategory::Io,
203
204            ErrorCode::MidiError
205            | ErrorCode::OscError
206            | ErrorCode::MappingNotFound
207            | ErrorCode::AutomatonNotFound
208            | ErrorCode::InvalidParameterValue => ErrorCategory::Control,
209
210            ErrorCode::ConfigNotFound
211            | ErrorCode::InvalidConfigFormat
212            | ErrorCode::MissingField => ErrorCategory::Config,
213
214            ErrorCode::RealtimeViolation
215            | ErrorCode::PriorityError
216            | ErrorCode::AlreadyRunning
217            | ErrorCode::NotRunning => ErrorCategory::Runtime,
218        }
219    }
220
221    /// Return a human-readable description of this error code.
222    pub fn description(&self) -> &'static str {
223        match self {
224            ErrorCode::Unknown => "Unknown error",
225            ErrorCode::InvalidParameter => "Invalid parameter",
226            ErrorCode::InvalidState => "Invalid state",
227            ErrorCode::Unsupported => "Unsupported operation",
228            ErrorCode::NotImplemented => "Not implemented",
229            ErrorCode::Timeout => "Operation timed out",
230
231            ErrorCode::BufferFull => "Buffer is full",
232            ErrorCode::BufferEmpty => "Buffer is empty",
233            ErrorCode::InvalidBufferSize => "Invalid buffer size",
234            ErrorCode::BufferMisaligned => "Buffer is misaligned for SIMD operations",
235            ErrorCode::BufferNotInitialized => "Buffer not initialized",
236
237            ErrorCode::QueueFull => "Queue is full",
238            ErrorCode::QueueEmpty => "Queue is empty",
239            ErrorCode::QueueClosed => "Queue is closed",
240            ErrorCode::InvalidQueueIndex => "Invalid queue index",
241
242            ErrorCode::NodeNotFound => "Node not found",
243            ErrorCode::PortNotFound => "Port not found",
244            ErrorCode::InvalidConnection => "Invalid connection",
245            ErrorCode::CycleDetected => "Cycle detected in graph",
246            ErrorCode::NodeAlreadyExists => "Node already exists",
247            ErrorCode::PortAlreadyConnected => "Port already connected",
248
249            ErrorCode::DeviceNotFound => "Device not found",
250            ErrorCode::DeviceBusy => "Device is busy",
251            ErrorCode::AlsaError => "ALSA error",
252            ErrorCode::JackError => "JACK error",
253            ErrorCode::PipeWireError => "PipeWire error",
254            ErrorCode::XRun => "Buffer underrun/overrun detected",
255
256            ErrorCode::MidiError => "MIDI error",
257            ErrorCode::OscError => "OSC error",
258            ErrorCode::MappingNotFound => "Mapping not found",
259            ErrorCode::AutomatonNotFound => "Automaton not found",
260            ErrorCode::InvalidParameterValue => "Invalid parameter value",
261
262            ErrorCode::ConfigNotFound => "Configuration not found",
263            ErrorCode::InvalidConfigFormat => "Invalid configuration format",
264            ErrorCode::MissingField => "Missing required field",
265
266            ErrorCode::RealtimeViolation => "Real-time violation detected",
267            ErrorCode::PriorityError => "Failed to set thread priority",
268            ErrorCode::AlreadyRunning => "Already running",
269            ErrorCode::NotRunning => "Not running",
270        }
271    }
272}
273
274/// Source location where an error originated.
275#[derive(Debug, Clone)]
276pub struct ErrorLocation {
277    /// Source file name.
278    pub file: &'static str,
279    /// Line number in the source file.
280    pub line: u32,
281    /// Column number in the source file.
282    pub column: u32,
283}
284
285impl fmt::Display for ErrorLocation {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        write!(f, "{}:{}:{}", self.file, self.line, self.column)
288    }
289}
290
291// =============================================================================
292// Error implementation
293// =============================================================================
294
295impl Error {
296    /// Create a new error with the given code and message.
297    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
298        Self {
299            category: code.category(),
300            code,
301            message: message.into(),
302            cause: None,
303            location: None,
304        }
305    }
306
307    /// Add a cause to this error (builder-style).
308    pub fn with_cause(mut self, cause: Error) -> Self {
309        self.cause = Some(Box::new(cause));
310        self
311    }
312
313    /// Attach source location info (builder-style).
314    pub fn at(mut self, file: &'static str, line: u32, column: u32) -> Self {
315        self.location = Some(ErrorLocation { file, line, column });
316        self
317    }
318
319    /// Walk the cause chain to find the root cause.
320    pub fn root_cause(&self) -> &Error {
321        let mut current = self;
322        while let Some(cause) = &current.cause {
323            current = cause;
324        }
325        current
326    }
327
328    /// Whether this error is critical for a real-time thread.
329    pub fn is_realtime_critical(&self) -> bool {
330        matches!(
331            self.code,
332            ErrorCode::RealtimeViolation
333                | ErrorCode::PriorityError
334                | ErrorCode::BufferFull
335                | ErrorCode::XRun
336        )
337    }
338
339    /// Whether this error is recoverable (non-critical).
340    pub fn is_recoverable(&self) -> bool {
341        !self.is_realtime_critical()
342    }
343}
344
345impl fmt::Display for Error {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        if let Some(loc) = &self.location {
348            write!(
349                f,
350                "[{}] at {}: {} ({})",
351                self.category,
352                loc,
353                self.message,
354                self.code.description()
355            )?;
356        } else {
357            write!(
358                f,
359                "[{}]: {} ({})",
360                self.category,
361                self.message,
362                self.code.description()
363            )?;
364        }
365
366        if let Some(cause) = &self.cause {
367            write!(f, "\n  caused by: {}", cause)?;
368        }
369
370        Ok(())
371    }
372}
373
374impl StdError for Error {
375    fn source(&self) -> Option<&(dyn StdError + 'static)> {
376        self.cause.as_ref().map(|c| c as &dyn StdError)
377    }
378}
379
380// =============================================================================
381// Result type
382// =============================================================================
383
384/// Result type alias for Rill Core operations.
385pub type Result<T> = std::result::Result<T, Error>;
386
387// =============================================================================
388// Conversion from standard errors
389// =============================================================================
390
391impl From<std::io::Error> for Error {
392    fn from(err: std::io::Error) -> Self {
393        Error::new(ErrorCode::Unknown, err.to_string())
394    }
395}
396
397impl From<std::num::ParseIntError> for Error {
398    fn from(err: std::num::ParseIntError) -> Self {
399        Error::new(ErrorCode::InvalidParameter, err.to_string())
400    }
401}
402
403impl From<std::num::ParseFloatError> for Error {
404    fn from(err: std::num::ParseFloatError) -> Self {
405        Error::new(ErrorCode::InvalidParameter, err.to_string())
406    }
407}
408
409impl From<std::str::Utf8Error> for Error {
410    fn from(err: std::str::Utf8Error) -> Self {
411        Error::new(ErrorCode::InvalidParameter, err.to_string())
412    }
413}
414
415// =============================================================================
416// Macros for convenient error creation
417// =============================================================================
418
419/// Create an error with a code and message.
420#[macro_export]
421macro_rules! error {
422    ($code:expr, $msg:expr) => {
423        $crate::error::Error::new($code, $msg)
424    };
425    ($code:expr, $fmt:expr, $($arg:tt)*) => {
426        $crate::error::Error::new($code, format!($fmt, $($arg)*))
427    };
428}
429
430/// Create an error with source location attached.
431#[macro_export]
432macro_rules! error_at {
433    ($code:expr, $msg:expr) => {
434        $crate::error::Error::new($code, $msg).at(file!(), line!(), column!())
435    };
436    ($code:expr, $fmt:expr, $($arg:tt)*) => {
437        $crate::error::Error::new($code, format!($fmt, $($arg)*))
438            .at(file!(), line!(), column!())
439    };
440}
441
442/// Return early with an error (convenience for `return Err(...)`).
443#[macro_export]
444macro_rules! bail {
445    ($code:expr, $msg:expr) => {
446        return Err($crate::error::Error::new($code, $msg))
447    };
448    ($code:expr, $fmt:expr, $($arg:tt)*) => {
449        return Err($crate::error::Error::new($code, format!($fmt, $($arg)*)))
450    };
451}
452
453/// Transform a `Result` by mapping the error with additional context.
454#[macro_export]
455macro_rules! context {
456    ($expr:expr, $code:expr, $msg:expr) => {
457        $expr.map_err(|e| $crate::error::Error::new($code, $msg).with_cause(e))
458    };
459    ($expr:expr, $code:expr, $fmt:expr, $($arg:tt)*) => {
460        $expr.map_err(|e| $crate::error::Error::new($code, format!($fmt, $($arg)*)).with_cause(e))
461    };
462}
463
464// =============================================================================
465// Specialized error types for different components
466// =============================================================================
467
468/// I/O error constructors.
469pub mod io {
470    #![allow(unused)]
471    use super::*;
472
473    /// Create a `DeviceNotFound` error.
474    pub fn device_not_found(name: &str) -> Error {
475        error!(ErrorCode::DeviceNotFound, "Device not found: {}", name)
476    }
477
478    /// Create a `DeviceBusy` error.
479    pub fn device_busy(name: &str) -> Error {
480        error!(ErrorCode::DeviceBusy, "Device is busy: {}", name)
481    }
482
483    /// Create an `AlsaError` with a description.
484    pub fn alsa_error(desc: &str) -> Error {
485        error!(ErrorCode::AlsaError, "ALSA error: {}", desc)
486    }
487
488    /// Create a `JackError` with a description.
489    pub fn jack_error(desc: &str) -> Error {
490        error!(ErrorCode::JackError, "JACK error: {}", desc)
491    }
492
493    /// Create a `PipeWireError` with a description.
494    pub fn pipewire_error(desc: &str) -> Error {
495        error!(ErrorCode::PipeWireError, "PipeWire error: {}", desc)
496    }
497
498    /// Create an `XRun` (buffer underrun/overrun) error.
499    pub fn xrun() -> Error {
500        Error::new(ErrorCode::XRun, "Buffer underrun/overrun detected")
501    }
502}
503
504/// Control error constructors (MIDI, OSC, automation).
505pub mod control {
506    use super::*;
507
508    /// Create a `MidiError` with a description.
509    pub fn midi_error(desc: &str) -> Error {
510        error!(ErrorCode::MidiError, "MIDI error: {}", desc)
511    }
512
513    /// Create an `OscError` with a description.
514    pub fn osc_error(desc: &str) -> Error {
515        error!(ErrorCode::OscError, "OSC error: {}", desc)
516    }
517
518    /// Create a `MappingNotFound` error for the given mapping ID.
519    pub fn mapping_not_found(id: &str) -> Error {
520        error!(ErrorCode::MappingNotFound, "Mapping not found: {}", id)
521    }
522
523    /// Create an `AutomatonNotFound` error for the given automaton ID.
524    pub fn automaton_not_found(id: &str) -> Error {
525        error!(ErrorCode::AutomatonNotFound, "Automaton not found: {}", id)
526    }
527
528    /// Create an `InvalidParameterValue` error for a value outside the allowed range.
529    pub fn invalid_parameter_value(param: &str, value: f64, min: f64, max: f64) -> Error {
530        error!(
531            ErrorCode::InvalidParameterValue,
532            "Invalid value for parameter {}: {} (allowed range: {} - {})", param, value, min, max
533        )
534    }
535}
536
537/// Configuration error constructors.
538pub mod config {
539    use super::*;
540
541    /// Create a `ConfigNotFound` error for the given path.
542    pub fn not_found(path: &str) -> Error {
543        error!(
544            ErrorCode::ConfigNotFound,
545            "Configuration not found: {}", path
546        )
547    }
548
549    /// Create an `InvalidConfigFormat` error with details.
550    pub fn invalid_format(details: &str) -> Error {
551        error!(
552            ErrorCode::InvalidConfigFormat,
553            "Invalid configuration format: {}", details
554        )
555    }
556
557    /// Create a `MissingField` error for the required field name.
558    pub fn missing_field(field: &str) -> Error {
559        error!(ErrorCode::MissingField, "Missing required field: {}", field)
560    }
561}
562
563/// Runtime error constructors (thread priority, critical violations).
564pub mod runtime {
565    use super::*;
566
567    /// Create a `RealtimeViolation` error with details.
568    pub fn realtime_violation(details: &str) -> Error {
569        error!(
570            ErrorCode::RealtimeViolation,
571            "Real-time violation: {}", details
572        )
573    }
574
575    /// Create a `PriorityError` with details about the failure.
576    pub fn priority_error(details: &str) -> Error {
577        error!(
578            ErrorCode::PriorityError,
579            "Failed to set thread priority: {}", details
580        )
581    }
582
583    /// Create an `AlreadyRunning` error.
584    pub fn already_running() -> Error {
585        Error::new(ErrorCode::AlreadyRunning, "Already running")
586    }
587
588    /// Create a `NotRunning` error.
589    pub fn not_running() -> Error {
590        Error::new(ErrorCode::NotRunning, "Not running")
591    }
592}
593
594// =============================================================================
595// Tests
596// =============================================================================
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn test_error_creation() {
604        let err = Error::new(ErrorCode::BufferFull, "Test error");
605        assert_eq!(err.code, ErrorCode::BufferFull);
606        assert_eq!(err.message, "Test error");
607        assert_eq!(err.category, ErrorCategory::Core);
608    }
609
610    #[test]
611    fn test_error_with_cause() {
612        let cause = Error::new(ErrorCode::BufferEmpty, "Cause");
613        let err = Error::new(ErrorCode::BufferFull, "Main error").with_cause(cause);
614
615        assert!(err.cause.is_some());
616        assert_eq!(err.root_cause().code, ErrorCode::BufferEmpty);
617    }
618
619    #[test]
620    fn test_error_macros() {
621        let err = error!(ErrorCode::BufferFull, "Buffer is full");
622        assert_eq!(err.code, ErrorCode::BufferFull);
623
624        let err = error!(ErrorCode::BufferFull, "Buffer {} is full", "test");
625        assert_eq!(err.message, "Buffer test is full");
626    }
627
628    #[test]
629    fn test_specialized_errors() {
630        let err = io::device_not_found("hw:0");
631        assert_eq!(err.code, ErrorCode::DeviceNotFound);
632        assert!(err.message.contains("hw:0"));
633    }
634
635    #[test]
636    fn test_error_category() {
637        assert_eq!(ErrorCode::BufferFull.category(), ErrorCategory::Core);
638        assert_eq!(ErrorCode::NodeNotFound.category(), ErrorCategory::Graph);
639        assert_eq!(ErrorCode::AlsaError.category(), ErrorCategory::Io);
640        assert_eq!(ErrorCode::MidiError.category(), ErrorCategory::Control);
641        assert_eq!(ErrorCode::ConfigNotFound.category(), ErrorCategory::Config);
642        assert_eq!(
643            ErrorCode::RealtimeViolation.category(),
644            ErrorCategory::Runtime
645        );
646    }
647
648    #[test]
649    fn test_realtime_critical() {
650        assert!(io::xrun().is_realtime_critical());
651        assert!(runtime::realtime_violation("test").is_realtime_critical());
652
653        assert!(!config::not_found("test").is_realtime_critical());
654    }
655}