Skip to main content

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}