fugue/
error.rs

1//! Error handling for probabilistic programming operations.
2//!
3//! This module provides structured error types with rich context information for graceful handling of common failure modes in probabilistic computation.
4
5use crate::core::address::Address;
6use crate::core::distribution::*;
7use std::fmt;
8
9/// Error codes for programmatic error handling and categorization.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum ErrorCode {
12    // Distribution parameter validation errors (1xx)
13    InvalidMean = 100,
14    InvalidVariance = 101,
15    InvalidProbability = 102,
16    InvalidRange = 103,
17    InvalidShape = 104,
18    InvalidRate = 105,
19    InvalidCount = 106,
20
21    // Numerical computation errors (2xx)
22    NumericalOverflow = 200,
23    NumericalUnderflow = 201,
24    NumericalInstability = 202,
25    InvalidLogDensity = 203,
26
27    // Model execution errors (3xx)
28    ModelExecutionFailed = 300,
29    AddressConflict = 301,
30    UnexpectedModelStructure = 302,
31
32    // Inference algorithm errors (4xx)
33    InferenceConvergenceFailed = 400,
34    InsufficientSamples = 401,
35    InvalidInferenceConfig = 402,
36
37    // Trace manipulation errors (5xx)
38    TraceAddressNotFound = 500,
39    TraceCorrupted = 501,
40    TraceReplayFailed = 502,
41
42    // Type system errors (6xx)
43    TypeMismatch = 600,
44    UnsupportedType = 601,
45}
46
47impl ErrorCode {
48    /// Get a human-readable description of the error code.
49    pub fn description(&self) -> &'static str {
50        match self {
51            ErrorCode::InvalidMean => "Distribution mean parameter is invalid",
52            ErrorCode::InvalidVariance => "Distribution variance/scale parameter is invalid",
53            ErrorCode::InvalidProbability => "Probability parameter is invalid",
54            ErrorCode::InvalidRange => "Parameter range is invalid",
55            ErrorCode::InvalidShape => "Shape parameter is invalid",
56            ErrorCode::InvalidRate => "Rate parameter is invalid",
57            ErrorCode::InvalidCount => "Count parameter is invalid",
58
59            ErrorCode::NumericalOverflow => "Numerical computation resulted in overflow",
60            ErrorCode::NumericalUnderflow => "Numerical computation resulted in underflow",
61            ErrorCode::NumericalInstability => "Numerical computation is unstable",
62            ErrorCode::InvalidLogDensity => "Log density computation is invalid",
63
64            ErrorCode::ModelExecutionFailed => "Model execution failed",
65            ErrorCode::AddressConflict => "Address already exists in trace",
66            ErrorCode::UnexpectedModelStructure => "Model structure is unexpected",
67
68            ErrorCode::InferenceConvergenceFailed => "Inference algorithm failed to converge",
69            ErrorCode::InsufficientSamples => "Insufficient samples for reliable inference",
70            ErrorCode::InvalidInferenceConfig => "Inference configuration is invalid",
71
72            ErrorCode::TraceAddressNotFound => "Address not found in trace",
73            ErrorCode::TraceCorrupted => "Trace data is corrupted",
74            ErrorCode::TraceReplayFailed => "Trace replay failed",
75
76            ErrorCode::TypeMismatch => "Type mismatch in trace value",
77            ErrorCode::UnsupportedType => "Unsupported type for operation",
78        }
79    }
80
81    /// Get the category of the error (first digit of the code).
82    pub fn category(&self) -> ErrorCategory {
83        match (*self as u32) / 100 {
84            1 => ErrorCategory::DistributionValidation,
85            2 => ErrorCategory::NumericalComputation,
86            3 => ErrorCategory::ModelExecution,
87            4 => ErrorCategory::InferenceAlgorithm,
88            5 => ErrorCategory::TraceManipulation,
89            6 => ErrorCategory::TypeSystem,
90            _ => ErrorCategory::Unknown,
91        }
92    }
93}
94
95/// High-level error categories for filtering and handling.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum ErrorCategory {
98    DistributionValidation,
99    NumericalComputation,
100    ModelExecution,
101    InferenceAlgorithm,
102    TraceManipulation,
103    TypeSystem,
104    Unknown,
105}
106
107/// Enhanced error context providing debugging information.
108#[derive(Debug, Clone)]
109pub struct ErrorContext {
110    /// Optional source location (file, line) where error occurred
111    pub source_location: Option<(String, u32)>,
112    /// Additional contextual information
113    pub context: Vec<(String, String)>,
114    /// Chain of causality (parent errors)
115    pub cause: Option<Box<FugueError>>,
116}
117
118impl ErrorContext {
119    /// Create a new empty error context.
120    pub fn new() -> Self {
121        Self {
122            source_location: None,
123            context: Vec::new(),
124            cause: None,
125        }
126    }
127
128    /// Add contextual key-value information.
129    pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
130        self.context.push((key.into(), value.into()));
131        self
132    }
133
134    /// Add source location information.
135    pub fn with_source_location(mut self, file: impl Into<String>, line: u32) -> Self {
136        self.source_location = Some((file.into(), line));
137        self
138    }
139
140    /// Chain another error as the cause.
141    pub fn with_cause(mut self, cause: FugueError) -> Self {
142        self.cause = Some(Box::new(cause));
143        self
144    }
145}
146
147impl Default for ErrorContext {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153/// Errors that can occur during probabilistic programming operations.
154#[derive(Debug, Clone)]
155#[allow(clippy::result_large_err)]
156pub enum FugueError {
157    /// Invalid distribution parameters
158    InvalidParameters {
159        distribution: String,
160        reason: String,
161        code: ErrorCode,
162        context: ErrorContext,
163    },
164    /// Numerical computation failed
165    NumericalError {
166        operation: String,
167        details: String,
168        code: ErrorCode,
169        context: ErrorContext,
170    },
171    /// Model execution failed
172    ModelError {
173        address: Option<Address>,
174        reason: String,
175        code: ErrorCode,
176        context: ErrorContext,
177    },
178    /// Inference algorithm failed
179    InferenceError {
180        algorithm: String,
181        reason: String,
182        code: ErrorCode,
183        context: ErrorContext,
184    },
185    /// Trace manipulation error
186    TraceError {
187        operation: String,
188        address: Option<Address>,
189        reason: String,
190        code: ErrorCode,
191        context: ErrorContext,
192    },
193    /// Type mismatch in trace value
194    TypeMismatch {
195        address: Address,
196        expected: String,
197        found: String,
198        code: ErrorCode,
199        context: ErrorContext,
200    },
201}
202
203impl fmt::Display for FugueError {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match self {
206            FugueError::InvalidParameters {
207                distribution,
208                reason,
209                code,
210                context,
211            } => {
212                write!(
213                    f,
214                    "[{}] Invalid parameters for {}: {}",
215                    *code as u32, distribution, reason
216                )?;
217                self.write_context(f, context)?;
218                Ok(())
219            }
220            FugueError::NumericalError {
221                operation,
222                details,
223                code,
224                context,
225            } => {
226                write!(
227                    f,
228                    "[{}] Numerical error in {}: {}",
229                    *code as u32, operation, details
230                )?;
231                self.write_context(f, context)?;
232                Ok(())
233            }
234            FugueError::ModelError {
235                address,
236                reason,
237                code,
238                context,
239            } => {
240                if let Some(addr) = address {
241                    write!(f, "[{}] Model error at {}: {}", *code as u32, addr, reason)?;
242                } else {
243                    write!(f, "[{}] Model error: {}", *code as u32, reason)?;
244                }
245                self.write_context(f, context)?;
246                Ok(())
247            }
248            FugueError::InferenceError {
249                algorithm,
250                reason,
251                code,
252                context,
253            } => {
254                write!(
255                    f,
256                    "[{}] Inference error in {}: {}",
257                    *code as u32, algorithm, reason
258                )?;
259                self.write_context(f, context)?;
260                Ok(())
261            }
262            FugueError::TraceError {
263                operation,
264                address,
265                reason,
266                code,
267                context,
268            } => {
269                if let Some(addr) = address {
270                    write!(
271                        f,
272                        "[{}] Trace error in {} at {}: {}",
273                        *code as u32, operation, addr, reason
274                    )?;
275                } else {
276                    write!(
277                        f,
278                        "[{}] Trace error in {}: {}",
279                        *code as u32, operation, reason
280                    )?;
281                }
282                self.write_context(f, context)?;
283                Ok(())
284            }
285            FugueError::TypeMismatch {
286                address,
287                expected,
288                found,
289                code,
290                context,
291            } => {
292                write!(
293                    f,
294                    "[{}] Type mismatch at {}: expected {}, found {}",
295                    *code as u32, address, expected, found
296                )?;
297                self.write_context(f, context)?;
298                Ok(())
299            }
300        }
301    }
302}
303
304impl FugueError {
305    /// Write additional context information to the formatter.
306    fn write_context(&self, f: &mut fmt::Formatter<'_>, context: &ErrorContext) -> fmt::Result {
307        // Write source location if available
308        if let Some((file, line)) = &context.source_location {
309            write!(f, " (at {}:{})", file, line)?;
310        }
311
312        // Write contextual information
313        if !context.context.is_empty() {
314            write!(f, " [")?;
315            for (i, (key, value)) in context.context.iter().enumerate() {
316                if i > 0 {
317                    write!(f, ", ")?;
318                }
319                write!(f, "{}={}", key, value)?;
320            }
321            write!(f, "]")?;
322        }
323
324        // Write cause chain
325        if let Some(cause) = &context.cause {
326            write!(f, "\n  Caused by: {}", cause)?;
327        }
328
329        Ok(())
330    }
331
332    /// Get the error code for programmatic handling.
333    pub fn code(&self) -> ErrorCode {
334        match self {
335            FugueError::InvalidParameters { code, .. } => *code,
336            FugueError::NumericalError { code, .. } => *code,
337            FugueError::ModelError { code, .. } => *code,
338            FugueError::InferenceError { code, .. } => *code,
339            FugueError::TraceError { code, .. } => *code,
340            FugueError::TypeMismatch { code, .. } => *code,
341        }
342    }
343
344    /// Get the error category for high-level handling.
345    pub fn category(&self) -> ErrorCategory {
346        self.code().category()
347    }
348
349    /// Get the error context for debugging.
350    pub fn context(&self) -> &ErrorContext {
351        match self {
352            FugueError::InvalidParameters { context, .. } => context,
353            FugueError::NumericalError { context, .. } => context,
354            FugueError::ModelError { context, .. } => context,
355            FugueError::InferenceError { context, .. } => context,
356            FugueError::TraceError { context, .. } => context,
357            FugueError::TypeMismatch { context, .. } => context,
358        }
359    }
360
361    /// Check if this error is caused by parameter validation issues.
362    pub fn is_validation_error(&self) -> bool {
363        matches!(self.category(), ErrorCategory::DistributionValidation)
364    }
365
366    /// Check if this error is caused by numerical computation issues.
367    pub fn is_numerical_error(&self) -> bool {
368        matches!(self.category(), ErrorCategory::NumericalComputation)
369    }
370
371    /// Check if this error is recoverable (can be handled and retried).
372    pub fn is_recoverable(&self) -> bool {
373        matches!(
374            self.code(),
375            ErrorCode::InsufficientSamples
376                | ErrorCode::NumericalInstability
377                | ErrorCode::InferenceConvergenceFailed
378        )
379    }
380}
381
382impl std::error::Error for FugueError {}
383
384/// Result type for fallible probabilistic operations.
385#[allow(clippy::result_large_err)]
386pub type FugueResult<T> = Result<T, FugueError>;
387
388// =============================================================================
389// Helper Methods and Constructors
390// =============================================================================
391
392impl FugueError {
393    /// Create an InvalidParameters error with enhanced context.
394    pub fn invalid_parameters(
395        distribution: impl Into<String>,
396        reason: impl Into<String>,
397        code: ErrorCode,
398    ) -> Self {
399        Self::InvalidParameters {
400            distribution: distribution.into(),
401            reason: reason.into(),
402            code,
403            context: ErrorContext::new(),
404        }
405    }
406
407    /// Create an InvalidParameters error with context.
408    pub fn invalid_parameters_with_context(
409        distribution: impl Into<String>,
410        reason: impl Into<String>,
411        code: ErrorCode,
412        context: ErrorContext,
413    ) -> Self {
414        Self::InvalidParameters {
415            distribution: distribution.into(),
416            reason: reason.into(),
417            code,
418            context,
419        }
420    }
421
422    /// Create a NumericalError with enhanced context.
423    pub fn numerical_error(
424        operation: impl Into<String>,
425        details: impl Into<String>,
426        code: ErrorCode,
427    ) -> Self {
428        Self::NumericalError {
429            operation: operation.into(),
430            details: details.into(),
431            code,
432            context: ErrorContext::new(),
433        }
434    }
435
436    /// Create a TraceError with enhanced context.
437    pub fn trace_error(
438        operation: impl Into<String>,
439        address: Option<Address>,
440        reason: impl Into<String>,
441        code: ErrorCode,
442    ) -> Self {
443        Self::TraceError {
444            operation: operation.into(),
445            address,
446            reason: reason.into(),
447            code,
448            context: ErrorContext::new(),
449        }
450    }
451
452    /// Create a TypeMismatch error with enhanced context.
453    pub fn type_mismatch(
454        address: Address,
455        expected: impl Into<String>,
456        found: impl Into<String>,
457    ) -> Self {
458        Self::TypeMismatch {
459            address,
460            expected: expected.into(),
461            found: found.into(),
462            code: ErrorCode::TypeMismatch,
463            context: ErrorContext::new(),
464        }
465    }
466
467    /// Add context to an existing error.
468    pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
469        match &mut self {
470            FugueError::InvalidParameters { context, .. } => {
471                context.context.push((key.into(), value.into()));
472            }
473            FugueError::NumericalError { context, .. } => {
474                context.context.push((key.into(), value.into()));
475            }
476            FugueError::ModelError { context, .. } => {
477                context.context.push((key.into(), value.into()));
478            }
479            FugueError::InferenceError { context, .. } => {
480                context.context.push((key.into(), value.into()));
481            }
482            FugueError::TraceError { context, .. } => {
483                context.context.push((key.into(), value.into()));
484            }
485            FugueError::TypeMismatch { context, .. } => {
486                context.context.push((key.into(), value.into()));
487            }
488        }
489        self
490    }
491
492    /// Add source location to an existing error.
493    pub fn with_source_location(mut self, file: impl Into<String>, line: u32) -> Self {
494        match &mut self {
495            FugueError::InvalidParameters { context, .. } => {
496                context.source_location = Some((file.into(), line));
497            }
498            FugueError::NumericalError { context, .. } => {
499                context.source_location = Some((file.into(), line));
500            }
501            FugueError::ModelError { context, .. } => {
502                context.source_location = Some((file.into(), line));
503            }
504            FugueError::InferenceError { context, .. } => {
505                context.source_location = Some((file.into(), line));
506            }
507            FugueError::TraceError { context, .. } => {
508                context.source_location = Some((file.into(), line));
509            }
510            FugueError::TypeMismatch { context, .. } => {
511                context.source_location = Some((file.into(), line));
512            }
513        }
514        self
515    }
516}
517
518// =============================================================================
519// From Trait Implementations for Common Conversions
520// =============================================================================
521
522/// Convert from standard library errors to FugueError.
523impl From<std::num::ParseFloatError> for FugueError {
524    fn from(err: std::num::ParseFloatError) -> Self {
525        FugueError::numerical_error(
526            "parse_float",
527            format!("Failed to parse float: {}", err),
528            ErrorCode::NumericalInstability,
529        )
530    }
531}
532
533impl From<std::num::ParseIntError> for FugueError {
534    fn from(err: std::num::ParseIntError) -> Self {
535        FugueError::numerical_error(
536            "parse_int",
537            format!("Failed to parse integer: {}", err),
538            ErrorCode::NumericalInstability,
539        )
540    }
541}
542
543/// Helper for converting string errors (common in examples).
544impl From<&str> for FugueError {
545    fn from(msg: &str) -> Self {
546        FugueError::ModelError {
547            address: None,
548            reason: msg.to_string(),
549            code: ErrorCode::ModelExecutionFailed,
550            context: ErrorContext::new(),
551        }
552    }
553}
554
555impl From<String> for FugueError {
556    fn from(msg: String) -> Self {
557        FugueError::ModelError {
558            address: None,
559            reason: msg,
560            code: ErrorCode::ModelExecutionFailed,
561            context: ErrorContext::new(),
562        }
563    }
564}
565
566// =============================================================================
567// Macros for Convenient Error Creation
568// =============================================================================
569
570/// Create an InvalidParameters error with optional context.
571///
572/// Example:
573/// ```rust
574/// # use fugue::*;
575/// let err = invalid_params!("Normal", "sigma must be positive", InvalidVariance);
576/// let err_with_ctx = invalid_params!("Normal", "sigma must be positive", InvalidVariance,
577///     "sigma" => "-1.0", "expected" => "> 0.0");
578/// ```
579#[macro_export]
580macro_rules! invalid_params {
581    ($dist:expr, $reason:expr, $code:ident) => {
582        $crate::error::FugueError::invalid_parameters($dist, $reason, $crate::error::ErrorCode::$code)
583    };
584    ($dist:expr, $reason:expr, $code:ident, $($key:expr => $value:expr),+ $(,)?) => {
585        $crate::error::FugueError::invalid_parameters($dist, $reason, $crate::error::ErrorCode::$code)
586            $(.with_context($key, $value))*
587    };
588}
589
590/// Create a NumericalError with optional context.
591///
592/// Example:
593/// ```rust
594/// # use fugue::*;
595/// let err = numerical_error!("log", "input was negative", NumericalInstability);
596/// let err_with_ctx = numerical_error!("log", "input was negative", NumericalInstability,
597///     "input" => "-1.5");
598/// ```
599#[macro_export]
600macro_rules! numerical_error {
601    ($op:expr, $details:expr, $code:ident) => {
602        $crate::error::FugueError::numerical_error($op, $details, $crate::error::ErrorCode::$code)
603    };
604    ($op:expr, $details:expr, $code:ident, $($key:expr => $value:expr),+ $(,)?) => {
605        $crate::error::FugueError::numerical_error($op, $details, $crate::error::ErrorCode::$code)
606            $(.with_context($key, $value))*
607    };
608}
609
610/// Create a TraceError with optional context.
611///
612/// Example:
613/// ```rust
614/// # use fugue::*;
615/// let err = trace_error!("get_f64", Some(addr!("mu")), "address not found", TraceAddressNotFound);
616/// ```
617#[macro_export]
618macro_rules! trace_error {
619    ($op:expr, $addr:expr, $reason:expr, $code:ident) => {
620        $crate::error::FugueError::trace_error($op, $addr, $reason, $crate::error::ErrorCode::$code)
621    };
622    ($op:expr, $addr:expr, $reason:expr, $code:ident, $($key:expr => $value:expr),+ $(,)?) => {
623        $crate::error::FugueError::trace_error($op, $addr, $reason, $crate::error::ErrorCode::$code)
624            $(.with_context($key, $value))*
625    };
626}
627
628/// Trait for validating distribution parameters.
629pub trait Validate {
630    fn validate(&self) -> FugueResult<()>;
631}
632
633impl Validate for Normal {
634    fn validate(&self) -> FugueResult<()> {
635        if !self.mu().is_finite() {
636            return Err(invalid_params!(
637                "Normal",
638                "Mean (mu) must be finite",
639                InvalidMean,
640                "mu" => format!("{}", self.mu())
641            ));
642        }
643        if self.sigma() <= 0.0 || !self.sigma().is_finite() {
644            return Err(invalid_params!(
645                "Normal",
646                "Standard deviation (sigma) must be positive and finite",
647                InvalidVariance,
648                "sigma" => format!("{}", self.sigma()),
649                "expected" => "> 0.0 and finite"
650            ));
651        }
652        Ok(())
653    }
654}
655
656impl Validate for Exponential {
657    fn validate(&self) -> FugueResult<()> {
658        if self.rate() <= 0.0 || !self.rate().is_finite() {
659            return Err(invalid_params!(
660                "Exponential",
661                "Rate parameter must be positive and finite",
662                InvalidRate,
663                "rate" => format!("{}", self.rate()),
664                "expected" => "> 0.0 and finite"
665            ));
666        }
667        Ok(())
668    }
669}
670
671impl Validate for Beta {
672    fn validate(&self) -> FugueResult<()> {
673        if self.alpha() <= 0.0 || !self.alpha().is_finite() {
674            return Err(invalid_params!(
675                "Beta",
676                "Alpha parameter must be positive and finite",
677                InvalidShape,
678                "alpha" => format!("{}", self.alpha()),
679                "expected" => "> 0.0 and finite"
680            ));
681        }
682        if self.beta() <= 0.0 || !self.beta().is_finite() {
683            return Err(invalid_params!(
684                "Beta",
685                "Beta parameter must be positive and finite",
686                InvalidShape,
687                "beta" => format!("{}", self.beta()),
688                "expected" => "> 0.0 and finite"
689            ));
690        }
691        Ok(())
692    }
693}
694
695impl Validate for Gamma {
696    fn validate(&self) -> FugueResult<()> {
697        if self.shape() <= 0.0 || !self.shape().is_finite() {
698            return Err(invalid_params!(
699                "Gamma",
700                "Shape parameter must be positive and finite",
701                InvalidShape,
702                "shape" => format!("{}", self.shape()),
703                "expected" => "> 0.0 and finite"
704            ));
705        }
706        if self.rate() <= 0.0 || !self.rate().is_finite() {
707            return Err(invalid_params!(
708                "Gamma",
709                "Rate parameter must be positive and finite",
710                InvalidRate,
711                "rate" => format!("{}", self.rate()),
712                "expected" => "> 0.0 and finite"
713            ));
714        }
715        Ok(())
716    }
717}
718
719impl Validate for Uniform {
720    fn validate(&self) -> FugueResult<()> {
721        if !self.low().is_finite() || !self.high().is_finite() {
722            return Err(invalid_params!(
723                "Uniform",
724                "Bounds must be finite",
725                InvalidRange,
726                "low" => format!("{}", self.low()),
727                "high" => format!("{}", self.high())
728            ));
729        }
730        if self.low() >= self.high() {
731            return Err(invalid_params!(
732                "Uniform",
733                "Lower bound must be less than upper bound",
734                InvalidRange,
735                "low" => format!("{}", self.low()),
736                "high" => format!("{}", self.high())
737            ));
738        }
739        Ok(())
740    }
741}
742
743impl Validate for Bernoulli {
744    fn validate(&self) -> FugueResult<()> {
745        if !self.p().is_finite() || self.p() < 0.0 || self.p() > 1.0 {
746            return Err(invalid_params!(
747                "Bernoulli",
748                "Probability must be in [0, 1]",
749                InvalidProbability,
750                "p" => format!("{}", self.p()),
751                "expected" => "[0.0, 1.0]"
752            ));
753        }
754        Ok(())
755    }
756}
757
758impl Validate for Categorical {
759    fn validate(&self) -> FugueResult<()> {
760        if self.probs().is_empty() {
761            return Err(invalid_params!(
762                "Categorical",
763                "Probability vector cannot be empty",
764                InvalidProbability,
765                "length" => "0"
766            ));
767        }
768
769        let sum: f64 = self.probs().iter().sum();
770        if (sum - 1.0).abs() > 1e-6 {
771            return Err(invalid_params!(
772                "Categorical",
773                "Probabilities must sum to 1.0",
774                InvalidProbability,
775                "sum" => format!("{:.6}", sum),
776                "expected" => "1.0",
777                "tolerance" => "1e-6"
778            ));
779        }
780
781        for (i, &p) in self.probs().iter().enumerate() {
782            if !p.is_finite() || p < 0.0 {
783                return Err(invalid_params!(
784                    "Categorical",
785                    "All probabilities must be non-negative and finite",
786                    InvalidProbability,
787                    "index" => format!("{}", i),
788                    "value" => format!("{}", p),
789                    "expected" => ">= 0.0 and finite"
790                ));
791            }
792        }
793
794        Ok(())
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use crate::addr;
802
803    #[test]
804    fn error_code_category_and_description() {
805        let code = ErrorCode::InvalidMean;
806        assert!(ErrorCode::InvalidMean.description().contains("mean"));
807        assert_eq!(code.category(), ErrorCategory::DistributionValidation);
808
809        let code = ErrorCode::NumericalOverflow;
810        assert_eq!(code.category(), ErrorCategory::NumericalComputation);
811    }
812
813    #[test]
814    fn invalid_parameters_constructor_and_context() {
815        let err = FugueError::invalid_parameters("Normal", "bad params", ErrorCode::InvalidMean)
816            .with_context("mu", "nan")
817            .with_source_location("file.rs", 10);
818
819        let msg = format!("{}", err);
820        assert!(msg.contains("Invalid parameters for Normal"));
821        assert!(msg.contains("mu=nan"));
822        assert_eq!(err.code(), ErrorCode::InvalidMean);
823        assert_eq!(err.category(), ErrorCategory::DistributionValidation);
824    }
825
826    #[test]
827    fn error_macros_create_expected_variants() {
828        let e1 = invalid_params!("Uniform", "bad range", InvalidRange, "low" => "1", "high" => "0");
829        match e1 {
830            FugueError::InvalidParameters { code, .. } => assert_eq!(code, ErrorCode::InvalidRange),
831            _ => panic!("expected InvalidParameters"),
832        }
833
834        let e2 = numerical_error!("compute", "overflow", NumericalOverflow, "x" => "1e309");
835        match e2 {
836            FugueError::NumericalError { code, .. } => {
837                assert_eq!(code, ErrorCode::NumericalOverflow)
838            }
839            _ => panic!("expected NumericalError"),
840        }
841
842        let e3 = trace_error!("lookup", Some(addr!("x")), "missing", TraceAddressNotFound);
843        match e3 {
844            FugueError::TraceError { code, .. } => {
845                assert_eq!(code, ErrorCode::TraceAddressNotFound)
846            }
847            _ => panic!("expected TraceError"),
848        }
849    }
850
851    #[test]
852    fn type_mismatch_constructor() {
853        let e = FugueError::type_mismatch(addr!("a"), "f64", "bool");
854        assert_eq!(e.code(), ErrorCode::TypeMismatch);
855        assert_eq!(e.category(), ErrorCategory::TypeSystem);
856        let msg = format!("{}", e);
857        assert!(msg.contains("Type mismatch"));
858    }
859
860    #[test]
861    fn validate_trait_on_valid_distributions() {
862        assert!(Normal::new(0.0, 1.0).unwrap().validate().is_ok());
863        assert!(Uniform::new(0.0, 1.0).unwrap().validate().is_ok());
864        assert!(Bernoulli::new(0.5).unwrap().validate().is_ok());
865        assert!(Categorical::new(vec![0.2, 0.8]).unwrap().validate().is_ok());
866    }
867
868    #[test]
869    fn error_cause_chaining_and_display_variants() {
870        // Build a cause chain
871        let base = FugueError::invalid_parameters("Normal", "bad", ErrorCode::InvalidMean);
872        let ctx = ErrorContext::new().with_cause(base.clone());
873        let inf = FugueError::InferenceError {
874            algorithm: "MH".into(),
875            reason: "did not converge".into(),
876            code: ErrorCode::InferenceConvergenceFailed,
877            context: ctx.clone(),
878        };
879        let msg = format!("{}", inf);
880        assert!(msg.contains("Inference error"));
881
882        let model_err = FugueError::ModelError {
883            address: Some(crate::addr!("x")),
884            reason: "failed".into(),
885            code: ErrorCode::ModelExecutionFailed,
886            context: ctx,
887        };
888        let msg2 = format!("{}", model_err);
889        assert!(msg2.contains("Model error"));
890    }
891
892    #[test]
893    fn from_conversions_cover_paths() {
894        // ParseFloatError
895        let e_float: FugueError = "abc".parse::<f64>().unwrap_err().into();
896        assert!(matches!(e_float, FugueError::NumericalError { .. }));
897
898        // ParseIntError
899        let e_int: FugueError = "abc".parse::<i32>().unwrap_err().into();
900        assert!(matches!(e_int, FugueError::NumericalError { .. }));
901
902        // From<&str>
903        let e_str: FugueError = "oops".into();
904        assert!(matches!(e_str, FugueError::ModelError { .. }));
905
906        // From<String>
907        let e_string: FugueError = String::from("oops").into();
908        assert!(matches!(e_string, FugueError::ModelError { .. }));
909    }
910}