amberfork_align/
params.rs1use crate::diff::DiffParams;
16use crate::fork::ForkParams;
17use crate::nw::AlignParams;
18use std::fmt;
19
20#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum ParamError {
24 TauOutOfRange(f64),
26 ResyncKZero,
28 GapOpenNotPositive(f64),
30 GapExtNotPositive(f64),
32 GapExtExceedsOpen { gap_open: f64, gap_ext: f64 },
34 StepsExceedMax { steps: usize, max: usize },
38}
39
40impl fmt::Display for ParamError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 Self::TauOutOfRange(tau) => {
44 write!(f, "tau must be within [0, 1], got {tau}")
45 }
46 Self::ResyncKZero => {
47 write!(
48 f,
49 "resync_k must be at least 1 (0 would recover every blip)"
50 )
51 }
52 Self::GapOpenNotPositive(v) => {
53 write!(f, "gap_open must be positive and finite, got {v}")
54 }
55 Self::GapExtNotPositive(v) => {
56 write!(f, "gap_ext must be positive and finite, got {v}")
57 }
58 Self::GapExtExceedsOpen { gap_open, gap_ext } => write!(
59 f,
60 "gap_ext ({gap_ext}) must not exceed gap_open ({gap_open}): extending a gap \
61 costing more than opening one breaks the affine premise"
62 ),
63 Self::StepsExceedMax { steps, max } => write!(
64 f,
65 "trace has {steps} steps, over the {max}-step alignment guard (memory and \
66 time grow with steps²); pass --max-steps {steps} to proceed anyway"
67 ),
68 }
69 }
70}
71
72impl std::error::Error for ParamError {}
73
74impl AlignParams {
75 pub fn validated(self) -> Result<Self, ParamError> {
80 if !self.gap_open.is_finite() || self.gap_open <= 0.0 {
81 return Err(ParamError::GapOpenNotPositive(self.gap_open));
82 }
83 if !self.gap_ext.is_finite() || self.gap_ext <= 0.0 {
84 return Err(ParamError::GapExtNotPositive(self.gap_ext));
85 }
86 if self.gap_ext > self.gap_open {
87 return Err(ParamError::GapExtExceedsOpen {
88 gap_open: self.gap_open,
89 gap_ext: self.gap_ext,
90 });
91 }
92 Ok(self)
93 }
94}
95
96impl ForkParams {
97 pub fn validated(self) -> Result<Self, ParamError> {
102 if !self.tau.is_finite() || !(0.0..=1.0).contains(&self.tau) {
103 return Err(ParamError::TauOutOfRange(self.tau));
104 }
105 if self.resync_k == 0 {
106 return Err(ParamError::ResyncKZero);
107 }
108 Ok(self)
109 }
110}
111
112impl DiffParams {
113 pub fn validated(self) -> Result<Self, ParamError> {
119 self.align.validated()?;
120 self.fork.validated()?;
121 Ok(self)
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn dev_calibrated_defaults_are_valid() {
131 assert_eq!(
132 DiffParams::default().validated(),
133 Ok(DiffParams::default()),
134 "the shipped defaults must satisfy their own invariants"
135 );
136 }
137
138 #[test]
139 fn tau_outside_the_cost_scale_is_rejected() {
140 for tau in [-0.1, 1.1, f64::NAN, f64::INFINITY] {
141 let params = ForkParams {
142 tau,
143 ..ForkParams::default()
144 };
145 match params.validated() {
146 Err(ParamError::TauOutOfRange(seen)) => {
147 assert!(seen.is_nan() == tau.is_nan() || seen == tau)
148 }
149 other => panic!("tau {tau} must be rejected, got {other:?}"),
150 }
151 }
152 }
153
154 #[test]
155 fn resync_k_zero_is_rejected() {
156 let params = ForkParams {
157 resync_k: 0,
158 ..ForkParams::default()
159 };
160 assert_eq!(params.validated(), Err(ParamError::ResyncKZero));
161 }
162
163 #[test]
164 fn non_positive_or_non_finite_gaps_are_rejected() {
165 for gap_open in [0.0, -0.5, f64::NAN] {
166 let params = AlignParams {
167 gap_open,
168 ..AlignParams::default()
169 };
170 assert!(
171 matches!(params.validated(), Err(ParamError::GapOpenNotPositive(_))),
172 "gap_open {gap_open} must be rejected"
173 );
174 }
175 for gap_ext in [0.0, -0.5, f64::INFINITY] {
176 let params = AlignParams {
177 gap_ext,
178 ..AlignParams::default()
179 };
180 assert!(
181 matches!(
182 params.validated(),
183 Err(ParamError::GapExtNotPositive(_) | ParamError::GapExtExceedsOpen { .. })
184 ),
185 "gap_ext {gap_ext} must be rejected"
186 );
187 }
188 }
189
190 #[test]
191 fn gap_ext_above_gap_open_breaks_the_affine_premise() {
192 let params = AlignParams {
193 gap_open: 0.3,
194 gap_ext: 0.6,
195 ..AlignParams::default()
196 };
197 assert_eq!(
198 params.validated(),
199 Err(ParamError::GapExtExceedsOpen {
200 gap_open: 0.3,
201 gap_ext: 0.6
202 })
203 );
204 }
205
206 #[test]
207 fn diff_params_compose_both_validations() {
208 let bad_fork = DiffParams {
209 fork: ForkParams {
210 tau: 2.0,
211 ..ForkParams::default()
212 },
213 ..DiffParams::default()
214 };
215 assert!(matches!(
216 bad_fork.validated(),
217 Err(ParamError::TauOutOfRange(_))
218 ));
219
220 let bad_align = DiffParams {
221 align: AlignParams {
222 gap_open: -1.0,
223 ..AlignParams::default()
224 },
225 ..DiffParams::default()
226 };
227 assert!(matches!(
228 bad_align.validated(),
229 Err(ParamError::GapOpenNotPositive(_))
230 ));
231 }
232
233 #[test]
234 fn size_guard_error_names_the_override_flag() {
235 let msg = ParamError::StepsExceedMax {
236 steps: 6000,
237 max: 2000,
238 }
239 .to_string();
240 assert!(msg.contains("6000") && msg.contains("2000"), "got: {msg}");
241 assert!(
242 msg.contains("--max-steps"),
243 "the escape hatch must be discoverable from the refusal itself: {msg}"
244 );
245 }
246
247 #[test]
248 fn error_messages_name_the_offending_value() {
249 let msg = ParamError::TauOutOfRange(1.5).to_string();
250 assert!(msg.contains("1.5"), "got: {msg}");
251 let msg = ParamError::GapExtExceedsOpen {
252 gap_open: 0.3,
253 gap_ext: 0.6,
254 }
255 .to_string();
256 assert!(msg.contains("0.3") && msg.contains("0.6"), "got: {msg}");
257 }
258}