Skip to main content

tinyquant_core/
errors.rs

1//! Error enums for the `TinyQuant` core.
2//!
3//! Three enums cover the three architectural layers:
4//!
5//! - [`CodecError`] — codec layer (config, codebook, quantize, compress)
6//! - [`CorpusError`] — corpus aggregate layer
7//! - [`BackendError`] — backend/search layer
8//!
9//! All variants are `Clone + PartialEq` so they can be threaded
10//! through `Arc` and matched in tests.
11//!
12//! See `docs/design/rust/error-model.md` for the full taxonomy,
13//! Python-exception mapping, and FFI conversion table.
14
15use alloc::sync::Arc;
16use thiserror::Error;
17
18use crate::types::VectorId;
19
20/// Errors produced by the codec layer.
21///
22/// Maps 1:1 to `tinyquant_cpu.codec._errors` Python exceptions; see the
23/// mapping table in `docs/design/rust/error-model.md`.
24#[non_exhaustive]
25#[derive(Debug, Error, Clone, PartialEq, Eq)]
26pub enum CodecError {
27    /// `bit_width` is outside the supported set `{2, 4, 8}`.
28    #[error("bit_width must be one of [2, 4, 8], got {got}")]
29    UnsupportedBitWidth {
30        /// The unsupported bit width value that was provided.
31        got: u8,
32    },
33
34    /// `dimension` is zero — every codec operation requires a non-zero dim.
35    #[error("dimension must be > 0, got {got}")]
36    InvalidDimension {
37        /// The invalid dimension value that was provided.
38        got: u32,
39    },
40
41    /// `dimension` exceeds the supported upper bound.
42    #[error("dimension must be <= {max}, got {got}")]
43    DimensionTooLarge {
44        /// The over-large dimension value that was provided.
45        got: u32,
46        /// The maximum supported dimension.
47        max: u32,
48    },
49
50    /// Input vector length does not match the config's declared dimension.
51    #[error("vector length {got} does not match config dimension {expected}")]
52    DimensionMismatch {
53        /// The dimension declared in the codec config.
54        expected: u32,
55        /// The actual length of the input vector.
56        got: u32,
57    },
58
59    /// Codebook was trained under a different `bit_width` than the config.
60    #[error("codebook bit_width {got} does not match config bit_width {expected}")]
61    CodebookIncompatible {
62        /// The bit width from the codec config.
63        expected: u8,
64        /// The bit width embedded in the codebook.
65        got: u8,
66    },
67
68    /// A `CompressedVector` carries a `config_hash` that differs from the
69    /// supplied `CodecConfig`.
70    #[error("compressed config_hash {got:?} does not match config hash {expected:?}")]
71    ConfigMismatch {
72        /// Expected hash from the codec config.
73        expected: Arc<str>,
74        /// Actual hash found in the compressed vector.
75        got: Arc<str>,
76    },
77
78    /// Codebook entry count is inconsistent with `bit_width`.
79    #[error("codebook must have {expected} entries for bit_width={bit_width}, got {got}")]
80    CodebookEntryCount {
81        /// Expected number of entries.
82        expected: u32,
83        /// Actual number of entries found.
84        got: u32,
85        /// The bit width that determines the expected count.
86        bit_width: u8,
87    },
88
89    /// Codebook entries are not sorted in non-decreasing order.
90    #[error("codebook entries must be sorted ascending")]
91    CodebookNotSorted,
92
93    /// Codebook contains non-unique entries (violates `np.unique` invariant).
94    #[error("codebook must contain {expected} distinct values, got {got}")]
95    CodebookDuplicate {
96        /// Expected number of distinct values.
97        expected: u32,
98        /// Actual number of distinct values found.
99        got: u32,
100    },
101
102    /// Training data contains fewer distinct values than needed to fill
103    /// the codebook.
104    #[error("insufficient training data for {expected} distinct entries")]
105    InsufficientTrainingData {
106        /// Minimum distinct entries required.
107        expected: u32,
108    },
109
110    /// A codebook index exceeds the codebook's valid range `[0, 2^bit_width)`.
111    #[error("index {index} is out of range [0, {bound})")]
112    IndexOutOfRange {
113        /// The out-of-range index value.
114        index: u8,
115        /// The exclusive upper bound.
116        bound: u32,
117    },
118
119    /// An internal length invariant was violated (caller or internal bug).
120    #[error("input and output lengths disagree: {left} vs {right}")]
121    LengthMismatch {
122        /// Length of the left operand.
123        left: usize,
124        /// Length of the right operand.
125        right: usize,
126    },
127
128    /// The residual flag byte in a serialized payload was not 0x00 or 0x01.
129    #[error("invalid residual flag: expected 0x00 or 0x01, got {got:#04x}")]
130    InvalidResidualFlag {
131        /// The invalid flag byte found.
132        got: u8,
133    },
134
135    /// GPU backend is not available or failed to initialize.
136    #[error("GPU unavailable: {0}")]
137    GpuUnavailable(alloc::sync::Arc<str>),
138
139    /// A GPU compute operation returned an error.
140    #[error("GPU error: {0}")]
141    GpuError(alloc::sync::Arc<str>),
142}
143
144/// Errors produced by the corpus aggregate layer.
145#[non_exhaustive]
146#[derive(Debug, Error, Clone, PartialEq, Eq)]
147pub enum CorpusError {
148    /// An upstream codec operation failed.
149    ///
150    /// `#[from]` generates `impl From<CodecError> for CorpusError`.
151    /// `#[source]` makes thiserror implement `source()` as `Some(self.0)`,
152    /// so the inner `CodecError` is accessible via the standard error chain —
153    /// matching the manual `impl` shown in `docs/design/rust/error-model.md`.
154    ///
155    /// Do NOT use `#[error(transparent)]` — that delegates `source()` to
156    /// `self.0.source()` (None for leaf variants), losing the chain link.
157    #[error("codec error: {0}")]
158    Codec(
159        #[from]
160        #[source]
161        CodecError,
162    ),
163
164    /// A vector with this id already exists in the corpus.
165    #[error("duplicate vector_id {id:?}")]
166    DuplicateVectorId {
167        /// The duplicate vector identifier.
168        id: VectorId,
169    },
170
171    /// A lookup requested a `VectorId` that does not exist.
172    #[error("unknown vector_id {id:?}")]
173    UnknownVectorId {
174        /// The unknown vector identifier.
175        id: VectorId,
176    },
177
178    /// An attempt was made to change the compression policy after it was
179    /// frozen.
180    #[error("compression policy is immutable once set")]
181    PolicyImmutable,
182
183    /// A vector's dimension does not match the corpus's declared dimension.
184    #[error("dimension mismatch: expected {expected}, got {got}")]
185    DimensionMismatch {
186        /// The dimension declared in the corpus's `CodecConfig`.
187        expected: u32,
188        /// The actual length of the supplied vector.
189        got: u32,
190    },
191
192    /// `insert_batch` failed atomically: the vector at `index` produced
193    /// `source`; the corpus was not modified.
194    ///
195    /// `source` is boxed to keep `CorpusError` `Sized`.
196    #[error("batch atomicity failure at index {index}: {source}")]
197    BatchAtomicityFailure {
198        /// Zero-based index of the first failing vector in the batch.
199        index: usize,
200        /// The error produced by the failing vector.
201        source: alloc::boxed::Box<Self>,
202    },
203}
204
205/// Errors produced by the search-backend layer.
206#[non_exhaustive]
207#[derive(Debug, Error, Clone)]
208pub enum BackendError {
209    /// The backend contains no vectors — search cannot proceed.
210    ///
211    /// This is informational; callers may treat it as an empty result
212    /// rather than a hard failure.
213    #[error("backend is empty")]
214    Empty,
215
216    /// `top_k` was zero or would overflow internal heap sizing.
217    #[error("top_k must be > 0")]
218    InvalidTopK,
219
220    /// An adapter-specific (e.g. pgvector) failure, with a human-readable
221    /// message. `Arc<str>` avoids allocation on the hot path when the
222    /// error is not inspected.
223    #[error("adapter error: {0}")]
224    Adapter(Arc<str>),
225}