Skip to main content

alice_zip/
dictionary.rs

1//! Dictionary coding (= phrase → index mapping with FIFO eviction)
2//!
3//! A simple string-table style dictionary that assigns u32 indices to
4//! variable-length byte phrases. When the entry limit is hit, the oldest
5//! phrase is evicted (= arena drain + offsets shift), so indices of the
6//! remaining phrases shift down by one.
7//!
8//! Law (pinned by `tests/analytic_oracle.rs`): after `k` distinct `add`s into
9//! a dictionary of capacity `c`, `len() == min(k, c)` and `lookup(i)` returns
10//! the `(k - min(k, c) + i)`-th phrase added
11
12use alloc::vec::Vec;
13
14use crate::error::ZipError;
15
16#[derive(Debug, Clone)]
17pub struct Dictionary {
18    arena: Vec<u8>,
19    offsets: Vec<u32>,
20    max_entries: usize,
21}
22
23impl Dictionary {
24    /// Create an empty dictionary holding at most `max_entries` phrases
25    /// A capacity of `0` is allowed but every [`add`](Self::add) then fails
26    /// with [`ZipError::DictionaryFull`]
27    #[must_use]
28    pub const fn new(max_entries: usize) -> Self {
29        Self {
30            arena: Vec::new(),
31            offsets: Vec::new(),
32            max_entries,
33        }
34    }
35
36    /// Insert `phrase` (or return the index of an identical existing phrase)
37    /// Evicts the oldest phrase when the dictionary is at capacity
38    ///
39    /// # Errors
40    ///
41    /// [`ZipError::DictionaryFull`] when the capacity is `0`, or when the
42    /// arena would exceed the `u32` offset range (4 GiB)
43    #[allow(clippy::cast_possible_truncation)]
44    pub fn add(&mut self, phrase: &[u8]) -> Result<u32, ZipError> {
45        if let Some(pos) = (0..self.offsets.len()).position(|i| self.get_entry(i) == phrase) {
46            return Ok(pos as u32);
47        }
48        if self.max_entries == 0 {
49            return Err(ZipError::DictionaryFull);
50        }
51        if self.offsets.len() >= self.max_entries {
52            let first_end = if self.offsets.len() > 1 {
53                self.offsets[1] as usize
54            } else {
55                self.arena.len()
56            };
57            self.arena.drain(..first_end);
58            self.offsets.remove(0);
59            for off in &mut self.offsets {
60                *off -= first_end as u32;
61            }
62        }
63        if self.arena.len().saturating_add(phrase.len()) > u32::MAX as usize {
64            return Err(ZipError::DictionaryFull);
65        }
66        self.offsets.push(self.arena.len() as u32);
67        self.arena.extend_from_slice(phrase);
68        Ok((self.offsets.len() - 1) as u32)
69    }
70
71    #[must_use]
72    pub fn lookup(&self, idx: u32) -> Option<&[u8]> {
73        let i = idx as usize;
74        if i >= self.offsets.len() {
75            return None;
76        }
77        Some(self.get_entry(i))
78    }
79
80    #[inline]
81    fn get_entry(&self, i: usize) -> &[u8] {
82        let start = self.offsets[i] as usize;
83        let end = if i + 1 < self.offsets.len() {
84            self.offsets[i + 1] as usize
85        } else {
86            self.arena.len()
87        };
88        &self.arena[start..end]
89    }
90
91    #[must_use]
92    pub const fn len(&self) -> usize {
93        self.offsets.len()
94    }
95
96    #[must_use]
97    pub const fn is_empty(&self) -> bool {
98        self.offsets.is_empty()
99    }
100}
101
102#[cfg(test)]
103#[allow(
104    clippy::doc_markdown,
105    clippy::unreadable_literal,
106    clippy::cast_possible_truncation,
107    clippy::cast_sign_loss,
108    clippy::cast_precision_loss,
109    clippy::cast_possible_wrap,
110    clippy::uninlined_format_args
111)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn dictionary_basic() {
117        let mut dict = Dictionary::new(100);
118        let idx1 = dict.add(b"hello").unwrap();
119        let idx2 = dict.add(b"world").unwrap();
120        assert_eq!(dict.lookup(idx1), Some(b"hello".as_slice()));
121        assert_eq!(dict.lookup(idx2), Some(b"world".as_slice()));
122    }
123
124    #[test]
125    fn dictionary_dedup() {
126        let mut dict = Dictionary::new(100);
127        let idx1 = dict.add(b"hello").unwrap();
128        let idx2 = dict.add(b"hello").unwrap();
129        assert_eq!(idx1, idx2);
130        assert_eq!(dict.len(), 1);
131    }
132
133    #[test]
134    fn dictionary_eviction() {
135        let mut dict = Dictionary::new(3);
136        dict.add(b"a").unwrap();
137        dict.add(b"b").unwrap();
138        dict.add(b"c").unwrap();
139        dict.add(b"d").unwrap();
140        assert_eq!(dict.len(), 3);
141    }
142
143    // =========================================================================
144    // Dictionary — 追加テスト
145    // =========================================================================
146
147    /// 初期状態: 空
148    #[test]
149    fn dictionary_initial_empty() {
150        let dict = Dictionary::new(10);
151        assert!(dict.is_empty());
152        assert_eq!(dict.len(), 0);
153    }
154
155    /// 1件追加後はis_empty=false
156    #[test]
157    fn dictionary_not_empty_after_add() {
158        let mut dict = Dictionary::new(10);
159        dict.add(b"abc").unwrap();
160        assert!(!dict.is_empty());
161        assert_eq!(dict.len(), 1);
162    }
163
164    /// 存在しないインデックスのlookupはNone
165    #[test]
166    fn dictionary_lookup_out_of_range() {
167        let dict = Dictionary::new(10);
168        assert_eq!(dict.lookup(0), None);
169        assert_eq!(dict.lookup(100), None);
170        assert_eq!(dict.lookup(u32::MAX), None);
171    }
172
173    /// 空フレーズの追加
174    #[test]
175    fn dictionary_add_empty_phrase() {
176        let mut dict = Dictionary::new(10);
177        let idx = dict.add(b"").unwrap();
178        assert_eq!(dict.lookup(idx), Some(b"".as_slice()));
179    }
180
181    /// 最大エントリ数=1の辞書
182    #[test]
183    fn dictionary_max_one() {
184        let mut dict = Dictionary::new(1);
185        dict.add(b"first").unwrap();
186        assert_eq!(dict.len(), 1);
187        dict.add(b"second").unwrap();
188        // evictionで最初のエントリが消え、secondのみ残る
189        assert_eq!(dict.len(), 1);
190        assert_eq!(dict.lookup(0), Some(b"second".as_slice()));
191    }
192
193    /// eviction後に同じフレーズを再追加
194    #[test]
195    fn dictionary_re_add_after_eviction() {
196        let mut dict = Dictionary::new(2);
197        dict.add(b"A").unwrap();
198        dict.add(b"B").unwrap();
199        // "C"を追加 → "A"がevictされる
200        dict.add(b"C").unwrap();
201        // "A"はもう存在しないので新規追加
202        let idx = dict.add(b"A").unwrap();
203        assert_eq!(dict.lookup(idx), Some(b"A".as_slice()));
204    }
205
206    /// 重複追加はインデックスを返す(eviction後)
207    #[test]
208    fn dictionary_dedup_after_eviction() {
209        let mut dict = Dictionary::new(3);
210        dict.add(b"x").unwrap();
211        dict.add(b"y").unwrap();
212        dict.add(b"z").unwrap();
213        // "w"を追加 → "x"がevictされる
214        dict.add(b"w").unwrap();
215        // "y"はまだ存在するので重複インデックスが返る
216        let idx = dict.add(b"y").unwrap();
217        assert_eq!(dict.lookup(idx), Some(b"y".as_slice()));
218        assert_eq!(dict.len(), 3);
219    }
220
221    /// 大量エントリ追加テスト
222    #[test]
223    fn dictionary_many_entries() {
224        let mut dict = Dictionary::new(1000);
225        for i in 0u32..500 {
226            let phrase = alloc::format!("entry_{i}");
227            dict.add(phrase.as_bytes()).unwrap();
228        }
229        assert_eq!(dict.len(), 500);
230    }
231
232    /// Cloneの動作確認
233    #[test]
234    fn dictionary_clone() {
235        let mut dict = Dictionary::new(10);
236        dict.add(b"test").unwrap();
237        let dict2 = dict.clone();
238        assert_eq!(dict2.len(), 1);
239        assert_eq!(dict2.lookup(0), Some(b"test".as_slice()));
240    }
241
242    /// Debugの動作確認
243    #[test]
244    fn dictionary_debug() {
245        let dict = Dictionary::new(5);
246        let s = alloc::format!("{:?}", dict);
247        assert!(s.contains("Dictionary"));
248    }
249
250    /// バイナリデータ(非UTF-8)のフレーズ追加
251    #[test]
252    fn dictionary_binary_phrase() {
253        let mut dict = Dictionary::new(10);
254        let binary = &[0x00, 0xFF, 0x80, 0x7F];
255        let idx = dict.add(binary).unwrap();
256        assert_eq!(dict.lookup(idx), Some(binary.as_slice()));
257    }
258
259    /// 同一フレーズ複数回addで長さが増えないことの検証
260    #[test]
261    fn dictionary_dedup_multiple_times() {
262        let mut dict = Dictionary::new(100);
263        for _ in 0..10 {
264            dict.add(b"same").unwrap();
265        }
266        assert_eq!(dict.len(), 1);
267    }
268
269    /// evictionで先頭が削除されることの確認
270    #[test]
271    fn dictionary_eviction_removes_first() {
272        let mut dict = Dictionary::new(2);
273        dict.add(b"alpha").unwrap();
274        dict.add(b"beta").unwrap();
275        // 満杯状態で"gamma"追加 → "alpha"が削除される
276        dict.add(b"gamma").unwrap();
277        assert_eq!(dict.len(), 2);
278        // "alpha"のlookupは見つからないか位置がずれている
279        // "beta"はインデックス0、"gamma"はインデックス1に移動
280        assert_eq!(dict.lookup(0), Some(b"beta".as_slice()));
281        assert_eq!(dict.lookup(1), Some(b"gamma".as_slice()));
282    }
283}