stats-claw 0.2.1

Data science on the hot path: in-process, zero-dependency statistical computing for Rust (distributions, hypothesis tests, resampling) validated against scipy
Documentation
//! Typed error and `Result` alias for the framework's fallible numerics.
//!
//! This module defines the crate-wide `Error` enum and the `Result<T>` alias
//! that all hand-written numerics return on bad input or domain violations
//! (e.g. a negative scale parameter, an empty sample). It is the single place
//! recoverable failures are modelled, so call sites never reach for `unwrap`.

use std::fmt;

/// A recoverable failure raised by the framework's hand-written numerics.
///
/// Numerics never panic on bad input — they return one of these variants so the
/// caller can decide how to recover. The message-carrying variants embed a short
/// human-readable reason; the unit variants are self-describing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
    /// A required sample or collection was empty.
    EmptyInput,
    /// A parameter or value violated a precondition; carries the reason.
    InvalidInput(String),
    /// The input is technically valid but degenerate (e.g. zero variance);
    /// carries the reason.
    DegenerateInput(String),
    /// There were too few observations to compute the requested quantity.
    InsufficientData,
    /// An iterative procedure exhausted its budget without converging.
    NotConverged,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyInput => write!(f, "input was empty"),
            Self::InvalidInput(m) => write!(f, "invalid input: {m}"),
            Self::DegenerateInput(m) => write!(f, "degenerate input: {m}"),
            Self::InsufficientData => write!(f, "insufficient data"),
            Self::NotConverged => write!(f, "did not converge"),
        }
    }
}

impl std::error::Error for Error {}

/// Result alias for the framework's fallible numerics, defaulting the error to
/// [`Error`].
pub type Result<T> = std::result::Result<T, Error>;

/// Kani formal-verification harness for the error type.
///
/// Compiled only under `cargo kani` (behind `#[cfg(kani)]`); invisible to normal
/// build/test/clippy.
///
/// ## Scope note (honest disclosure)
///
/// [`Error`] is plain data: four data-free variants and two `String`-carrying
/// variants, with a [`fmt::Display`] impl whose arms are single `write!` calls.
/// The message-variant arms (`write!(f, "invalid input: {m}")`) are total by
/// inspection — `write!` into a `String` formatter cannot fail — but their `String`
/// payload is not a tractable symbolic input for CBMC, so they are covered by the
/// `#[cfg(test)]` suite rather than a proof. The harness below proves the
/// data-free path exhaustively: formatting every unit variant is panic-free and
/// yields a non-empty message.
#[cfg(kani)]
mod verification {
    use super::Error;

    /// Proves that displaying each data-free [`Error`] variant neither panics nor
    /// produces an empty message. A symbolic index selects the variant so the proof
    /// covers all three data-free arms in one harness.
    #[kani::proof]
    fn error_display_unit_variants_total() {
        let which: u8 = kani::any();
        let err = match which % 3 {
            0 => Error::EmptyInput,
            1 => Error::InsufficientData,
            _ => Error::NotConverged,
        };
        let text = err.to_string();
        assert!(!text.is_empty(), "error display produced an empty message");
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn display_is_human_readable() {
        assert_eq!(
            Error::EmptyInput.to_string(),
            "input was empty",
            "EmptyInput display text changed"
        );
    }

    #[test]
    fn display_includes_detail_for_message_variants() {
        assert_eq!(
            Error::InvalidInput("scale must be > 0".to_owned()).to_string(),
            "invalid input: scale must be > 0"
        );
        assert_eq!(
            Error::DegenerateInput("zero variance".to_owned()).to_string(),
            "degenerate input: zero variance"
        );
    }

    #[test]
    fn usable_as_std_error() {
        fn boxed() -> Box<dyn std::error::Error> {
            Box::new(Error::NotConverged)
        }
        assert_eq!(boxed().to_string(), "did not converge");
    }
}