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}