Skip to main content

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//!
5//! ## Scope (finding FG-33)
6//!
7//! [`ErrorCode`] intentionally only enumerates codes that fugue's own code paths
8//! actually construct today, verified with `grep -rn 'ErrorCode::' src/`. An
9//! earlier revision of this module carried 22 variants across 6 categories
10//! (numerical instability, inference non-convergence, trace corruption, ...) of
11//! which only 11 were ever produced by real logic; the rest were aspirational
12//! placeholders that overstated how much of the crate's failure surface was
13//! actually captured by structured errors (the numerical/model-execution paths
14//! they were meant for return `NaN`/`-inf` or panic-free `Option`s instead, or —
15//! for [`crate::inference::vi::GuideError`] and [`crate::inference::abc::ABCError`]
16//! — got dedicated, more precise algorithm-specific error types rather than being
17//! shoehorned into this general enum). See `CHANGELOG.md` for the removed list.
18//!
19//! The live codes today:
20//!
21//! | Code | Category | Constructed in |
22//! |------|----------|-----------------|
23//! | `InvalidMean`/`InvalidVariance`/`InvalidProbability`/`InvalidRange`/`InvalidShape`/`InvalidRate`/`InvalidCount` | Distribution validation (1xx) | `core::distribution` constructors |
24//! | `AddressConflict` | Model execution (3xx) | `runtime::interpreters` (duplicate sample address) |
25//! | `UnexpectedModelStructure` | Model execution (3xx) | `runtime::interpreters` (replay/score structure mismatch) |
26//! | `TraceAddressNotFound` | Trace manipulation (5xx) | `runtime::trace` typed accessors |
27//! | `TypeMismatch` | Type system (6xx) | `runtime::trace` typed accessors |
28
29use crate::core::address::Address;
30use crate::core::distribution::*;
31use std::fmt;
32
33/// Error codes for programmatic error handling and categorization.
34///
35/// Every variant here is constructed by real logic somewhere in the crate — see
36/// the module-level table. If you're adding a new failure mode, add the code
37/// here *and* wire it into the code path that detects it in the same change;
38/// don't add speculative codes for failure modes nothing produces yet (FG-33).
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum ErrorCode {
41    // Distribution parameter validation errors (1xx)
42    InvalidMean = 100,
43    InvalidVariance = 101,
44    InvalidProbability = 102,
45    InvalidRange = 103,
46    InvalidShape = 104,
47    InvalidRate = 105,
48    InvalidCount = 106,
49
50    // Model execution errors (3xx)
51    AddressConflict = 301,
52    UnexpectedModelStructure = 302,
53
54    // Trace manipulation errors (5xx)
55    TraceAddressNotFound = 500,
56
57    // Type system errors (6xx)
58    TypeMismatch = 600,
59}
60
61impl ErrorCode {
62    /// Get a human-readable description of the error code.
63    pub fn description(&self) -> &'static str {
64        match self {
65            ErrorCode::InvalidMean => "Distribution mean parameter is invalid",
66            ErrorCode::InvalidVariance => "Distribution variance/scale parameter is invalid",
67            ErrorCode::InvalidProbability => "Probability parameter is invalid",
68            ErrorCode::InvalidRange => "Parameter range is invalid",
69            ErrorCode::InvalidShape => "Shape parameter is invalid",
70            ErrorCode::InvalidRate => "Rate parameter is invalid",
71            ErrorCode::InvalidCount => "Count parameter is invalid",
72
73            ErrorCode::AddressConflict => "Address already exists in trace",
74            ErrorCode::UnexpectedModelStructure => "Model structure is unexpected",
75
76            ErrorCode::TraceAddressNotFound => "Address not found in trace",
77
78            ErrorCode::TypeMismatch => "Type mismatch in trace value",
79        }
80    }
81
82    /// Get the category of the error (first digit of the code).
83    pub fn category(&self) -> ErrorCategory {
84        match (*self as u32) / 100 {
85            1 => ErrorCategory::DistributionValidation,
86            3 => ErrorCategory::ModelExecution,
87            5 => ErrorCategory::TraceManipulation,
88            6 => ErrorCategory::TypeSystem,
89            _ => ErrorCategory::Unknown,
90        }
91    }
92}
93
94/// High-level error categories for filtering and handling.
95///
96/// Only categories with at least one live [`ErrorCode`] are represented (FG-33):
97/// numerical-computation and inference-algorithm buckets were removed because no
98/// code in the crate constructs an error in either category today.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum ErrorCategory {
101    DistributionValidation,
102    ModelExecution,
103    TraceManipulation,
104    TypeSystem,
105    Unknown,
106}
107
108/// Enhanced error context providing debugging information.
109#[derive(Debug, Clone)]
110pub struct ErrorContext {
111    /// Optional source location (file, line) where error occurred
112    pub source_location: Option<(String, u32)>,
113    /// Additional contextual information
114    pub context: Vec<(String, String)>,
115    /// Chain of causality (parent errors)
116    pub cause: Option<Box<FugueError>>,
117}
118
119impl ErrorContext {
120    /// Create a new empty error context.
121    pub fn new() -> Self {
122        Self {
123            source_location: None,
124            context: Vec::new(),
125            cause: None,
126        }
127    }
128
129    /// Add contextual key-value information.
130    pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
131        self.context.push((key.into(), value.into()));
132        self
133    }
134
135    /// Add source location information.
136    pub fn with_source_location(mut self, file: impl Into<String>, line: u32) -> Self {
137        self.source_location = Some((file.into(), line));
138        self
139    }
140
141    /// Chain another error as the cause.
142    pub fn with_cause(mut self, cause: FugueError) -> Self {
143        self.cause = Some(Box::new(cause));
144        self
145    }
146}
147
148impl Default for ErrorContext {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154/// Errors that can occur during probabilistic programming operations.
155///
156/// Every variant is constructed by real logic in the crate (FG-33): the
157/// speculative `NumericalError` and `InferenceError` variants were removed
158/// because nothing produced them — see the module-level docs.
159#[derive(Debug, Clone)]
160#[allow(clippy::result_large_err)]
161pub enum FugueError {
162    /// Invalid distribution parameters
163    InvalidParameters {
164        distribution: String,
165        reason: String,
166        code: ErrorCode,
167        context: ErrorContext,
168    },
169    /// Model execution failed
170    ModelError {
171        address: Option<Address>,
172        reason: String,
173        code: ErrorCode,
174        context: ErrorContext,
175    },
176    /// Trace manipulation error
177    TraceError {
178        operation: String,
179        address: Option<Address>,
180        reason: String,
181        code: ErrorCode,
182        context: ErrorContext,
183    },
184    /// Type mismatch in trace value
185    TypeMismatch {
186        address: Address,
187        expected: String,
188        found: String,
189        code: ErrorCode,
190        context: ErrorContext,
191    },
192}
193
194impl fmt::Display for FugueError {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        match self {
197            FugueError::InvalidParameters {
198                distribution,
199                reason,
200                code,
201                context,
202            } => {
203                write!(
204                    f,
205                    "[{}] Invalid parameters for {}: {}",
206                    *code as u32, distribution, reason
207                )?;
208                self.write_context(f, context)?;
209                Ok(())
210            }
211            FugueError::ModelError {
212                address,
213                reason,
214                code,
215                context,
216            } => {
217                if let Some(addr) = address {
218                    write!(f, "[{}] Model error at {}: {}", *code as u32, addr, reason)?;
219                } else {
220                    write!(f, "[{}] Model error: {}", *code as u32, reason)?;
221                }
222                self.write_context(f, context)?;
223                Ok(())
224            }
225            FugueError::TraceError {
226                operation,
227                address,
228                reason,
229                code,
230                context,
231            } => {
232                if let Some(addr) = address {
233                    write!(
234                        f,
235                        "[{}] Trace error in {} at {}: {}",
236                        *code as u32, operation, addr, reason
237                    )?;
238                } else {
239                    write!(
240                        f,
241                        "[{}] Trace error in {}: {}",
242                        *code as u32, operation, reason
243                    )?;
244                }
245                self.write_context(f, context)?;
246                Ok(())
247            }
248            FugueError::TypeMismatch {
249                address,
250                expected,
251                found,
252                code,
253                context,
254            } => {
255                write!(
256                    f,
257                    "[{}] Type mismatch at {}: expected {}, found {}",
258                    *code as u32, address, expected, found
259                )?;
260                self.write_context(f, context)?;
261                Ok(())
262            }
263        }
264    }
265}
266
267impl FugueError {
268    /// Write additional context information to the formatter.
269    fn write_context(&self, f: &mut fmt::Formatter<'_>, context: &ErrorContext) -> fmt::Result {
270        // Write source location if available
271        if let Some((file, line)) = &context.source_location {
272            write!(f, " (at {}:{})", file, line)?;
273        }
274
275        // Write contextual information
276        if !context.context.is_empty() {
277            write!(f, " [")?;
278            for (i, (key, value)) in context.context.iter().enumerate() {
279                if i > 0 {
280                    write!(f, ", ")?;
281                }
282                write!(f, "{}={}", key, value)?;
283            }
284            write!(f, "]")?;
285        }
286
287        // Write cause chain
288        if let Some(cause) = &context.cause {
289            write!(f, "\n  Caused by: {}", cause)?;
290        }
291
292        Ok(())
293    }
294
295    /// Get the error code for programmatic handling.
296    pub fn code(&self) -> ErrorCode {
297        match self {
298            FugueError::InvalidParameters { code, .. } => *code,
299            FugueError::ModelError { code, .. } => *code,
300            FugueError::TraceError { code, .. } => *code,
301            FugueError::TypeMismatch { code, .. } => *code,
302        }
303    }
304
305    /// Get the error category for high-level handling.
306    pub fn category(&self) -> ErrorCategory {
307        self.code().category()
308    }
309
310    /// Get the error context for debugging.
311    pub fn context(&self) -> &ErrorContext {
312        match self {
313            FugueError::InvalidParameters { context, .. } => context,
314            FugueError::ModelError { context, .. } => context,
315            FugueError::TraceError { context, .. } => context,
316            FugueError::TypeMismatch { context, .. } => context,
317        }
318    }
319
320    /// Check if this error is caused by parameter validation issues.
321    pub fn is_validation_error(&self) -> bool {
322        matches!(self.category(), ErrorCategory::DistributionValidation)
323    }
324
325    /// Add context to an existing error.
326    pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
327        match &mut self {
328            FugueError::InvalidParameters { context, .. } => {
329                context.context.push((key.into(), value.into()));
330            }
331            FugueError::ModelError { context, .. } => {
332                context.context.push((key.into(), value.into()));
333            }
334            FugueError::TraceError { context, .. } => {
335                context.context.push((key.into(), value.into()));
336            }
337            FugueError::TypeMismatch { context, .. } => {
338                context.context.push((key.into(), value.into()));
339            }
340        }
341        self
342    }
343
344    /// Add source location to an existing error.
345    pub fn with_source_location(mut self, file: impl Into<String>, line: u32) -> Self {
346        match &mut self {
347            FugueError::InvalidParameters { context, .. } => {
348                context.source_location = Some((file.into(), line));
349            }
350            FugueError::ModelError { context, .. } => {
351                context.source_location = Some((file.into(), line));
352            }
353            FugueError::TraceError { context, .. } => {
354                context.source_location = Some((file.into(), line));
355            }
356            FugueError::TypeMismatch { context, .. } => {
357                context.source_location = Some((file.into(), line));
358            }
359        }
360        self
361    }
362}
363
364impl std::error::Error for FugueError {}
365
366/// Result type for fallible probabilistic operations.
367#[allow(clippy::result_large_err)]
368pub type FugueResult<T> = Result<T, FugueError>;
369
370// =============================================================================
371// Helper Methods and Constructors
372// =============================================================================
373
374impl FugueError {
375    /// Create an InvalidParameters error with enhanced context.
376    pub fn invalid_parameters(
377        distribution: impl Into<String>,
378        reason: impl Into<String>,
379        code: ErrorCode,
380    ) -> Self {
381        Self::InvalidParameters {
382            distribution: distribution.into(),
383            reason: reason.into(),
384            code,
385            context: ErrorContext::new(),
386        }
387    }
388
389    /// Create an InvalidParameters error with context.
390    pub fn invalid_parameters_with_context(
391        distribution: impl Into<String>,
392        reason: impl Into<String>,
393        code: ErrorCode,
394        context: ErrorContext,
395    ) -> Self {
396        Self::InvalidParameters {
397            distribution: distribution.into(),
398            reason: reason.into(),
399            code,
400            context,
401        }
402    }
403
404    /// Create a TraceError with enhanced context.
405    pub fn trace_error(
406        operation: impl Into<String>,
407        address: Option<Address>,
408        reason: impl Into<String>,
409        code: ErrorCode,
410    ) -> Self {
411        Self::TraceError {
412            operation: operation.into(),
413            address,
414            reason: reason.into(),
415            code,
416            context: ErrorContext::new(),
417        }
418    }
419
420    /// Create a TypeMismatch error with enhanced context.
421    pub fn type_mismatch(
422        address: Address,
423        expected: impl Into<String>,
424        found: impl Into<String>,
425    ) -> Self {
426        Self::TypeMismatch {
427            address,
428            expected: expected.into(),
429            found: found.into(),
430            code: ErrorCode::TypeMismatch,
431            context: ErrorContext::new(),
432        }
433    }
434}
435
436// =============================================================================
437// Macros for Convenient Error Creation
438// =============================================================================
439
440/// Create an InvalidParameters error with optional context.
441///
442/// Example:
443/// ```rust
444/// # use fugue::*;
445/// let err = invalid_params!("Normal", "sigma must be positive", InvalidVariance);
446/// let err_with_ctx = invalid_params!("Normal", "sigma must be positive", InvalidVariance,
447///     "sigma" => "-1.0", "expected" => "> 0.0");
448/// ```
449#[macro_export]
450macro_rules! invalid_params {
451    ($dist:expr, $reason:expr, $code:ident) => {
452        $crate::error::FugueError::invalid_parameters($dist, $reason, $crate::error::ErrorCode::$code)
453    };
454    ($dist:expr, $reason:expr, $code:ident, $($key:expr => $value:expr),+ $(,)?) => {
455        $crate::error::FugueError::invalid_parameters($dist, $reason, $crate::error::ErrorCode::$code)
456            $(.with_context($key, $value))*
457    };
458}
459
460/// Create a TraceError with optional context.
461///
462/// Example:
463/// ```rust
464/// # use fugue::*;
465/// let err = trace_error!("get_f64", Some(addr!("mu")), "address not found", TraceAddressNotFound);
466/// ```
467#[macro_export]
468macro_rules! trace_error {
469    ($op:expr, $addr:expr, $reason:expr, $code:ident) => {
470        $crate::error::FugueError::trace_error($op, $addr, $reason, $crate::error::ErrorCode::$code)
471    };
472    ($op:expr, $addr:expr, $reason:expr, $code:ident, $($key:expr => $value:expr),+ $(,)?) => {
473        $crate::error::FugueError::trace_error($op, $addr, $reason, $crate::error::ErrorCode::$code)
474            $(.with_context($key, $value))*
475    };
476}
477
478/// Trait for validating distribution parameters.
479pub trait Validate {
480    fn validate(&self) -> FugueResult<()>;
481}
482
483impl Validate for Normal {
484    fn validate(&self) -> FugueResult<()> {
485        if !self.mu().is_finite() {
486            return Err(invalid_params!(
487                "Normal",
488                "Mean (mu) must be finite",
489                InvalidMean,
490                "mu" => format!("{}", self.mu())
491            ));
492        }
493        if self.sigma() <= 0.0 || !self.sigma().is_finite() {
494            return Err(invalid_params!(
495                "Normal",
496                "Standard deviation (sigma) must be positive and finite",
497                InvalidVariance,
498                "sigma" => format!("{}", self.sigma()),
499                "expected" => "> 0.0 and finite"
500            ));
501        }
502        Ok(())
503    }
504}
505
506impl Validate for Exponential {
507    fn validate(&self) -> FugueResult<()> {
508        if self.rate() <= 0.0 || !self.rate().is_finite() {
509            return Err(invalid_params!(
510                "Exponential",
511                "Rate parameter must be positive and finite",
512                InvalidRate,
513                "rate" => format!("{}", self.rate()),
514                "expected" => "> 0.0 and finite"
515            ));
516        }
517        Ok(())
518    }
519}
520
521impl Validate for Beta {
522    fn validate(&self) -> FugueResult<()> {
523        if self.alpha() <= 0.0 || !self.alpha().is_finite() {
524            return Err(invalid_params!(
525                "Beta",
526                "Alpha parameter must be positive and finite",
527                InvalidShape,
528                "alpha" => format!("{}", self.alpha()),
529                "expected" => "> 0.0 and finite"
530            ));
531        }
532        if self.beta() <= 0.0 || !self.beta().is_finite() {
533            return Err(invalid_params!(
534                "Beta",
535                "Beta parameter must be positive and finite",
536                InvalidShape,
537                "beta" => format!("{}", self.beta()),
538                "expected" => "> 0.0 and finite"
539            ));
540        }
541        Ok(())
542    }
543}
544
545impl Validate for Gamma {
546    fn validate(&self) -> FugueResult<()> {
547        if self.shape() <= 0.0 || !self.shape().is_finite() {
548            return Err(invalid_params!(
549                "Gamma",
550                "Shape parameter must be positive and finite",
551                InvalidShape,
552                "shape" => format!("{}", self.shape()),
553                "expected" => "> 0.0 and finite"
554            ));
555        }
556        if self.rate() <= 0.0 || !self.rate().is_finite() {
557            return Err(invalid_params!(
558                "Gamma",
559                "Rate parameter must be positive and finite",
560                InvalidRate,
561                "rate" => format!("{}", self.rate()),
562                "expected" => "> 0.0 and finite"
563            ));
564        }
565        Ok(())
566    }
567}
568
569impl Validate for Uniform {
570    fn validate(&self) -> FugueResult<()> {
571        if !self.low().is_finite() || !self.high().is_finite() {
572            return Err(invalid_params!(
573                "Uniform",
574                "Bounds must be finite",
575                InvalidRange,
576                "low" => format!("{}", self.low()),
577                "high" => format!("{}", self.high())
578            ));
579        }
580        if self.low() >= self.high() {
581            return Err(invalid_params!(
582                "Uniform",
583                "Lower bound must be less than upper bound",
584                InvalidRange,
585                "low" => format!("{}", self.low()),
586                "high" => format!("{}", self.high())
587            ));
588        }
589        Ok(())
590    }
591}
592
593impl Validate for Bernoulli {
594    fn validate(&self) -> FugueResult<()> {
595        if !self.p().is_finite() || self.p() < 0.0 || self.p() > 1.0 {
596            return Err(invalid_params!(
597                "Bernoulli",
598                "Probability must be in [0, 1]",
599                InvalidProbability,
600                "p" => format!("{}", self.p()),
601                "expected" => "[0.0, 1.0]"
602            ));
603        }
604        Ok(())
605    }
606}
607
608impl Validate for Categorical {
609    fn validate(&self) -> FugueResult<()> {
610        if self.probs().is_empty() {
611            return Err(invalid_params!(
612                "Categorical",
613                "Probability vector cannot be empty",
614                InvalidProbability,
615                "length" => "0"
616            ));
617        }
618
619        let sum: f64 = self.probs().iter().sum();
620        if (sum - 1.0).abs() > 1e-6 {
621            return Err(invalid_params!(
622                "Categorical",
623                "Probabilities must sum to 1.0",
624                InvalidProbability,
625                "sum" => format!("{:.6}", sum),
626                "expected" => "1.0",
627                "tolerance" => "1e-6"
628            ));
629        }
630
631        for (i, &p) in self.probs().iter().enumerate() {
632            if !p.is_finite() || p < 0.0 {
633                return Err(invalid_params!(
634                    "Categorical",
635                    "All probabilities must be non-negative and finite",
636                    InvalidProbability,
637                    "index" => format!("{}", i),
638                    "value" => format!("{}", p),
639                    "expected" => ">= 0.0 and finite"
640                ));
641            }
642        }
643
644        Ok(())
645    }
646}
647
648// FG-55: the seven impls above historically covered only part of the exported
649// distribution suite. The impls below extend `Validate` to the remaining ten
650// exported distributions (LogNormal, Binomial, Poisson, StudentT, Cauchy,
651// Laplace, Weibull, ChiSquared, InverseGamma, DiscreteUniform) so the standalone
652// trait is complete for all 17 distributions re-exported at the crate root. Each
653// impl mirrors the validation performed by the corresponding `new()` constructor
654// in `core::distribution` exactly (same predicates, messages, error codes, and
655// context keys). `tests/f_validate_coverage.rs` guards against future drift.
656
657impl Validate for LogNormal {
658    fn validate(&self) -> FugueResult<()> {
659        if !self.mu().is_finite() {
660            return Err(invalid_params!(
661                "LogNormal",
662                "Mean (mu) must be finite",
663                InvalidMean,
664                "mu" => format!("{}", self.mu())
665            ));
666        }
667        if self.sigma() <= 0.0 || !self.sigma().is_finite() {
668            return Err(invalid_params!(
669                "LogNormal",
670                "Standard deviation (sigma) must be positive and finite",
671                InvalidVariance,
672                "sigma" => format!("{}", self.sigma()),
673                "expected" => "> 0.0 and finite"
674            ));
675        }
676        Ok(())
677    }
678}
679
680impl Validate for Binomial {
681    fn validate(&self) -> FugueResult<()> {
682        if !self.p().is_finite() || !(0.0..=1.0).contains(&self.p()) {
683            return Err(invalid_params!(
684                "Binomial",
685                "Probability must be in [0, 1]",
686                InvalidProbability,
687                "p" => format!("{}", self.p()),
688                "expected" => "[0.0, 1.0]"
689            ));
690        }
691        Ok(())
692    }
693}
694
695impl Validate for Poisson {
696    fn validate(&self) -> FugueResult<()> {
697        if self.lambda() <= 0.0 || !self.lambda().is_finite() {
698            return Err(invalid_params!(
699                "Poisson",
700                "Rate parameter lambda must be positive and finite",
701                InvalidRate,
702                "lambda" => format!("{}", self.lambda()),
703                "expected" => "> 0.0 and finite"
704            ));
705        }
706        Ok(())
707    }
708}
709
710impl Validate for StudentT {
711    fn validate(&self) -> FugueResult<()> {
712        if self.df() <= 0.0 || !self.df().is_finite() {
713            return Err(invalid_params!(
714                "StudentT",
715                "Degrees of freedom must be positive and finite",
716                InvalidShape,
717                "df" => format!("{}", self.df()),
718                "expected" => "> 0.0 and finite"
719            ));
720        }
721        if !self.loc().is_finite() {
722            return Err(invalid_params!(
723                "StudentT",
724                "Location (loc) must be finite",
725                InvalidMean,
726                "loc" => format!("{}", self.loc())
727            ));
728        }
729        if self.scale() <= 0.0 || !self.scale().is_finite() {
730            return Err(invalid_params!(
731                "StudentT",
732                "Scale must be positive and finite",
733                InvalidVariance,
734                "scale" => format!("{}", self.scale()),
735                "expected" => "> 0.0 and finite"
736            ));
737        }
738        Ok(())
739    }
740}
741
742impl Validate for Cauchy {
743    fn validate(&self) -> FugueResult<()> {
744        if !self.loc().is_finite() {
745            return Err(invalid_params!(
746                "Cauchy",
747                "Location (loc) must be finite",
748                InvalidMean,
749                "loc" => format!("{}", self.loc())
750            ));
751        }
752        if self.scale() <= 0.0 || !self.scale().is_finite() {
753            return Err(invalid_params!(
754                "Cauchy",
755                "Scale must be positive and finite",
756                InvalidVariance,
757                "scale" => format!("{}", self.scale()),
758                "expected" => "> 0.0 and finite"
759            ));
760        }
761        Ok(())
762    }
763}
764
765impl Validate for Laplace {
766    fn validate(&self) -> FugueResult<()> {
767        if !self.loc().is_finite() {
768            return Err(invalid_params!(
769                "Laplace",
770                "Location (loc) must be finite",
771                InvalidMean,
772                "loc" => format!("{}", self.loc())
773            ));
774        }
775        if self.scale() <= 0.0 || !self.scale().is_finite() {
776            return Err(invalid_params!(
777                "Laplace",
778                "Scale must be positive and finite",
779                InvalidVariance,
780                "scale" => format!("{}", self.scale()),
781                "expected" => "> 0.0 and finite"
782            ));
783        }
784        Ok(())
785    }
786}
787
788impl Validate for Weibull {
789    fn validate(&self) -> FugueResult<()> {
790        if self.shape() <= 0.0 || !self.shape().is_finite() {
791            return Err(invalid_params!(
792                "Weibull",
793                "Shape parameter must be positive and finite",
794                InvalidShape,
795                "shape" => format!("{}", self.shape()),
796                "expected" => "> 0.0 and finite"
797            ));
798        }
799        if self.scale() <= 0.0 || !self.scale().is_finite() {
800            return Err(invalid_params!(
801                "Weibull",
802                "Scale parameter must be positive and finite",
803                InvalidVariance,
804                "scale" => format!("{}", self.scale()),
805                "expected" => "> 0.0 and finite"
806            ));
807        }
808        Ok(())
809    }
810}
811
812impl Validate for ChiSquared {
813    fn validate(&self) -> FugueResult<()> {
814        if self.k() <= 0.0 || !self.k().is_finite() {
815            return Err(invalid_params!(
816                "ChiSquared",
817                "Degrees of freedom must be positive and finite",
818                InvalidShape,
819                "k" => format!("{}", self.k()),
820                "expected" => "> 0.0 and finite"
821            ));
822        }
823        Ok(())
824    }
825}
826
827impl Validate for InverseGamma {
828    fn validate(&self) -> FugueResult<()> {
829        if self.shape() <= 0.0 || !self.shape().is_finite() {
830            return Err(invalid_params!(
831                "InverseGamma",
832                "Shape parameter must be positive and finite",
833                InvalidShape,
834                "shape" => format!("{}", self.shape()),
835                "expected" => "> 0.0 and finite"
836            ));
837        }
838        if self.rate() <= 0.0 || !self.rate().is_finite() {
839            return Err(invalid_params!(
840                "InverseGamma",
841                "Rate parameter must be positive and finite",
842                InvalidRate,
843                "rate" => format!("{}", self.rate()),
844                "expected" => "> 0.0 and finite"
845            ));
846        }
847        Ok(())
848    }
849}
850
851impl Validate for DiscreteUniform {
852    fn validate(&self) -> FugueResult<()> {
853        if self.high() < self.low() {
854            return Err(invalid_params!(
855                "DiscreteUniform",
856                "Upper bound must be >= lower bound",
857                InvalidRange,
858                "low" => format!("{}", self.low()),
859                "high" => format!("{}", self.high())
860            ));
861        }
862        Ok(())
863    }
864}
865
866#[cfg(test)]
867mod tests {
868    use super::*;
869    use crate::addr;
870
871    /// FG-33: every remaining `ErrorCode` variant must be live (constructed
872    /// somewhere in the crate) and correctly categorized. This test enumerates
873    /// all 11 surviving variants; if a future change adds a variant without
874    /// updating this list, the mismatch is a signal to double check it's wired
875    /// into a real code path rather than left aspirational again.
876    #[test]
877    fn error_code_taxonomy_is_exactly_the_live_set() {
878        let all = [
879            ErrorCode::InvalidMean,
880            ErrorCode::InvalidVariance,
881            ErrorCode::InvalidProbability,
882            ErrorCode::InvalidRange,
883            ErrorCode::InvalidShape,
884            ErrorCode::InvalidRate,
885            ErrorCode::InvalidCount,
886            ErrorCode::AddressConflict,
887            ErrorCode::UnexpectedModelStructure,
888            ErrorCode::TraceAddressNotFound,
889            ErrorCode::TypeMismatch,
890        ];
891        assert_eq!(all.len(), 11);
892        for code in all {
893            // Every code must have a non-empty description and a known category.
894            assert!(!code.description().is_empty());
895            assert_ne!(code.category(), ErrorCategory::Unknown);
896        }
897    }
898
899    #[test]
900    fn error_code_category_and_description() {
901        let code = ErrorCode::InvalidMean;
902        assert!(ErrorCode::InvalidMean.description().contains("mean"));
903        assert_eq!(code.category(), ErrorCategory::DistributionValidation);
904
905        assert_eq!(
906            ErrorCode::AddressConflict.category(),
907            ErrorCategory::ModelExecution
908        );
909        assert_eq!(
910            ErrorCode::TraceAddressNotFound.category(),
911            ErrorCategory::TraceManipulation
912        );
913        assert_eq!(
914            ErrorCode::TypeMismatch.category(),
915            ErrorCategory::TypeSystem
916        );
917    }
918
919    #[test]
920    fn invalid_parameters_constructor_and_context() {
921        let err = FugueError::invalid_parameters("Normal", "bad params", ErrorCode::InvalidMean)
922            .with_context("mu", "nan")
923            .with_source_location("file.rs", 10);
924
925        let msg = format!("{}", err);
926        assert!(msg.contains("Invalid parameters for Normal"));
927        assert!(msg.contains("mu=nan"));
928        assert_eq!(err.code(), ErrorCode::InvalidMean);
929        assert_eq!(err.category(), ErrorCategory::DistributionValidation);
930        assert!(err.is_validation_error());
931    }
932
933    #[test]
934    fn error_macros_create_expected_variants() {
935        let e1 = invalid_params!("Uniform", "bad range", InvalidRange, "low" => "1", "high" => "0");
936        match e1 {
937            FugueError::InvalidParameters { code, .. } => assert_eq!(code, ErrorCode::InvalidRange),
938            _ => panic!("expected InvalidParameters"),
939        }
940
941        let e2 = trace_error!("lookup", Some(addr!("x")), "missing", TraceAddressNotFound);
942        match e2 {
943            FugueError::TraceError { code, .. } => {
944                assert_eq!(code, ErrorCode::TraceAddressNotFound)
945            }
946            _ => panic!("expected TraceError"),
947        }
948    }
949
950    #[test]
951    fn type_mismatch_constructor() {
952        let e = FugueError::type_mismatch(addr!("a"), "f64", "bool");
953        assert_eq!(e.code(), ErrorCode::TypeMismatch);
954        assert_eq!(e.category(), ErrorCategory::TypeSystem);
955        let msg = format!("{}", e);
956        assert!(msg.contains("Type mismatch"));
957    }
958
959    #[test]
960    fn validate_trait_on_valid_distributions() {
961        // FG-55: `Validate` is implemented for all 17 exported distributions;
962        // exercise a valid instance of each here. `tests/f_validate_coverage.rs`
963        // is the public-API drift guard.
964        assert!(Normal::new(0.0, 1.0).unwrap().validate().is_ok());
965        assert!(Exponential::new(1.0).unwrap().validate().is_ok());
966        assert!(Beta::new(2.0, 3.0).unwrap().validate().is_ok());
967        assert!(Gamma::new(2.0, 1.0).unwrap().validate().is_ok());
968        assert!(Uniform::new(0.0, 1.0).unwrap().validate().is_ok());
969        assert!(Bernoulli::new(0.5).unwrap().validate().is_ok());
970        assert!(Categorical::new(vec![0.2, 0.8]).unwrap().validate().is_ok());
971        assert!(LogNormal::new(0.0, 1.0).unwrap().validate().is_ok());
972        assert!(Binomial::new(10, 0.5).unwrap().validate().is_ok());
973        assert!(Poisson::new(3.0).unwrap().validate().is_ok());
974        assert!(StudentT::new(5.0, 0.0, 1.0).unwrap().validate().is_ok());
975        assert!(Cauchy::new(0.0, 1.0).unwrap().validate().is_ok());
976        assert!(Laplace::new(0.0, 1.0).unwrap().validate().is_ok());
977        assert!(Weibull::new(2.0, 1.5).unwrap().validate().is_ok());
978        assert!(ChiSquared::new(4.0).unwrap().validate().is_ok());
979        assert!(InverseGamma::new(3.0, 2.0).unwrap().validate().is_ok());
980        assert!(DiscreteUniform::new(1, 6).unwrap().validate().is_ok());
981    }
982
983    #[test]
984    fn error_cause_chaining_and_display_variants() {
985        // Build a cause chain
986        let base = FugueError::invalid_parameters("Normal", "bad", ErrorCode::InvalidMean);
987        let ctx = ErrorContext::new().with_cause(base.clone());
988        let model_err = FugueError::ModelError {
989            address: Some(crate::addr!("x")),
990            reason: "failed".into(),
991            code: ErrorCode::UnexpectedModelStructure,
992            context: ctx,
993        };
994        let msg = format!("{}", model_err);
995        assert!(msg.contains("Model error"));
996        assert!(msg.contains("Caused by"));
997    }
998}