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    /// Input vector length does not match the config's declared dimension.
42    #[error("vector length {got} does not match config dimension {expected}")]
43    DimensionMismatch {
44        /// The dimension declared in the codec config.
45        expected: u32,
46        /// The actual length of the input vector.
47        got: u32,
48    },
49
50    /// Codebook was trained under a different `bit_width` than the config.
51    #[error("codebook bit_width {got} does not match config bit_width {expected}")]
52    CodebookIncompatible {
53        /// The bit width from the codec config.
54        expected: u8,
55        /// The bit width embedded in the codebook.
56        got: u8,
57    },
58
59    /// A `CompressedVector` carries a `config_hash` that differs from the
60    /// supplied `CodecConfig`.
61    #[error("compressed config_hash {got:?} does not match config hash {expected:?}")]
62    ConfigMismatch {
63        /// Expected hash from the codec config.
64        expected: Arc<str>,
65        /// Actual hash found in the compressed vector.
66        got: Arc<str>,
67    },
68
69    /// Codebook entry count is inconsistent with `bit_width`.
70    #[error("codebook must have {expected} entries for bit_width={bit_width}, got {got}")]
71    CodebookEntryCount {
72        /// Expected number of entries.
73        expected: u32,
74        /// Actual number of entries found.
75        got: u32,
76        /// The bit width that determines the expected count.
77        bit_width: u8,
78    },
79
80    /// Codebook entries are not sorted in non-decreasing order.
81    #[error("codebook entries must be sorted ascending")]
82    CodebookNotSorted,
83
84    /// Codebook contains non-unique entries (violates `np.unique` invariant).
85    #[error("codebook must contain {expected} distinct values, got {got}")]
86    CodebookDuplicate {
87        /// Expected number of distinct values.
88        expected: u32,
89        /// Actual number of distinct values found.
90        got: u32,
91    },
92
93    /// Training data contains fewer distinct values than needed to fill
94    /// the codebook.
95    #[error("insufficient training data for {expected} distinct entries")]
96    InsufficientTrainingData {
97        /// Minimum distinct entries required.
98        expected: u32,
99    },
100
101    /// A codebook index exceeds the codebook's valid range `[0, 2^bit_width)`.
102    #[error("index {index} is out of range [0, {bound})")]
103    IndexOutOfRange {
104        /// The out-of-range index value.
105        index: u8,
106        /// The exclusive upper bound.
107        bound: u32,
108    },
109
110    /// An internal length invariant was violated (caller or internal bug).
111    #[error("input and output lengths disagree: {left} vs {right}")]
112    LengthMismatch {
113        /// Length of the left operand.
114        left: usize,
115        /// Length of the right operand.
116        right: usize,
117    },
118
119    /// The residual flag byte in a serialized payload was not 0x00 or 0x01.
120    #[error("invalid residual flag: expected 0x00 or 0x01, got {got:#04x}")]
121    InvalidResidualFlag {
122        /// The invalid flag byte found.
123        got: u8,
124    },
125}
126
127/// Errors produced by the corpus aggregate layer.
128#[non_exhaustive]
129#[derive(Debug, Error, Clone, PartialEq, Eq)]
130pub enum CorpusError {
131    /// An upstream codec operation failed.
132    ///
133    /// `#[from]` generates `impl From<CodecError> for CorpusError`.
134    /// `#[source]` makes thiserror implement `source()` as `Some(self.0)`,
135    /// so the inner `CodecError` is accessible via the standard error chain —
136    /// matching the manual `impl` shown in `docs/design/rust/error-model.md`.
137    ///
138    /// Do NOT use `#[error(transparent)]` — that delegates `source()` to
139    /// `self.0.source()` (None for leaf variants), losing the chain link.
140    #[error("codec error: {0}")]
141    Codec(
142        #[from]
143        #[source]
144        CodecError,
145    ),
146
147    /// A vector with this id already exists in the corpus.
148    #[error("duplicate vector_id {id:?}")]
149    DuplicateVectorId {
150        /// The duplicate vector identifier.
151        id: VectorId,
152    },
153
154    /// A lookup requested a `VectorId` that does not exist.
155    #[error("unknown vector_id {id:?}")]
156    UnknownVectorId {
157        /// The unknown vector identifier.
158        id: VectorId,
159    },
160
161    /// An attempt was made to change the compression policy after it was
162    /// frozen.
163    #[error("compression policy is immutable once set")]
164    PolicyImmutable,
165
166    /// A vector's dimension does not match the corpus's declared dimension.
167    #[error("dimension mismatch: expected {expected}, got {got}")]
168    DimensionMismatch {
169        /// The dimension declared in the corpus's `CodecConfig`.
170        expected: u32,
171        /// The actual length of the supplied vector.
172        got: u32,
173    },
174
175    /// `insert_batch` failed atomically: the vector at `index` produced
176    /// `source`; the corpus was not modified.
177    ///
178    /// `source` is boxed to keep `CorpusError` `Sized`.
179    #[error("batch atomicity failure at index {index}: {source}")]
180    BatchAtomicityFailure {
181        /// Zero-based index of the first failing vector in the batch.
182        index: usize,
183        /// The error produced by the failing vector.
184        source: alloc::boxed::Box<CorpusError>,
185    },
186}
187
188/// Errors produced by the search-backend layer.
189#[non_exhaustive]
190#[derive(Debug, Error, Clone)]
191pub enum BackendError {
192    /// The backend contains no vectors — search cannot proceed.
193    ///
194    /// This is informational; callers may treat it as an empty result
195    /// rather than a hard failure.
196    #[error("backend is empty")]
197    Empty,
198
199    /// `top_k` was zero or would overflow internal heap sizing.
200    #[error("top_k must be > 0")]
201    InvalidTopK,
202
203    /// An adapter-specific (e.g. pgvector) failure, with a human-readable
204    /// message. `Arc<str>` avoids allocation on the hot path when the
205    /// error is not inspected.
206    #[error("adapter error: {0}")]
207    Adapter(Arc<str>),
208}