Skip to main content

fugue_evo/
error.rs

1//! Error types for fugue-evo
2//!
3//! This module defines all error types used throughout the library.
4
5use thiserror::Error;
6
7/// Address type for genome trace operations
8pub type Address = String;
9
10/// Error type for genome operations
11#[derive(Debug, Error, Clone, PartialEq)]
12pub enum GenomeError {
13    /// A required address was missing from the trace
14    #[error("Missing address in trace: {0}")]
15    MissingAddress(Address),
16
17    /// Type mismatch when converting from trace
18    #[error("Type mismatch at address {address}: expected {expected}, got {actual}")]
19    TypeMismatch {
20        address: Address,
21        expected: String,
22        actual: String,
23    },
24
25    /// Invalid genome structure
26    #[error("Invalid genome structure: {0}")]
27    InvalidStructure(String),
28
29    /// Constraint violation in genome
30    #[error("Constraint violation: {0}")]
31    ConstraintViolation(String),
32
33    /// Dimension mismatch
34    #[error("Dimension mismatch: expected {expected}, got {actual}")]
35    DimensionMismatch { expected: usize, actual: usize },
36}
37
38/// Error type for operator failures
39#[derive(Debug, Error, Clone, PartialEq)]
40pub enum OperatorError {
41    /// Crossover operation failed
42    #[error("Crossover failed: {0}")]
43    CrossoverFailed(String),
44
45    /// Mutation operation failed
46    #[error("Mutation failed: {0}")]
47    MutationFailed(String),
48
49    /// Selection operation failed
50    #[error("Selection failed: {0}")]
51    SelectionFailed(String),
52
53    /// Invalid operator configuration
54    #[error("Invalid operator configuration: {0}")]
55    InvalidConfiguration(String),
56}
57
58/// Error type for checkpoint operations
59#[derive(Debug, Error)]
60pub enum CheckpointError {
61    /// IO error during checkpoint (native only)
62    #[cfg(not(target_arch = "wasm32"))]
63    #[error("IO error: {0}")]
64    Io(#[from] std::io::Error),
65
66    /// Storage error (WASM-compatible alternative to IO)
67    #[cfg(target_arch = "wasm32")]
68    #[error("Storage error: {0}")]
69    Storage(String),
70
71    /// Serialization error (message-only; retained for callers that do not
72    /// have a structured source to attach).
73    #[error("Serialization error: {0}")]
74    Serialization(String),
75
76    /// Deserialization error (message-only; retained for callers that do not
77    /// have a structured source to attach).
78    #[error("Deserialization error: {0}")]
79    Deserialization(String),
80
81    /// Serialization failed while writing a checkpoint file.
82    ///
83    /// Unlike [`CheckpointError::Serialization`], this variant preserves the
84    /// underlying error via [`std::error::Error::source`], so callers can
85    /// `source()`/`downcast_ref()` into the real cause (EV-89).
86    #[error("Failed to serialize checkpoint")]
87    SerializeError(#[source] Box<dyn std::error::Error + Send + Sync>),
88
89    /// Deserialization failed while reading a checkpoint file.
90    ///
91    /// Unlike [`CheckpointError::Deserialization`], this variant preserves the
92    /// underlying error via [`std::error::Error::source`], so callers can
93    /// distinguish e.g. an EOF/truncation from a type mismatch (EV-89).
94    #[error("Failed to deserialize checkpoint")]
95    DeserializeError(#[source] Box<dyn std::error::Error + Send + Sync>),
96
97    /// Checkpoint file exceeds the configured size limit.
98    ///
99    /// Guards against unbounded allocation when deserializing a corrupted or
100    /// hostile checkpoint file (EV-48).
101    #[error("Checkpoint too large: {size} bytes exceeds limit of {limit} bytes")]
102    TooLarge {
103        /// Size of the checkpoint payload in bytes.
104        size: u64,
105        /// Configured maximum in bytes.
106        limit: u64,
107    },
108
109    /// Checkpoint version mismatch
110    #[error("Version mismatch: expected {expected}, found {found}")]
111    VersionMismatch { expected: u32, found: u32 },
112
113    /// Checkpoint version is too new
114    #[error("Checkpoint version {0} is newer than supported")]
115    VersionTooNew(u32),
116
117    /// Checkpoint version is too old
118    #[error("Checkpoint version {0} is too old to load")]
119    VersionTooOld(u32),
120
121    /// Checkpoint file not found
122    #[error("Checkpoint not found: {0}")]
123    NotFound(String),
124
125    /// Corrupted checkpoint data
126    #[error("Corrupted checkpoint: {0}")]
127    Corrupted(String),
128}
129
130/// Top-level error type for evolution operations
131#[derive(Debug, Error)]
132pub enum EvolutionError {
133    /// Genome error
134    #[error("Genome error: {0}")]
135    Genome(#[from] GenomeError),
136
137    /// Operator error
138    #[error("Operator error: {0}")]
139    Operator(#[from] OperatorError),
140
141    /// Fitness evaluation failed
142    #[error("Fitness evaluation failed: {0}")]
143    FitnessEvaluation(String),
144
145    /// Invalid configuration
146    #[error("Invalid configuration: {0}")]
147    Configuration(String),
148
149    /// Checkpoint error
150    #[error("Checkpoint error: {0}")]
151    Checkpoint(#[from] CheckpointError),
152
153    /// Numerical instability
154    #[error("Numerical instability: {0}")]
155    Numerical(String),
156
157    /// Empty population
158    #[error("Empty population")]
159    EmptyPopulation,
160
161    /// Interactive evaluation error
162    #[error("Interactive evaluation error: {0}")]
163    InteractiveEvaluation(String),
164
165    /// Insufficient evaluation coverage
166    #[error("Insufficient coverage: {coverage:.1}% (need {required:.1}%)")]
167    InsufficientCoverage {
168        /// Actual coverage achieved
169        coverage: f64,
170        /// Required coverage threshold
171        required: f64,
172    },
173}
174
175/// Result type alias for evolution operations
176pub type EvoResult<T> = Result<T, EvolutionError>;
177
178/// Repair information when an operator needs to fix a constraint violation
179#[derive(Debug, Clone)]
180pub struct RepairInfo {
181    /// List of constraint violations that were repaired
182    pub constraint_violations: Vec<String>,
183    /// Method used to repair the genome
184    pub repair_method: &'static str,
185}
186
187/// Result of an operator application with optional repair information
188#[derive(Debug, Clone)]
189pub enum OperatorResult<G> {
190    /// Operation succeeded without repairs
191    Success(G),
192    /// Operation succeeded but required repairs
193    Repaired(G, RepairInfo),
194    /// Operation failed unrecoverably
195    Failed(OperatorError),
196}
197
198impl<G> OperatorResult<G> {
199    /// Returns the genome if successful or repaired, None if failed
200    pub fn genome(self) -> Option<G> {
201        match self {
202            Self::Success(g) | Self::Repaired(g, _) => Some(g),
203            Self::Failed(_) => None,
204        }
205    }
206
207    /// Returns true if the operation was successful (with or without repairs)
208    pub fn is_ok(&self) -> bool {
209        !matches!(self, Self::Failed(_))
210    }
211
212    /// Returns true if repairs were needed
213    pub fn was_repaired(&self) -> bool {
214        matches!(self, Self::Repaired(_, _))
215    }
216
217    /// Maps the genome type
218    pub fn map<U, F: FnOnce(G) -> U>(self, f: F) -> OperatorResult<U> {
219        match self {
220            Self::Success(g) => OperatorResult::Success(f(g)),
221            Self::Repaired(g, info) => OperatorResult::Repaired(f(g), info),
222            Self::Failed(e) => OperatorResult::Failed(e),
223        }
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_genome_error_display() {
233        let err = GenomeError::MissingAddress("gene_0".to_string());
234        assert_eq!(err.to_string(), "Missing address in trace: gene_0");
235
236        let err = GenomeError::TypeMismatch {
237            address: "gene_1".to_string(),
238            expected: "f64".to_string(),
239            actual: "bool".to_string(),
240        };
241        assert_eq!(
242            err.to_string(),
243            "Type mismatch at address gene_1: expected f64, got bool"
244        );
245
246        let err = GenomeError::DimensionMismatch {
247            expected: 10,
248            actual: 5,
249        };
250        assert_eq!(err.to_string(), "Dimension mismatch: expected 10, got 5");
251    }
252
253    #[test]
254    fn test_operator_error_display() {
255        let err = OperatorError::CrossoverFailed("incompatible parents".to_string());
256        assert_eq!(err.to_string(), "Crossover failed: incompatible parents");
257
258        let err = OperatorError::InvalidConfiguration("eta must be positive".to_string());
259        assert_eq!(
260            err.to_string(),
261            "Invalid operator configuration: eta must be positive"
262        );
263    }
264
265    #[test]
266    fn test_evolution_error_from_genome_error() {
267        let genome_err = GenomeError::InvalidStructure("bad shape".to_string());
268        let evo_err: EvolutionError = genome_err.into();
269        assert!(matches!(evo_err, EvolutionError::Genome(_)));
270    }
271
272    #[test]
273    fn test_operator_result_success() {
274        let result: OperatorResult<i32> = OperatorResult::Success(42);
275        assert!(result.is_ok());
276        assert!(!result.was_repaired());
277        assert_eq!(result.genome(), Some(42));
278    }
279
280    #[test]
281    fn test_operator_result_repaired() {
282        let repair_info = RepairInfo {
283            constraint_violations: vec!["out of bounds".to_string()],
284            repair_method: "clamp",
285        };
286        let result: OperatorResult<i32> = OperatorResult::Repaired(42, repair_info);
287        assert!(result.is_ok());
288        assert!(result.was_repaired());
289        assert_eq!(result.genome(), Some(42));
290    }
291
292    #[test]
293    fn test_operator_result_failed() {
294        let result: OperatorResult<i32> =
295            OperatorResult::Failed(OperatorError::MutationFailed("test".to_string()));
296        assert!(!result.is_ok());
297        assert!(!result.was_repaired());
298        assert_eq!(result.genome(), None);
299    }
300
301    #[test]
302    fn test_operator_result_map() {
303        let result: OperatorResult<i32> = OperatorResult::Success(42);
304        let mapped = result.map(|x| x * 2);
305        assert_eq!(mapped.genome(), Some(84));
306    }
307
308    #[test]
309    fn test_checkpoint_error_preserves_source_chain() {
310        // regression: EV-89 - (de)serialization errors must preserve the
311        // underlying error via source() rather than stringifying it.
312        use std::error::Error;
313
314        let json_err = serde_json::from_str::<i32>("not a number").unwrap_err();
315        let err = CheckpointError::DeserializeError(Box::new(json_err));
316
317        // The source chain must be walkable and downcastable to the real cause.
318        let source = err.source().expect("source chain must be preserved");
319        assert!(
320            source.downcast_ref::<serde_json::Error>().is_some(),
321            "source must downcast to the original serde_json::Error"
322        );
323    }
324}