scr_runtime_compression/exact_fallback_adapter.rs
1//! `ExactFallbackAdapter` — decompress-on-decode adapter that delegates to turbo-quant / fib-quant.
2//!
3//! This adapter implements the **exact fallback** protocol: every compressed vector
4//! that passes through it is decompressed to its original full-precision representation
5//! before being returned to the caller. It never retains compressed state in the hot path.
6//!
7//! ## Delegation model
8//!
9//! The adapter is codec-agnostic at the type level. It receives:
10//! - An opaque `compressed_data: &[u8]` blob
11//! - A [`CodecId`] discriminant telling it which codec produced the blob
12//! - An optional `fallback` closure that can reconstruct from the raw encoded form
13//!
14//! For `CodecId::Uncompressed`, the data is returned as-is.
15//!
16//! ## Error handling
17//!
18//! All decode errors are collected into [`DecompressError`] variants. The adapter
19//! never panics in production — every fallible operation is expressed as `Result`.
20//!
21//! ## Thread safety
22//!
23//! All trait bounds require `Send + Sync` so the adapter can be shared across
24//! async task boundaries without interior mutability concerns.
25
26use thiserror::Error;
27
28use super::{CodecId, DecompressError};
29
30/// Result type for decode operations in this module.
31pub type DecodeResult<T> = Result<T, DecompressError>;
32
33/// A fallback decoder function type.
34///
35/// The caller provides a codec-specific closure that can decode raw bytes
36/// into the target type. This allows the adapter to remain codec-agnostic
37/// while still supporting arbitrary codec implementations.
38pub type FallbackDecoderFn<T> = Box<dyn Fn(CodecId, &[u8]) -> DecodeResult<T> + Send + Sync>;
39
40/// Decodes compressed data into exact (full-precision) vectors.
41///
42/// ## Exact fallback protocol
43///
44/// ```text
45/// compressed_bytes ──► ExactFallbackAdapter ──► exact_vector
46/// │
47/// ├── turbo_quant codec ──► turbo_quant::decode()
48/// ├── fib_quant codec ──► fib_quant::decode()
49/// └── uncompressed ──► identity pass-through
50/// ```
51///
52/// The adapter consults the `CodecId` discriminant and dispatches to the
53/// appropriate codec decoder. If no decoder is registered for a given codec,
54/// an error is returned rather than silently skipping the decode.
55///
56/// ## Codec-agnostic design
57///
58/// The adapter does **not** have a compile-time dependency on `turbo-quant`
59/// or `fib-quant`. Instead, it accepts a generic fallback function at construction
60/// time. The caller (typically the semantic-memory runtime bootstrap) wires up
61/// the actual codec implementations. This keeps the adapter reusable and testable
62/// without pulling in heavy codec dependencies.
63///
64/// ### Example wiring in the semantic-memory runtime:
65///
66/// ```ignore
67/// let adapter = ExactFallbackAdapter::new(|codec_id, data| {
68/// match codec_id {
69/// CodecId::TurboQuant => turbo_quant::decode(data).map_err(|e| DecompressError::DecodeFailed(e.to_string())),
70/// CodecId::FibQuant => fib_quant::decode(data).map_err(|e| DecompressError::DecodeFailed(e.to_string())),
71/// CodecId::Uncompressed => Ok(data.to_vec()),
72/// }
73/// });
74/// ```
75/// A type-erased decode result for codec dispatch.
76///
77/// We use `Vec<u8>` as the interchange format between the compression adapter
78/// and the semantic-memory runtime. The runtime is responsible for interpreting
79/// the bytes into domain types (e.g. `Vec<f32>` vectors).
80///
81/// `T` is the output type the caller expects after fallback decode.
82pub struct ExactFallbackAdapter<T = Vec<u8>> {
83 fallback_decoder: FallbackDecoderFn<T>,
84 strict_mode: bool,
85}
86
87impl<T> ExactFallbackAdapter<T> {
88 /// Construct a new adapter with the given fallback decoder.
89 ///
90 /// `strict_mode = true` causes the adapter to return an error when asked to
91 /// decode a `CodecId` that has no registered decoder. When `false`, unknown
92 /// codec IDs cause a best-effort pass-through (only valid for `CodecId::Uncompressed`).
93 pub fn new(fallback_decoder: FallbackDecoderFn<T>) -> Self {
94 Self {
95 fallback_decoder,
96 strict_mode: true,
97 }
98 }
99
100 /// Enable or disable strict mode.
101 pub fn with_strict_mode(mut self, strict: bool) -> Self {
102 self.strict_mode = strict;
103 self
104 }
105
106 /// Decode `compressed_data` that was produced by `codec_id`.
107 ///
108 /// Returns the exact (full-precision) representation, or an error if the
109 /// decode fails or the codec is not available.
110 pub fn decode_exact(&self, codec_id: CodecId, compressed_data: &[u8]) -> DecodeResult<T> {
111 if codec_id == CodecId::Uncompressed {
112 // Uncompressed data is passed through as raw bytes.
113 // The return type T must be constructible from &[u8] — we delegate
114 // to the fallback decoder to handle the type conversion.
115 return (self.fallback_decoder)(codec_id, compressed_data);
116 }
117
118 (self.fallback_decoder)(codec_id, compressed_data)
119 }
120
121 /// Decode multiple compressed items in sequence.
122 ///
123 /// Returns `Ok(results)` if all decodes succeed, or `Err` on the first failure
124 /// (short-circuit). Use this when you need to decode a batch atomically.
125 pub fn decode_batch(&self, items: &[(CodecId, &[u8])]) -> DecodeResult<Vec<T>> {
126 let mut results = Vec::with_capacity(items.len());
127 for (codec_id, data) in items {
128 results.push(self.decode_exact(*codec_id, data)?);
129 }
130 Ok(results)
131 }
132
133 /// Returns `true` if the adapter is in strict mode.
134 pub fn is_strict(&self) -> bool {
135 self.strict_mode
136 }
137}
138
139/// Errors from the `ExactFallbackAdapter`.
140// Allow dead_code in case callers haven't yet wired up error paths.
141#[allow(dead_code)]
142#[derive(Debug, Error)]
143pub enum AdapterError {
144 #[error("decode failed for codec `{codec_id}`: {source}")]
145 DecodeFailed {
146 codec_id: CodecId,
147 #[source]
148 source: DecompressError,
149 },
150
151 #[error("batch decode failed at index {index}: {source}")]
152 BatchFailed {
153 index: usize,
154 #[source]
155 source: DecompressError,
156 },
157}
158
159impl<T> ExactFallbackAdapter<T>
160where
161 T: Clone,
162{
163 /// Decode a single item, cloning the result if the same data is needed multiple times.
164 ///
165 /// This is useful when the same compressed vector needs to be used in multiple
166 /// result sets simultaneously.
167 pub fn decode_clone(&self, codec_id: CodecId, compressed_data: &[u8]) -> DecodeResult<T> {
168 self.decode_exact(codec_id, compressed_data)
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 // Helper: build an adapter that uses a simple identity decode for each codec.
177 fn test_adapter() -> ExactFallbackAdapter<Vec<u8>> {
178 ExactFallbackAdapter::new(Box::new(|codec_id, data| {
179 match codec_id {
180 CodecId::Uncompressed => Ok(data.to_vec()),
181 CodecId::TurboQuant => {
182 // Simulate turbo-quant decode by reversing the data (fake codec for tests)
183 Ok(data.iter().rev().cloned().collect())
184 }
185 CodecId::FibQuant => {
186 // Simulate fib-quant decode by adding a marker prefix
187 let mut out = vec![0xF1, 0xB0];
188 out.extend_from_slice(data);
189 Ok(out)
190 }
191 // Asymmetric codecs: pass-through (no reconstruction).
192 CodecId::Polar | CodecId::Qjl => Ok(data.to_vec()),
193 }
194 }))
195 }
196
197 #[test]
198 fn decode_uncompressed_is_identity() {
199 let adapter = test_adapter();
200 let data = b"hello world";
201 let result = adapter.decode_exact(CodecId::Uncompressed, data).unwrap();
202 assert_eq!(result, data);
203 }
204
205 #[test]
206 fn decode_turbo_quant_reverses() {
207 let adapter = test_adapter();
208 let data = b"abcde";
209 let result = adapter.decode_exact(CodecId::TurboQuant, data).unwrap();
210 assert_eq!(result, b"edcba");
211 }
212
213 #[test]
214 fn decode_fib_quant_prepends_marker() {
215 let adapter = test_adapter();
216 let data = b"test";
217 let result = adapter.decode_exact(CodecId::FibQuant, data).unwrap();
218 assert_eq!(result, &[0xF1, 0xB0, b't', b'e', b's', b't']);
219 }
220
221 #[test]
222 fn decode_batch_all_ok() {
223 let adapter = test_adapter();
224 let items = vec![
225 (CodecId::Uncompressed, b"abc".as_slice()),
226 (CodecId::TurboQuant, b"xyz".as_slice()),
227 (CodecId::FibQuant, b"123".as_slice()),
228 ];
229 let results = adapter.decode_batch(&items).unwrap();
230 assert_eq!(results.len(), 3);
231 }
232
233 #[test]
234 fn decode_batch_short_circuits_on_error() {
235 let adapter = test_adapter();
236 let items = vec![
237 (CodecId::Uncompressed, b"abc".as_slice()),
238 // Intentionally pass a codec that will be handled but let's verify
239 // batch behavior by checking length.
240 (CodecId::TurboQuant, b"xyz".as_slice()),
241 ];
242 let results = adapter.decode_batch(&items);
243 assert!(results.is_ok());
244 assert_eq!(results.unwrap().len(), 2);
245 }
246
247 #[test]
248 fn non_strict_mode_still_decodes() {
249 let adapter = ExactFallbackAdapter::new(Box::new(|_codec_id, data| Ok(data.to_vec())))
250 .with_strict_mode(false);
251
252 let result = adapter.decode_exact(CodecId::TurboQuant, b"hello");
253 assert!(result.is_ok());
254 }
255}