use thiserror::Error;
pub type Address = String;
#[derive(Debug, Error, Clone, PartialEq)]
pub enum GenomeError {
#[error("Missing address in trace: {0}")]
MissingAddress(Address),
#[error("Type mismatch at address {address}: expected {expected}, got {actual}")]
TypeMismatch {
address: Address,
expected: String,
actual: String,
},
#[error("Invalid genome structure: {0}")]
InvalidStructure(String),
#[error("Constraint violation: {0}")]
ConstraintViolation(String),
#[error("Dimension mismatch: expected {expected}, got {actual}")]
DimensionMismatch { expected: usize, actual: usize },
}
#[derive(Debug, Error, Clone, PartialEq)]
pub enum OperatorError {
#[error("Crossover failed: {0}")]
CrossoverFailed(String),
#[error("Mutation failed: {0}")]
MutationFailed(String),
#[error("Selection failed: {0}")]
SelectionFailed(String),
#[error("Invalid operator configuration: {0}")]
InvalidConfiguration(String),
}
#[derive(Debug, Error)]
pub enum CheckpointError {
#[cfg(not(target_arch = "wasm32"))]
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[cfg(target_arch = "wasm32")]
#[error("Storage error: {0}")]
Storage(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Deserialization error: {0}")]
Deserialization(String),
#[error("Failed to serialize checkpoint")]
SerializeError(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("Failed to deserialize checkpoint")]
DeserializeError(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error("Checkpoint too large: {size} bytes exceeds limit of {limit} bytes")]
TooLarge {
size: u64,
limit: u64,
},
#[error("Version mismatch: expected {expected}, found {found}")]
VersionMismatch { expected: u32, found: u32 },
#[error("Checkpoint version {0} is newer than supported")]
VersionTooNew(u32),
#[error("Checkpoint version {0} is too old to load")]
VersionTooOld(u32),
#[error("Checkpoint not found: {0}")]
NotFound(String),
#[error("Corrupted checkpoint: {0}")]
Corrupted(String),
}
#[derive(Debug, Error)]
pub enum EvolutionError {
#[error("Genome error: {0}")]
Genome(#[from] GenomeError),
#[error("Operator error: {0}")]
Operator(#[from] OperatorError),
#[error("Fitness evaluation failed: {0}")]
FitnessEvaluation(String),
#[error("Invalid configuration: {0}")]
Configuration(String),
#[error("Checkpoint error: {0}")]
Checkpoint(#[from] CheckpointError),
#[error("Numerical instability: {0}")]
Numerical(String),
#[error("Empty population")]
EmptyPopulation,
#[error("Interactive evaluation error: {0}")]
InteractiveEvaluation(String),
#[error("Insufficient coverage: {coverage:.1}% (need {required:.1}%)")]
InsufficientCoverage {
coverage: f64,
required: f64,
},
}
pub type EvoResult<T> = Result<T, EvolutionError>;
#[derive(Debug, Clone)]
pub struct RepairInfo {
pub constraint_violations: Vec<String>,
pub repair_method: &'static str,
}
#[derive(Debug, Clone)]
pub enum OperatorResult<G> {
Success(G),
Repaired(G, RepairInfo),
Failed(OperatorError),
}
impl<G> OperatorResult<G> {
pub fn genome(self) -> Option<G> {
match self {
Self::Success(g) | Self::Repaired(g, _) => Some(g),
Self::Failed(_) => None,
}
}
pub fn is_ok(&self) -> bool {
!matches!(self, Self::Failed(_))
}
pub fn was_repaired(&self) -> bool {
matches!(self, Self::Repaired(_, _))
}
pub fn map<U, F: FnOnce(G) -> U>(self, f: F) -> OperatorResult<U> {
match self {
Self::Success(g) => OperatorResult::Success(f(g)),
Self::Repaired(g, info) => OperatorResult::Repaired(f(g), info),
Self::Failed(e) => OperatorResult::Failed(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_genome_error_display() {
let err = GenomeError::MissingAddress("gene_0".to_string());
assert_eq!(err.to_string(), "Missing address in trace: gene_0");
let err = GenomeError::TypeMismatch {
address: "gene_1".to_string(),
expected: "f64".to_string(),
actual: "bool".to_string(),
};
assert_eq!(
err.to_string(),
"Type mismatch at address gene_1: expected f64, got bool"
);
let err = GenomeError::DimensionMismatch {
expected: 10,
actual: 5,
};
assert_eq!(err.to_string(), "Dimension mismatch: expected 10, got 5");
}
#[test]
fn test_operator_error_display() {
let err = OperatorError::CrossoverFailed("incompatible parents".to_string());
assert_eq!(err.to_string(), "Crossover failed: incompatible parents");
let err = OperatorError::InvalidConfiguration("eta must be positive".to_string());
assert_eq!(
err.to_string(),
"Invalid operator configuration: eta must be positive"
);
}
#[test]
fn test_evolution_error_from_genome_error() {
let genome_err = GenomeError::InvalidStructure("bad shape".to_string());
let evo_err: EvolutionError = genome_err.into();
assert!(matches!(evo_err, EvolutionError::Genome(_)));
}
#[test]
fn test_operator_result_success() {
let result: OperatorResult<i32> = OperatorResult::Success(42);
assert!(result.is_ok());
assert!(!result.was_repaired());
assert_eq!(result.genome(), Some(42));
}
#[test]
fn test_operator_result_repaired() {
let repair_info = RepairInfo {
constraint_violations: vec!["out of bounds".to_string()],
repair_method: "clamp",
};
let result: OperatorResult<i32> = OperatorResult::Repaired(42, repair_info);
assert!(result.is_ok());
assert!(result.was_repaired());
assert_eq!(result.genome(), Some(42));
}
#[test]
fn test_operator_result_failed() {
let result: OperatorResult<i32> =
OperatorResult::Failed(OperatorError::MutationFailed("test".to_string()));
assert!(!result.is_ok());
assert!(!result.was_repaired());
assert_eq!(result.genome(), None);
}
#[test]
fn test_operator_result_map() {
let result: OperatorResult<i32> = OperatorResult::Success(42);
let mapped = result.map(|x| x * 2);
assert_eq!(mapped.genome(), Some(84));
}
#[test]
fn test_checkpoint_error_preserves_source_chain() {
use std::error::Error;
let json_err = serde_json::from_str::<i32>("not a number").unwrap_err();
let err = CheckpointError::DeserializeError(Box::new(json_err));
let source = err.source().expect("source chain must be preserved");
assert!(
source.downcast_ref::<serde_json::Error>().is_some(),
"source must downcast to the original serde_json::Error"
);
}
}