Skip to main content

loeres/
error.rs

1//! Allocation-free error topology for `loeres` (RFC 003).
2//!
3//! [`SolverError`] is the canonical, copyable, allocation-free error returned by
4//! every fallible core API. It implements `Debug` but deliberately **not**
5//! `Display` or `core::error::Error`: formatting support encourages string
6//! paths and inflates device binaries, so human-facing presentation belongs to
7//! `loeres-cluster` or host-side tooling, not the edge baseline.
8//!
9//! Non-convergence is **not** an error here. A bounded loop that reaches its
10//! iteration cap returns `Ok` with `SolveStatus::NotConverged` (RFC 014).
11
12/// Allocation-free, copyable error categories shared across every Loeres crate.
13///
14/// Marked `#[non_exhaustive]`: downstream `match`es must include a wildcard arm,
15/// so future solvers can add categories without a breaking change.
16#[non_exhaustive]
17#[derive(Copy, Clone, Debug, Eq, PartialEq)]
18pub enum SolverError {
19    /// Two shapes that had to agree did not. Payloads are the mismatched extents.
20    DimensionMismatch {
21        /// Left-hand extent.
22        lhs: u32,
23        /// Right-hand extent.
24        rhs: u32,
25    },
26    /// A single dimension was itself invalid (e.g. zero where positive required).
27    InvalidDimension,
28    /// Input violated a declared domain or problem contract.
29    InvalidInput,
30    /// A floating-like input was NaN or infinite.
31    NonFiniteInput,
32    /// The problem is well-formed but unsupported by the selected solver/profile.
33    UnsupportedProblemStructure,
34    /// The system matrix was singular under the selected solver.
35    SingularMatrix,
36    /// Conditioning exceeded the solver's declared stability threshold.
37    IllConditioned,
38    /// An operation would leave the solver's valid numerical domain โ€” division by
39    /// zero, square root of a negative, or logarithm of a non-positive value.
40    NumericalDomain,
41    /// A checked scalar or storage operation overflowed.
42    Overflow,
43    /// The caller-provided workspace cannot hold the required scratch state.
44    WorkspaceTooSmall,
45    /// A cluster cancellation token was observed.
46    Cancelled,
47    /// An optional backend was unavailable.
48    BackendUnavailable,
49    /// A library invariant was violated โ€” a bug, surfaced as an error rather than
50    /// a panic so device callers can fail closed.
51    InternalInvariantViolation,
52}
53
54// RFC 003 ยง3.3: a bloated error bloats every `Result<T, SolverError>` return
55// path and raises device stack pressure. Keep it small, forever.
56const _: () = assert!(core::mem::size_of::<SolverError>() <= 16);
57
58impl SolverError {
59    /// True for malformed *caller input* โ€” bad dimensions, or non-finite /
60    /// otherwise invalid values supplied to a public entry point.
61    #[inline]
62    #[must_use]
63    pub const fn is_input_error(self) -> bool {
64        matches!(
65            self,
66            Self::DimensionMismatch { .. }
67                | Self::InvalidDimension
68                | Self::InvalidInput
69                | Self::NonFiniteInput
70        )
71    }
72
73    /// True for numerical failures encountered while solving.
74    #[inline]
75    #[must_use]
76    pub const fn is_numerical_error(self) -> bool {
77        matches!(
78            self,
79            Self::SingularMatrix | Self::IllConditioned | Self::NumericalDomain | Self::Overflow
80        )
81    }
82
83    /// True for resource / availability failures (workspace, cancellation, backend).
84    #[inline]
85    #[must_use]
86    pub const fn is_resource_error(self) -> bool {
87        matches!(
88            self,
89            Self::WorkspaceTooSmall | Self::Cancelled | Self::BackendUnavailable
90        )
91    }
92}
93
94/// Map an error to a stable, allocation-free `snake_case` identifier.
95///
96/// Intended for host-side logging and diagnostics. The mapping is part of the
97/// public contract and is pinned by tests; renaming a code is a reviewed change.
98#[inline]
99#[must_use]
100pub const fn error_code_to_str(err: SolverError) -> &'static str {
101    match err {
102        SolverError::DimensionMismatch { .. } => "dimension_mismatch",
103        SolverError::InvalidDimension => "invalid_dimension",
104        SolverError::InvalidInput => "invalid_input",
105        SolverError::NonFiniteInput => "non_finite_input",
106        SolverError::UnsupportedProblemStructure => "unsupported_problem_structure",
107        SolverError::SingularMatrix => "singular_matrix",
108        SolverError::IllConditioned => "ill_conditioned",
109        SolverError::NumericalDomain => "numerical_domain",
110        SolverError::Overflow => "overflow",
111        SolverError::WorkspaceTooSmall => "workspace_too_small",
112        SolverError::Cancelled => "cancelled",
113        SolverError::BackendUnavailable => "backend_unavailable",
114        SolverError::InternalInvariantViolation => "internal_invariant_violation",
115    }
116}
117
118#[cfg(test)]
119mod tests;