Skip to main content

rill_core/traits/
error.rs

1//! # Error Types for Rill Traits
2//!
3//! This module defines the error types used throughout the Rill ecosystem.
4//! All errors implement `std::error::Error` and are designed to be:
5//! - Thread-safe (`Send + Sync`)
6//! - Cloneable for passing between threads
7//! - Human-readable with detailed context
8//! - Real-time safe (no allocations in error paths)
9
10use std::fmt;
11use thiserror::Error;
12
13// ============================================================================
14// Core Process Error
15// ============================================================================
16
17/// Main error type for signal processing operations
18///
19/// This error can occur during node processing, parameter changes,
20/// or any other operation in the signal graph.
21#[derive(Error, Debug, Clone, PartialEq)]
22pub enum ProcessError {
23    /// Error during signal processing
24    #[error("Processing error: {0}")]
25    Processing(String),
26
27    /// Error with a parameter (invalid value, out of range, etc.)
28    #[error("Parameter error: {0}")]
29    Parameter(String),
30
31    /// Invalid port access
32    #[error("Invalid port: {0}")]
33    InvalidPort(String),
34
35    /// Buffer operation failed
36    #[error("Buffer error: {0}")]
37    Buffer(String),
38
39    /// Node not found
40    #[error("Node {0} not found")]
41    NodeNotFound(u32),
42
43    /// Port not found
44    #[error("Port {0} not found")]
45    PortNotFound(String),
46
47    /// Connection error
48    #[error("Connection error: {0}")]
49    Connection(String),
50
51    /// Type mismatch (e.g., trying to connect audio to control)
52    #[error("Type mismatch: expected {expected}, got {got}")]
53    TypeMismatch {
54        /// Expected type
55        expected: &'static str,
56        /// Actual type
57        got: &'static str,
58    },
59
60    /// Sample rate mismatch
61    #[error("Sample rate mismatch: expected {expected}, got {got}")]
62    SampleRateMismatch {
63        /// Expected sample rate
64        expected: f32,
65        /// Actual sample rate
66        got: f32,
67    },
68
69    /// Configuration error
70    #[error("Configuration error: {0}")]
71    Config(String),
72
73    /// Not initialized
74    #[error("Not initialized")]
75    NotInitialized,
76
77    /// Already initialized
78    #[error("Already initialized")]
79    AlreadyInitialized,
80
81    /// Unsupported operation
82    #[error("Unsupported operation: {0}")]
83    Unsupported(String),
84
85    /// Timeout occurred
86    #[error("Operation timed out")]
87    Timeout,
88
89    /// Real-time violation — operation exceeded its time budget or
90    /// performed an illegal action (allocation, blocking, etc.)
91    #[error("Realtime violation: {0}")]
92    RealtimeViolation(String),
93
94    /// Internal error (for implementation-specific errors)
95    #[error("Internal error: {0}")]
96    Internal(String),
97}
98
99/// Result type for signal processing operations
100pub type ProcessResult<T> = Result<T, ProcessError>;
101
102impl ProcessError {
103    /// Create a new processing error with a formatted message
104    pub fn processing(msg: impl Into<String>) -> Self {
105        Self::Processing(msg.into())
106    }
107
108    /// Create a new parameter error with a formatted message
109    pub fn parameter(msg: impl Into<String>) -> Self {
110        Self::Parameter(msg.into())
111    }
112
113    /// Create a new invalid port error
114    pub fn invalid_port(port: impl fmt::Display) -> Self {
115        Self::InvalidPort(format!("Invalid port: {}", port))
116    }
117
118    /// Create a new buffer error
119    pub fn buffer(msg: impl Into<String>) -> Self {
120        Self::Buffer(msg.into())
121    }
122
123    /// Create a new node not found error
124    pub fn node_not_found(id: u32) -> Self {
125        Self::NodeNotFound(id)
126    }
127
128    /// Create a new port not found error
129    pub fn port_not_found(port: impl fmt::Display) -> Self {
130        Self::PortNotFound(format!("{}", port))
131    }
132
133    /// Create a new connection error
134    pub fn connection(msg: impl Into<String>) -> Self {
135        Self::Connection(msg.into())
136    }
137
138    /// Create a new type mismatch error
139    pub fn type_mismatch(expected: &'static str, got: &'static str) -> Self {
140        Self::TypeMismatch { expected, got }
141    }
142
143    /// Create a new sample rate mismatch error
144    pub fn sample_rate_mismatch(expected: f32, got: f32) -> Self {
145        Self::SampleRateMismatch { expected, got }
146    }
147
148    /// Create a new configuration error
149    pub fn config(msg: impl Into<String>) -> Self {
150        Self::Config(msg.into())
151    }
152
153    /// Create a new unsupported operation error
154    pub fn unsupported(msg: impl Into<String>) -> Self {
155        Self::Unsupported(msg.into())
156    }
157
158    /// Create a new internal error
159    pub fn internal(msg: impl Into<String>) -> Self {
160        Self::Internal(msg.into())
161    }
162
163    /// Check if this error is recoverable
164    ///
165    /// Recoverable errors are those that don't require stopping the signal thread,
166    /// such as temporary buffer underflows or parameter errors.
167    pub fn is_recoverable(&self) -> bool {
168        match self {
169            Self::Processing(_) => true,
170            Self::Parameter(_) => true,
171            Self::InvalidPort(_) => false,
172            Self::Buffer(_) => true,
173            Self::NodeNotFound(_) => false,
174            Self::PortNotFound(_) => false,
175            Self::Connection(_) => false,
176            Self::TypeMismatch { .. } => false,
177            Self::SampleRateMismatch { .. } => false,
178            Self::Config(_) => false,
179            Self::NotInitialized => true,
180            Self::AlreadyInitialized => true,
181            Self::Unsupported(_) => false,
182            Self::Timeout => true,
183            Self::RealtimeViolation(_) => false,
184            Self::Internal(_) => false,
185        }
186    }
187
188    /// Get a short error code for this error (useful for logging)
189    pub fn code(&self) -> &'static str {
190        match self {
191            Self::Processing(_) => "ERR_PROCESSING",
192            Self::Parameter(_) => "ERR_PARAMETER",
193            Self::InvalidPort(_) => "ERR_INVALID_PORT",
194            Self::Buffer(_) => "ERR_BUFFER",
195            Self::NodeNotFound(_) => "ERR_NODE_NOT_FOUND",
196            Self::PortNotFound(_) => "ERR_PORT_NOT_FOUND",
197            Self::Connection(_) => "ERR_CONNECTION",
198            Self::TypeMismatch { .. } => "ERR_TYPE_MISMATCH",
199            Self::SampleRateMismatch { .. } => "ERR_SAMPLE_RATE",
200            Self::Config(_) => "ERR_CONFIG",
201            Self::NotInitialized => "ERR_NOT_INIT",
202            Self::AlreadyInitialized => "ERR_ALREADY_INIT",
203            Self::Unsupported(_) => "ERR_UNSUPPORTED",
204            Self::Timeout => "ERR_TIMEOUT",
205            Self::RealtimeViolation(_) => "ERR_RT_VIOLATION",
206            Self::Internal(_) => "ERR_INTERNAL",
207        }
208    }
209}
210
211// ============================================================================
212// Parameter Error
213// ============================================================================
214
215/// Errors that can occur during parameter operations
216#[derive(Error, Debug, Clone, PartialEq)]
217pub enum ParameterError {
218    /// Parameter name is empty
219    #[error("Parameter name cannot be empty")]
220    Empty,
221
222    /// Parameter name contains invalid character
223    #[error("Parameter name cannot contain '{0}'")]
224    InvalidCharacter(char),
225
226    /// Parameter name is too long
227    #[error("Parameter name too long (max {max} characters)")]
228    TooLong {
229        /// Maximum allowed length
230        max: usize,
231    },
232
233    /// Parameter name must start with a letter
234    #[error("Parameter name must start with a letter")]
235    MustStartWithLetter,
236
237    /// Parameter not found
238    #[error("Parameter '{0}' not found")]
239    NotFound(String),
240
241    /// Parameter type mismatch
242    #[error("Parameter type mismatch: expected {expected:?}, got {got:?}")]
243    TypeMismatch {
244        /// Expected parameter type
245        expected: crate::traits::ParamType,
246        /// Actual parameter type
247        got: crate::traits::ParamType,
248    },
249
250    /// Value out of range
251    #[error("Value {value} out of range [{min}, {max}]")]
252    OutOfRange {
253        /// The value that was out of range
254        value: f32,
255        /// Minimum allowed value
256        min: f32,
257        /// Maximum allowed value
258        max: f32,
259    },
260
261    /// Invalid choice (for Choice parameters)
262    #[error("Invalid choice '{0}'")]
263    InvalidChoice(String),
264
265    /// Duplicate parameter
266    #[error("Parameter '{0}' already exists")]
267    Duplicate(String),
268
269    /// Parameter is read-only
270    #[error("Parameter '{0}' is read-only")]
271    ReadOnly(String),
272}
273
274/// Result type for parameter operations
275pub type ParameterResult<T> = Result<T, ParameterError>;
276
277impl ParameterError {
278    /// Create a new not found error
279    pub fn not_found(name: impl Into<String>) -> Self {
280        Self::NotFound(name.into())
281    }
282
283    /// Create a new type mismatch error
284    pub fn type_mismatch(
285        expected: crate::traits::ParamType,
286        got: crate::traits::ParamType,
287    ) -> Self {
288        Self::TypeMismatch { expected, got }
289    }
290
291    /// Create a new out of range error
292    pub fn out_of_range(value: f32, min: f32, max: f32) -> Self {
293        Self::OutOfRange { value, min, max }
294    }
295
296    /// Create a new invalid choice error
297    pub fn invalid_choice(choice: impl Into<String>) -> Self {
298        Self::InvalidChoice(choice.into())
299    }
300
301    /// Create a new duplicate parameter error
302    pub fn duplicate(name: impl Into<String>) -> Self {
303        Self::Duplicate(name.into())
304    }
305
306    /// Create a new read-only error
307    pub fn read_only(name: impl Into<String>) -> Self {
308        Self::ReadOnly(name.into())
309    }
310}
311
312// ============================================================================
313// Port Error
314// ============================================================================
315
316/// Errors that can occur during port operations
317#[derive(Error, Debug, Clone, PartialEq, Eq)]
318pub enum PortError {
319    /// Port not found
320    #[error("Port {0} not found")]
321    NotFound(String),
322
323    /// Port direction mismatch (e.g., trying to connect output to output)
324    #[error("Port direction mismatch: expected {expected}, got {got}")]
325    DirectionMismatch {
326        /// Expected direction
327        expected: crate::traits::PortDirection,
328        /// Actual direction
329        got: crate::traits::PortDirection,
330    },
331
332    /// Port type mismatch (e.g., trying to connect audio to control)
333    #[error("Port type mismatch: expected {expected:?}, got {got:?}")]
334    TypeMismatch {
335        /// Expected port type
336        expected: crate::traits::PortType,
337        /// Actual port type
338        got: crate::traits::PortType,
339    },
340
341    /// Port already connected
342    #[error("Port {0} is already connected")]
343    AlreadyConnected(String),
344
345    /// Maximum connections reached
346    #[error("Maximum connections reached for port {0}")]
347    MaxConnectionsReached(String),
348
349    /// Invalid port index
350    #[error("Invalid port index: {0}")]
351    InvalidIndex(usize),
352}
353
354/// Result type for port operations
355pub type PortResult<T> = Result<T, PortError>;
356
357impl PortError {
358    /// Create a new not found error
359    pub fn not_found(port: impl fmt::Display) -> Self {
360        Self::NotFound(format!("{}", port))
361    }
362
363    /// Create a new direction mismatch error
364    pub fn direction_mismatch(
365        expected: crate::traits::PortDirection,
366        got: crate::traits::PortDirection,
367    ) -> Self {
368        Self::DirectionMismatch { expected, got }
369    }
370
371    /// Create a new type mismatch error
372    pub fn type_mismatch(expected: crate::traits::PortType, got: crate::traits::PortType) -> Self {
373        Self::TypeMismatch { expected, got }
374    }
375
376    /// Create a new already connected error
377    pub fn already_connected(port: impl fmt::Display) -> Self {
378        Self::AlreadyConnected(format!("{}", port))
379    }
380}
381
382// ============================================================================
383// Clock Error
384// ============================================================================
385
386/// Errors that can occur during clock operations
387#[derive(Error, Debug, Clone, PartialEq)]
388pub enum ClockError {
389    /// Hardware error (ALSA, JACK, etc.)
390    #[error("Hardware error: {0}")]
391    Hardware(String),
392
393    /// Invalid sample rate
394    #[error("Invalid sample rate: {0}")]
395    InvalidSampleRate(f32),
396
397    /// Clock not started
398    #[error("Clock not started")]
399    NotStarted,
400
401    /// Clock already started
402    #[error("Clock already started")]
403    AlreadyStarted,
404
405    /// Clock underflow
406    #[error("Clock underflow")]
407    Underflow,
408
409    /// Clock overflow
410    #[error("Clock overflow")]
411    Overflow,
412}
413
414/// Result type for clock operations
415pub type ClockResult<T> = Result<T, ClockError>;
416
417// ============================================================================
418// Connection Error
419// ============================================================================
420
421/// Errors that can occur during graph connections
422#[derive(Error, Debug, Clone, PartialEq, Eq)]
423pub enum ConnectionError {
424    /// Cannot connect node to itself
425    #[error("Cannot connect node to itself")]
426    SelfConnection,
427
428    /// Cycle detected in graph
429    #[error("Cycle detected in graph")]
430    CycleDetected,
431
432    /// Connection would create a cycle
433    #[error("Connection would create a cycle")]
434    WouldCreateCycle,
435
436    /// Invalid connection
437    #[error("Invalid connection: {0}")]
438    Invalid(String),
439}
440
441/// Result type for connection operations
442pub type ConnectionResult<T> = Result<T, ConnectionError>;
443
444// ============================================================================
445// Error Context (for adding extra information)
446// ============================================================================
447
448/// Additional context for errors
449///
450/// This can be attached to errors to provide more information
451/// about where and why they occurred.
452#[derive(Debug, Clone)]
453pub struct ErrorContext {
454    /// Source location (file:line)
455    pub location: Option<String>,
456
457    /// Thread ID where error occurred
458    pub thread_id: Option<std::thread::ThreadId>,
459
460    /// Timestamp when error occurred
461    pub timestamp: std::time::SystemTime,
462
463    /// Node ID (if applicable)
464    pub node_id: Option<crate::traits::NodeId>,
465
466    /// Port ID (if applicable)
467    pub port_id: Option<String>,
468
469    /// Parameter ID (if applicable)
470    pub parameter_id: Option<String>,
471
472    /// Additional key-value pairs
473    pub extras: Vec<(String, String)>,
474}
475
476impl Default for ErrorContext {
477    fn default() -> Self {
478        Self {
479            location: None,
480            thread_id: Some(std::thread::current().id()),
481            timestamp: std::time::SystemTime::now(),
482            node_id: None,
483            port_id: None,
484            parameter_id: None,
485            extras: Vec::new(),
486        }
487    }
488}
489
490impl ErrorContext {
491    /// Create new error context
492    pub fn new() -> Self {
493        Self::default()
494    }
495
496    /// Add source location
497    pub fn with_location(mut self, file: &str, line: u32) -> Self {
498        self.location = Some(format!("{}:{}", file, line));
499        self
500    }
501
502    /// Add node ID
503    pub fn with_node(mut self, node_id: crate::traits::NodeId) -> Self {
504        self.node_id = Some(node_id);
505        self
506    }
507
508    /// Add port ID
509    pub fn with_port(mut self, port_id: impl fmt::Display) -> Self {
510        self.port_id = Some(format!("{}", port_id));
511        self
512    }
513
514    /// Add parameter ID
515    pub fn with_parameter(mut self, param_id: impl AsRef<str>) -> Self {
516        self.parameter_id = Some(param_id.as_ref().to_string());
517        self
518    }
519
520    /// Add extra key-value pair
521    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
522        self.extras.push((key.into(), value.into()));
523        self
524    }
525
526    /// Format error with context
527    pub fn format(&self, error: &impl std::error::Error) -> String {
528        let mut msg = format!("{}", error);
529
530        if let Some(loc) = &self.location {
531            msg.push_str(&format!("\n  at {}", loc));
532        }
533
534        if let Some(id) = self.thread_id {
535            msg.push_str(&format!("\n  thread: {:?}", id));
536        }
537
538        if let Some(node) = self.node_id {
539            msg.push_str(&format!("\n  node: {}", node));
540        }
541
542        if let Some(port) = &self.port_id {
543            msg.push_str(&format!("\n  port: {}", port));
544        }
545
546        if let Some(param) = &self.parameter_id {
547            msg.push_str(&format!("\n  parameter: {}", param));
548        }
549
550        for (key, value) in &self.extras {
551            msg.push_str(&format!("\n  {}: {}", key, value));
552        }
553
554        msg
555    }
556}
557
558// ============================================================================
559// Conversion Implementations
560// ============================================================================
561
562impl From<ParameterError> for ProcessError {
563    fn from(err: ParameterError) -> Self {
564        match err {
565            ParameterError::NotFound(name) => {
566                Self::parameter(format!("Parameter not found: {}", name))
567            }
568            ParameterError::TypeMismatch { expected, got } => {
569                Self::type_mismatch(expected.name(), got.name())
570            }
571            ParameterError::OutOfRange { value, min, max } => {
572                Self::parameter(format!("Value {} out of range [{}, {}]", value, min, max))
573            }
574            ParameterError::InvalidChoice(choice) => {
575                Self::parameter(format!("Invalid choice: {}", choice))
576            }
577            ParameterError::Duplicate(name) => {
578                Self::parameter(format!("Duplicate parameter: {}", name))
579            }
580            ParameterError::ReadOnly(name) => {
581                Self::parameter(format!("Parameter is read-only: {}", name))
582            }
583            _ => Self::parameter(err.to_string()),
584        }
585    }
586}
587
588impl From<PortError> for ProcessError {
589    fn from(err: PortError) -> Self {
590        match err {
591            PortError::NotFound(port) => Self::port_not_found(port),
592            PortError::DirectionMismatch { expected, got } => {
593                Self::type_mismatch(expected.name(), got.name())
594            }
595            PortError::TypeMismatch { expected, got } => {
596                Self::type_mismatch(expected.name(), got.name())
597            }
598            PortError::AlreadyConnected(port) => {
599                Self::connection(format!("Port already connected: {}", port))
600            }
601            PortError::MaxConnectionsReached(port) => {
602                Self::connection(format!("Max connections reached for port: {}", port))
603            }
604            PortError::InvalidIndex(idx) => {
605                Self::invalid_port(format!("Invalid port index: {}", idx))
606            }
607        }
608    }
609}
610
611impl From<ClockError> for ProcessError {
612    fn from(err: ClockError) -> Self {
613        match err {
614            ClockError::Hardware(msg) => Self::processing(format!("Hardware error: {}", msg)),
615            ClockError::InvalidSampleRate(sr) => {
616                Self::config(format!("Invalid sample rate: {}", sr))
617            }
618            ClockError::NotStarted => Self::processing("Clock not started"),
619            ClockError::AlreadyStarted => Self::processing("Clock already started"),
620            ClockError::Underflow => Self::buffer("Clock underflow"),
621            ClockError::Overflow => Self::buffer("Clock overflow"),
622        }
623    }
624}
625
626impl From<ConnectionError> for ProcessError {
627    fn from(err: ConnectionError) -> Self {
628        match err {
629            ConnectionError::SelfConnection => Self::connection("Cannot connect node to itself"),
630            ConnectionError::CycleDetected => Self::connection("Cycle detected in graph"),
631            ConnectionError::WouldCreateCycle => {
632                Self::connection("Connection would create a cycle")
633            }
634            ConnectionError::Invalid(msg) => Self::connection(msg),
635        }
636    }
637}
638
639impl From<std::io::Error> for ProcessError {
640    fn from(err: std::io::Error) -> Self {
641        Self::Processing(format!("IO error: {}", err))
642    }
643}
644
645impl From<crate::error::Error> for ProcessError {
646    fn from(err: crate::error::Error) -> Self {
647        Self::Processing(err.to_string())
648    }
649}
650
651// ============================================================================
652// Tests
653// ============================================================================
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658    use crate::traits::{PortDirection, PortType};
659
660    #[test]
661    fn test_process_error_creation() {
662        let err = ProcessError::processing("test error");
663        assert!(matches!(err, ProcessError::Processing(_)));
664        assert_eq!(err.code(), "ERR_PROCESSING");
665        assert!(err.is_recoverable());
666
667        let err = ProcessError::node_not_found(42);
668        assert!(matches!(err, ProcessError::NodeNotFound(42)));
669        assert_eq!(err.code(), "ERR_NODE_NOT_FOUND");
670        assert!(!err.is_recoverable());
671    }
672
673    #[test]
674    fn test_parameter_error_creation() {
675        let err = ParameterError::not_found("gain");
676        assert!(matches!(err, ParameterError::NotFound(_)));
677
678        let err = ParameterError::out_of_range(2.0, 0.0, 1.0);
679        assert!(matches!(err, ParameterError::OutOfRange { value: 2.0, .. }));
680    }
681
682    #[test]
683    fn test_port_error_creation() {
684        let err = PortError::direction_mismatch(PortDirection::Input, PortDirection::Output);
685        assert!(matches!(err, PortError::DirectionMismatch { .. }));
686
687        let err = PortError::type_mismatch(PortType::Signal, PortType::Control);
688        assert!(matches!(err, PortError::TypeMismatch { .. }));
689    }
690
691    #[test]
692    fn test_error_conversions() {
693        let param_err = ParameterError::not_found("test");
694        let proc_err: ProcessError = param_err.into();
695        assert!(matches!(proc_err, ProcessError::Parameter(_)));
696
697        let port_err = PortError::not_found("port");
698        let proc_err: ProcessError = port_err.into();
699        assert!(matches!(proc_err, ProcessError::PortNotFound(_)));
700
701        let clock_err = ClockError::Underflow;
702        let proc_err: ProcessError = clock_err.into();
703        assert!(matches!(proc_err, ProcessError::Buffer(_)));
704    }
705
706    #[test]
707    fn test_error_context() {
708        let ctx = ErrorContext::new()
709            .with_location("test.rs", 42)
710            .with_node(crate::traits::NodeId(1))
711            .with_extra("sample_rate", "44100");
712
713        let err = ProcessError::processing("test");
714        let formatted = ctx.format(&err);
715
716        assert!(formatted.contains("test.rs:42"));
717        assert!(formatted.contains("node: Node(1)"));
718        assert!(formatted.contains("sample_rate: 44100"));
719    }
720
721    #[test]
722    fn test_recoverable_flags() {
723        assert!(ProcessError::processing("test").is_recoverable());
724        assert!(ProcessError::parameter("test").is_recoverable());
725        assert!(ProcessError::buffer("test").is_recoverable());
726        assert!(!ProcessError::node_not_found(42).is_recoverable());
727        assert!(!ProcessError::port_not_found("port").is_recoverable());
728    }
729
730    #[test]
731    fn test_error_codes() {
732        assert_eq!(ProcessError::processing("").code(), "ERR_PROCESSING");
733        assert_eq!(ProcessError::node_not_found(0).code(), "ERR_NODE_NOT_FOUND");
734    }
735
736    #[test]
737    fn test_parameter_error_details() {
738        let err = ParameterError::out_of_range(1.5, 0.0, 1.0);
739        match err {
740            ParameterError::OutOfRange { value, min, max } => {
741                assert_eq!(value, 1.5);
742                assert_eq!(min, 0.0);
743                assert_eq!(max, 1.0);
744            }
745            _ => panic!("Wrong error type"),
746        }
747    }
748}