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}