use crate::diff::DiffParams;
use crate::fork::ForkParams;
use crate::nw::AlignParams;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ParamError {
TauOutOfRange(f64),
ResyncKZero,
GapOpenNotPositive(f64),
GapExtNotPositive(f64),
GapExtExceedsOpen { gap_open: f64, gap_ext: f64 },
StepsExceedMax { steps: usize, max: usize },
}
impl fmt::Display for ParamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TauOutOfRange(tau) => {
write!(f, "tau must be within [0, 1], got {tau}")
}
Self::ResyncKZero => {
write!(
f,
"resync_k must be at least 1 (0 would recover every blip)"
)
}
Self::GapOpenNotPositive(v) => {
write!(f, "gap_open must be positive and finite, got {v}")
}
Self::GapExtNotPositive(v) => {
write!(f, "gap_ext must be positive and finite, got {v}")
}
Self::GapExtExceedsOpen { gap_open, gap_ext } => write!(
f,
"gap_ext ({gap_ext}) must not exceed gap_open ({gap_open}): extending a gap \
costing more than opening one breaks the affine premise"
),
Self::StepsExceedMax { steps, max } => write!(
f,
"trace has {steps} steps, over the {max}-step alignment guard (memory and \
time grow with steps²); pass --max-steps {steps} to proceed anyway"
),
}
}
}
impl std::error::Error for ParamError {}
impl AlignParams {
pub fn validated(self) -> Result<Self, ParamError> {
if !self.gap_open.is_finite() || self.gap_open <= 0.0 {
return Err(ParamError::GapOpenNotPositive(self.gap_open));
}
if !self.gap_ext.is_finite() || self.gap_ext <= 0.0 {
return Err(ParamError::GapExtNotPositive(self.gap_ext));
}
if self.gap_ext > self.gap_open {
return Err(ParamError::GapExtExceedsOpen {
gap_open: self.gap_open,
gap_ext: self.gap_ext,
});
}
Ok(self)
}
}
impl ForkParams {
pub fn validated(self) -> Result<Self, ParamError> {
if !self.tau.is_finite() || !(0.0..=1.0).contains(&self.tau) {
return Err(ParamError::TauOutOfRange(self.tau));
}
if self.resync_k == 0 {
return Err(ParamError::ResyncKZero);
}
Ok(self)
}
}
impl DiffParams {
pub fn validated(self) -> Result<Self, ParamError> {
self.align.validated()?;
self.fork.validated()?;
Ok(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dev_calibrated_defaults_are_valid() {
assert_eq!(
DiffParams::default().validated(),
Ok(DiffParams::default()),
"the shipped defaults must satisfy their own invariants"
);
}
#[test]
fn tau_outside_the_cost_scale_is_rejected() {
for tau in [-0.1, 1.1, f64::NAN, f64::INFINITY] {
let params = ForkParams {
tau,
..ForkParams::default()
};
match params.validated() {
Err(ParamError::TauOutOfRange(seen)) => {
assert!(seen.is_nan() == tau.is_nan() || seen == tau)
}
other => panic!("tau {tau} must be rejected, got {other:?}"),
}
}
}
#[test]
fn resync_k_zero_is_rejected() {
let params = ForkParams {
resync_k: 0,
..ForkParams::default()
};
assert_eq!(params.validated(), Err(ParamError::ResyncKZero));
}
#[test]
fn non_positive_or_non_finite_gaps_are_rejected() {
for gap_open in [0.0, -0.5, f64::NAN] {
let params = AlignParams {
gap_open,
..AlignParams::default()
};
assert!(
matches!(params.validated(), Err(ParamError::GapOpenNotPositive(_))),
"gap_open {gap_open} must be rejected"
);
}
for gap_ext in [0.0, -0.5, f64::INFINITY] {
let params = AlignParams {
gap_ext,
..AlignParams::default()
};
assert!(
matches!(
params.validated(),
Err(ParamError::GapExtNotPositive(_) | ParamError::GapExtExceedsOpen { .. })
),
"gap_ext {gap_ext} must be rejected"
);
}
}
#[test]
fn gap_ext_above_gap_open_breaks_the_affine_premise() {
let params = AlignParams {
gap_open: 0.3,
gap_ext: 0.6,
..AlignParams::default()
};
assert_eq!(
params.validated(),
Err(ParamError::GapExtExceedsOpen {
gap_open: 0.3,
gap_ext: 0.6
})
);
}
#[test]
fn diff_params_compose_both_validations() {
let bad_fork = DiffParams {
fork: ForkParams {
tau: 2.0,
..ForkParams::default()
},
..DiffParams::default()
};
assert!(matches!(
bad_fork.validated(),
Err(ParamError::TauOutOfRange(_))
));
let bad_align = DiffParams {
align: AlignParams {
gap_open: -1.0,
..AlignParams::default()
},
..DiffParams::default()
};
assert!(matches!(
bad_align.validated(),
Err(ParamError::GapOpenNotPositive(_))
));
}
#[test]
fn size_guard_error_names_the_override_flag() {
let msg = ParamError::StepsExceedMax {
steps: 6000,
max: 2000,
}
.to_string();
assert!(msg.contains("6000") && msg.contains("2000"), "got: {msg}");
assert!(
msg.contains("--max-steps"),
"the escape hatch must be discoverable from the refusal itself: {msg}"
);
}
#[test]
fn error_messages_name_the_offending_value() {
let msg = ParamError::TauOutOfRange(1.5).to_string();
assert!(msg.contains("1.5"), "got: {msg}");
let msg = ParamError::GapExtExceedsOpen {
gap_open: 0.3,
gap_ext: 0.6,
}
.to_string();
assert!(msg.contains("0.3") && msg.contains("0.6"), "got: {msg}");
}
}