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 signal 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 signal 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    /// Timestamp when error occurred
458    pub timestamp: std::time::SystemTime,
459
460    /// Node ID (if applicable)
461    pub node_id: Option<crate::traits::NodeId>,
462
463    /// Port ID (if applicable)
464    pub port_id: Option<String>,
465
466    /// Parameter ID (if applicable)
467    pub parameter_id: Option<String>,
468
469    /// Additional key-value pairs
470    pub extras: Vec<(String, String)>,
471}
472
473impl Default for ErrorContext {
474    fn default() -> Self {
475        Self {
476            location: None,
477            timestamp: std::time::SystemTime::now(),
478            node_id: None,
479            port_id: None,
480            parameter_id: None,
481            extras: Vec::new(),
482        }
483    }
484}
485
486impl ErrorContext {
487    /// Create new error context
488    pub fn new() -> Self {
489        Self::default()
490    }
491
492    /// Add source location
493    pub fn with_location(mut self, file: &str, line: u32) -> Self {
494        self.location = Some(format!("{}:{}", file, line));
495        self
496    }
497
498    /// Add node ID
499    pub fn with_node(mut self, node_id: crate::traits::NodeId) -> Self {
500        self.node_id = Some(node_id);
501        self
502    }
503
504    /// Add port ID
505    pub fn with_port(mut self, port_id: impl fmt::Display) -> Self {
506        self.port_id = Some(format!("{}", port_id));
507        self
508    }
509
510    /// Add parameter ID
511    pub fn with_parameter(mut self, param_id: impl AsRef<str>) -> Self {
512        self.parameter_id = Some(param_id.as_ref().to_string());
513        self
514    }
515
516    /// Add extra key-value pair
517    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
518        self.extras.push((key.into(), value.into()));
519        self
520    }
521
522    /// Format error with context
523    pub fn format(&self, error: &impl std::error::Error) -> String {
524        let mut msg = format!("{}", error);
525
526        if let Some(loc) = &self.location {
527            msg.push_str(&format!("\n  at {}", loc));
528        }
529
530        if let Some(node) = self.node_id {
531            msg.push_str(&format!("\n  node: {}", node));
532        }
533
534        if let Some(port) = &self.port_id {
535            msg.push_str(&format!("\n  port: {}", port));
536        }
537
538        if let Some(param) = &self.parameter_id {
539            msg.push_str(&format!("\n  parameter: {}", param));
540        }
541
542        for (key, value) in &self.extras {
543            msg.push_str(&format!("\n  {}: {}", key, value));
544        }
545
546        msg
547    }
548}
549
550// ============================================================================
551// Conversion Implementations
552// ============================================================================
553
554impl From<ParameterError> for ProcessError {
555    fn from(err: ParameterError) -> Self {
556        match err {
557            ParameterError::NotFound(name) => {
558                Self::parameter(format!("Parameter not found: {}", name))
559            }
560            ParameterError::TypeMismatch { expected, got } => {
561                Self::type_mismatch(expected.name(), got.name())
562            }
563            ParameterError::OutOfRange { value, min, max } => {
564                Self::parameter(format!("Value {} out of range [{}, {}]", value, min, max))
565            }
566            ParameterError::InvalidChoice(choice) => {
567                Self::parameter(format!("Invalid choice: {}", choice))
568            }
569            ParameterError::Duplicate(name) => {
570                Self::parameter(format!("Duplicate parameter: {}", name))
571            }
572            ParameterError::ReadOnly(name) => {
573                Self::parameter(format!("Parameter is read-only: {}", name))
574            }
575            _ => Self::parameter(err.to_string()),
576        }
577    }
578}
579
580impl From<PortError> for ProcessError {
581    fn from(err: PortError) -> Self {
582        match err {
583            PortError::NotFound(port) => Self::port_not_found(port),
584            PortError::DirectionMismatch { expected, got } => {
585                Self::type_mismatch(expected.name(), got.name())
586            }
587            PortError::TypeMismatch { expected, got } => {
588                Self::type_mismatch(expected.name(), got.name())
589            }
590            PortError::AlreadyConnected(port) => {
591                Self::connection(format!("Port already connected: {}", port))
592            }
593            PortError::MaxConnectionsReached(port) => {
594                Self::connection(format!("Max connections reached for port: {}", port))
595            }
596            PortError::InvalidIndex(idx) => {
597                Self::invalid_port(format!("Invalid port index: {}", idx))
598            }
599        }
600    }
601}
602
603impl From<ClockError> for ProcessError {
604    fn from(err: ClockError) -> Self {
605        match err {
606            ClockError::Hardware(msg) => Self::processing(format!("Hardware error: {}", msg)),
607            ClockError::InvalidSampleRate(sr) => {
608                Self::config(format!("Invalid sample rate: {}", sr))
609            }
610            ClockError::NotStarted => Self::processing("Clock not started"),
611            ClockError::AlreadyStarted => Self::processing("Clock already started"),
612            ClockError::Underflow => Self::buffer("Clock underflow"),
613            ClockError::Overflow => Self::buffer("Clock overflow"),
614        }
615    }
616}
617
618impl From<ConnectionError> for ProcessError {
619    fn from(err: ConnectionError) -> Self {
620        match err {
621            ConnectionError::SelfConnection => Self::connection("Cannot connect node to itself"),
622            ConnectionError::CycleDetected => Self::connection("Cycle detected in graph"),
623            ConnectionError::WouldCreateCycle => {
624                Self::connection("Connection would create a cycle")
625            }
626            ConnectionError::Invalid(msg) => Self::connection(msg),
627        }
628    }
629}
630
631impl From<std::io::Error> for ProcessError {
632    fn from(err: std::io::Error) -> Self {
633        Self::Processing(format!("IO error: {}", err))
634    }
635}
636
637impl From<crate::error::Error> for ProcessError {
638    fn from(err: crate::error::Error) -> Self {
639        Self::Processing(err.to_string())
640    }
641}
642
643// ============================================================================
644// Tests
645// ============================================================================
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650    use crate::traits::{PortDirection, PortType};
651
652    #[test]
653    fn test_process_error_creation() {
654        let err = ProcessError::processing("test error");
655        assert!(matches!(err, ProcessError::Processing(_)));
656        assert_eq!(err.code(), "ERR_PROCESSING");
657        assert!(err.is_recoverable());
658
659        let err = ProcessError::node_not_found(42);
660        assert!(matches!(err, ProcessError::NodeNotFound(42)));
661        assert_eq!(err.code(), "ERR_NODE_NOT_FOUND");
662        assert!(!err.is_recoverable());
663    }
664
665    #[test]
666    fn test_parameter_error_creation() {
667        let err = ParameterError::not_found("gain");
668        assert!(matches!(err, ParameterError::NotFound(_)));
669
670        let err = ParameterError::out_of_range(2.0, 0.0, 1.0);
671        assert!(matches!(err, ParameterError::OutOfRange { value: 2.0, .. }));
672    }
673
674    #[test]
675    fn test_port_error_creation() {
676        let err = PortError::direction_mismatch(PortDirection::Input, PortDirection::Output);
677        assert!(matches!(err, PortError::DirectionMismatch { .. }));
678
679        let err = PortError::type_mismatch(PortType::Signal, PortType::Control);
680        assert!(matches!(err, PortError::TypeMismatch { .. }));
681    }
682
683    #[test]
684    fn test_error_conversions() {
685        let param_err = ParameterError::not_found("test");
686        let proc_err: ProcessError = param_err.into();
687        assert!(matches!(proc_err, ProcessError::Parameter(_)));
688
689        let port_err = PortError::not_found("port");
690        let proc_err: ProcessError = port_err.into();
691        assert!(matches!(proc_err, ProcessError::PortNotFound(_)));
692
693        let clock_err = ClockError::Underflow;
694        let proc_err: ProcessError = clock_err.into();
695        assert!(matches!(proc_err, ProcessError::Buffer(_)));
696    }
697
698    #[test]
699    fn test_error_context() {
700        let ctx = ErrorContext::new()
701            .with_location("test.rs", 42)
702            .with_node(crate::traits::NodeId(1))
703            .with_extra("sample_rate", "44100");
704
705        let err = ProcessError::processing("test");
706        let formatted = ctx.format(&err);
707
708        assert!(formatted.contains("test.rs:42"));
709        assert!(formatted.contains("node: Node(1)"));
710        assert!(formatted.contains("sample_rate: 44100"));
711    }
712
713    #[test]
714    fn test_recoverable_flags() {
715        assert!(ProcessError::processing("test").is_recoverable());
716        assert!(ProcessError::parameter("test").is_recoverable());
717        assert!(ProcessError::buffer("test").is_recoverable());
718        assert!(!ProcessError::node_not_found(42).is_recoverable());
719        assert!(!ProcessError::port_not_found("port").is_recoverable());
720    }
721
722    #[test]
723    fn test_error_codes() {
724        assert_eq!(ProcessError::processing("").code(), "ERR_PROCESSING");
725        assert_eq!(ProcessError::node_not_found(0).code(), "ERR_NODE_NOT_FOUND");
726    }
727
728    #[test]
729    fn test_parameter_error_details() {
730        let err = ParameterError::out_of_range(1.5, 0.0, 1.0);
731        match err {
732            ParameterError::OutOfRange { value, min, max } => {
733                assert_eq!(value, 1.5);
734                assert_eq!(min, 0.0);
735                assert_eq!(max, 1.0);
736            }
737            _ => panic!("Wrong error type"),
738        }
739    }
740}