Skip to main content

stats_claw/
error.rs

1//! Typed error and `Result` alias for the framework's fallible numerics.
2//!
3//! This module defines the crate-wide `Error` enum and the `Result<T>` alias
4//! that all hand-written numerics return on bad input or domain violations
5//! (e.g. a negative scale parameter, an empty sample). It is the single place
6//! recoverable failures are modelled, so call sites never reach for `unwrap`.
7
8use std::fmt;
9
10/// A recoverable failure raised by the framework's hand-written numerics.
11///
12/// Numerics never panic on bad input — they return one of these variants so the
13/// caller can decide how to recover. The message-carrying variants embed a short
14/// human-readable reason; the unit variants are self-describing.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum Error {
17    /// A required sample or collection was empty.
18    EmptyInput,
19    /// A parameter or value violated a precondition; carries the reason.
20    InvalidInput(String),
21    /// The input is technically valid but degenerate (e.g. zero variance);
22    /// carries the reason.
23    DegenerateInput(String),
24    /// There were too few observations to compute the requested quantity.
25    InsufficientData,
26    /// An iterative procedure exhausted its budget without converging.
27    NotConverged,
28}
29
30impl fmt::Display for Error {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            Self::EmptyInput => write!(f, "input was empty"),
34            Self::InvalidInput(m) => write!(f, "invalid input: {m}"),
35            Self::DegenerateInput(m) => write!(f, "degenerate input: {m}"),
36            Self::InsufficientData => write!(f, "insufficient data"),
37            Self::NotConverged => write!(f, "did not converge"),
38        }
39    }
40}
41
42impl std::error::Error for Error {}
43
44/// Result alias for the framework's fallible numerics, defaulting the error to
45/// [`Error`].
46pub type Result<T> = std::result::Result<T, Error>;
47
48/// Kani formal-verification harness for the error type.
49///
50/// Compiled only under `cargo kani` (behind `#[cfg(kani)]`); invisible to normal
51/// build/test/clippy.
52///
53/// ## Scope note (honest disclosure)
54///
55/// [`Error`] is plain data: four data-free variants and two `String`-carrying
56/// variants, with a [`fmt::Display`] impl whose arms are single `write!` calls.
57/// The message-variant arms (`write!(f, "invalid input: {m}")`) are total by
58/// inspection — `write!` into a `String` formatter cannot fail — but their `String`
59/// payload is not a tractable symbolic input for CBMC, so they are covered by the
60/// `#[cfg(test)]` suite rather than a proof. The harness below proves the
61/// data-free path exhaustively: formatting every unit variant is panic-free and
62/// yields a non-empty message.
63#[cfg(kani)]
64mod verification {
65    use super::Error;
66
67    /// Proves that displaying each data-free [`Error`] variant neither panics nor
68    /// produces an empty message. A symbolic index selects the variant so the proof
69    /// covers all three data-free arms in one harness.
70    #[kani::proof]
71    fn error_display_unit_variants_total() {
72        let which: u8 = kani::any();
73        let err = match which % 3 {
74            0 => Error::EmptyInput,
75            1 => Error::InsufficientData,
76            _ => Error::NotConverged,
77        };
78        let text = err.to_string();
79        assert!(!text.is_empty(), "error display produced an empty message");
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn display_is_human_readable() {
89        assert_eq!(
90            Error::EmptyInput.to_string(),
91            "input was empty",
92            "EmptyInput display text changed"
93        );
94    }
95
96    #[test]
97    fn display_includes_detail_for_message_variants() {
98        assert_eq!(
99            Error::InvalidInput("scale must be > 0".to_owned()).to_string(),
100            "invalid input: scale must be > 0"
101        );
102        assert_eq!(
103            Error::DegenerateInput("zero variance".to_owned()).to_string(),
104            "degenerate input: zero variance"
105        );
106    }
107
108    #[test]
109    fn usable_as_std_error() {
110        fn boxed() -> Box<dyn std::error::Error> {
111            Box::new(Error::NotConverged)
112        }
113        assert_eq!(boxed().to_string(), "did not converge");
114    }
115}